3 应对秒杀

Redis05 应对秒杀 - 图1

3.1 全局唯一ID

Redis05 应对秒杀 - 图2

Redis05 应对秒杀 - 图3

Redis05 应对秒杀 - 图4

  1. public class RedisIdWorker {
  2. private static final long START_TIME = LocalDateTime.of(2022, 1, 1, 0, 0, 0).toEpochSecond(ZoneOffset.UTC);
  3. private static final int COUNT_BITS = 32;
  4. @Autowired
  5. private StringRedisTemplate stringRedisTemplate;
  6. public long nextId(String keyPrefix){
  7. // 时间戳
  8. long now = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);
  9. now = now - START_TIME;
  10. // 生成序列号
  11. String key = "icr:" + keyPrefix + ":" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
  12. long count = stringRedisTemplate.opsForValue().increment(key);
  13. // 拼接并返回
  14. return now << COUNT_BITS | count;
  15. }
  16. public static void main(String[] args) {
  17. int i = 0 << 32 | 100;
  18. System.out.println(i);
  19. }
  20. }

Redis05 应对秒杀 - 图5

3.2 秒杀 超卖

Redis05 应对秒杀 - 图6

Redis05 应对秒杀 - 图7Redis05 应对秒杀 - 图8

  1. seckillVoucher.setStock((seckillVoucher.getStock() - 1));
  2. boolean update = seckillVoucherService.update()
  3. .setSql("stock = stock -1")
  4. .eq("voucher_id",voucherId)
  5. .gt("stock",0)
  6. .update()
  7. ;
  8. if (!update) {
  9. return Result.fail("优惠卷已售空");
  10. }
  1. @Transactional
  2. public Result createUserOrderVoucher(Long voucherId, SeckillVoucher seckillVoucher, LocalDateTime now) {
  3. Long id = UserHolder.getUser().getId();
  4. // 对id一样的字符串常量池加锁 不影响 this 这把钥匙的使用权
  5. // 因为使用的是 防止同一个人下单两次
  6. synchronized (id.toString().intern()) {
  7. int count = query().eq("user_id", id).eq("voucher_id", voucherId).count();
  8. if (count > 0) {
  9. return Result.fail("您只能下一单");
  10. }
  11. seckillVoucher.setStock((seckillVoucher.getStock() - 1));
  12. boolean update = seckillVoucherService.update()
  13. .setSql("stock = stock -1")
  14. .eq("voucher_id", voucherId)
  15. .gt("stock", 0)
  16. .update();
  17. if (!update) {
  18. return Result.fail("优惠卷已售空");
  19. }
  20. VoucherOrder voucherOrder = new VoucherOrder();
  21. voucherOrder.setId(idWorker.nextId("order"));
  22. voucherOrder.setUserId(id);
  23. voucherOrder.setVoucherId(voucherId);
  24. voucherOrder.setStatus(1);
  25. voucherOrder.setUpdateTime(now);
  26. voucherOrder.setCreateTime(now);
  27. voucherOrder.setPayType(1);
  28. save(voucherOrder);
  29. return Result.ok("下单成功! 请尽快支付!");
  30. }
  31. }

微服务情况

Redis05 应对秒杀 - 图9

3.3 分布式锁

redis实现分布式锁相关链接

Redis05 应对秒杀 - 图10

Redis05 应对秒杀 - 图11

Redis05 应对秒杀 - 图12

Redis05 应对秒杀 - 图13

  1. SimpleRedisLock redisLock = new SimpleRedisLock("order:"+id, stringRedisTemplate); // 自定义的锁对象实现
  2. boolean isLock = redisLock.tryLock(10, TimeUnit.SECONDS);
  3. if (!isLock) {
  4. // TODO 注意 这里的锁对象是不同 进程 同一个 用户 的id 不同用户不会产生争抢锁现象 只会判断 where stock > 0
  5. return Result.fail("不允许重复下单");
  6. }
  7. try {
  8. int count = query().eq("user_id", id).eq("voucher_id", voucherId).count();
  9. if (count > 0) {
  10. return Result.fail("您只能下一单");
  11. }
  12. // ......
  13. return Result.ok("下单成功! 请尽快支付!");
  14. //}
  15. } finally {
  16. redisLock.unlock();
  17. }

