0%

安全的使用redis分布式锁

问题

问题描述:你在使用分布式锁对代码加一个锁,会不会业务没有执行完,锁释放了?

1
setnx?set?

如果回答setnx就掉入一个坑了

1
setnx key value

没有过期时间?没有过期时间就意味着我们必须有可靠且安全的措施保障最终锁能被我们自己手动释放,如以下代码

1
2
3
4
5
6
7
8
9
10
if (setnx('key', 1)) {
try {
// do buisness
} catch(Exception e) {
// handle exception
} finally {
// release lock
del('key')
}
}

有了finally是不是就真的安全了?tomcat崩了,jvm死了,机器断电,遭雷劈了?

错误解决方式1

接下来就引入了新版本redis(2.6.12)的过期策略来解决。

SET key value [EX seconds] [PX milliseconds] [NX|XX]

将字符串值 value 关联到 key 。

如果 key 已经持有其他值, SET 就覆写旧值,无视类型。

对于某个原本带有生存时间(TTL)的键来说, 当 SET 命令成功在这个键上执行时, 这个键原有的 TTL 将被清除。

可选参数

从 Redis 2.6.12 版本开始, SET 命令的行为可以通过一系列参数来修改:

EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。

PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。

NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。

XX :只在键已经存在时,才对键进行设置操作。

但是set并不是完全合适的,如果用NX没有过期策略支持,如果用PX又不支持排他性,如果getset组合又不具有原子性。

貌似解决方式2

1
2
3
4
5
6
7
8
127.0.0.1:6379> set mylock 1 EX 10 NX
OK
127.0.0.1:6379> ttl mylock
(integer) 6
127.0.0.1:6379> ttl mylock
(integer) 5
127.0.0.1:6379> ttl mylock
(integer) 2

但是spring-dataredis api又有下面一段话:

The lack of entry locking can lead to overlapping, non-atomic commands for the putIfAbsent and clean methods, as those require multiple commands to be sent to Redis.

有兴趣可以深入研究,我个人的出结论这里不适用(spring-data及网上的常规解决方案还有使用lua脚本来处理)。同时还有另外一个问题,如果我们的即使我们设置了一个合适的时间,比如10s(你如果设一年,PX还有什么意义呢),如果我们的实际任务线程因为某些原因超时了。这时redis自动超时释放了这个锁,又会引发问题。

Redisson解决方式

当然我们通常使用的分布式锁还有通过redisson实现的。

什么是Redisson?—— Redisson Wiki

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

依赖

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

配置

配置文件方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
spring:
redis:
redisson:
file: classpath:redisson.yaml
config: |
clusterServersConfig:
idleConnectionTimeout: 10000
connectTimeout: 10000
timeout: 3000
retryAttempts: 3
retryInterval: 1500
failedSlaveReconnectionInterval: 3000
failedSlaveCheckInterval: 60000
password: null
subscriptionsPerConnection: 5
clientName: null
loadBalancer: !<org.redisson.connection.balancer.RoundRobinLoadBalancer> {}
subscriptionConnectionMinimumIdleSize: 1
subscriptionConnectionPoolSize: 50
slaveConnectionMinimumIdleSize: 24
slaveConnectionPoolSize: 64
masterConnectionMinimumIdleSize: 24
masterConnectionPoolSize: 64
readMode: "SLAVE"
subscriptionMode: "SLAVE"
nodeAddresses:
- "redis://127.0.0.1:7004"
- "redis://127.0.0.1:7001"
- "redis://127.0.0.1:7000"
scanInterval: 1000
pingConnectionInterval: 0
keepAlive: false
tcpNoDelay: false
threads: 16
nettyThreads: 32
codec: !<org.redisson.codec.MarshallingCodec> {}
transportMode: "NIO"

Java配置方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class RedissionConfig {
@Value("${spring.redis.hosts}")
private List<String> redisHosts;

@Bean
public RedissonClient getRedisson() {
Config config = new Config();
config.useClusterServers()
.setScanInterval(2000) // 集群状态扫描间隔时间,单位是毫秒
//可以用"rediss://"来启用SSL连接
//.addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001")
.addNodeAddress(redisHosts);
config.setCodec(new JsonJacksonCodec());
return Redisson.create(config);
}
}

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyTest {

@Resource
private RedissonClient redissonClient;

public void completeBusiness() {
RLock rLock = redissonClient.getLock("mylock");
try {
boolean isLocked = rLock.tryLock(1000, TimeUnit.MILLISECONDS);
if (isLocked) {
// do mybusiness
}
} catch (Exception e) {
rLock.unlock();
}
}
}

实现分析

其内部在tryLock中会触发一个TimerTask对应于参数lockWatchdogTimeout组成一种看门狗的模式来刷新ttl,其内部为了保障原子性也会使用到lua脚本,同时加入了异步回调支持(PubSubFuture),提高处理吞吐量。

redis分布式锁一定安全么?

不一定,如果redissonclient获取锁之后,主节点尚未完成复制给从节点就掉线了。那么新的主节点就没有锁key。几率极低。尚不确定redisson本身是否有兜底措施。

参考链接

最强分布式工具Redisson(一):分布式锁

redis分布式锁可靠吗

redisson-spring-boot-starter