分布式锁

137 阅读2分钟

问题描述

随着业务的发展,原来的单体架构演变成分布式集群后,由于分布式系统多线程,多进程且分布在不同的机器上,这将使原单机部署情况下的并发控制锁策略失效,所以就需要分布式锁解决。

分布式锁主流的实现方案:

  • 基于数据库实现分布式锁
  • 基于缓存(redis)
  • 基于zookeeper

每一种分布式锁解决方案都有各自的优缺点:

  1. 性能:redis最高
  2. 可靠性:zookeeper最高

redis实现分布式锁

基于redis的命令: 不存在才设置并且设置过期时间,原子操作

set key value [EX|PX]  [NX|XX]

设置过期时间有两种方式

  1. 首先通过expire设置过期时间(缺乏原子性,如果在setnx和expire之间出现异常,锁也无法释放)
  2. 在set时指定过期时间(推荐)

image.png

存在问题

可能会释放其他服务器的锁。

场景:如果业务逻辑的执行时间是7s,执行流程如下

  1. index1业务逻辑没执行完,3秒后锁被自动释放。
  2. index2获取到锁,执行业务逻辑,3秒后被自动释放。
  3. index3获取到锁,执行业务逻辑。
  4. index1业务逻辑执行完成,开始调用del释放锁,这时释放的是index3的锁,导致index3的业务只执行1s就被别人释放,相当于index3没有锁。

此方案存在两个问题:

  1. 业务还没执行完,锁就已经过期了,
  2. 可能会释放其他线程的锁。

解决方案:set设置锁的时候把value设置为唯一值(例如uuid),释放前获取这个值,判断是否自己的锁。

优化之UUID防止误删

image.png

但还是存在问题,判断是否是自己的锁和删除锁的操作不具有原子性

比如以下场景:

  1. index1执行删除时,查询到的lock值确实是自己设置的uuid
  2. index1执行del前,刚好锁过期时间到了,此时redis已经没有锁了
  3. index2获取到了锁,开始执行方法
  4. index1开始执行del,此时会把index2的lock删除。因为index1已经判断过uuid并通过了,是具有del删除权限的。

优化之LUA脚本保证删除的原子性

image.png

为了确保分布式锁的可用,我们至少要确保锁的实现同时满足一下四个条件:

  • 互斥性,在任意时刻,只有一个客户端能持有锁
  • 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也会保证后续其他客户端能加锁。
  • 加锁和解锁必须是同一个客户端,不能释放其他人的锁。
  • 加锁和解锁必须具有原子性