服务器之家:专注于服务器技术及软件下载分享
分类导航

Mysql|Sql Server|Oracle|Redis|MongoDB|PostgreSQL|Sqlite|DB2|mariadb|Access|数据库技术|

服务器之家 - 数据库 - Redis - 分布式锁实现汇总-详述基于Redis实现的那些细节

分布式锁实现汇总-详述基于Redis实现的那些细节

2023-12-06 05:00未知服务器之家 Redis

为了保证同一时间只有一个线程访问某一代码块,Java中可以使用synchronized语法和ReentrantLock等本地锁的方式。但是在分布式环境下,需要使用分布式锁来保证不同节点的线程同步执行。 常用的分布式锁实现包括以下几种: 基于数据

分布式锁实现汇总-详述基于Redis实现的那些细节

为了保证同一时间只有一个线程访问某一代码块,Java中可以使用synchronized语法和ReentrantLock等本地锁的方式。但是在分布式环境下,需要使用分布式锁来保证不同节点的线程同步执行。

常用的分布式锁实现包括以下几种:

  • 基于数据库的分布式锁:使用数据库的事务和行级锁来实现分布式锁,通过在数据库中创建一张锁表来记录锁的状态。
  • 基于Redis的分布式锁:利用Redis的原子操作和过期时间特性,使用SETNX命令来获取锁,使用DEL命令来释放锁。
  • 基于Zookeeper的分布式锁:利用Zookeeper的有序节点和watch机制,通过创建临时有序节点来实现锁的竞争和释放。

三种分布式锁对比


优点

缺点

数据库

简单,使用方便,不需要引入Redis、zookeeper等中间件

不适合高并发的场景

db操作性能较差,有锁表的风险

redis

性能好,适合高并发场景

较轻量级

较好的框架支持,如Redisson

过期时间不好控制。

需要考虑锁被别的线程误删场景

zookeeper

有较好的性能和可靠性。

有封装较好的框架,如Curator

性能不如redis实现的分布式锁

比较重的分布式锁

【基于Redis实现的分布式锁】

早期版本实现

目前Redis版本已经发布到7.x,生产项目应该不会再使用2.x的版本,这里主要是为了更好的理解各种情况。

在redis2.6.12之前,是通过setnx与expire两个命令配合使用来实现的。setNX命令代表当key不存在时返回成功,否则返回失败,即锁已被其他线程占用。

setnx(key,value);
expire(key,seconds)

这种实现方式把加锁和设置过期时间的步骤分成两步,并不是原子操作,如果加锁成功之后程序崩溃、服务宕机等异常情况,导致没有设置过期时间,那么就会导致死锁的问题,其他线程永远都无法获取这个锁。

如何避免上述问题呢?早期redis版本,可以使用lua脚本,之后的版本,则也可以利用redis命令的扩展参数来实现。继续...

Lua脚本

可以使用Lua脚本来保证原子性(包含setnx和expire两条指令),lua脚本如下:

if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then redis.call('expire',KEYS[1],ARGV[2]) 
	return 1 
else 
  return 0 
end

加锁代码如下:

// 使用lua脚本 保证原子性
public boolean tryLock_with_lua(String key, String UniqueId, int seconds) {
    String lua_scripts = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" +
            "redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end";
    List<String> keys = new ArrayList<>();
    List<String> values = new ArrayList<>();
    keys.add(key);
    values.add(UniqueId);
    values.add(String.valueOf(seconds));
    Object result = jedis.eval(lua_scripts, keys, values);
    return result.equals(1L);
}

SET的扩展命令(SET EX PX NX)

除了使用,使用Lua脚本,保证SETNX + EXPIRE两条指令的原子性,我们还可以使用redis的SET指令扩展参数,它也是原子性的!

SET key value[EX seconds][PX milliseconds][NX|XX]
  • EX seconds: 设定过期时间,单位为秒。
  • PX milliseconds: 设定过期时间,单位为毫秒。
  • NX: 表示key不存在的时候,才能set成功,也即保证只有第一个客户端请求才能获得锁,而其他客户端请求只能等其释放锁,才能获取。
  • XX: 仅当key存在时设置值。

代码如下:

public boolean tryLock_with_set(String key, String UniqueId, int seconds) {
    return "OK".equals(jedis.set(key, UniqueId, "NX", "EX", seconds));
}

value必须要具有唯一性,可以用UUID来做,设置随机字符串保证唯一性。为什么要保证唯一呢?继续...

释放锁

虽然我们在加锁的时候,设置了默认过期时间,但我们肯定不能等着过期再释放锁。当前线程执行完任务之后,需要手动删除key,即释放锁。

// 错误的解锁方法—直接删除key
public void unlock_with_del(Jedis jedis,String key) {
  jedis.del(key);
}

通过del命令直接删除,是否可行呢?结合下图我们来分析一下:

分布式锁实现汇总-详述基于Redis实现的那些细节

线程A加锁同时设置超时时间5秒,结果5s之后程序逻辑还没有执行完成,锁已经释放。线程B此时也来尝试加锁并获得了锁,这时线程A业务执行完成,释放锁,结果释放了线程B持有的锁。也就是说锁被别的线程误删了。如何解决呢?这里就用到了前面提到的UUID,给value值设置一个标记当前线程唯一的随机数,在删除的时候,校验一下。同时,判断是不是当前线程加的锁和释放锁也要保证原子性。

// 使用Lua脚本进行解锁操纵,解锁的时候验证value值
public boolean unlock(Jedis jedis,String key,String value) {
    String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then " +
            "return redis.call('del',KEYS[1]) else return 0 end";
    return jedis.eval(luaScript, Collections.singletonList(key), Collections.singletonList(value)).equals(1L);
}

完成了锁的获取、锁的释放,这就OK了嘛?然而,并非如此。继续...

Redission

(1)超时释放

在分析锁误删问题时提到,线程A设置了5秒超时,但5秒内,业务并未执行完成,而锁已超时释放,从而导致了线程A和B同时持有了锁。如何解决呢?把锁过期时间设置长一些,算是一种解决方案,有没有更好的呢?其实我们设想一下,是否可以给获得锁的线程,开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。这其实就是开源框架Redission采用的思路。

分布式锁实现汇总-详述基于Redis实现的那些细节

redission watchdog (源于网络)

只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间。因此,Redisson就是使用watch dog解决了锁过期释放,业务没执行完问题。

(2)可重入

另外还有一个问题,上述实现并非可重入锁。所谓可重入锁,即当线程在持有锁的情况下再次请求加锁,如果一个锁支持一个线程多次加锁,那么这个锁就是可重入的。如果一个不可重入锁被再次加锁,由于该锁已经被持有,再次加锁会失败。Redis 可通过对锁进行重入计数,加锁时加 1,解锁时减 1,当计数归 0 时释放锁。Redission中也有相关的实现。

分布式锁实现汇总-详述基于Redis实现的那些细节

如果key不存在,通过hash的方式保存,同时设置过期时间,反之如果存在就是+1。对应的就是'hincrby', KEYS[1], ARGV[2], 1这段命令,对hash结构的锁重入次数+1。

Redlock+Redisson

如果线程一在Redis的master节点上拿到了锁,但是加锁的key还没同步到slave节点。恰好这时,master节点发生故障,一个slave节点就会升级为master节点。线程二就可以获取同个key的锁啦,但线程一也已经拿到锁了,锁的安全性就没了。

分布式锁实现汇总-详述基于Redis实现的那些细节

为了解决这个问题,Redis作者 antirez提出一种高级的分布式锁算法:Redlock(Redission中也有相关的实现)。Redlock核心思想是这样的:

搞多个Redis master部署,以保证它们不会同时宕掉。并且这些master节点是完全相互独立的,相互之间不存在数据同步。同时,需要确保在这多个master实例上,是与在Redis单实例,使用相同方法来获取和释放锁。

假设当前有5个Redis master节点,在5台服务器上面运行这些Redis实例。

分布式锁实现汇总-详述基于Redis实现的那些细节

RedLock的实现步骤如下:

  • 获取当前Unix时间,以毫秒为单位。
  • 依次尝试从5个实例,使用相同的key和具有唯一性的value(例如UUID)获取锁。当向Redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试去另外一个Redis实例请求获取锁。
  • 客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(N/2+1,这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
  • 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。
  • 如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁

延伸 · 阅读

精彩推荐
  • RedisRedis如何存储对象与集合示例详解

    Redis如何存储对象与集合示例详解

    redis是一个key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、 zset(sorted set --有序集合)和hash(哈...

    sunjian7942019-11-13
  • Redis如何使用Redis实现电商系统的库存扣减

    如何使用Redis实现电商系统的库存扣减

    在日常开发中有很多地方都有类似扣减库存的操作,本文主要介绍了如何使用Redis实现电商系统的库存扣减,具有一定的参考价值,感兴趣的可以了解一下...

    PHP开源社区6642022-02-10
  • RedisRedis缓存穿透出现原因及解决方案

    Redis缓存穿透出现原因及解决方案

    这篇文章主要介绍了Redis缓存穿透出现原因及解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以...

    Tracydzf2222020-08-04
  • RedisRedis教程之代理ip池设计方法详解

    Redis教程之代理ip池设计方法详解

    这篇文章主要介绍了Redis实现代理ip池的设计方法,文中给出了详细的介绍与示例代码,相信对大家的理解和学习具有一定的参考借鉴价值,有需要的朋友们...

    daisy3252019-11-04
  • Redisredis事务_动力节点Java学院整理

    redis事务_动力节点Java学院整理

    这篇文章主要介绍了redis事务,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧 ...

    huangxincheng5332019-11-07
  • Redisredis 数据删除策略和逐出算法的问题小结

    redis 数据删除策略和逐出算法的问题小结

    这篇文章主要介绍了redis 数据删除策略和逐出算法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下 ...

    BlackMonkey5592020-07-05
  • RedisCentOS系统安装Redis及Redis的PHP扩展详解

    CentOS系统安装Redis及Redis的PHP扩展详解

    这篇文章主要介绍了CentOS系统下安装Redis数据的教程,以及详解了Redis数据库的PHP扩展,文中介绍的很详细,相信对大家的理解和学习具有一定的参考借鉴价...

    上品物语3042019-11-01
  • Redisredis内存空间效率问题的深入探究

    redis内存空间效率问题的深入探究

    redis缓存固然高效,可是它会占用我们系统中宝贵的内存资源,那该如何解决呢?这篇文章主要给大家介绍了关于redis内存空间效率问题的相关资料,需要的...

    CB9512021-08-04