提示:学习如何利用Redisson实现秒杀下单一人一单
解决方案:
0.数据库表结构
所有的优惠券存放表
CREATE TABLE `tb_voucher` (
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
`shop_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '商铺id',
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '代金券标题',
`sub_title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '副标题',
`rules` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '使用规则',
`pay_value` bigint UNSIGNED NOT NULL COMMENT '支付金额,单位是分。例如200代表2元',
`actual_value` bigint NOT NULL COMMENT '抵扣金额,单位是分。例如200代表2元',
`type` tinyint UNSIGNED NOT NULL DEFAULT 0 COMMENT '0,普通券;1,秒杀券',
`status` tinyint UNSIGNED NOT NULL DEFAULT 1 COMMENT '1,上架; 2,下架; 3,过期',
`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`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 211 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = COMPACT;
CREATE TABLE `tb_seckill_voucher` (
`voucher_id` bigint UNSIGNED NOT NULL COMMENT '关联的优惠券的id',
`stock` int NOT NULL COMMENT '库存',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`begin_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '生效时间',
`end_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '失效时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`voucher_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '秒杀优惠券表,与优惠券是一对一关系' ROW_FORMAT = COMPACT;
CREATE TABLE `tb_voucher_order` (
`id` bigint NOT NULL COMMENT '主键',
`user_id` bigint UNSIGNED NOT NULL COMMENT '下单的用户id',
`voucher_id` bigint UNSIGNED NOT NULL COMMENT '购买的代金券id',
`pay_type` tinyint UNSIGNED NOT NULL DEFAULT 1 COMMENT '支付方式 1:余额支付;2:支付宝;3:微信',
`status` tinyint UNSIGNED NOT NULL DEFAULT 1 COMMENT '订单状态,1:未支付;2:已支付;3:已核销;4:已取消;5:退款中;6:已退款',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '下单时间',
`pay_time` timestamp NULL DEFAULT NULL COMMENT '支付时间',
`use_time` timestamp NULL DEFAULT NULL COMMENT '核销时间',
`refund_time` timestamp NULL DEFAULT NULL COMMENT '退款时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = COMPACT;
1. 准备pom环境
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.17</version>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.23.1</version>
</dependency>
2. 配置ThreadLocal和过滤器
public class UserHolder {
private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();
public static void saveUser(UserDTO user){
tl.set(user);
}
public static UserDTO getUser(){
return tl.get();
}
public static void removeUser(){
tl.remove();
}
}
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Autowired
private StringRedisTemplate redis;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/user/code","/user/login","/blog/hot","/shop/**","/shop-type/**","/voucher/**").order(2);
registry.addInterceptor(new RefreshTokenInterceptor(redis)).addPathPatterns("/**").order(1);
}
}
---------------------------------------------
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
//controller执行之前
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.判断是否需要拦截ThreadLocal
if (UserHolder.getUser()==null) {
response.setStatus(401);
return false;
}
//7.放行
return true;
}
//渲染后返回给前台数据前
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//移除用户,避免内存泄露
UserHolder.removeUser();
}
}
---------------------------------------------------
@Slf4j
public class RefreshTokenInterceptor implements HandlerInterceptor {
//这个对象不是由spring管理的所以不能用注解自动注入
private StringRedisTemplate redis;
public RefreshTokenInterceptor(StringRedisTemplate redis) {
this.redis = redis;
}
//controller执行之前
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.获取请求头中的token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
return true;
}
//2.基于token获取redis中的用户
//通过key取到hash中的map集合数据
Map<Object, Object> userMap = redis.opsForHash().entries("login:token:" + token);
//3.判断用户是否存在
if (userMap.isEmpty()) {
return true;
}
//5.将查询到的hash数据转为userDto对象
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//6.存在,保存用户信息到ThreadLocal中
UserHolder.saveUser(userDTO);
//7.刷新token有效期
redis.expire(LOGIN_USER_KEY + token, 30, TimeUnit.MINUTES);
log.info("我是第一个拦截器当前拦截所有请求的用户为,线程为{},{}",UserHolder.getUser(),Thread.currentThread());
//8.放行
return true;
}
3. 配置Redisson
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer().setAddress("redis://IP地址:6379").
setPassword("密码");
return Redisson.create(config);
}
}
3. Controller层:负责接收请求和向下分配
@RestController
@RequestMapping("/voucher-order")
public class VoucherOrderController {
@Resource
private IVoucherOrderService voucherOrderService;
@PostMapping("seckill/{id}")
public Result seckillVoucher(@PathVariable("id") Long voucherId) {
return voucherOrderService.seckillVoucher(voucherId);
}
}
4. Service层:负责业务的处理逻辑秒杀下单一人一单
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisWorker redisWorker;
@Autowired
private RedissonClient redisson;
@Override
public Result seckillVoucher(Long voucherId) {
//1.查询优惠券
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
//2.判断秒杀是否开始
LocalDateTime currDate = LocalDateTime.now();
LocalDateTime beginTime = seckillVoucher.getBeginTime();
LocalDateTime endTime = seckillVoucher.getEndTime();
//3.判断秒杀是否结束
//当前时间在秒杀时间前,说明未开始
if (currDate.isBefore(beginTime)) {
return Result.fail("秒杀已开始");
}
//当前时间在秒杀时间后,说明已结束
if (currDate.isAfter(endTime)) {
return Result.fail("秒杀已结束");
}
//4.判断库存是否充足
if (seckillVoucher.getStock() < 1) {
return Result.fail("库存不足");
}
Long userId = UserHolder.getUser().getId();
RLock lock = redisson.getLock("lock:order:" + userId);
//上锁
boolean flag = false;
try {
flag = lock.tryLock(1,10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!flag) {
Result.fail("不允许重复下单");
}
try {
return createVoucherOrder(voucherId);
} finally {
lock.unlock();
}
}
@Transactional
public Result createVoucherOrder(Long voucherId) {
//一人一单 查询订单查看当前用户是否存在
int count = query().eq("user_id", UserHolder.getUser().getId()).eq("voucher_id", voucherId).count();
if (count > 0) {
System.out.println("用户已经购买过");
return Result.fail("用户已经购买过");
}
//5.扣减库存
boolean success = seckillVoucherService.update().setSql("stock = stock -1").eq("voucher_id", voucherId).gt("stock", 0).update();
if (!success) {
return Result.fail("库存不足");
}
//6.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//生成唯一ID,这里可以用UUID替换或者雪花算法替代
long orderId = redisWorker.nextId("order");
voucherOrder.setId(orderId);
voucherOrder.setUserId(UserHolder.getUser().getId());
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
//7.返回订单id
return Result.ok(orderId);
}
}