java——redis随笔——实战——优惠券秒杀
黑马视频地址:https://www.bilibili.com/video/BV1cr4y1671t?p=49&spm_id_from=pageDriver&vd_source=79bbd5b76bfd74c2ef1501653cee29d6
参考博客代码:https://cyborg2077.github.io/2022/10/22/RedisPractice/#%E4%BC%98%E6%83%A0%E5%88%B8%E7%A7%92%E6%9D%80
csdn地址:https://blog.csdn.net/weixin_50523986/article/details/131815165
- stringRedisTemplate.opsForValue().increment函数:
package com.hmdp.utils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; @Component public class RedisIdWorker { // 定义一个初始时间戳 public static final long BEGIN_TIME = 1640995200L; // 序列号位数 public static final long COUNT_BITS = 32; // 用到redis的自增长 @Autowired private StringRedisTemplate stringRedisTemplate; public long nextId(String keyPrefix){ // 1 生成时间戳 LocalDateTime now = LocalDateTime.now(); long nowSecond = now.toEpochSecond(ZoneOffset.UTC); long timeStamp = nowSecond - BEGIN_TIME; // 2 生成序列号 // 确定当天序列号的key 获取当天日期 精确到天 String data = now.format(DateTimeFormatter.ofPattern("yyyy:MM:DD")); // 实现自增长 long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + data); // 3 拼接并返回 借助位运算 // 移位之后后面32位全都是0 或运算可以保证原来的样子 return timeStamp << COUNT_BITS | count; } public static void main(String[] args) { // LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0); // long timeToSecond = time.toEpochSecond(ZoneOffset.UTC); // System.out.println(timeToSecond); } }
- com/hmdp/service/impl/VoucherOrderServiceImpl.java
package com.hmdp.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.hmdp.dto.Result; import com.hmdp.entity.SeckillVoucher; import com.hmdp.entity.Voucher; import com.hmdp.entity.VoucherOrder; import com.hmdp.mapper.VoucherOrderMapper; import com.hmdp.service.ISeckillVoucherService; import com.hmdp.service.IVoucherOrderService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.hmdp.utils.RedisIdWorker; import com.hmdp.utils.UserHolder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; /** * <p> * 服务实现类 * </p> * * @author 虎哥 * @since 2021-12-22 */ @Service public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService { @Autowired private ISeckillVoucherService seckillVoucherService; @Autowired private StringRedisTemplate stringRedisTemplate; @Autowired private RedisIdWorker redisIdWorker; /** * 优惠券秒杀功能 * @param voucherId * @return */ @Override @Transactional public Result seckillVoucher(Long voucherId) { // 1 根据id查询优惠券信息 LambdaQueryWrapper<SeckillVoucher> lqw = new LambdaQueryWrapper<>(); lqw.eq(SeckillVoucher::getVoucherId,voucherId); SeckillVoucher seckillVoucher = seckillVoucherService.getOne(lqw); // 2 判断秒杀是否开始 if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())){ return Result.fail("秒杀尚未开始"); } // 3 判断秒杀是否结束 if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())){ return Result.fail("秒杀已经结束"); } // 4 判断库存是否充足 if (seckillVoucher.getStock()<1){ return Result.fail("秒杀券已经不足"); } // 5 扣减库存 seckillVoucher.setStock(seckillVoucher.getStock()-1); boolean success = seckillVoucherService.update(seckillVoucher, null); if (!success){ return Result.fail("秒杀券已经不足"); } // 6 创建订单 写入数据库 VoucherOrder voucherOrder = new VoucherOrder(); // 生成全局唯一Id long orderId = redisIdWorker.nextId("order"); voucherOrder.setId(orderId); voucherOrder.setUserId(UserHolder.getUser().getId()); voucherOrder.setVoucherId(voucherId); baseMapper.insert(voucherOrder); // 7 返回订单id return Result.ok(orderId); } }
@Override public Result seckillVoucher(Long voucherId) { LambdaQueryWrapper<SeckillVoucher> queryWrapper = new LambdaQueryWrapper<>(); //1. 查询优惠券 queryWrapper.eq(SeckillVoucher::getVoucherId, voucherId); SeckillVoucher seckillVoucher = seckillVoucherService.getOne(queryWrapper); //2. 判断秒杀时间是否开始 if (LocalDateTime.now().isBefore(seckillVoucher.getBeginTime())) { return Result.fail("秒杀还未开始,请耐心等待"); } //3. 判断秒杀时间是否结束 if (LocalDateTime.now().isAfter(seckillVoucher.getEndTime())) { return Result.fail("秒杀已经结束!"); } //4. 判断库存是否充足 if (seckillVoucher.getStock() < 1) { return Result.fail("优惠券已被抢光了哦,下次记得手速快点"); } + // 一人一单逻辑 + Long userId = UserHolder.getUser().getId(); + int count = query().eq("voucher_id", voucherId).eq("user_id", userId).count(); + if (count > 0){ + 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(); //6.1 设置订单id long orderId = redisIdWorker.nextId("order"); //6.2 设置用户id Long id = UserHolder.getUser().getId(); //6.3 设置代金券id voucherOrder.setVoucherId(voucherId); voucherOrder.setId(orderId); voucherOrder.setUserId(id); //7. 将订单数据保存到表中 save(voucherOrder); //8. 返回订单id return Result.ok(orderId); }
一人一单
- 需求:修改秒杀业务,要求同一个优惠券,一个用户只能抢一张
- 具体操作逻辑如下:我们在判断库存是否充足之后,根据我们保存的订单数据,判断用户订单是否已存在
- 如果已存在,则不能下单,返回错误信息
- 如果不存在,则继续下单,获取优惠券
- 初步代码
DIFF
复制成功
1
|
@Override
|
存在问题
:还是和之前一样,如果这个用户故意开多线程抢优惠券,那么在判断库存充足之后,执行一人一单逻辑之前,在这个区间如果进来了多个线程,还是可以抢多张优惠券的,那我们这里使用悲观锁来解决这个问题- 初步代码,我们把一人一单逻辑之后的代码都提取到一个
createVoucherOrder
方法中,然后给这个方法加锁 - 不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。
JAVA
1
|
private Result createVoucherOrder(Long voucherId) {
|
- 但是这样加锁,锁的细粒度太粗了,在使用锁的过程中,控制锁粒度是一个非常重要的事情,因为如果锁的粒度太大,会导致每个线程进来都会被锁住,现在的情况就是所有用户都公用这一把锁,串行执行,效率很低,我们现在要完成的业务是
一人一单
,所以这个锁,应该只加在单个用户上,用户标识可以用userId
JAVA
1
|
|
- 由于toString的源码是new String,所以如果我们只用
userId.toString()
拿到的也不是同一个用户,需要使用intern()
,如果字符串常量池中已经包含了一个等于这个string对象的字符串(由equals(object)方法确定),那么将返回池中的字符串。否则,将此String对象添加到池中,并返回对此String对象的引用。
JAVA
1
|
public static String toString(long i) {
|
- 但是以上代码还是存在问题,问题的原因在于当前方法被Spring的事务控制,如果你在内部加锁,可能会导致当前方法事务还没有提交,但是锁已经释放了,这样也会导致问题,所以我们选择将当前方法整体包裹起来,确保事务不会出现问题
JAVA
1
|
|
- 但是以上做法依然有问题,因为你调用的方法,其实是this.的方式调用的,事务想要生效,还得利用代理来生效,所以这个地方,我们需要获得原始的事务对象, 来操作事务,这里可以使用
AopContext.currentProxy()
来获取当前对象的代理对象,然后再用代理对象调用方法,记得要去IVoucherOrderService
中创建createVoucherOrder
方法
JAVA
1
|
Long userId = UserHolder.getUser().getId();
|
- 但是该方法会用到一个依赖,我们需要导入一下
XML
1
|
<dependency>
|
- 同时在启动类上加上
@EnableAspectJAutoProxy(exposeProxy = true)
注解
JAVA
1
|
|
- 重启服务器,再次使用Jmeter测试,200个线程并发,但是只能抢到一张优惠券,目的达成