原理与演进

参考: https://www.cnblogs.com/MrLiuZF/p/15110559.html (原文非常好, 强烈推荐细读原文)

分布式锁定义

分布式锁定义: 保证同一时间只有一个客户端对共享资源进行操作
要求:

  1. 不会发生死锁, 即使客户端在持有锁的期间崩溃而没有主动解锁, 也能保证后续其他客户端能加锁
  2. 具有容错性, 只要大部分Redis节点正常运行, 客户端就可以加锁解锁
  3. 解铃还须系铃人, 加锁和解锁必须是同一客户端

锁实现演进

(建议看上面的原文, 此处为个人总结)

  1. setnx

为保证同一时刻只有一个线程操作共享资源, 由于redis是单线程的, 因此可在操作之前往redis设置一个标志位, 操作结束后释放, 使用的是 setIfAbsent(key, val) -> setnx(key, val)

  1. setnx+finally+expire

若程序拿到标志位后执行异常, 锁未释放, 则会发生死锁, 因此释放锁的操作需在finally代码块中;
但若程序拿到标志位后服务直接宕机, 锁依然不会被释放, 因此除finally代码块外, 还需要设置标志位的过期时间, 超时则自动释放锁

  1. set px nx

设置标志位以及过期时间需为一个原子操作, 因此, 需使用set px nx 来保证原子性

  1. SET key value [expiration EX seconds|PX milliseconds] [NX|XX]
  2. 可选参数如下:
  3. EX: 设置超时时间,单位是秒
  4. PX: 设置超时时间,单位是毫秒
  5. NX: IF NOT EXIST 的缩写,只有 KEY不存在的前提下 才会设置值
  6. XX: IF EXIST 的缩写,只有在 KEY存在的前提下 才会设置值
  1. set px nx+客户端id

当程序执行时间超过标志位的过期时间时, 锁过期被自动释放, 线程二拿到锁, 随后线程一程序执行完成在finally代码块中把线程二的锁释放掉了, 为解决这一问题, 加锁解锁需为同一客户端, 简单来说, 锁的key需要附带客户端标识, 在解锁时, 需匹配标识一致

  1. lua

解锁时, 需匹配标识一致, 先判断后解锁存在竞态条件, 因此需将解锁删除标志位的操作写在lua脚本中保证原子性

  1. KEYS[1]: lockKey
  2. ARGV[1]: lockValue
  3. # 获取 KEYS[1] 对应的 Val
  4. local cliVal = redis.call('get', KEYS[1])
  5. # 判断 KEYS[1] ARGV[1] 是否保持一致
  6. if(cliVal == ARGV[1]) then
  7. # 删除 KEYS[1]
  8. redis.call('del', KEYS[1])
  9. return 'OK'
  10. else
  11. return nil
  12. end
  1. watch dog

上述分布式锁无法续期, 万一锁被超时释放, 可能会导致不可预料的问题, 因此reddisson在加锁成功后会启动一个watch dog后台线程, 每隔10秒检查一下, 若客户端还持有锁, 那么就会不断延长锁的过期时间
image.png

  1. redlock

参考: https://www.jianshu.com/p/2c7855e648ca
当redis集群为主从结构时, 主节点加锁但还未同步从节点, 此刻主节点宕机, 主备切换, 线程二能在新的主节点再次获得同一把锁, 官方推荐使用redlock来解决这一问题, redlock使用算法要求超过半数的节点加锁成功才算最终加锁成功

获取锁的执行步骤 1、获取当前时间
2、依次N个节点获取锁,并设置响应超时时间,防止单节点获取锁时间过长
3、锁有效时间=锁过期时间-获取锁耗费时间,如果第2步骤中获取成功的节点数大于
N/2+1,且锁有效时间大于0,则获得锁成功
4、若获得锁失败,则向所有节点释放锁


框架实现

对比
参考: https://gitee.com/zhaokuner/redission
image.png

spring-integration-redis

参考: https://zhuanlan.zhihu.com/p/76532718

  1. <dependency>
  2. <groupId>org.springframework.integration</groupId>
  3. <artifactId>spring-integration-redis</artifactId>
  4. <version>5.5.11</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.springframework.boot</groupId>
  8. <artifactId>spring-boot-starter-data-redis</artifactId>
  9. </dependency>
  10. <dependency>
  11. <groupId>org.springframework.boot</groupId>
  12. <artifactId>spring-boot-autoconfigure</artifactId>
  13. </dependency>

