写点什么

Redis 实现分布式锁的几种方案

作者:GreatSQL
  • 2023-03-09
    福建
  • 本文字数:2364 字

    阅读完需:约 8 分钟

Redis实现分布式锁的几种方案
  • GreatSQL 社区原创内容未经授权不得随意使用,转载请联系小编并注明来源。

  • GreatSQL 是 MySQL 的国产分支版本,使用上与 MySQL 一致。

  • 文章来源:GreatSQL 社区原创

1.前言

对于 Redis 实现分布式锁的几种方案这个话题,展开之前我想先简单聊聊什么是分布式锁,分布式锁的使用场景,除了 Redis 外还有什么技术实现分布式锁等一系列内容。

1.1 分布式锁

说大一点,就是在现在发展越来越迅速的大背景下,去中心化分布式系统越来越普及,在我们实际的生产开发当中,有一种不可避免的场景就是多个进程互斥的对其资源的使用,为了保证数据不重复,要求在同一时刻,同一任务只在一个节点上运行,且保证在多进程下的数据安全,分布式锁就十分重要了。

1.2 分布式锁的几种方案

方式有很多种,根据技术角度的不同


有基于 MySQL 的方式,通过表的唯一索引,通过 insert 和 delete 就可以实现加锁和解锁的效果;


有基于 zookeeper 的方式,通过创建临时有序节点,判断创建的节点序号是否最小。若是,则表示获取到锁,不是,则 watch /lock 目录下序号比自身小的前一个节点,解锁只需要删除节点;


有基于 Redis 的方式。通过执行 setnx,若成功再执行 expire 添加过期时间的方式加锁,解锁执行 delete 命令。


方式有很多,不一一列举了。

1.3Redis 分布式锁需要满足的条件

  • 互斥性。在任意时刻,只有一个客户端能持有锁。

  • 不发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁也能保证后续其他客户端能加锁。

  • 同一性。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了,即不能误解锁。

  • 容错性。只要大多数 Redis 节点正常运行,客户端就能够获取和释放锁。

2.Redis 实现分布式锁的几种方案

可以通过以下方式实现(包括但不限于):


  • SETNX + EXPIRE

  • SETNX + value(系统时间+过期时间)

  • 通过开源框架-Redisson


简单来说说,用 Java 代码演示:

2.1 SETNX + EXPIRE

setnx(SET IF NOT EXISTS)+ expire命令。先用setnx来抢锁,如果抢到锁,再用expire给锁设置一个过期时间,这样持有锁超时时释放锁,防止锁忘记释放。但此时setnxexpire两个命令无法保证原子性,例如:


if(jedis.setnx(key_resource_id,lock_value) == 1){ //加锁    expire(key_resource_id,100); //设置过期时间    try {        //业务代码块    }catch() {    }finally {       jedis.del(key_resource_id); //释放锁    }}
复制代码

2.2 SETNX + value(系统时间+过期时间)

可以把过期时间放到setnx的 value 值里面。如果加锁失败,再拿出 value 值校验一下即可。加锁代码如下:


long expires = System.currentTimeMillis() + expireTime; //系统时间+设置的过期时间String expiresStr = String.valueOf(expires);// 如果当前锁不存在,则加锁成功if (jedis.setnx(key_resource_id, expiresStr) == 1) {        return true;} // 如果锁已经存在,获取锁的过期时间String currentValueStr = jedis.get(key_resource_id);
// 如果获取到的过期时间,小于系统当前时间,表示已经过期if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) { // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间 String oldValueStr = jedis.getSet(key_resource_id, expiresStr); if (oldValueStr != null && oldValueStr.equals(currentValueStr)) { // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才可以加锁 return true; }}//其他情况均返回加锁失败return false;
复制代码

2.3 通过开源框架-Redisson

那么此时就要去想了,如果已经超过了加锁的过期时间,可是业务还没执行完成,这个时候怎么做呢?是把过期时间延长吗?显然不合理,可以通过开源框架-Redisson 优化这个问题,简单来说,Redisson 就是当一个线程获得锁以后,给该线程开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。假设两个线程争夺统一公共资源:线程 A 获取锁,并通过哈希算法选择节点,执行 Lua 脚本加锁,同时其看门狗机制会启动一个watch dog(后台线程),每隔 10 秒检查线程,如果线程 A 还持有锁,那么就会不断的延长锁 key 的生存时间。线程 B 获得锁失败,就会订阅解锁消息,当获取锁到剩余过期时间后,调用信号量方法阻塞住,直到被唤醒或等待超时。一旦线程 A 释放了锁,就会广播解锁消息。于是,解锁消息的监听器会释放信号量,获取锁被阻塞的线程 B 就会被唤醒,并重新尝试获取锁。


Redisson 支持单点模式、主从模式、哨兵模式、集群模式,假设现为单点模式:


//构造ConfigConfig config = new Config();config.useSingleServer().setAddress("redis://ip:port").setPassword("Password.~#").setDatabase(0);//构造RedissonClientRedissonClient redissonClient = Redisson.create(config);//获取锁实例RLock rLock = redissonClient.getLock(lockKey);try {    //获取锁,waitTimeout为最大等待时间,超过这个值,则认为获取锁失败。leaseTime为锁的持有时间    boolean res = rLock.tryLock((long)waitTimeout, (long)leaseTime, TimeUnit.SECONDS);    if (res) {        //业务块    }} catch (Exception e) {}finally{    //解锁    rLock.unlock();}
复制代码

3.小结

Redis 的分布式锁实现方式有很多,这里不一一列举了,有机会再展开 Lua 脚本、分布式锁 Redlock 等内容。




Enjoy GreatSQL :)

关于 GreatSQL

GreatSQL 是由万里数据库维护的 MySQL 分支,专注于提升 MGR 可靠性及性能,支持 InnoDB 并行查询特性,是适用于金融级应用的 MySQL 分支版本。


相关链接: GreatSQL社区 Gitee GitHub Bilibili

GreatSQL 社区:

社区博客有奖征稿详情:https://greatsql.cn/thread-100-1-1.html


技术交流群:

微信:扫码添加GreatSQL社区助手微信好友,发送验证信息加群



发布于: 刚刚阅读数: 3
用户头像

GreatSQL

关注

GreatSQL社区 2023-01-31 加入

GreatSQL是由万里数据库维护的MySQL分支,专注于提升MGR可靠性及性能,支持InnoDB并行查询特性,是适用于金融级应用的MySQL分支版本。 社区:https://greatsql.cn/ Gitee: https://gitee.com/GreatSQL/GreatSQL

评论

发布
暂无评论
Redis实现分布式锁的几种方案_greatsql_GreatSQL_InfoQ写作社区