Redis系列13:分布式锁实现

redis,系列,分布式,实现 · 浏览次数 : 632

小编点评

**Redis系列 1:深刻理解 Redis** **1. 什么是 Redis?** Redis 是一个分布式内存数据库,它是一种用于缓存数据的分布式系统。它使用 Hash 数据结构来存储数据,并通过一个称为 Hash 的键值对存储数据。 **2. Hash 数据结构的特点** * **key-value 对:**每个 key 对应一个 value。 * **数据类型:**值可以是字符串、数字、布尔值等。 * **分布:**Redis 可以分布到多个节点,从而提高性能。 **3. 常见操作** * **GET:**从 Hash 中获取值。 * **SET:**将键值对添加到 Hash 中。 * **SETEX:**设置 Hash 中的值,并指定有效时间。 * **INCRBY:**将值加到 Hash 中。 * **EXPIRE:**删除 Hash 中的指定时间失效的值。 **4. 可重入锁** 可重入锁是一种可以重新获取锁的锁。这使得在多个线程获取锁之前等待锁释放之前再次获取锁变得可能。 **5. 简单的可重入锁实现** 可以使用 Redis Hash 结构来实现可重入锁。key 表示被锁的共享资源,hash 结构的 fieldKey 的 value 则保存加锁的次数。 **6. 代码示例** ```python import redis # 创建一个 Redis 连接 client = redis.Redis() # 设置可重入锁的键值对 client.set("lock.user_063105015", 10086, 60) # 获取锁的有效时间 lock_name = "lock.user_063105015" unlock_time = client.get("pexpire", lock_name) # 打印锁的有效时间 print("锁的有效时间:", unlock_time) ``` **7. 总结** Redis 是一个强大的分布式内存数据库,可用于缓存数据。Hash 数据结构是 Redis 中常见的数据结构,可用于存储键值对。可重入锁是一种可以重新获取锁的锁,有助于提高性能。

正文

Redis系列1:深刻理解高性能Redis的本质
Redis系列2:数据持久化提高可用性
Redis系列3:高可用之主从架构
Redis系列4:高可用之Sentinel(哨兵模式)
Redis系列5:深入分析Cluster 集群模式
追求性能极致:Redis6.0的多线程模型
追求性能极致:客户端缓存带来的革命
Redis系列8:Bitmap实现亿万级数据计算
Redis系列9:Geo 类型赋能亿级地图位置计算
Redis系列10:HyperLogLog实现海量数据基数统计
Redis系列11:内存淘汰策略
Redis系列12:Redis 的事务机制

1 先来了解下分布式锁

1.1 什么是分布式锁

分布式锁,即分布式系统中的锁,我们通过锁解决 控制共享资源访问 的问题,来保证只有一个线程可以访问被保护的资源。

1.2 分布式锁的实现方案

  • 基于数据库实现分布式锁
  • 基于Zookeeper实现分布式锁
  • 基于Redis实现分布式锁

等等,本篇基于Redis角度进行讨论

1.3 分布式锁满足哪些特性

  • 互斥性:在分布式系统下,一个事件在同一个时间内只能被一个线程执行,即只能有一个线程持有锁。
  • 安全性:可以方便的获取锁和释放锁,不产生死锁情况
  • 过期性:具备锁失效机制,即可以在时效预期外自动解锁,防止死锁
  • 可重入:具备可重入特性(可理解为重新进入,由多于一个任务并
  • 高性能:高性能的获取锁与释放锁
  • 高可用性:高可用的获取锁与释放锁

1.4 互斥特性

1.4.1 实现互斥特性

1.4.1.1 SETNX命令

SETNX 是 set if not exists 的缩写,当且仅当 key 不存在时,则设置 value 给这个key。若给定的 key 已经存在,则 SETNX 不做任何动作。
命令的返回值说明:

  • 1:说明该进程获得锁,将 key 的值设为 value
  • 0:说明其他进程已经获得了锁,进程不能进入临界区。

举例说明:setnx lock.key lock.value

> SETNX lock.user_063105015 1
(integer) 1 # 获取编号为 063105015 用户成功

如果已经被获取过了,则获取失败

> SETNX lock.user_063105015 1
(integer) 0 # 获取编号为 063105015 用户失败

1.4.1.2 get命令

获取key的值,如果存在,则返回;如果不存在,则返回nil

# 获取成功
> GET lock.user_063105015
"1"

# 获取失败
> GET lock.user_123456789
(nil)

1.4.1.3 getset命令

原子的设置值的办法,对key设置newValue这个值,并且返回key原来的旧值。

# 重置用户信息
> getset lock.user_063105015 0
"1"  # 原值为1

# 再次重置
> getset lock.user_063105015 1
"0"  # 原值为0

1.4.1.4 删除命令,用完之后进行锁释放

> DEL lock.user_063105015
(integer) 1

具体执行流程如下:
image

1.3.1.5 异常导致的锁释放问题

可能会因为一些场景,造成锁无法释放,如下:

  • 调用服务或者客户端崩溃,无法正确的处理锁释放的工作。
  • 业务程序的异常执行,没有操作释放锁的 DEL指令。
    这种情况下,锁就会一直占用着,不会被释放,其他线程也无法获得。所以必须得有个自动释放锁的过程。

1.4.2 超时释放

超时释放其实就是重置,目的是避免因为各种原因导致的锁长时间无法释放。
做法就是我们给锁加个过期时间(EXPIRE Time):

# 给用户 063105015 加锁
> SETNX lock.user_063105015 1 
(integer) 1

# 设置过期时间,到时间没删除则自动释放
> EXPIRE lock.user_063105015 120 # 120秒之后自动释放
(integer) 1

为了保证执行时的原子性,Redis 官方扩展了 SET 命令,既能满足获取对象,又能保证设置超时的时间语义。
避免出现了获取锁完成之后,执行超时设置失败微软无法释放锁的情况。保证要么都成功,要么都不执行。

# 示例如下:
SET lock.user_063105015 1  NX PX 60000
  • NX:就是Not Exist,表示只有用户编号为 063105015 不存在的时候才可以 SET 成功,并且只有单个线程可以获取锁;
  • PX 60000:表示对这个锁设置一个60s的过期时间。

1.4.3 对锁进行唯一标识

经常会出现一种情况,就是你获取到锁之后,因为各种原因(比如你的服务线程故障、网络抖动 等等),没有执行完成,或者没有释放锁,
这时候锁也过了 EXPIRE TIME,就自动释放了。当另外一个线程开锁成功,你的线程响应过来了,把人家的锁给释放了,这样就有问题了。
为了避免这种操作,我们要对同一个的锁做唯一识别码,在释放锁之前,先判断下是不是自己设置的那个锁,如下:

# 设置10086专用值
> SET lock.user_063105015 10086 NX PX 60000
OK

# 设置成功,获取检查确实是10086
> get lock.user_063105015
"10086"

# 伪代码:删除前进项确认是不是自己加的那个锁
if ( redis.get("lock.user_063105015").equals("10086")) {
   redis.del("lock.user_063105015");  // 只有对比成功才进行删除,释放锁
 }

1.4.4 实现可重入锁

可重入锁可以理解为重新进入,由多于一个任务并发使用,而不必担心数据错误。

  • 可重入性就就保证线程能继续执行,防止在同一线程中多次获取锁而导致死锁发生
  • 不可重入就是需要等待锁释放之后,再次获取锁成功,才能继续往下执行

这边说说可重入锁,比如你执行线程的方案a获取锁之后,你的a方法后,线程继续执行b方法也需要获取锁,如果这时候不可重入,
线程就需要等待锁的释放,进入争抢。
这边的解法就是对线程加锁的锁值进行增减,同一个线程的方法遇到加锁则锁值+1,遇到退锁则锁值-1,当前仅当锁值=0的时候,说明这个锁真正的被释放了。
Java中的Redisson 类库就是通过 Redis Hash 来实现可重入锁。

加锁的逻辑
我们可以使用 Redis hash 结构实现,key 表示被锁的共享资源, hash 结构的 fieldKey 的 value 则保存加锁的次数。
image
实现如下( KEYS1 = "lock.user_063105015", ARGV [10000,uuid):
KEYS[1] = key的值
ARGV[1]) = 持有锁的时间
ARGV[2] = getLockName(threadId) 下面id就算系统在启动的时候会全局生成的uuid 来作为当前进程的id,加上线程id就是getLockName(threadId)了,可以理解为:进程ID+系统ID = ARGV[2]

# 1 为 true
# 0 为 false

if (redis.call('exists', KEYS[1]) == 0) then 
    redis.call('hincrby', KEYS[1], ARGV[2], 1); 
    redis.call('pexpire', KEYS[1], ARGV[1]); 
    return nil; 
    end; 
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then 
    redis.call('hincrby', KEYS[1], ARGV[2], 1); 
    redis.call('pexpire', KEYS[1], ARGV[1]); 
    return nil; 
    end; 
return redis.call('pttl', KEYS[1]);

参数说明

  • hincrby :将hash中指定域的值增加给定的数字
  • pexpire:设置key的有效时间以毫秒为单位
  • hexists:判断field是否存在于hash中
  • pttl:获取key的有效毫秒数

程序说明

  • Redis exists 命令判断 lock.user_063105015 锁是否存在
  • 锁不存在,hincrby 创建一个键为 lock.user_063105015 的 hash 表,键为 uuid,初始化值为 0,然后再次加 1,最后设置过期时间。
  • 锁存在,hexists判断 lock 对应的 hash 表中是否存在 uuid 键,存在则 + 1,并重置过期时间
  • 不符合以上的条件的都走到默认返回

与Redis系列13:分布式锁实现相似的内容:

Redis系列13:分布式锁实现

[Redis系列1:深刻理解高性能Redis的本质](https://www.cnblogs.com/wzh2010/p/15886787.html "Redis系列1:深刻理解高性能Redis的本质") [Redis系列2:数据持久化提高可用性](https://www.cnblogs.com/w

redis系列02---缓存过期、穿透、击穿、雪崩

一、缓存过期 问题产生的原由: 内存空间有限,给缓存设置过期时间,但有些键值运气比较好,每次都没有被我的随机算法选中,每次都能幸免于难,这可不行,这些长时间过期的数据一直霸占着不少的内存空间! 解决方案: redis提供8种策略供应用程序选择,用于我遇到内存不足时该如何决策: * noevictio

[转帖]Redis系列(十五)、Redis6新特性之集群代理(Cluster Proxy)

在之前的文章中介绍了Redis6的集群搭建和原理,我们可以使用dummy和smart客户端连接集群,本篇介绍Redis6新增的一个功能:集群代理。客户端不需要知道集群中的具体节点个数和主从身份,可以直接通过代理访问集群,对于客户端来说通过集群代理访问的集群就和单机的Redis一样,因此也能解决很多集

[转帖]Redis系列(十六)、Redis6新特性之IO多线程

https://blog.csdn.net/wsdc0521/article/details/106766587 终于,Redis的多线程版本横空出世,大大提高了并发,本篇就带大家来看看什么是IO多线程,和我们理解的多线程有什么区别,与Memcached的多线程又有什么区别。 目录 介绍 为什么Re

[转帖]Redis系列(十七)、Redis中的内存淘汰策略和过期删除策略

我们知道Redis是分布式内存数据库,基于内存运行,可是有没有想过比较好的服务器内存也不过几百G,能存多少数据呢,当内存占用满了之后该怎么办呢?Redis的内存是否可以设置限制? 过期的key是怎么从内存中删除的?不要怕,本篇我们一起来看一下Redis的内存淘汰策略是如何释放内存的,以及过期的key

[转帖]Redis系列(十五)、Redis6新特性之集群代理(Cluster Proxy)

在之前的文章中介绍了Redis6的集群搭建和原理,我们可以使用dummy和smart客户端连接集群,本篇介绍Redis6新增的一个功能:集群代理。客户端不需要知道集群中的具体节点个数和主从身份,可以直接通过代理访问集群,对于客户端来说通过集群代理访问的集群就和单机的Redis一样,因此也能解决很多集

[转帖]【Redis系列】Redis发布版本历史及特性

目录 概述Redis2.6Redis2.8Redis3.0Redis3.2Redis4.0Redis5.0Redis6.0Redis7.0 概述 Redis 使用标准版本标记进行版本控制:major.minor.patchlevel。 偶数的版本号表示稳定的版本, 例如 1.2,2.0,2.2,2.

Redis系列8:Bitmap实现亿万级数据计算

Redis系列1:深刻理解高性能Redis的本质 Redis系列2:数据持久化提高可用性 Redis系列3:高可用之主从架构 Redis系列4:高可用之Sentinel(哨兵模式) Redis系列5:深入分析Cluster 集群模式 追求性能极致:Redis6.0的多线程模型 追求性能极致:客户端缓

Redis系列9:Geo 类型赋能亿级地图位置计算

Redis系列1:深刻理解高性能Redis的本质 Redis系列2:数据持久化提高可用性 Redis系列3:高可用之主从架构 Redis系列4:高可用之Sentinel(哨兵模式) Redis系列5:深入分析Cluster 集群模式 追求性能极致:Redis6.0的多线程模型 追求性能极致:客户端缓

Redis系列10:HyperLogLog实现海量数据基数统计

Redis系列1:深刻理解高性能Redis的本质 Redis系列2:数据持久化提高可用性 Redis系列3:高可用之主从架构 Redis系列4:高可用之Sentinel(哨兵模式) Redis系列5:深入分析Cluster 集群模式 追求性能极致:Redis6.0的多线程模型 追求性能极致:客户端缓