redisson为此就做了一些封装,使得我们使用分布式锁时应用就可以简单许多。

1、Maven依赖

  1. <parent>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-parent</artifactId>
  4. <version>2.1.4.RELEASE</version>
  5. </parent>
  6. <dependencies>
  7. <dependency>
  8. <groupId>org.redisson</groupId>
  9. <artifactId>redisson-spring-boot-starter</artifactId>
  10. <version>3.11.5</version>
  11. </dependency>
  12. </dependencies>

2、redisson配置

2.1、yml配置

  1. server:
  2. port: 30800
  3. spring:
  4. redis:
  5. cluster:
  6. nodes:
  7. - 148.70.153.63:9426
  8. - 148.70.153.63:9427
  9. - 148.70.153.63:9428
  10. - 148.70.153.63:9429
  11. - 148.70.153.63:9430
  12. - 148.70.153.63:9431
  13. password: password
  14. timeout: 2000

这里沿用之前使用RedisTemplate时的配置方式。

2.2、构建RedissonClient。

集群Cluster模式下配置

  1. @Configuration
  2. public class DisLockConfig {
  3. @Autowired
  4. private RedisProperties redisProperties;
  5. /**
  6. * Cluster集群模式构建 RedissonClient
  7. *
  8. * @return
  9. */
  10. @Bean
  11. public RedissonClient clusterRedissonClient() {
  12. Config config = new Config();
  13. ClusterServersConfig clusterServersConfig = config.useClusterServers()
  14. .setPassword(redisProperties.getPassword())
  15. .setScanInterval(5000);
  16. // config.setLockWatchdogTimeout(60 * 1000); // watch dog看门狗默认锁过期时间
  17. // 注册集群各个节点
  18. for (String node : redisProperties.getCluster().getNodes()) {
  19. clusterServersConfig.addNodeAddress("redis://".concat(node));
  20. }
  21. RedissonClient redissonClient = Redisson.create(config);
  22. return redissonClient;
  23. }
  24. }

在redis的不同模式下,构造config的方式是有区别的。
单机模式

  1. @Bean
  2. public RedissonClient singleRedissonClient() {
  3. Config config = new Config();
  4. config.useSingleServer().setAddress("redis://ip:port")
  5. .setPassword("password")
  6. .setDatabase(0);
  7. RedissonClient redissonClient = Redisson.create(config);
  8. return redissonClient;
  9. }

哨兵模式Sentinel

  1. @Bean
  2. public RedissonClient sentinelRedissonClient() {
  3. Config config = new Config();
  4. config.useSentinelServers().addSentinelAddress("redis://ip1:port1",
  5. "redis://ip2:port2",
  6. "redis://ip3:port3")
  7. .setMasterName("mymaster")
  8. .setPassword("password")
  9. .setDatabase(0);
  10. RedissonClient redissonClient = Redisson.create(config);
  11. return redissonClient;
  12. }

3、锁应用

3.1、锁自动过期

  1. @RunWith(SpringRunner.class)
  2. @SpringBootTest
  3. @Slf4j
  4. public class RedissonApplicationTests {
  5. @Autowired
  6. private RedissonClient redissonClient;
  7. @Test
  8. public void testDisLock() {
  9. // 5个线程并发去获取锁
  10. IntStream.range(0, 5).parallel().forEach(i -> tryLock());
  11. }
  12. @SneakyThrows
  13. private void tryLock() {
  14. RLock disLock = redissonClient.getLock("disLock");
  15. // 获取锁最多等待500ms,10s后key过期自动释放锁
  16. boolean tryLock = disLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
  17. if (tryLock) {
  18. // 获取到锁后开始执行对资源的操作
  19. try {
  20. log.info("当前线程:[{}]获得锁", Thread.currentThread().getName());
  21. // 操作资源...
  22. } finally {
  23. disLock.unlock();
  24. }
  25. } else {
  26. log.info("当前线程:[{}]没有获得锁", Thread.currentThread().getName());
  27. }
  28. }
  29. }
  1. 获取RLock同时指定key。
  2. 尝试获取锁,同时指定获取锁的最大阻塞时间、锁过期时间。
  3. 获得锁的线程进行资源操作。
  4. 最后一定要释放锁。

