使用 Redis 快速实现分布式锁

144 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第17天,点击查看活动详情

分布式环境下,实现分布式锁的方式就是通过 set key ,然后并发情况下,谁先抢得 key 时就有权优先处理,其他进程都需要等待。但我们还需要考虑很多问题,如确保临界资源的串行执行,如何及时释放等各种问题。

使用 setnx 实现分布式锁

Redis 支持 setnx 命令,当 key 不存在时可 set 值,当 key 存在时不可进行任何操作。所以使用 setnx 实现分布式锁方式极其简单。

if setnx(key,value) ==1 { // 可设置值,即获得到锁
    // 业务处理
    defer del(key) // 处理完业务后 删除 key,即释放锁
} else{
    // 没有获取到锁,或等待锁释放 或结束
}

根据以上伪代码,使用 setnx 实现分布式锁方案:

  • 使用 setnx 指令设置 key,若返回 1 则说明设置成功,即是得到锁,然后就可以进行相关的业务处理。
  • 释放锁只需要删除对应的 key 即可
  • 若返回 0 则说明锁已被抢占。则进程无法进入临界区,可以结束操作,也可以增加等待功能,即增加循环阻塞当前进程,在循环不断尝试 setnx 操作

但是该方式会有一个很明显的问题:不支持超时释放锁,如果进程在加锁后宕机,那将导致锁无法删除,其他进程就无法获取锁。

所以,为了避免死锁,可以增加过期时间 expire 进一步优化。

使用 setnx 和 expire 实现

在分布式锁的实现过程中,因释放锁依赖业务,所以如果进程宕机就会发生死锁情况。所以通过给 key 设置过期时间,就可以在进程宕机后 key 自动过期,其他进程就可以继续抢占锁。

在每次使用 setnx 后,需再使用 expire 给锁增加个过期时间。

if setnx(key,value) ==1 { 
    expire(key ,time.second*10)
    // 业务处理
    defer del(key) // 删除锁,释放
} else{
    // todo something
}

增加过期时间解决了超时导致死锁的问题。需要注意的是,超时时间必须大于业务处理时间,不然会容易出现业务还没处理完,key 已超时,这样就会导致其他线程可以获取到锁。

还存在问题:setnx 和 expire 不是在同一个事务内处理,不具有原子性。所以如果 setnx 操作完后线程崩溃导致没有执行 expire ,这样还是会导致死锁情况。

使用 set 扩展命令实现

在 Redis 2.8版本之中,扩展了 set 命令,支持 set 和expire 指令组合的原子操作,避免了加锁失败的问题。

set key value expireTime nx

nx 表示仅在键不存在时设置,这样就可以在同一时间内完成设置值和设置超时时间两个操作。需要注意的时超时时间一定要大于业务处理时间。

所以,使用 redis 设置分布式锁尽量使用在业务逻辑比较短的场景。

解决该问题,也可以通过给 value 设置一个随机值,然后在释放时进行读取对比,确保释放的是当前线程拥有的锁。即通过 Redis 结合 Lua 脚本的方案实现。

参考资料: