redisson为此就做了一些封装,使得我们使用分布式锁时应用就可以简单许多。
1、Maven依赖
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.4.RELEASE</version></parent><dependencies><dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.11.5</version></dependency></dependencies>
2、redisson配置
2.1、yml配置
server:port: 30800spring:redis:cluster:nodes:- 148.70.153.63:9426- 148.70.153.63:9427- 148.70.153.63:9428- 148.70.153.63:9429- 148.70.153.63:9430- 148.70.153.63:9431password: passwordtimeout: 2000
2.2、构建RedissonClient。
集群Cluster模式下配置
@Configurationpublic class DisLockConfig {@Autowiredprivate RedisProperties redisProperties;/*** Cluster集群模式构建 RedissonClient** @return*/@Beanpublic RedissonClient clusterRedissonClient() {Config config = new Config();ClusterServersConfig clusterServersConfig = config.useClusterServers().setPassword(redisProperties.getPassword()).setScanInterval(5000);// config.setLockWatchdogTimeout(60 * 1000); // watch dog看门狗默认锁过期时间// 注册集群各个节点for (String node : redisProperties.getCluster().getNodes()) {clusterServersConfig.addNodeAddress("redis://".concat(node));}RedissonClient redissonClient = Redisson.create(config);return redissonClient;}}
在redis的不同模式下,构造config的方式是有区别的。
单机模式
@Beanpublic RedissonClient singleRedissonClient() {Config config = new Config();config.useSingleServer().setAddress("redis://ip:port").setPassword("password").setDatabase(0);RedissonClient redissonClient = Redisson.create(config);return redissonClient;}
哨兵模式Sentinel
@Beanpublic RedissonClient sentinelRedissonClient() {Config config = new Config();config.useSentinelServers().addSentinelAddress("redis://ip1:port1","redis://ip2:port2","redis://ip3:port3").setMasterName("mymaster").setPassword("password").setDatabase(0);RedissonClient redissonClient = Redisson.create(config);return redissonClient;}
3、锁应用
3.1、锁自动过期
@RunWith(SpringRunner.class)@SpringBootTest@Slf4jpublic class RedissonApplicationTests {@Autowiredprivate RedissonClient redissonClient;@Testpublic void testDisLock() {// 5个线程并发去获取锁IntStream.range(0, 5).parallel().forEach(i -> tryLock());}@SneakyThrowsprivate void tryLock() {RLock disLock = redissonClient.getLock("disLock");// 获取锁最多等待500ms,10s后key过期自动释放锁boolean tryLock = disLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);if (tryLock) {// 获取到锁后开始执行对资源的操作try {log.info("当前线程:[{}]获得锁", Thread.currentThread().getName());// 操作资源...} finally {disLock.unlock();}} else {log.info("当前线程:[{}]没有获得锁", Thread.currentThread().getName());}}}
- 获取RLock同时指定key。
- 尝试获取锁,同时指定获取锁的最大阻塞时间、锁过期时间。
- 获得锁的线程进行资源操作。
- 最后一定要释放锁。
结果
当前线程:[ForkJoinPool.commonPool-worker-11]获得锁 当前线程:[main]获得锁 当前线程:[ForkJoinPool.commonPool-worker-2]没有获得锁 当前线程:[ForkJoinPool.commonPool-worker-13]没有获得锁 当前线程:[ForkJoinPool.commonPool-worker-9]获得锁
多次测试可以看出,至少会有1个线程可以获取到锁,其它线程能否获取到锁取决于之前的锁是否已经被释放了。
查看redis
127.0.0.1:9426> hgetall disLock af0cc1b2-7896-4eb4-ba2b-efe5bbcb403a:53 1
第一个元素:uuid:线程id。
第二个元素:当前线程持有锁的次数,即重入的次数。
3.2、watch dog看门狗机制
如果使用锁自动过期方式,假设客户端在拿到锁之后执行的业务时间比较长,在此期间锁被释放,其它线程依旧可以获取到锁,redisson提供一种watch dog看门狗的机制来解决这个问题。
@RunWith(SpringRunner.class)@SpringBootTest@Slf4jpublic class RedissonApplicationTests {@Autowiredprivate RedissonClient redissonClient;@Testpublic void testDisLock() {// 5个线程并发去获取锁IntStream.range(0, 5).parallel().forEach(i -> lock());}@SneakyThrowsprivate void lock() {RLock disLock = redissonClient.getLock("disLock");// 获取锁最多等待500ms,这里不要显示指定锁过期时间// 默认30秒后自动过期,每隔30/3=10秒,看门狗(守护线程)会去续期锁,重设为30秒boolean tryLock = disLock.tryLock(500, TimeUnit.MILLISECONDS);if (tryLock) {// 获取到锁后开始执行对资源的操作try {log.info("当前线程:[{}]获得锁", Thread.currentThread().getName());// 操作资源...} finally {disLock.unlock();}} else {log.info("当前线程:[{}]没有获得锁", Thread.currentThread().getName());}}}
看门狗机制如下图所示:
默认情况下,看门狗的过期时间是30s,每隔30/3=10秒,看门狗(守护线程)会去续期锁,重设为30秒。可以通过修改Config.lockWatchdogTimeout来另行指定看门狗的过期时间。
看门狗机制真的万无一失吗?极端情况下:
- P:Process Pause,进程暂停(GC)
客户端获取到锁之后进入GC进而导致看门狗没有及时续期,最后锁过期。本质上还是锁过期时间设短导致的,一般只要远大于通常GC所暂停的时间就可以了,一般不太会发生。
C:Clock Drift,时钟漂移
redis服务端所在的服务器时钟发生较大的向前跳跃,导致锁提前过期被释放。这个一般也不会发生,除非人为的进行暴力运维。4、锁的重入
redisson支持锁的可重入,代码如下:
@Test@SneakyThrowspublic void testTryLockAgain() {RLock disLock = redissonClient.getLock("disLock");// 获取锁最多等待500ms,10s后key过期自动释放锁boolean tryLock = disLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);if (tryLock) {try {log.info("当前线程:[{}]获得锁,持有锁次数:[{}]", Thread.currentThread().getName(), disLock.getHoldCount());// 操作资源...// 测试可重入,锁过期时间会重新计时boolean tryLockAgain = disLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);log.info("当前线程:[{}]是否再次拿到锁:[{}],持有锁次数:[{}]", Thread.currentThread().getName(), tryLockAgain, disLock.getHoldCount());// 再次操作资源...} finally {disLock.unlock();log.info("当前线程是否持有锁:[{}],持有锁次数:[{}]", disLock.isHeldByCurrentThread(), disLock.getHoldCount());}} else {log.info("当前线程:[{}]没有获得锁", Thread.currentThread().getName());}}
结果
当前线程:[main]获得锁,持有锁次数:[1] 当前线程:[main]是否再次拿到锁:[true],持有锁次数:[2] 当前线程是否持有锁:[true],持有锁次数:[1]
redisson则支持锁的可重入和等待获取锁,并在解锁时判断是否是当前线程持有的锁,以及有看门狗机制防止锁过期程序还未执行完的问题,对于这些功能redisson已经做好了封装,简化了业务代码。
但是依旧会有1个问题,主从切换导致的锁丢失,场景如下:在Redis的master节点上拿到了锁;
- 但是这个加锁的key还没有同步到slave节点;
- master故障,发生故障转移,slave节点升级为master节点;
- 导致锁丢失。
