分布式锁的几种常见实现方式

数据库乐观锁

实现方式

用表中一行记录来表示锁,其中表示锁的字段采用唯一约束

当多个线程同时执行插入语句时,只有一个能插入成功,可以认为获得到了锁,其他则会报错:ERROR 1062 (23000): Duplicate entry ‘1’ for key ‘uiq_idx_resource’),这种情况可以认为没有获得到锁

当需要释放锁时,可以删除这一条记录就可以了

这种锁的特点:

  • 锁没有失效时间,一旦锁释放失败,便会一直留在数据库中,影响其他线程获取锁。可以采用定时任务对其进行清理
  • 锁依赖数据库,需要考虑单点故障问题。同时也要考虑给数据库性能带来的影响
  • 锁是非阻塞的,一旦插入失败,直接便会报错,需要采用轮询的方式来实现
  • 锁也是非可重入的,一旦一个线程获取到了锁,由于数据库中已经存在了这一条记录,其本身也不能再次获取到这个锁。这一点可以将主机和线程信息存入数据库,如果存在锁,并且锁也归属于当前线程,则可以将此锁再次分配给它

Redis实现

基本上基于setnx命令实现

SETNX 是SET if Not eXists的简写

解释:

当且仅当 key 不存在,将 key 的值设为 value,返回1;
若给定的 key 已经存在,则 SETNX 不做任何动作,返回0。

@Component
public class RedisService {

    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    private static final String COMPARE_AND_DELETE = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";


    public Boolean getLock(String key,String value){
        return redisTemplate.opsForValue().setIfAbsent(key, value, 2, TimeUnit.MINUTES);
    }

    public void releaseLock(String key,String value){
        List<String> keys = Collections.singletonList(key);
        redisTemplate.execute(new DefaultRedisScript<>(COMPARE_AND_DELETE,Long.class), keys, value);
    }


}

这里并没有直接使用delete命令,是为了防止删除其他线程的key。、

比如:A线程锁超时了,B线程使用同样的key获取到了锁,正在执行任务,这时A执行完任务,删除key便会将B的key删除,造成混乱。

使用redis执行lua脚本原子特性,先判断key对应的value,如果和预期的value相同,才删除key(释放锁)。

注:

  • 锁值要保证唯一, 使用4位随机字符串+时间戳基本可满足需求

  • UUID.randomUUID()在高并发情况下性能不佳

  • 使用redisson实现分布式锁

Zookeeper

Zookeeper的数据存储结构就像一棵树,这棵树由节点组成,这种节点叫做Znode。

这种节点可以分为四种:持久节点、顺序节点、临时节点、临时顺序节点,这四种节点的具体含义在此不做赘述。

实现

采用临时顺序节点的方式来实现。

获取锁

  • 如果一个线程想获取一个锁,它就到一个LOCK目录下创建一个临时顺序节点,然后判断自己创建的节点编号是不是最小的那个
  • 如果是,则获得锁
  • 如果不是,则在自己比自己编号小的相邻节点上注册Watcher,用于监听左邻节点是否存在,一旦左邻节点不存在了,则可以通知自己获取锁

PS:

未争抢到锁的线程将会,根据创建节点的顺序在LOCK目录下排好序,类似于AQS

释放锁

  • 当获得锁的线程执行完任务逻辑后,可以删除自己所创建的临时节点,并通知后面一个节点获取到锁

  • 如果线程自己崩溃了,由于创建的临时顺序节点,Zookeeper会将其对应的节点删除,触发后一个线程获取锁

总结

分布式锁优点缺点
数据乐观锁不需依赖其他中间件,实现简单不可续期、不可自动失效、有锁表风险,影响性能
Zookeeper1.有封装好的框架,容易实现。2.有等待锁的队列,大大提升抢锁效率。3. 客户段宕机可自动释放锁,防止死锁。 4. 不会有锁续期的问题。添加和删除节点性能较低
RedisSet和Del指令性能较高1.实现复杂,需要考虑超时,原子性,误删等情形。2.没有等待锁的队列,只能在客户端自旋来等待,效率低下。3. 锁续期的问题需要考虑