• 黑马点评--分布式锁


    黑马点评–分布式锁

    基本原理与不同实现方式对比:

    什么是分布式锁:

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

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C5QtJqnw-1668929831830)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221116131531634.png)]

    分布式锁的核心是实现多进程之间互斥,而满足这一点的方式有很多,常见的有三种:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vHYKZwwQ-1668929831832)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221116132436494.png)]

    基于Redis的分布式锁

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

    • 获取锁:

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

      • set lock thread1 nx ex 10
        
        • 1
    • 释放锁:

      • 手动释放

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

      • Del key
        
        • 1

    流程:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ihnxfa0x-1668929831832)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221116134016213.png)]

    基于Redis实现分布式锁初级版本:

    需求:定义一个类,实现下面接口,利用Redis实现分布式锁功能

    public interface ILock {
    
        /**
         * 尝试获取锁
         * @param timeoutSec 锁持有的超时时间,过期后自动释放
         * @return true代表获取锁成功;false代表获取锁失败
         */
        boolean tryLock(long timeoutSec);
    
    
        /**
         * 释放锁
         */
        void unlock();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    实现ILock接口:

    public class SimpleRedisLock implements ILock{
    
        private StringRedisTemplate stringRedisTemplate;
    
        //锁的名称
        private String name;
    
        public SimpleRedisLock(String name,StringRedisTemplate stringRedisTemplate) {
            this.stringRedisTemplate = stringRedisTemplate;
            this.name = name;
        }
    
        //锁的前缀
        private static final String KEY_PREFIX ="lock:";
    
    
        @Override
        public boolean tryLock(long timeoutSec) {
            //获取线程表示
            long threadId = Thread.currentThread().getId();
            //获取锁
            Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX+name,threadId+"", timeoutSec, TimeUnit.SECONDS);
            return Boolean.TRUE.equals(success);
        }
    
        @Override
        public void unlock() {
                //释放锁
                stringRedisTemplate.delete(KEY_PREFIX+name);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    对秒杀劵一人一单进行分布式锁实现:

      @Autowired
        private ISeckillVoucherService iSeckillVoucherService;
    
        @Autowired
        private RedisIdWorker redisIdWorker;
    
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
        @Override
        public Result seckillVoucher(Long voucherId) {
            //1.查询优惠劵
            SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
            //2.判断秒杀是否开始
            LocalDateTime beginTime = voucher.getBeginTime();
            if (beginTime.isAfter(LocalDateTime.now())) {
                //尚未开始
                return Result.fail("活动尚未开始");
            }
            //3.判断秒杀是否已经结束
            LocalDateTime endTime = voucher.getEndTime();
            if (LocalDateTime.now().isAfter(endTime)) {
                //已结束
                return Result.fail("活动已经结束");
            }
            //4判断库存是否充足
            if (voucher.getStock() < 1) {
                //库存不足
                return Result.fail("库存不足!");
            }
            Long userId = UserHolder.getUser().getId();
    //        synchronized (userId.toString().intern()){
            //创建锁对象
            SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
            //获取锁
            boolean tryLock = lock.tryLock(1200);
            //判断获取锁成功
            if (!tryLock){
                //获取锁失败,返回错误或重试
                return Result.fail("一个人允许下一单");
            }
            try {
                //获取spring事务代理对象
                IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
                return proxy.createVoucherOrder(voucherId);
            } catch (IllegalStateException e) {
                e.printStackTrace();
            }finally {
                //释放锁
                lock.unlock();
            }
    //    }
            return Result.fail("抢购失败");
        }
    
        @Transactional
        public  Result createVoucherOrder(Long voucherId) {
                //6.一个人一单
                Long userId = UserHolder.getUser().getId();
                //6.1查询订单
                int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
                //6.2判断是否存在
                if (count > 0) {
                    //用户以及购买过
                    return Result.fail("用户已经购买过一次");
                }
                //7.扣减库存
                boolean success = iSeckillVoucherService.update()
                        .setSql("stock =stock -1")
                        .eq("voucher_id", voucherId)
                        .gt("stock", 0).update();
                if (!success) {
                    //扣减失败
                    return Result.fail("库存不足!");
                }
                //8.创建订单
                VoucherOrder voucherOrder = new VoucherOrder();
                //8.1 订单id
                long orderId = redisIdWorker.nextId("order");
                voucherOrder.setId(orderId);
                //8.2 用户id
                voucherOrder.setUserId(userId);
                //8.3 代金券id
                voucherOrder.setVoucherId(voucherId);
                save(voucherOrder);
                // 9.返回订单id
                return Result.ok(orderId);
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87

    解决Redis分布式锁误删问题:

    需求:修改之前的分布式锁实现,满足:

    1. 在获取锁时存入线程标示(可以用UUID表示)
    2. 在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致
      • 如果一致则释放锁
      • 如果不一致则不释放锁
    public class SimpleRedisLock implements ILock{
    
        private StringRedisTemplate stringRedisTemplate;
    
        //锁的名称
        private String name;
    
        public SimpleRedisLock(String name,StringRedisTemplate stringRedisTemplate) {
            this.stringRedisTemplate = stringRedisTemplate;
            this.name = name;
        }
    
        //锁的前缀
        private static final String KEY_PREFIX ="lock:";
        private static final String ID_PREFIX = UUID.randomUUID().toString(true)+"-";
    
    
        @Override
        public boolean tryLock(long timeoutSec) {
            //获取线程表示
            String threadId =ID_PREFIX+Thread.currentThread().getId();
            //获取锁
            Boolean success = stringRedisTemplate.opsForValue()
                    .setIfAbsent(KEY_PREFIX+name,threadId+"", timeoutSec, TimeUnit.SECONDS);
            return Boolean.TRUE.equals(success);
        }
    
        @Override
        public void unlock() {
            //获取线程标示
            String threadId =ID_PREFIX+Thread.currentThread().getId();
            // 获取锁中的标示
            String id =stringRedisTemplate.opsForValue().get(KEY_PREFIX+name);
            //判断标示是否一致
            if (threadId.equals(id)){
                //释放锁
                stringRedisTemplate.delete(KEY_PREFIX+name);
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    分布式锁的原子性问题:

    当获取锁标示并判断是一致时,jvm执行gc时改线程发生阻塞,导致没有及时释放锁。如果在阻塞阶段锁超时释放,就会导致其他线程获得到锁。这时如果改线程阻塞结束,去释放锁就会导致误释放其他线程的锁。引发线程安全问题。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s5tsvF7P-1668929831833)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221117175732336.png)]

    解决方法:使判断锁和释放锁为原子性(同成功,同时失败)

    Redis的Lua脚本

    Redis提供了Lua脚本功能,在一个脚本中编写多余Redis命令,确保多条命令执行时的原子性。

    Redis提供的调用函数,语法如下:

    redis.call('命令','key','其它参数'...)
    
    • 1

    例如,执行set name jack 脚本为:

    redis.cll('set','name','jack')
    
    • 1

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N0l9SJuP-1668929831834)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221117233539943.png)]

    再次改进Redis的分布式锁:

    需求:基于Lua脚本实现分布式锁的释放锁逻辑

    提示:RedisTemplate调用Lua脚本的API如下:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DjreDlCR-1668929831834)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221118163922608.png)]

    释放锁的逻辑改变

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

    lua脚本:

    -- 比较线程标示与锁中的标示是否一致
    if (redis.call('get', KEYS[1]) == ARGV[1]) then
        --释放锁 del key
        return redis.call('del',KEYS[1])
    end
    return 0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    总结基于Redis的分布式锁实现思路:

    • 利用set nx ex获取锁,并设置过期时间,保存线程标示
    • 释放锁时先判断线程标示是否与自己一致,一致则删除锁

    特性:

    • 利用set nx满足互斥性
    • 利用set ex保证故障时锁依然能释放。避免死锁,提高安全性
    • 利用Redis集群保证高可用和高并发特性

    基于Redis的分布式锁优化:

    基于setnx实现的分布式锁存在下面的问题:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A72BP6zL-1668929831835)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221118174134161.png)]

    Redisson:

    Redisson是一个在Redis的基础上实现的Java驻内存数据网格。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vzZTyMPi-1668929831835)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221118174520032.png)]

    Redisson快速入门:

    1.引入依赖:

            <dependency>
                <groupId>org.redissongroupId>
                <artifactId>redissonartifactId>
                <version>3.13.6version>
            dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2.配置Redisson客户端:

    @Configuration
    public class RedisConfig {
        @Bean
        public RedissonClient redissonClient(){
            //配置类
            Config config=new Config();
            //添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址
            config.useSingleServer().setAddress("redis://43.138.50.132:6379").setPassword("123321");
            //创建客户端
            return Redisson.create(config);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    3.使用Redisson的分布式锁

     @Resource
        private RedissonClient redissonClient;
        @Test
        void testRedisson() throws InterruptedException{
            //获取锁(可重入),指定锁的名称
            RLock lock = redissonClient.getLock("anyLock");
            //尝试获取锁,参数分别:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
            boolean isLock =lock.tryLock(1,10, TimeUnit.SECONDS);
            //判断释放获取成功
            if (isLock){
                try {
                    System.out.println("执行业务");
                }finally {
                    //释放锁
                    lock.unlock();
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    Redisson可重入锁原理:

    锁的存储使用hash结构

    获取锁:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OwMMZ49P-1668929831836)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221118230056580.png)]

    释放锁:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zfevVvyF-1668929831837)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221118230253311.png)]

    基于setnx实现的分布式锁存在下面的问题:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yG4MSpoo-1668929831838)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221120143212767.png)]

    Redisson分布式锁原理:

    • 可重入:利用hash结构记录线程id和重入次数
    • 可重试:利用信号量和PubSub功能实现等待,唤醒,获取锁失败的重试机制
    • 超时续约:利用watchDog,每隔一段时间(releaseTime/3),重置超时时间

    Redisson的multiLock解决:

    分布式锁主从一致性问题----没听懂。。。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u0oAlqch-1668929831839)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221120152726282.png)]

    总结:

    不可重入Redis分布式锁:

    • 原理:利用setnx的互斥性;利用ex避免死锁;释放锁时判断线程标示
    • 缺陷:不可重入,无法重试,锁超时失效

    可重入的Redis分布式锁:

    • 原理:利用hash结构,记录线程标示和重入次数;利用watchDog延续锁时间;利用信号量控制重试等待
    • 缺陷:redis宕机引起锁失效问题

    Redisson的multiLock:

    • 原理:多个独立的Redis节点,必须在所有节点都获取重入锁,才算获取锁成功
    • 缺陷:运维成本高,实现复杂
  • 相关阅读:
    [VTK] vtkWindowedSincPolyDataFilter 源码注释解读
    Java手写插入排序和算法案例拓展
    基于PHP的校园二手信息网站的设计与实现毕业设计源码251656
    工具:Linux 应急检测脚本
    esp32-S3 + visual studio code 开发环境搭建
    C51--PWN-舵机控制
    第十七届智能车竞赛 - 磁力计角度数据处理
    p6spy代理MyBatis控制台打印完整SQL执行语句
    SpringBoot + Servlet + Mybatis+ layui 学生选课管理系统
    docker安装mysql8
  • 原文地址:https://blog.csdn.net/weixin_53050118/article/details/127949759