摘要:本部落格將介紹如何使用 Spring Boot 實現一個簡單的商城秒殺系統,並透過使用 Redis 和 MySQL 來增強其效能和可靠性。
本文分享自華為雲社群《Spring Boot實現商城高併發秒殺案例》,作者:林欣。
隨著經濟的發展和人們消費觀念的轉變,電子商務逐漸成為人們購物的主要方式之一。高併發是電子商務網站面臨的一個重要挑戰。本部落格將介紹如何使用 Spring Boot 實現一個簡單的商城秒殺系統,並透過使用 Redis 和 MySQL 來增強其效能和可靠性。
準備工作
在開始之前,您需要準備以下工具和環境:
- JDK 1.8 或更高版本
- Redis
- MySQL
- MyBatis
實現步驟
步驟一:建立資料庫
首先,我們需要建立一個資料庫來儲存商品資訊、訂單資訊和秒殺活動資訊。在這裡,我們使用 MySQL 資料庫,建立一個名為 shop 的資料庫,並建立三個表 goods、order 和 seckill。
表 goods 儲存了所有的商品資訊,包括商品編號、名稱、描述、價格和庫存數量等等。
CREATE TABLE `goods` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '商品ID', `name` varchar(50) NOT NULL COMMENT '商品名稱', `description` varchar(100) NOT NULL COMMENT '商品描述', `price` decimal(10,2) NOT NULL COMMENT '商品價格', `stock_count` int(11) NOT NULL COMMENT '商品庫存', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';
表 order 儲存了所有的訂單資訊,包括訂單編號、使用者ID、商品ID、秒殺活動ID 和訂單狀態等等。
CREATE TABLE `order` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '訂單ID', `user_id` BIGINT(20) NOT NULL COMMENT '使用者ID', `goods_id` BIGINT(20) NOT NULL COMMENT '商品ID', `seckill_id` BIGINT(20) DEFAULT NULL COMMENT '秒殺活動ID', `status` TINYINT(4) NOT NULL COMMENT '訂單狀態,0-未支付,1-已支付,2-已發貨,3-已收貨,4-已退款,5-已完成', `create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間', `update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間', PRIMARY KEY (`id`), UNIQUE KEY `unique_order` (`user_id`,`goods_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='訂單表';
表 seckill 儲存了所有的秒殺活動資訊,包括秒殺活動編號、商品ID、開始時間和結束時間等等。
CREATE TABLE `seckill` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '秒殺活動ID', `goods_id` BIGINT(20) NOT NULL COMMENT '商品ID', `start_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '開始時間', `end_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '結束時間', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒殺活動表';
步驟二:建立 Spring Boot 專案
接下來,我們需要建立一個 Spring Boot 專案,用於實現商城高併發秒殺案例。可以使用 Spring Initializr 來快速建立一個基本的 Spring Boot 專案。
步驟三:配置 Redis 和 MySQL
在 Spring Boot 專案中,我們需要配置 Redis 和 MySQL 的連線資訊。可以在 application.properties 檔案中設定以下屬性:
spring.redis.host=127.0.0.1 spring.redis.port=6379 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/shop?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true spring.datasource.username=root spring.datasource.password=123456
步驟四:編寫實體類和 DAO 介面
在這一步中,我們需要定義三個實體類分別對應資料庫中的 goods、order 和 seckill 表。同時,我們需要編寫相應的 DAO 介面,用於操作這些實體類。
// 商品實體類 @Data public class Goods { private Long id; private String name; private String description; private BigDecimal price; private Integer stockCount; } // 商品 DAO 介面 @Mapper public interface GoodsDao { @Select("SELECT * FROM goods WHERE id = #{id}") Goods getGoodsById(Long id); @Update("UPDATE goods SET stock_count = stock_count - 1 WHERE id = #{id} AND stock_count > 0") int reduceStockCount(Long id); } // 訂單實體類 @Data public class Order { private Long id; private Long userId; private Long goodsId; private Long seckillId; private Byte status; private Date createTime; private Date updateTime; } // 訂單 DAO 介面 @Mapper public interface OrderDao { @Select("SELECT * FROM `order` WHERE user_id = #{userId} AND goods_id = #{goodsId}") Order getOrderByUserIdAndGoodsId(@Param("userId") Long userId, @Param("goodsId") Long goodsId); @Insert("INSERT INTO `order` (user_id, goods_id, seckill_id, status, create_time, update_time) VALUES (#{userId}, #{goodsId}, #{seckillId}, #{status},#{createTime},#{updateTime})") int insertOrder(Order order); @Select("SELECT o.*, g.name, g.price FROM `order` o LEFT JOIN goods g ON o.goods_id = g.id WHERE o.user_id = #{userId}") List<OrderVo> getOrderListByUserId(Long userId); } // 秒殺活動實體類 @Data public class Seckill { private Long id; private Long goodsId; private Date startTime; private Date endTime; } // 秒殺活動 DAO 介面 @Mapper public interface SeckillDao { @Select("SELECT * FROM seckill WHERE id = #{id}") Seckill getSeckillById(Long id); @Update("UPDATE seckill SET end_time = #{endTime} WHERE id = #{id}") int updateSeckillEndTime(@Param("id") Long id, @Param("endTime") Date endTime); }
步驟五:編寫 Service 層和 Controller
在這一步中,我們需要編寫 Service 層和 Controller 類,用於實現商城高併發秒殺案例的核心功能。
- 商品 Service 層:用於獲取商品資訊和減少商品庫存數量。
@Service public class GoodsService { private final GoodsDao goodsDao; @Autowired public GoodsService(GoodsDao goodsDao) { this.goodsDao = goodsDao; } public Goods getGoodsById(Long id) { return goodsDao.getGoodsById(id); } public boolean reduceStockCount(Long id) { return goodsDao.reduceStockCount(id) > 0; } }
- 訂單 Service 層:用於建立訂單和獲取訂單資訊。
@Service public class OrderService { private final OrderDao orderDao; @Autowired public OrderService(OrderDao orderDao) { this.orderDao = orderDao; } public Order createOrder(Long userId, Long goodsId, Long seckillId) { Order order = new Order(); order.setUserId(userId); order.setGoodsId(goodsId); order.setSeckillId(seckillId); order.setStatus((byte) 0); order.setCreateTime(new Date()); order.setUpdateTime(new Date()); orderDao.insertOrder(order); return order; } public List<OrderVo> getOrderListByUserId(Long userId) { return orderDao.getOrderListByUserId(userId); } }
- 秒殺活動 Service 層:用於獲取秒殺活動資訊和更新秒殺活動結束時間。
@Service public class SeckillService { private final SeckillDao seckillDao; @Autowired public SeckillService(SeckillDao seckillDao) { this.seckillDao = seckillDao; } public Seckill getSeckillById(Long id) { return seckillDao.getSeckillById(id); } public boolean updateSeckillEndTime(Long id, Date endTime) { return seckillDao.updateSeckillEndTime(id, endTime) > 0; } }
- 訂單 Controller:用於處理訂單相關的請求。
@RestController @RequestMapping("/order") public class OrderController { private final OrderService orderService; @Autowired public OrderController(OrderService orderService) { this.orderService = orderService; } @PostMapping("/create") public CommonResult<Order> createOrder(@RequestParam("userId") Long userId, @RequestParam("goodsId") Long goodsId, @RequestParam("seckillId") Long seckillId) { Order order = orderService.createOrder(userId, goodsId, seckillId); if (order == null) { return CommonResult.failed(ResultCode.FAILURE); } return CommonResult.success(order); } @GetMapping("/list") public CommonResult<List<OrderVo>> getOrderListByUserId(@RequestParam("userId") Long userId) { List<OrderVo> orderList = orderService.getOrderListByUserId(userId); return CommonResult.success(orderList); } }
秒殺活動 Controller:用於處理秒殺活動相關的請求。
@RestController @RequestMapping("/seckill") public class SeckillController { private final SeckillService seckillService; private final GoodsService goodsService; private final OrderService orderService; @Autowired public SeckillController(SeckillService seckillService, GoodsService goodsService, OrderService orderService) { this.seckillService = seckillService; this.goodsService = goodsService; this.orderService = orderService; } @PostMapping("/start") public CommonResult<Object> startSeckill(@RequestParam("userId") Long userId, @RequestParam("goodsId") Long goodsId, @RequestParam("seckillId") Long seckillId) { // 查詢秒殺活動是否有效 Seckill seckill = seckillService.getSeckillById(seckillId); if (seckill == null || seckill.getStartTime().after(new Date()) || seckill.getEndTime().before(new Date())) { return CommonResult.failed(ResultCode.FAILURE, "秒殺活動不存在或已結束"); } // 判斷商品庫存是否充足 Goods goods = goodsService.getGoodsById(goodsId); if (goods == null || goods.getStockCount() <= 0) { return CommonResult.failed(ResultCode.FAILURE, "商品庫存不足"); } // 生成訂單 Order order = orderService.createOrder(userId, goodsId, seckillId); if (order == null) { return CommonResult.failed(ResultCode.FAILURE, "訂單建立失敗,請稍後再試"); } // 減少商品庫存 boolean success = goodsService.reduceStockCount(goodsId); if (!success) { return CommonResult.failed(ResultCode.FAILURE, "減少商品庫存失敗,請稍後再試"); } return CommonResult.success("秒殺成功"); } }
步驟六:使用 Redis 實現分散式鎖
在商城高併發秒殺案例中,一個重要的問題是如何保證商品庫存數量的一致性和秒殺結果的正確性。為了解決這個問題,我們可以使用 Redis 實現分散式鎖。
在 RedisService 類中實現分散式鎖:
@Service public class RedisService { private final RedisTemplate<String, Object> redisTemplate; @Autowired public RedisService(RedisTemplate<String, Object> redisTemplate) { this.redisTemplate = redisTemplate; } public boolean lock(String key, String value, long expire) { Boolean result = redisTemplate.opsForValue().setIfAbsent(key, value, Duration.ofSeconds(expire)); return result != null && result; } public void unlock(String key, String value) { if (value.equals(redisTemplate.opsForValue().get(key))) { redisTemplate.delete(key); } } }
在 SeckillService 中使用分散式鎖實現秒殺介面:
@Service public class SeckillService { private final RedisService redisService; private final SeckillDao seckillDao; private final GoodsDao goodsDao; private final OrderDao orderDao; @Autowired public SeckillService(RedisService redisService, SeckillDao seckillDao, GoodsDao goodsDao, OrderDao orderDao) { this.redisService = redisService; this.seckillDao = seckillDao; this.goodsDao = goodsDao; this.orderDao = orderDao; } public CommonResult<Object> startSeckill(Long userId, Long goodsId, Long seckillId) { String lockKey = "seckill:lock:" + goodsId; String lockValue = UUID.randomUUID().toString(); try { // 獲取分散式鎖 if (!redisService.lock(lockKey, lockValue, 10)) { return CommonResult.failed(ResultCode.FAILURE, "當前請求太過頻繁,請稍後再試"); } // 查詢秒殺活動是否有效 Seckill seckill = seckillDao.getSeckillById(seckillId); if (seckill == null || seckill.getStartTime().after(new Date()) || seckill.getEndTime().before(new Date())) { return CommonResult.failed(ResultCode.FAILURE, "秒殺活動不存在或已結束"); } // 判斷商品庫存是否充足 Goods goods = goodsDao.getGoodsById(goodsId); if (goods == null || goods.getStockCount() <= 0) { return CommonResult.failed(ResultCode.FAILURE, "商品庫存不足"); } // 建立訂單 Order order = new Order(); order.setUserId(userId); order.setGoodsId(goodsId); order.setSeckillId(seckillId); order.setStatus((byte) 0); order.setCreateTime(new Date()); order.setUpdateTime(new Date()); int count = orderDao.insertOrder(order); if (count <= 0) { return CommonResult.failed(ResultCode.FAILURE, "訂單建立失敗,請稍後再試"); } // 減少商品庫存 boolean success = goodsDao.reduceStockCount(goodsId) > 0; if (!success) { throw new Exception("減少商品庫存失敗,請稍後再試"); } return CommonResult.success("秒殺成功"); } catch (Exception e) { e.printStackTrace(); return CommonResult.failed(ResultCode.FAILURE, "秒殺失敗," + e.getMessage()); } finally { // 釋放分散式鎖 redisService.unlock(lockKey, lockValue); } } }