有时效性key的失效导致key误删问题

Redis05 应对秒杀 - 图14

  1. ## 图解:
  2. - 线程1 获取到锁 但是由于业务执行时间过长导致锁Key失效 直接会使抢夺锁的线程2 获取到锁并且生成key
  3. * 于此同时线程1执行业务完成 直接删除线程2 对应的key 导致线程3 在线程2 执行业务时同时获取到了key
  4. **导致了一系列数据不一致问题**

Redis05 应对秒杀 - 图15

根据存入的value(Thread.id) 判断释放的线程id是否一致

  1. public class SimpleRedisLock implements ILock {
  2. private final static String LOCK_PREFIX = "lock:";
  3. // 每个服务的jvm初始化放在字符串常量池的 中的UUID 作为唯一标识前缀
  4. private static final String ID_PREFIX = UUID.randomUUID().toString(true);
  5. private final String name;
  6. private final StringRedisTemplate stringRedisTemplate;
  7. public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
  8. this.name = name;
  9. this.stringRedisTemplate = stringRedisTemplate;
  10. }
  11. @Override
  12. public boolean tryLock(long timeoutSec,TimeUnit unit) {
  13. String threadId = ID_PREFIX + Thread.currentThread();
  14. Boolean ownLock = stringRedisTemplate.opsForValue().setIfAbsent(LOCK_PREFIX + name, threadId,timeoutSec,unit);
  15. return Boolean.TRUE.equals(ownLock);
  16. }
  17. @Override
  18. public void unlock() {
  19. String threadId = ID_PREFIX + Thread.currentThread();
  20. String id = stringRedisTemplate.opsForValue().get(LOCK_PREFIX + name);
  21. // 判断将要执行删除锁的服务线程是否为加锁线程 解决锁误删问题
  22. if (! threadId.equals(id)) {
  23. throw new BusinessLock("释放锁异常");
  24. }
  25. stringRedisTemplate.delete(LOCK_PREFIX + name);
  26. }
  27. class BusinessLock extends RuntimeException {
  28. private String msg;
  29. public BusinessLock(String msg) {
  30. super(msg);
  31. }
  32. public BusinessLock(Throwable cause, String msg) {
  33. super(msg,cause);
  34. }
  35. }
  36. }

阻塞释放问题

Redis05 应对秒杀 - 图16

  1. # 这里是因为线程1 `释放阻塞` 由于前面对比的版本/id是 value 线程1释放是已经判断过的 不会对比value直接删除 `key` 而
  2. # 此时 `key` 中存放的是线程2 的锁value 导致线程3乘虚而入
  3. # 这里 key 一致是要保证分布式多个服务之间互斥 value是防止误删

所以要保证获取锁和释放锁是**原子性操作**

  1. if (! threadId.equals(id)) {
  2. throw new BusinessLock("释放锁异常");
  3. }
  4. stringRedisTemplate.delete(LOCK_PREFIX + name);
  5. // 这一段执行 `equals` `delete` 是组合操作 很难保证原子性

lua 保证原子性

Redis05 应对秒杀 - 图17

Redis05 应对秒杀 - 图18

  1. 127.0.0.1:6379> EVAL "return redis.call('mset',KEYS[1],ARGV[1],KEYS[2],ARGV[2])" 2 k1 k2 v1 v2
  2. 127.0.0.1:6379> EVAL "return redis.call('hset',KEYS[1],AVGS[1],AVGS[2])" h1 name zs
  1. local id = redis.call('GET',KEYS[1])
  2. if(id == ARGV[1]) then
  3. return redis.call('DEL',KEYS[1])
  4. end
  5. return 0

Redis05 应对秒杀 - 图19

Redis05 应对秒杀 - 图20

