1.使用redisson pom文件添加redisson
<!--redisson--><dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.23.4</version></dependency>
2.mysql數據庫表設計
CREATE TABLE `order` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '訂單ID',`user_id` bigint(20) NOT NULL COMMENT '用戶ID',`product_id` bigint(20) NOT NULL COMMENT '商品ID',`order_no` varchar(50) NOT NULL COMMENT '訂單編號',`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '訂單狀態(0:未支付,1:已支付,2:已取消)',`create_time` datetime NOT NULL COMMENT '創建時間',`pay_time` datetime DEFAULT NULL COMMENT '支付時間',PRIMARY KEY (`id`),UNIQUE KEY `idx_order_no` (`order_no`),KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1929810703233400835 DEFAULT CHARSET=utf8mb4 COMMENT='訂單表';CREATE TABLE `product` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品ID',`name` varchar(100) NOT NULL COMMENT '商品名稱',`price` decimal(10,2) NOT NULL COMMENT '商品價格',`stock` int(11) NOT NULL COMMENT '庫存數量',`start_time` datetime DEFAULT NULL COMMENT '秒殺開始時間',`end_time` datetime DEFAULT NULL COMMENT '秒殺結束時間',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1929810179796844547 DEFAULT CHARSET=utf8mb4 COMMENT='秒殺商品表';CREATE TABLE `seckill_record` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵ID',`user_id` bigint(20) NOT NULL COMMENT '用戶ID',`product_id` bigint(20) NOT NULL COMMENT '商品ID',`order_no` varchar(50) DEFAULT NULL COMMENT '訂單編號',`seckill_time` datetime NOT NULL COMMENT '秒殺時間',`status` tinyint(4) NOT NULL COMMENT '狀態(0:秒殺成功未支付,1:已支付,2:已取消)',PRIMARY KEY (`id`),UNIQUE KEY `idx_user_product` (`user_id`,`product_id`) COMMENT '用戶商品唯一索引',KEY `idx_product_id` (`product_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1929810703233400836 DEFAULT CHARSET=utf8mb4 COMMENT='秒殺記錄表';
3.實現邏輯
1.新增秒殺商品,添加庫存數量,設置秒殺開始時間和結束時間
2.確認開始秒殺,將庫存數量入redis中,避免進行超賣
3.進行秒殺
?3.1.根據商品id查詢商品是否存在
?3.2.判斷當前時間是否在秒殺開始時間
?3.3. 獲取Redisson鎖
?3.4.判斷當前人是否已經秒殺過,避免重復秒殺
?3.5.判斷庫存是否充足,避免超賣,庫存不足,返回商品已售罄
?3.6.判斷庫存充足,扣減庫存并記錄用戶
?3.7.秒殺成功創建訂單,返回訂單號
?3.8.關閉鎖,避免死鎖
4.代碼實現
1.秒殺控制器
@RestController
@RequestMapping("/seckill")
public class SeckillController {@Autowiredprivate SeckillService seckillService;/*** 確認開始秒殺,初始化商品庫存到Redis* @param product* @return*/@PostMapping("/initStock")public AjaxResult initStock(@RequestBody Product product) {return seckillService.initStock(product);}/*** 開始秒殺* @return*/@PostMapping("/start")public AjaxResult startSeckill(Long userId, Long productId) {return seckillService.startSeckill(userId,productId);}}
2.秒殺實現類
package com.thk.service.impl;import com.thk.domain.Order;
import com.thk.domain.Product;
import com.thk.domain.SeckillRecord;
import com.thk.mapper.OrderMapper;
import com.thk.mapper.ProductMapper;
import com.thk.mapper.SeckillRecordMapper;
import com.thk.service.SeckillService;
import com.thk.utils.AjaxResult;
import org.redisson.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;@Service
public class SeckillServiceImpl implements SeckillService {@Autowiredprivate ProductMapper productMapper;@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate SeckillRecordMapper seckillRecordMapper;@Autowiredprivate RedissonClient redissonClient;// 商品庫存key前綴private static final String STOCK_PREFIX = "seckill:stock:";// 秒殺用戶集合key前綴(用于防止重復秒殺)private static final String USER_SET_PREFIX = "seckill:users:";// 分布式鎖key前綴private static final String LOCK_PREFIX = "seckill:lock:";/*** 確認開始秒殺,初始化商品庫存到Redis** @param product* @return*/@Override@Transactional(rollbackFor = Exception.class)public AjaxResult initStock(Product product) {Product product1 = productMapper.selectById(product.getId());if (product1 != null) {String stockKey = STOCK_PREFIX + product1.getId();RAtomicLong atomicLong = redissonClient.getAtomicLong( stockKey );atomicLong.set(product1.getStock());// 設置過期時間(秒)long expireSeconds = (product1.getEndTime().getTime() - System.currentTimeMillis()) / 1000;boolean expireResult = atomicLong.expire(expireSeconds, TimeUnit.SECONDS);return AjaxResult.success(expireResult);}return AjaxResult.error("商品不存在");}/*** 開始秒殺** @return*/@Override@Transactional(rollbackFor = Exception.class)public AjaxResult startSeckill(Long userId, Long productId) {// 1. 驗證秒殺時間Product product = productMapper.selectById(productId);if (product == null) return AjaxResult.error("商品不存在");long now = System.currentTimeMillis();if (now < product.getStartTime().getTime()) {return AjaxResult.error("秒殺未開始");}if (now > product.getEndTime().getTime()) {return AjaxResult.error("秒殺已結束");}// 2. 獲取Redisson分布式鎖String lockKey = LOCK_PREFIX + productId;RLock lock = redissonClient.getLock(lockKey);try {boolean locked = lock.tryLock(3, 10, TimeUnit.SECONDS);if (!locked) return AjaxResult.error("系統繁忙,請稍后再試");// 3. 使用Redisson的RSet檢查重復秒殺String userKey = USER_SET_PREFIX + productId;RSet<String> userSet = redissonClient.getSet(userKey);if (userSet.contains(userId.toString())) {return AjaxResult.error("不能重復秒殺");}// 4. 使用Redisson的RAtomicLong檢查庫存String stockKey = STOCK_PREFIX + productId;RAtomicLong atomicStock = redissonClient.getAtomicLong(stockKey);if (atomicStock.get() <= 0) {return AjaxResult.error("商品已售罄");}// 5. 扣減庫存并記錄用戶atomicStock.decrementAndGet();userSet.add(userId.toString());// 6. 創建訂單String orderNo = generateOrderNo();createOrder(userId, productId, orderNo);return AjaxResult.success(orderNo);} catch (InterruptedException e) {Thread.currentThread().interrupt();return AjaxResult.error("系統異常");} finally {if (lock.isLocked() && lock.isHeldByCurrentThread()) {//關閉鎖lock.unlock();}}}private void createOrder(Long userId, Long productId, String orderNo) {Order order = new Order();order.setUserId( userId );order.setProductId( productId );order.setOrderNo( orderNo );order.setStatus( 0 );order.setCreateTime( new Date() );orderMapper.insert( order );SeckillRecord record = new SeckillRecord();record.setUserId( userId );record.setProductId( productId );record.setOrderNo( orderNo );record.setSeckillTime( new Date() );record.setStatus( 0 );seckillRecordMapper.insert( record );}private String generateOrderNo() {return "O" + System.currentTimeMillis();}
}
5.測試
1.添加秒殺商品,設置庫存,開始時間,結束時間
2.確認開始,將庫存存入redis中
redis查看庫存是否存在
3.開始秒殺,輸入用戶id和商品編號進行秒殺
秒殺成功,創建訂單,新增商品秒殺記錄,返回訂單號,redis庫存減少,redis新增用戶秒殺記錄
訂單記錄
秒殺記錄
redis庫存
redis新增用戶秒殺記錄