什么是分布式锁
在单机部署的情况下, 要想保证特定业务在顺序执行, 通过 JDK 提供的 synchronized 关键字, Semaphore,ReentrantLock, 或者我们也可以基于 AQS 定制化锁. 单机部署的情况下, 锁是在多线程之间共享的, 但是分布式部署的情况下, 锁是多进程之间共享的. 那么分布式锁要保证锁资源的唯一性, 可以在多进程之间共享.
分布式锁特性
保证同一个方法在某一时刻只能在一台机器里一个进程中一个线程执行;
要保证是可重入锁(避免死锁);
要保证获取锁和释放锁的高可用;
分布式锁实现
锁释放(finally);
锁超时设置;
锁刷新(定时任务, 每 2/3 的锁生命周期执行);
如果锁超时了, 防止删除其他线程的锁(其他线程会拿到锁), 考虑 value 值用线程 id 标识, 当前线程释放锁的时候要判断是否为当前线程的线程 id;
可重入;
Redis 分布式锁
RedisLockRegistry
RedisLockRegistry 是 spring-integration-Redis 中提供 Redis 分布式锁实现类. 主要是通过 Redis 锁 + 本地锁双重锁的方式实现的一个比较好的锁.
OBTAIN_LOCK_SCRIPT 是一个上锁的 lua 脚本. KEYS[1]代表当前锁的 key 值, ARGV[1]代表当前的客户端标识, ARGV[2]代表过期时间.
基本逻辑是: 根据 KEYS[1]从 Redis 中拿到对应的客户端标识, 如已存在的客户端标识和 ARGV[1]相等, 那么重置过期时间为 ARGV[2]; 如果值不存在, 设置 KEYS[1]对应的值为 ARGV[1], 并且过期时间是 ARGV[2].
获取锁的过程也很简单, 首先通过本地锁 (localLock, 对应的是 ReentrantLock 实例) 获取锁, 然后通过 RedisTemplate 执行 OBTAIN_LOCK_SCRIPT 脚本获取 Redis 锁.
为什么要使用本地锁呢, 首先是为了锁的可重入, 其次是减轻 Redis 服务压力.
释放锁的过程也比较简单, 第一步通过本地锁判断当前线程是否持有锁, 第二步通过本地锁判断当前线程持有锁的计数.
如果当前线程持有锁的计数> 1, 说明本地锁被当前线程多次获取, 这时只释放本地锁(释放之后当前线程持有锁的计数 - 1).
如果当前线程持有锁的计数 = 1, 释放本地锁和 Redis 锁.
RedisLockRegistry 使用如上所示.
首先定义 RedisLockRegistry 对应的 Bean, 需要依赖 Redis 的 ConnectionFactory.
然后在服务层中注入 RedisLockRegistry 实例.
通过 lock 方法和 unlock 方法将业务逻辑包起来, 需要注意的是 unlock 方法要写在 finally 代码块中.
Redisson
Redisson https://redisson.org/ 是架设在 Redis http://redis.cn/ 基础上的一个 Java 驻内存数据网格(In-Memory Data Grid).
充分的利用了 Redis 键值数据库提供的一系列优势, 基于 Java 实用工具包中常用接口, 为使用者提供了一系列具有分布式特性的常用工具类.
使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力, 大大降低了设计和研发大规模分布式系统的难度.
同时结合各富特色的分布式服务, 更进一步简化了分布式环境中程序相互之间的协作.
首先感受一下通过 Redisson API 使用 Redis 分布式锁.
定义 RedissonBuilder, 通过 Redis 集群地址构建 RedissonClient.
定义 RedissonClient 类型的 Bean.
业务代码里, 通过 RedissonClient 获取分布式锁.
由于对 Redisson 分布式锁实现原理了解的也不是很透彻, 这里推荐一篇文章: Redisson 分布式锁实现分析 https://www.jianshu.com/p/de5a69622e49 .
Redisson 和 RedisLockRegistry 对比
RedisLockRegistry 通过本地锁 (ReentrantLock) 和 Redis 锁, 双重锁实现, Redission 通过 Netty Future 机制, Semaphore (jdk 信号量),Redis 锁实现.
RedisLockRegistry 和 Redssion 都是实现的可重入锁.
RedisLockRegistry 对锁的刷新没有处理, Redisson 通过 Netty 的 TimerTask,Timeout 工具完成锁的定期刷新任务.
RedisLockRegistry 仅仅是实现了分布式锁, 而 Redisson 处理分布式锁, 还提供了了队列, 集合, 列表等丰富的 API.
动手实现分布式锁
实现原理
本地锁(ReentrantLock)+ Redis 锁
获取锁 lua 脚本
锁刷新 lua 脚本
锁释放 lua 脚本
本地锁定义
每一个 lock key 对应唯一的一个本地锁
线程标识定义
分布式环境下, 每一个线程对应一个唯一标识
锁刷新定时任务定义
通过 JDK ConcurrentTaskScheduler 完成定时任务执行, ScheduledFuture 完成定时任务销毁. 其中 taskId 对应线程标识.
定义分布式锁注解
分布式锁切面
通过 RedisLock 注解实例 lockInfo 获取到锁 key 值, 锁过期时间信息.
获取锁过程
通过 lockInfo.key()方法获取到锁 key 值, 通过锁 key 值拿到对应的本地锁(ReentrantLock)
本地锁获取锁对象
进入获取 Redis 锁的循环
通过缓存服务组件执行获取锁的 lua 脚本
如果获取到 Redis 锁, 判断当前线程是否第一次获取到锁并且开启了锁刷新, 相应的注册锁刷新定时任务
如果没有获取到 Redis 锁, 休眠 lockInfo.sleep()毫秒的时间, 再次重试
释放锁过程
获取到当前锁 key 值对应的本地锁
判断当前线程是否为本地锁锁的持有者
如果本地锁的重入次数大于 1, 则只释放本地锁
如果本地锁的重入次数等于 1, 释放本地锁和 Redis 锁
分布式锁测试
定义测试类, 测试方法注上 @RedisLock 注解, 制定锁的 key 值为 "redis-lock-test", 测试方法内随机休眠.
开启 20 个线程, 同时调用测试方法.
多线程 Redis 分布式锁测试结果如下.
定义可重入测试类, 方法内获取当前代理对象, 递归调用测试方法.
测试方法中, 调用可重入测试类注有 @RedisLock 的测试方法.
分布式锁可重入测试结果如下.
来源: https://www.cnblogs.com/hujunzheng/p/11295345.html