使用redis实现分布式锁

使用redis实现分布式锁

在我们的修改数据时,需要先读取数据,然后进行保存,这时就会遇到并发问题;由于修改和保存不是原子操作,所以在并发场景下,出现脏读幻读等现象;在单机情况下,我们通常使用本地锁来避免并发问题,但是当服务采用集群式的部署方式时,我们就需要使用分布式锁来保证数据的一致性。

如何实现

Redis 分布式锁主要使用 setnx 命令。

  • 加锁:使用setnx <key> <value>命令获取锁,如果对应的key不存在,则进行赋值,并返回成功,如果key存在则直接返回失败;
  • 解锁:使用del <key>命令释放锁;
  • 锁超时:使用expire <key> <timeout>命令设置超时时间,如果锁长时间没有被释放,会在一定时间内自动释放,避免造成死锁。

伪代码:

1
2
3
4
5
6
7
8
if (setnx(key, 1) == 1){
expire(key, 30)
try {
//TODO 业务逻辑
} finally {
del(key)
}
}

SETNX 和 EXPIRE 非原子性

如果获取锁成功,但是出现了宕机等异常,导致后续的设置超时时间的命令没有被执行,就会出现死锁。

可以使用set扩展命令:

1
SET <key> <value> NX EX <timeout>

Spring BootRedisTemplate的实现:

锁误解除

如果线程A成功获取到了锁,并且设置了10秒的超时时间,但是线程A执行时间超过了10秒,锁超时释放,此时线程B又获取到了锁,然后线程A执行完成,执行释放锁的命令,但是线程A释放了线程B的锁。

可以将线程标识(比如UUID)设置为锁的value,再删除锁之前验证锁的key是否与当先线程的标识相同,如果相同再释放锁。

1
2
3
4
5
6
7
8
9
if (setIfAbsent(key, UUID, timeOut) == 1){
try {
//TODO 业务逻辑
} finally {
if(get(key) == UUID){
del(key)
}
}
}

解锁超时导致并发执行

让我们继续来看刚才的问题,如果A的执行时间超过了锁自动释放的时间,而恰巧B又获取到了锁,就会导致AB线程并发执行。

解决方式:

  • 将超时时间设置的足够长,确保业务逻辑在锁释放之前能够执行完成,但是这样会导致并发能力下降;
  • 为获取锁的线程创建守护线程,在线程执行完成之前为锁添加有效时间。

作者

ero

发布于

2022-03-25

更新于

2022-06-11

许可协议

评论