Redis05 应对秒杀 - 图21

  1. public class SimpleRedisLock implements ILock {
  2. private final static String LOCK_PREFIX = "lock:";
  3. // 每个服务的jvm初始化放在字符串常量池的 中的UUID 作为唯一标识前缀
  4. private static final String ID_PREFIX = UUID.randomUUID().toString(true);
  5. private final String name;
  6. private final StringRedisTemplate stringRedisTemplate;
  7. private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
  8. public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
  9. this.name = name;
  10. this.stringRedisTemplate = stringRedisTemplate;
  11. }
  12. static {
  13. // 加载脚本
  14. UNLOCK_SCRIPT = new DefaultRedisScript<>();
  15. UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
  16. UNLOCK_SCRIPT.setResultType(Long.class);
  17. }
  18. @Override
  19. public boolean tryLock(long timeoutSec, TimeUnit unit) {
  20. String threadId = ID_PREFIX + Thread.currentThread();
  21. Boolean ownLock = stringRedisTemplate.opsForValue().setIfAbsent(LOCK_PREFIX + name, threadId, timeoutSec, unit);
  22. return Boolean.TRUE.equals(ownLock);
  23. }
  24. @Override
  25. public void unlock() {
  26. Long result = stringRedisTemplate
  27. .execute(
  28. UNLOCK_SCRIPT,
  29. Collections.singletonList(LOCK_PREFIX + name),
  30. ID_PREFIX + Thread.currentThread().getId());
  31. // todo 缩减为一行代码 优化误删问题 但是归根还是超时问题
  32. if (result == 0) {
  33. throw new BusinessLock("释放锁异常");
  34. }
  35. }
  36. /*@Override
  37. public void unlock() {
  38. String threadId = ID_PREFIX + Thread.currentThread();
  39. String id = stringRedisTemplate.opsForValue().get(LOCK_PREFIX + name);
  40. // 判断将要执行删除锁的服务线程是否为加锁线程 解决锁误删问题
  41. if (! threadId.equals(id)) {
  42. throw new BusinessLock("释放锁异常");
  43. }
  44. stringRedisTemplate.delete(LOCK_PREFIX + name);
  45. }*/
  46. class BusinessLock extends RuntimeException {
  47. private String msg;
  48. public BusinessLock(String msg) {
  49. super(msg);
  50. }
  51. public BusinessLock(Throwable cause, String msg) {
  52. super(msg, cause);
  53. }
  54. }
  55. }

相关业务层

  1. @Transactional
  2. public Result createUserOrderVoucher(Long voucherId, SeckillVoucher seckillVoucher, LocalDateTime now) {
  3. Long id = UserHolder.getUser().getId();
  4. // 对id一样的字符串常量池加锁 不影响 this 这把钥匙的使用权
  5. // 因为使用的是 防止同一个人下单两次
  6. // synchronized (id.toString().intern()) {
  7. // 只锁该对象的下单重复
  8. // 多用户的秒杀有 where stock > 0 的原子锁保证
  9. SimpleRedisLock redisLock = new SimpleRedisLock("order:"+id, stringRedisTemplate);
  10. boolean isLock = redisLock.tryLock(10, TimeUnit.SECONDS);
  11. if (!isLock) {
  12. // TODO 注意 这里的锁对象是不同 进程 同一个 用户 的id 不同用户不会产生争抢锁现象 只会判断 where stock > 0
  13. return Result.fail("不允许重复下单");
  14. }
  15. try {
  16. int count = query().eq("user_id", id).eq("voucher_id", voucherId).count();
  17. if (count > 0) {
  18. return Result.fail("您只能下一单");
  19. }
  20. seckillVoucher.setStock((seckillVoucher.getStock() - 1));
  21. boolean update = seckillVoucherService.update()
  22. .setSql("stock = stock -1")
  23. .eq("voucher_id", voucherId)
  24. .gt("stock", 0)
  25. .update();
  26. if (!update) {
  27. return Result.fail("优惠卷已售空");
  28. }
  29. VoucherOrder voucherOrder = new VoucherOrder();
  30. voucherOrder.setId(idWorker.nextId("order"));
  31. voucherOrder.setUserId(id);
  32. voucherOrder.setVoucherId(voucherId);
  33. voucherOrder.setStatus(1);
  34. voucherOrder.setUpdateTime(now);
  35. voucherOrder.setCreateTime(now);
  36. voucherOrder.setPayType(1);
  37. save(voucherOrder);
  38. return Result.ok("下单成功! 请尽快支付!");
  39. //}
  40. } finally {
  41. redisLock.unlock();
  42. }
  43. }