• 黑马点评项目Redis实现分布式锁


    上文我们提到了基于Redis的秒杀券抢购业务的实现,在单体系统下式完全能够满足的,但是随着业务的扩展,需要进行集群部署,因此仍然会出现一人多单现象。

    Redis解决优惠券秒杀_兜兜转转m的博客-CSDN博客

    本文基于Redis实现分布式锁,解决在集群部署下出现的一人多单现象。

    【分析】

    为什么在集群部署下会出现一人多单问题呢?

    因为在集群部署下,每一个项目都有自己的JVM,那么就都有字节锁监视器,因此在访问时仍然会出现一人多单,解决方法,我们设置全局唯一的锁监视器,那么任何项目都要访问这个全局唯一的锁监视器,因此就可以解决一人多单问题。

    分布式锁

    基本原理和实现方式对比

    分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。

    分布式锁的核心思想就是让大家都使用同一把锁,只要大家使用的是同一把锁,那么我们就能锁住线程,不让线程进行,让程序串行执行,这就是分布式锁的核心思路

     

    Redis分布式锁的实现核心思路

    实现分布式锁时需要实现的两个基本方法:

    • 获取锁:

      • 互斥:确保只能有一个线程获取锁

      • 非阻塞:尝试一次,成功返回true,失败返回false

    • 释放锁:

      • 手动释放

      • 超时释放:获取锁时添加一个超时时间

    核心思路:

    我们利用redis 的setNx 方法,当有多个线程进入时,我们就利用该方法,第一个线程进入时,redis 中就有这个key 了,返回了1,如果结果是1,则表示他抢到了锁,那么他去执行业务,然后再删除锁,退出锁逻辑,没有抢到锁的哥们,等待一定时间后重试即可(或者立即退出)

     

    实现分布式锁版本一

    定义接口

     

    SimpleRedisLock

    利用setnx方法进行加锁,同时增加过期时间,防止死锁,此方法可以保证加锁和增加过期时间具有原子性

    1. private static final String KEY_PREFIX="lock:"
    2. @Override
    3. public boolean tryLock(long timeoutSec) {
    4. // 获取线程标示
    5. String threadId = Thread.currentThread().getId()
    6. // 获取锁
    7. Boolean success = stringRedisTemplate.opsForValue()
    8. .setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
    9. return Boolean.TRUE.equals(success);
    10. }
    • 释放锁逻辑

    SimpleRedisLock

    释放锁,防止删除别人的锁

    1. public void unlock() {
    2. //通过del删除锁
    3. stringRedisTemplate.delete(KEY_PREFIX + name);
    4. }
    1. @Override
    2. public Result seckillVoucher(Long voucherId) {
    3. // 1.查询优惠券
    4. SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
    5. // 2.判断秒杀是否开始
    6. if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
    7. // 尚未开始
    8. return Result.fail("秒杀尚未开始!");
    9. }
    10. // 3.判断秒杀是否已经结束
    11. if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
    12. // 尚未开始
    13. return Result.fail("秒杀已经结束!");
    14. }
    15. // 4.判断库存是否充足
    16. if (voucher.getStock() < 1) {
    17. // 库存不足
    18. return Result.fail("库存不足!");
    19. }
    20. Long userId = UserHolder.getUser().getId();
    21. //创建锁对象(新增代码)
    22. SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
    23. //获取锁对象
    24. boolean isLock = lock.tryLock(1200);
    25. //加锁失败
    26. if (!isLock) {
    27. return Result.fail("不允许重复下单");
    28. }
    29. try {
    30. //获取代理对象(事务)
    31. IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
    32. return proxy.createVoucherOrder(voucherId);
    33. } finally {
    34. //释放锁
    35. lock.unlock();
    36. }
    37. }

    【分析代码】可能会出现的问题-锁误删现象,当线程1获得锁对象,此时业务被阻塞了,锁超时释放了,然后线程2看到锁释放了,因此就获得了新锁,然后线程1又完成任务,直接把锁释放了,此时其他线程就又获得了锁,可能会出现多线程安全问题。

    解决方法:判断锁是否是自己的,如果是自己的再释放。 

    【业务逻辑】

    【代码实现】

    1. private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
    2. @Override
    3. public boolean tryLock(long timeoutSec) {
    4. // 获取线程标示
    5. String threadId = ID_PREFIX + Thread.currentThread().getId();
    6. // 获取锁
    7. Boolean success = stringRedisTemplate.opsForValue()
    8. .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
    9. return Boolean.TRUE.equals(success);
    10. }
    11. public void unlock() {
    12. // 获取线程标示
    13. String threadId = ID_PREFIX + Thread.currentThread().getId();
    14. // 获取锁中的标示
    15. String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
    16. // 判断标示是否一致
    17. if(threadId.equals(id)) {
    18. // 释放锁
    19. stringRedisTemplate.delete(KEY_PREFIX + name);
    20. }
    21. }

     【分析代码】出现问题,判断锁和释放锁不是原子性的,因此在极端情况下仍然会出现多线程问题

    解决方法:采用lua脚本进行改写。

    接下来我们来回一下我们释放锁的逻辑:

    释放锁的业务流程是这样的

    • 获取锁中的线程标示
    • 判断是否与指定的标示(当前线程标示)一致
    • 如果一致则释放锁(删除)
    • 如果不一致则什么都不做

    如果用Lua脚本来表示则是这样的:

    最终我们操作redis的拿锁比锁删锁的lua脚本就会变成这样

    1. -- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
    2. -- 获取锁中的标示,判断是否与当前线程标示一致
    3. if (redis.call('GET', KEYS[1]) == ARGV[1]) then
    4. -- 一致,则删除锁
    5. return redis.call('DEL', KEYS[1])
    6. end
    7. -- 不一致,则直接返回
    8. return 0

    最终加锁和释放锁的逻辑如下:

    1. private static final DefaultRedisScript UNLOCK_SCRIPT;
    2. static {
    3. UNLOCK_SCRIPT = new DefaultRedisScript<>();
    4. UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
    5. UNLOCK_SCRIPT.setResultType(Long.class);
    6. }
    7. public void unlock() {
    8. // 调用lua脚本
    9. stringRedisTemplate.execute(
    10. UNLOCK_SCRIPT,
    11. Collections.singletonList(KEY_PREFIX + name),
    12. ID_PREFIX + Thread.currentThread().getId());
    13. }

  • 相关阅读:
    java网上拍卖系统计算机毕业设计MyBatis+系统+LW文档+源码+调试部署
    RocketMQ并行消费浅析
    谷歌研究科学家:ChatGPT秘密武器的演进与局限
    基于eXosip2实现的客户端和服务端
    【chatQA】nvm包版本管理
    DRF: 序列化器、View、APIView、GenericAPIView、Mixin、ViewSet、ModelViewSet的源码解析
    1.安装vue3+typeScript+antd环境
    得知女儿被猥亵,35岁男子将对方打至轻伤二级,法院作出不起诉决定
    你已经是个成熟的 985 大学了,请不要在大一教 C 语言!
    JAVA面试技巧之项目介绍
  • 原文地址:https://blog.csdn.net/abc123mma/article/details/127597155