结果

当前线程:[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看门狗的机制来解决这个问题。

  1. @RunWith(SpringRunner.class)
  2. @SpringBootTest
  3. @Slf4j
  4. public class RedissonApplicationTests {
  5. @Autowired
  6. private RedissonClient redissonClient;
  7. @Test
  8. public void testDisLock() {
  9. // 5个线程并发去获取锁
  10. IntStream.range(0, 5).parallel().forEach(i -> lock());
  11. }
  12. @SneakyThrows
  13. private void lock() {
  14. RLock disLock = redissonClient.getLock("disLock");
  15. // 获取锁最多等待500ms,这里不要显示指定锁过期时间
  16. // 默认30秒后自动过期,每隔30/3=10秒,看门狗(守护线程)会去续期锁,重设为30秒
  17. boolean tryLock = disLock.tryLock(500, TimeUnit.MILLISECONDS);
  18. if (tryLock) {
  19. // 获取到锁后开始执行对资源的操作
  20. try {
  21. log.info("当前线程:[{}]获得锁", Thread.currentThread().getName());
  22. // 操作资源...
  23. } finally {
  24. disLock.unlock();
  25. }
  26. } else {
  27. log.info("当前线程:[{}]没有获得锁", Thread.currentThread().getName());
  28. }
  29. }
  30. }

看门狗机制如下图所示:
image.png
默认情况下,看门狗的过期时间是30s,每隔30/3=10秒,看门狗(守护线程)会去续期锁,重设为30秒。可以通过修改Config.lockWatchdogTimeout来另行指定看门狗的过期时间。
看门狗机制真的万无一失吗?极端情况下:

  1. P:Process Pause,进程暂停(GC)
    客户端获取到锁之后进入GC进而导致看门狗没有及时续期,最后锁过期。本质上还是锁过期时间设短导致的,一般只要远大于通常GC所暂停的时间就可以了,一般不太会发生。
  • C:Clock Drift,时钟漂移
    redis服务端所在的服务器时钟发生较大的向前跳跃,导致锁提前过期被释放。这个一般也不会发生,除非人为的进行暴力运维。

    4、锁的重入

    redisson支持锁的可重入,代码如下:

    1. @Test
    2. @SneakyThrows
    3. public void testTryLockAgain() {
    4. RLock disLock = redissonClient.getLock("disLock");
    5. // 获取锁最多等待500ms,10s后key过期自动释放锁
    6. boolean tryLock = disLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
    7. if (tryLock) {
    8. try {
    9. log.info("当前线程:[{}]获得锁,持有锁次数:[{}]", Thread.currentThread().getName(), disLock.getHoldCount());
    10. // 操作资源...
    11. // 测试可重入,锁过期时间会重新计时
    12. boolean tryLockAgain = disLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
    13. log.info("当前线程:[{}]是否再次拿到锁:[{}],持有锁次数:[{}]", Thread.currentThread().getName(), tryLockAgain, disLock.getHoldCount());
    14. // 再次操作资源...
    15. } finally {
    16. disLock.unlock();
    17. log.info("当前线程是否持有锁:[{}],持有锁次数:[{}]", disLock.isHeldByCurrentThread(), disLock.getHoldCount());
    18. }
    19. } else {
    20. log.info("当前线程:[{}]没有获得锁", Thread.currentThread().getName());
    21. }
    22. }

    结果

    当前线程:[main]获得锁,持有锁次数:[1] 当前线程:[main]是否再次拿到锁:[true],持有锁次数:[2] 当前线程是否持有锁:[true],持有锁次数:[1]


    redisson则支持锁的可重入和等待获取锁,并在解锁时判断是否是当前线程持有的锁,以及有看门狗机制防止锁过期程序还未执行完的问题,对于这些功能redisson已经做好了封装,简化了业务代码。
    但是依旧会有1个问题,主从切换导致的锁丢失,场景如下:

  • 在Redis的master节点上拿到了锁;

  • 但是这个加锁的key还没有同步到slave节点;
  • master故障,发生故障转移,slave节点升级为master节点;
  • 导致锁丢失。


链接:https://www.jianshu.com/p/59ffff18e1ff