一、redis实现分布式锁

问题一:释放了不是自己加的锁

  1. 1.客户 1 获取锁成功并设置设置 30 秒超时;
  2. 2.客户 1 因为一些原因导致执行很慢(网络问题、发生 FullGC……),过了 30 秒依然没执行完,但是锁过期「自动释放了」;
  3. 3.客户 2 申请加锁成功;
  4. 4.客户 1 执行完成,执行 DEL 释放锁指令,这个时候就把客户 2 的锁给释放了。

解决:

  1. 在加锁的时候设置一个「唯一标识」作为 value 代表加锁的客户端。SET resource_name random_value NX PX 30000
  2. 在释放锁的时候,客户端将自己的「唯一标识」与锁上的「标识」比较是否相等,匹配上则删除,否则没有权利释放锁。

具体实现:为保证原子性使用lua脚本

  1. // 获取锁的 value 与 ARGV[1] 是否匹配,匹配则执行 del
  2. if redis.call("get",KEYS[1]) == ARGV[1] then
  3. return redis.call("del",KEYS[1])
  4. else
  5. return 0
  6. end

问题二:设置锁的超时时间(超时时间设置多久合适)

  1. 根据在测试环境多次测试,然后压测多轮之后,比如计算出平均执行时间 200 ms。因为如果锁的操作逻辑中有网络 IO 操作、JVM FullGC 等,线上的网络不会总一帆风顺,我们要给网络抖动留有缓冲时间,那么锁的超时时间就放大为平均执行时间的 3~5 倍。设置短了导致锁提前释放,设置长了导致锁长期被占有,不管时间怎么设置都不大合适。

解决

  1. 可以让获得锁的线程开启一个守护线程,用来给快要过期的锁「续航」。
  2. 加锁的时候设置一个过期时间,同时客户端开启一个「守护线程」,定时去检测这个锁的失效时间。
  3. 如果快要过期,但是业务逻辑还没执行完成,自动对这个锁进行续期,重新设置过期时间。

二、springboot集成Redisson

依赖

  1. <!--redisson-->
  2. <dependency>
  3. <groupId>org.redisson</groupId>
  4. <artifactId>redisson-spring-boot-starter</artifactId>
  5. <version>3.13.6</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>org.springframework.boot</groupId>
  9. <artifactId>spring-boot-starter-web</artifactId>
  10. <version>2.3.2.RELEASE</version>
  11. </dependency>
  12. <dependency>
  13. <groupId>org.projectlombok</groupId>
  14. <artifactId>lombok</artifactId>
  15. <version>1.18.12</version>
  16. </dependency>

配置文件

  1. spring:
  2. ## Redis配置
  3. redis:
  4. password:
  5. cluster:
  6. nodes: 127.0.0.1:6379
  7. jedis:
  8. pool:
  9. #最大连接数
  10. max-active: 8
  11. #最大阻塞等待时间(负数表示没限制)
  12. max-wait: -1
  13. #最小空闲
  14. min-idle: 0
  15. #最大空闲
  16. max-idle: 8
  17. #连接超时时间
  18. timeout: 10000
  19. ##Redisson配置
  20. redisson:
  21. enable: true
  22. cluster-servers-config:
  23. cluster-nodes: ${spring.redis.cluster.nodes}
  24. load-balancer-mode: RADOM
  25. password: ${spring.redis.password}
  26. slave-connection-minimum-idle-size: 8
  27. slave-connection-pool-size: 16
  28. sslEnableEndpointIdentification: false
  29. threads: 8
  30. nettyThreads: 8
  31. transportMode: NIO

使用redisson每隔10秒会自动续期

  1. @RestController
  2. @Slf4j
  3. public class RedissonTestController {
  4. @Autowired
  5. private RedissonClient redissonClient;
  6. @RequestMapping(value = "/redisson")
  7. public void redissonTest() {
  8. RLock lock = redissonClient.getLock("9999");
  9. try {
  10. log.info("开始上锁了");
  11. lock.lock();
  12. Thread.sleep(40000);
  13. } catch (Exception e) {
  14. } finally {
  15. lock.unlock();
  16. }
  17. log.info("已解锁");
  18. }
  19. }

springboot使用Redisson - 图1
redisson续约源码

  1. private void renewExpiration() {
  2. RedissonLock.ExpirationEntry ee = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
  3. if (ee != null) {
  4. Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
  5. public void run(Timeout timeout) throws Exception {
  6. RedissonLock.ExpirationEntry ent = (RedissonLock.ExpirationEntry)RedissonLock.EXPIRATION_RENEWAL_MAP.get(RedissonLock.this.getEntryName());
  7. if (ent != null) {
  8. Long threadId = ent.getFirstThreadId();
  9. if (threadId != null) {
  10. RFuture<Boolean> future = RedissonLock.this.renewExpirationAsync(threadId);
  11. future.onComplete((res, e) -> {
  12. if (e != null) {
  13. RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", e);
  14. } else {
  15. if (res) {
  16. RedissonLock.this.renewExpiration();
  17. }
  18. }
  19. });
  20. }
  21. }
  22. }
  23. }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
  24. ee.setTimeout(task);
  25. }
  26. }
  27. private void scheduleExpirationRenewal(long threadId) {
  28. RedissonLock.ExpirationEntry entry = new RedissonLock.ExpirationEntry();
  29. RedissonLock.ExpirationEntry oldEntry = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry);
  30. if (oldEntry != null) {
  31. oldEntry.addThreadId(threadId);
  32. } else {
  33. entry.addThreadId(threadId);
  34. this.renewExpiration();
  35. }
  36. }
  37. protected RFuture<Boolean> renewExpirationAsync(long threadId) {
  38. return this.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0;", Collections.singletonList(this.getName()), this.internalLockLeaseTime, this.getLockName(threadId));
  39. }
  40. protected <T> RFuture<T> evalWriteAsync(String key, Codec codec, RedisCommand<T> evalCommandType, String script, List<Object> keys, Object... params) {
  41. CommandBatchService executorService = this.createCommandBatchService();
  42. RFuture<T> result = executorService.evalWriteAsync(key, codec, evalCommandType, script, keys, params);
  43. if (!(this.commandExecutor instanceof CommandBatchService)) {
  44. executorService.executeAsync();
  45. }
  46. return result;
  47. }
  1. 1.独享(相互排斥)。在任意一个时刻,只有一个客户端持有锁。
  2. 2无死锁。即便持有锁的客户端崩溃(crashed)或者网络被分裂(gets parttioned),锁仍然可以被获取。
  3. 3.容错。只要大部分Redis节点都活着,客户端就可以获取和释放锁

问题:

  1. 1.客户端Amaster获取到锁
  2. 2.master将锁同步到slave之前,master宕掉了。
  3. 3. slave节点被晋级为master节点
  4. 4.客户端B取得了同一个资源被客户端A已经获取到的另外一个锁。安全失效!