封装示例

DistributedLock 分布式锁接口

  1. /**
  2. * 分布式锁操作的接口
  3. *
  4. * @author xinzhang
  5. * @version 2022/4/29
  6. */
  7. public interface DistributedLock {
  8. /**
  9. * 加锁
  10. */
  11. void lock(String key);
  12. /**
  13. * 尝试加锁
  14. */
  15. boolean tryLock(String key, long timeout, TimeUnit timeUnit);
  16. /**
  17. * 解锁
  18. */
  19. void unlock(String key);
  20. }

SpringRedisLock 锁实现

  1. /**
  2. * SpringRedisLock
  3. *
  4. * @author xinzhang
  5. * @version 2022/4/29
  6. */
  7. @Slf4j
  8. public class SpringRedisLock implements DistributedLock {
  9. private static final String SPRING_REDIS_LOCK_PREFIX = "SRL:";
  10. private RedisLockRegistry redisLockRegistry;
  11. public SpringRedisLock(RedisLockRegistry redisLockRegistry) {
  12. this.redisLockRegistry = redisLockRegistry;
  13. }
  14. /**
  15. * 构建分布式锁的key
  16. */
  17. public static String buildKey(String original) {
  18. return SPRING_REDIS_LOCK_PREFIX + original;
  19. }
  20. @Override
  21. public void lock(String key) {
  22. this.redisLockRegistry.obtain(buildKey(key)).lock();
  23. if (log.isDebugEnabled()) {
  24. log.debug(String.format("线程%s获取锁成功", Thread.currentThread().getName()));
  25. }
  26. }
  27. @Override
  28. public boolean tryLock(String key, long waitTimeout, TimeUnit timeUnit) {
  29. Lock lock = this.redisLockRegistry.obtain(buildKey(key));
  30. try {
  31. boolean isLockSuccess = lock.tryLock(waitTimeout, timeUnit);
  32. if (log.isDebugEnabled()) {
  33. log.debug(String.format("线程%s获取锁%s", Thread.currentThread().getName(), isLockSuccess ? "成功" : "失败"));
  34. }
  35. return isLockSuccess;
  36. } catch (InterruptedException e) {
  37. return false;
  38. }
  39. }
  40. @Override
  41. public void unlock(String key) {
  42. this.redisLockRegistry.obtain(buildKey(key)).unlock();
  43. if (log.isDebugEnabled()) {
  44. log.debug(String.format("线程%s释放锁", Thread.currentThread().getName()));
  45. }
  46. }
  47. }

SpringRedisLockAutoConfig 自动装配

  1. /**
  2. * SpringRedisLockAutoConfig
  3. * RedisOperations在spring-data-redis, redis的自动装配RedisAutoConfiguration在spring-boot-autoconfigure
  4. * RedisAutoConfiguration的条件装配也是基于RedisOperations
  5. * @author xinzhang
  6. * @version 2022/4/29
  7. */
  8. @Configuration
  9. public class SpringRedisLockAutoConfig {
  10. private static final String DEFAULT_SPRING_REDIS_LOCK = "DSRL";
  11. /**
  12. * 初始化redis分布式锁配置.
  13. *
  14. * 注意,这里的分布式锁的解锁时间默认为60秒,这可能会导致以下安全性问题(出现概率依次递减):
  15. * 1. 分布式锁用在网络IO的场景,必须设置超时时间,否则可能会因为对方超时导致锁自动释放
  16. * 2. 服务器时钟跳跃,可能会出现不可预料的锁到期情况,可能出现的场景:业务因其他原因执行时间过长 + 服务器时钟跳跃 可能会导致分布式锁自动释放
  17. * 3. JVM GC的STW过长导致
  18. *
  19. * @param connectionFactory redis连接工厂
  20. * @return redis分布式锁配置
  21. */
  22. @Bean
  23. public RedisLockRegistry redisLockRegistry(RedisConnectionFactory connectionFactory) {
  24. return new RedisLockRegistry(connectionFactory, DEFAULT_SPRING_REDIS_LOCK);
  25. }
  26. @Bean
  27. public DistributedLock distributedLock(RedisLockRegistry redisLockRegistry) {
  28. return new SpringRedisLock(redisLockRegistry);
  29. }
  30. }

使用示例

  1. /**
  2. * ConcurrencyController
  3. *
  4. * @author xinzhang
  5. * @version 2022/4/29
  6. */
  7. @RestController
  8. @RequestMapping("/service_a/concurrency/snack")
  9. public class SnackController {
  10. @Autowired
  11. private DistributedLock distributedLock;
  12. private Random random = new Random();
  13. private Integer snack = 3;
  14. @PostMapping("/unsafe_acquire")
  15. public Boolean unsafeAcquireSnack() {
  16. return acquire();
  17. }
  18. @PostMapping("/safe_acquire")
  19. public Boolean safeAcquireSnack() {
  20. boolean res;
  21. try {
  22. distributedLock.lock("snack");
  23. res = acquire();
  24. } finally {
  25. distributedLock.unlock("snack");
  26. }
  27. return res;
  28. }
  29. /**
  30. * 获取小吃, 线程不安全
  31. */
  32. private boolean acquire() {
  33. if (snack <= 0) {
  34. return false;
  35. }
  36. // mock logical
  37. try {
  38. Thread.sleep(random.nextInt(200));
  39. snack--;
  40. System.out.println("get snack, o yeah~~!");
  41. } catch (InterruptedException e) {
  42. e.printStackTrace();
  43. }
  44. return true;
  45. }
  46. }

jmeter20个线程请求即可对比出结果
image.png

Redission

参考: https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95
https://github.com/redisson/redisson/tree/master/redisson-spring-boot-starter#spring-boot-starter

  1. <dependency>
  2. <groupId>org.redisson</groupId>
  3. <artifactId>redisson-spring-boot-starter</artifactId>
  4. <version>3.17.1</version>
  5. </dependency>

封装示例

总体DistributedLock接口设计同上
RedissonLock

  1. /**
  2. * ReddsionLock
  3. *
  4. * @author xinzhang
  5. * @version 2022/5/5
  6. */
  7. @Slf4j
  8. public class RedissonLock implements DistributedLock {
  9. private RedissonClient redissonClient;
  10. public RedissonLock(RedissonClient redissonClient) {
  11. this.redissonClient = redissonClient;
  12. }
  13. @Override
  14. public void lock(String key) {
  15. redissonClient.getLock(key).lock();
  16. if (log.isDebugEnabled()) {
  17. log.debug(String.format("线程%s获取锁成功", Thread.currentThread().getName()));
  18. }
  19. }
  20. @Override
  21. public boolean tryLock(String key, long timeout, TimeUnit timeUnit) {
  22. RLock lock = redissonClient.getLock(key);
  23. try {
  24. boolean isLockSuccess = lock.tryLock(timeout, timeUnit);
  25. if (log.isDebugEnabled()) {
  26. log.debug(String.format("线程%s获取锁%s", Thread.currentThread().getName(), isLockSuccess ? "成功" : "失败"));
  27. }
  28. return isLockSuccess;
  29. } catch (InterruptedException e) {
  30. return false;
  31. }
  32. }
  33. @Override
  34. public void unlock(String key) {
  35. redissonClient.getLock(key).unlock();
  36. if (log.isDebugEnabled()) {
  37. log.debug(String.format("线程%s释放锁", Thread.currentThread().getName()));
  38. }
  39. }
  40. }

RedissonLockAutoConfig

  1. /**
  2. * RedissonLockAutoConfig
  3. *
  4. * @author xinzhang
  5. * @version 2022/5/5
  6. */
  7. @Configuration
  8. public class RedissonLockAutoConfig {
  9. @Bean
  10. public DistributedLock distributedLock(RedissonClient redisson) {
  11. return new RedissonLock(redisson);
  12. }
  13. }

Redis客户端补充

RedisTemplate

Spring-data-redis是spring大家族的一部分,提供了在srping应用中通过简单的配置访问redis服务,对reids底层开发包(Jedis, JRedis, and RJC)进行了高度封装,RedisTemplate提供了redis各种操作、异常处理及序列化,支持发布订阅,并对spring 3.1 cache进行了实现。

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-data-redis</artifactId>
  4. </dependency>

Jedis