Redis事务机制

Redis的事务机制

一、事务

所谓事务(Transaction) ,是指作为单个逻辑工作单元执行的一系列操作。

事务的ACID特性回顾

  • Atomicity(原子性):构成事务的的所有操作必须是一个逻辑单元,要么全部执行,要么全部不执行;
  • Consistency(一致性):数据库在事务执行前后状态都必须是稳定的或者是一致的;
  • Isolation(隔离性):事务之间不会相互影响;
  • Durability(持久性):事务执行成功后必须全部写入磁盘;

redis中的ACID解释:

  • Atomicity(原子性):一个队列中的命令 执行或者不执行;
  • Consistency(一致性):单机可以,集群中不能保证实时的一致性,只能保证最终一致性;
  • Isolation(隔离性):redis单进程单线程的,Redis命令是顺序执行的,在一个事务中,有可能被执行其他客户端的命令;
  • Durability(持久性):Redis有持久化但不保证数据的完整性;

 

二、Redis事务

  • Redis的事务是通过multi、exec、discard和watch这四个命令来完成的;
  • Redis的单个命令都是原子性的,所以这里需要确保事务性的对象是命令集合;
  • Redis将命令集合序列化并确保处于同一事务的命令集合连续且不被打断的执行;
  • Redis不支持回滚操作;

应用场景:实现乐观锁的。

扩展理解:

  • Redis事务就是一个命令执行的队列,将一系列预定义命令包装成一个整体(一个队列), 当执行时,一次性按照添加顺序依次执行,中间不会被打断或者干扰。
  • 简单的理解,就是一个队列中,一次性,顺序性,排他性的执行一系列命令。
  • 为了避免Redis执行指令过程中,多条连续执行的指令被干扰, 打断,,插队这种情况, 就需要开启事务。

 

三、事务命令

  • 开启事务 multi:用于标记事务块的开始,Redis会将后续的命令逐个放入队列中,然后使用exec原子化地执行这个命令队列;
  • 执行事务 exec:执行命令队列,与multi成对出现,成对使用。
    • 注意: 加入事务的命令暂时进入到任务队列中,并没有立即执行,只有与执行exec命令才开始执行。
  • 取消事务 discard:清除命令队列,发生在multi之后,exec之前;
  • watch:监视key
  • unwatch:清除监视key

 

在 Redis 中使用事务会经过 3 个过程:

(1)开启事务:multi 执行该命令表示一个事务快的开始,在开启完事务的时候,每次操作的命令将会被插入到一个队列中并返回QUEUED,同时这个队列中的命令在事务没有被提交之前不会被实际执行 –>没有隔离级别的概念,但是总归是具有隔离性,毕竟不会受到别的命令打断

例:

(2)执行事务:exec 执行该命令后,redis会执行事务块里面的所有命令,该命令需要和 multi 命令成对使用,事务不保证原子性且没有回滚,任意命令执行失败还是会接着往下执行 –>不保证原子性

例:

(3)取消事务:discard 执行命令后,放弃执行该事务的所有命令,取消该事务,该命令需要和 multi 命令成对使用。

总体执行流程:

 

 

四、事务机制

事务的执行

(1)事务开始 multi

在RedisClient中,有属性flags,用来表示是否在事务中;

flags=REDIS_MULTI标识在事务中;

(2)命令入队

RedisClient将命令存放在事务队列中(EXEC,DISCARD,WATCH,MULTI除外);

(3)事务队列

multiCmd *commands 用于存放命令;

(4)执行事务 exec

RedisClient向服务器端发送exec命令,RedisServer会遍历事务队列,执行队列中的命令,最后将执行的结果一次性返回给客户端。

如果某条命令在入队过程中发生错误,redisClient将flags置为REDIS_DIRTY_EXEC,EXEC命令将会失败返回。

 

事务的工作流程:

事务工作流程解读:

  1. 执行指令
  2. 服务器接收指令
  3. 服务器判断是否存在事务状态
    1. 不存在事务状态
    2. 判断是否是事务指令
      1. 不是事务指令
        1. 普通指令就普通执行
      2. 是事务指令
        1. 是multi指令,开启队列 !
        2. 是discard指令,报错:没有multi
        3. 是exec指令,报错:没有multi
    3. 存在事务状态
    4. 判断是否是事务指令
      1. 不是事务指令
        1. 普通指令就普通执行
      2. 是事务指令
        1. 是multi指令,报错:不能被嵌套
        2. 是discard指令,销毁队列 !
        3. 是exec指令,执行事务队列 !

 

Watch的执行

使用WATCH命令监视数据库键:

redisDb有一个watched_keys字典,key是某个被监视的数据的key,值是一个链表。记录了所有监视这个数据的客户端。

监视机制的触发:

当修改数据后,监视这个数据的客户端的flags置为REDIS_DIRTY_CAS。

事务执行:

RedisClient向服务器端发送exec命令,服务器判断RedisClient的flags,如果为REDIS_DIRTY_CAS,则清空事务队列。

 

 

五、Redis的弱事务性

Redis语法错误:原子性

整个事务的命令在队列里都清除; flag = multi_dirty

可以看到下图,exec之后直接error了:

Redis运行错误:弱事务性 --保证不了原子性

在队列里正确的命令可以执行 (弱事务性),错误的不被执行,不会整体回滚,只能手动回滚数据。

弱事务性 :

  1. 在队列里正确的命令可以执行 (非原子操作);
  2. 不支持回滚;

可以看到下图,exec之后,第一个命令OK,第二命令error了:

Redis不支持事务回滚(为什么呢)

  1. 大多数事务失败是因为语法错误或者类型错误,这两种错误,在开发阶段都是可以预见的
  2. Redis为了性能方面就忽略了事务回滚。 (回滚记录历史版本)

 

实战:语法错误和执行错误

保证不了原子性

注意:

  • 单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的,也不具有一致性。
  • redis的事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。

 

事务回滚

如何手动进行事务回滚呢?

(1)记录操作过程中被影响的数据之前的状态

单数据: String

多数据:hash, list, set, zset

(2)设置指令恢复所有的被修改的项

单数据: 直接set回去,注意该值是否有时效, 如有也有设置回去

多数据: 修改对应值或者整体克隆复制.

 

六、锁 - watch key

watch key 监听key的是否被修改,如果修改过不执行事务!

Watch的执行

  • 使用WATCH命令监视数据库键,redisDb有一个watched_keys字典,key是某个被监视的数据的key,值是一个链表。记录了所有监视这个数据的客户端。
  • 监视机制的触发,当修改数据后,监视这个数据的客户端的flags置为REDIS_DIRTY_CAS。
  • 事务执行时,RedisClient向服务器端发送exec命令,服务器判断RedisClient的flags,如果为REDIS_DIRTY_CAS,则清空事务队列。

watch 命令

watch key1 [key2……]

watch命令用于监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被拒绝执行,并向客户端返回代表事务执行失败的空回复,其实就是大家都监控同一个或几个点,我想操作什么东西的时候,只要大家都没有动它,那么我就会进行操作,如果发现有人动了即会被监控发现触发了这个条件,那我就取消这次事务操作!

每个redis数据库都保存着一个 watched_keys 字典,这个字典的键是某个被 watch 命令监视的数据库键,而字典的值则是一个链表,链表中记录了所有监视相应数据库键的客户端,数据结构如下图:

c1…c2代表有c1和c2两个客户端在监控name这个key

所有对数据库进行修改命令,在执行之后都会对 watched_keys 字典进行检查,查看是否有客户端正在监视刚刚被命令修改过的数据库键,如果有,则会将监视被修改键的 reids_dirty_cas 标识打开,表示该客户端的事务安全性已经被破坏。如果在事务提交时,检测到该标识被打开,则会拒绝执行它们提交的事务,以此来保证事务的安全性。

WATCH 只能在客户端进入事务状态之前执行,在事务状态下发送 WATCH 命令会引发一个错误,但它不会造成整个事务失败,也不会修改事务队列中已有的数据。

那么 watch 会不会出现ABA问题?不会存在ABA问题;

unwatch 命令

unwatch //取消对所有key的监控

如果在执行 WATCH 命令之后, EXEC 命令或 DISCARD 命令先被执行了的话,那么就不需要再执行 UNWATCH 。因为 EXEC 命令会执行事务,因此 WATCH 命令的效果已经产生了;而 DISCARD 命令在取消事务的同时也会取消所有对 key 的监视,因此这两个命令执行之后,就没有必要执行 UNWATCH 。

 

应用实例:事务-锁

实际开发过程中,为了高可用,会有多个客户端有操作Redis的权限。 为了保证同一个key只能被一个的客户端增删改操作,需要对同一数据加锁。

场景分析:

天猫双11热卖过程中, 对已经售罄的货物追加补货, 4个业务员都有权限补货, 只能被一个业务员连续操作, 不能被多个业务员重复补货, 如何解决呢?

  1. 多个客户端有可能同时操作同一个key, 且该key只能被修改一次.
  2. 如果修改后, 后面再被修改则要终止当前的操作.
  3. 肯定也要添加事务, 涉及增删改问题肯定是要添加事务的.

解决方案

  • 对同一个要修改的key添加监听锁
  • 开启事务
  • 一旦该key发生改变, 当前客户端操作的事务则失败

watch key1 [key2, key3......] # 监听指定的key multi # 开启事务 ... # 进行一些列操作 exec # 提交事务

  • 如何取消监视呢? 可以取消对所有key的监视

unwatch

因此, 这也是Redis的一个应用场景,Redis应用基于状态控制的批量任务执行。

 

watch监控的是key的增删改,setnx则是value的;

 

Redis分布式锁

分布式锁是什么

实际开发过程中, 也会遇到如下场景,不但要保证同一个key只能被一个的客户端增删改操作,还要监控该key对应的value值,这时就需要设置分布式锁了.

场景分析:

还是天猫双11热卖过程中, 怎么避免最后一件商品不被多人同时购买(超卖问题)

  1. watch监听能监听特定的key是否被修改, 但是无法监听被修改的值, 此处要监控的是具体的数据.
  2. 虽然Redis是单线程的, 但是多个客户端对同一数据同时进行操作时, 如何避免不被同时修改呢?

解决方案

  1. 使用setnx设置一个公共锁setnx lock-key value, value可以为随机任意值.
  2. setnx命令能返回value值.只有第一次执行的才会成功并返回1,其它情况返回0:
    1. 如果返回是1, 说明没有人持有锁, 当前客户端设置锁成功,可以进行下一步的具体业务操作.
    2. 如果返回是0, 说明有人持有了锁, 当前客户端设置锁失败, 那么需要排队或等待锁的释放.
  3. 操作完毕通过del操作释放锁.

这就是Redis分布式锁的雏形,当然实际开发中要考虑锁时效性避免死锁问题,还要避免锁误删问题,因此有接下来几种版本的分布式锁

 

 

3.2 分布式锁版本1

3.2.1 业务场景 --死锁的解决方案-超时时间

场景分析

依赖分布式锁的机制, 某个用户操作Redis时对应的客户端宕机了, 且此时已经获取到锁,导致锁一直被持有, 其他客户端拿不到锁,这就是死锁问题, 如何解决呢?

  • 由于锁操作由用户控制加锁解锁, 必定会存在加锁未解锁的风险
  • 需要解锁操作不能仅依赖用户控制, 系统级别要给出对应的保底处理方案.

解决方案

  1. 使用expire为锁key添加时间限定, 到时不释放锁, 则放弃锁. 非原子操作还是存在死锁问题

setnx lock-key 001 # 设置分布式锁 有值则返回设置失败: 0;无值则返回设置成功: 1 expire lock-key second # 设置单位为秒 pexpire lock-key milliseconds # 设置单位为毫秒

  1. 或者直接设置的时候添加时间限制,可以解决死锁问题

et lock-key value NX PX 毫秒数 # 比如为key为name设置分布式锁 set lock-name 001 NX PX 5000

value可以是任意值

NX代码只有lock-key不存在时才设置值

实际开发中如何知道设置多少时间合适?》

由于操作通常都是微秒或毫秒级,因此该锁定时间不宜设置过大。具体时间需要业务测试后确认。

例如:持有锁的操作最长执行时间127ms,最短执行时间7ms。

  1. 测试百万次最长执行时间对应命令的最大耗时,测试百万次网络延迟平均耗时锁
  2. 时间设定推荐:最大耗时*120%+平均网络延迟*110%
  3. 如果二者相差2个数量级,取其中单个耗时较长即可

 

3.2.2 流程图

  • 1、通过set命令设置锁
  • 2、判断返回结果是否是OK
    • 1)Nil,获取失败,结束或重试(自旋锁)
    • 2)OK,获取锁成功
      • 执行业务
      • 释放锁,DEL 删除key即可
  • 3、异常情况,服务宕机。超时时间EX结束,会自动释放锁

 

3.3 分布式锁版本2

3.3.1 业务场景-怎么解决删除别人的锁

场景分析

  1. 三个进程:A和B和C,在执行任务,并争抢锁,此时A获取了锁,并设置自动过期时间为20ms
  2. A开始执行业务,因为某种原因,业务阻塞,耗时超过了20ms,此时锁自动释放了。
  3. B恰好此时开始尝试获取锁,因为锁已经自动释放,成功获取锁。
  4. A此时业务执行完毕,执行释放锁逻辑(删除key),于是B的锁被释放了,而B其实还在执行业务。
  5. 此时进程C尝试获取锁,也成功了,因为A把B的锁删除了。

问题(1):使用版本1的分布式锁,有可能B和C同时获取到锁,违反了锁只能被一个客户端持有的特性。

如何解决这个问题呢?我们应该在删除锁之前,判断这个锁是否是自己设置的锁,如果不是(例如自己的锁已经超时释放),那么就不要删除了.

问题(2):如何得知当前获取锁的是不是自己呢?

 

解决方案

  1. 我们可以在set 锁时,存入自己的信息!删除锁前,判断下里面的值是不是与自己相等,如果不等,就不要删除了。
  2. 这里自己的信息通常是一个随机值 + 当前线程的id,通过UUID.randomUUID().toString()+Thread.currentThread().getId()获取到。

3.3.2 流程图

 

  • 1、通过set命令设置锁
  • id通常是一个随机值+当前线程的id.
  • 2、判断返回结果是否是OK
    • 1)Nil,获取失败,结束或重试(自旋锁)
    • 2)OK,获取锁成功
      • 执行业务
      • get lock判断返回的id是否一致
        • 一致则释放锁,DEL 删除key即可
        • 不一致则不释放锁.
  • 3、异常情况,服务宕机。超时时间EX结束,会自动释放锁

 

3.4 分布式锁版本3

3.4.1 业务场景--重入锁

场景分析

就是实际开发过程中,一段代码内部会有嵌套方法, 外层方法获取到锁后, 内层再去获取时由于锁已经存在了就无法获取了, 但内层代码不执行完外层也释放不了锁啊,这就是方法嵌套导致的死锁问题,怎么解决呢?

解决方案

  1. 让锁成为可重入锁, 也就是外层代码获取到这把锁, 内层代码可以获取到该锁。
  2. 获取时判断是不是自己的锁,是则继续使用,而且要记录重入的次数。
  3. 这里的锁不能使用之前的String类型作为lock-key的值了, 锁的value要使用hash结构

hset lock-key 线程信息,重入次数(默认1) NX PX 毫秒数

key: lock-key

value-key:线程信息

value-value:重入次数

3.4.2 流程图

下面我们假设锁的key为“lock”,hashKey是当前线程的id:“threadId”,锁自动释放时间假设为20

 

获取锁的步骤:

  • 1、判断lock是否存在 EXISTS lock
  • 返回1则存在,说明有人获取锁了,下面判断是不是自己的锁
    • 判断当前线程id作为hashKey是否存在:HEXISTS lock threadId
      • 存在,说明是自己获取的锁,重入次数+1:HINCRBY lock threadId 1,去到步骤3
      • 不存在,说明锁已经有了,且不是自己获取的,锁获取失败,end
  • 2、返回0则不存在,说明可以获取锁,并存入value值HSET key threadId 1
  • 3、设置锁自动释放时间,EXPIRE lock 20

 

释放锁的步骤:

1、判断当前线程id作为hashKey是否存在:HEXISTS lock threadId

  • 存在,说明锁还在,重入次数减1:HINCRBY lock threadId -1,获取新的重入次数.
  • 不存在,说明锁已经失效,不用管了

2、判断重入次数是否为0:

  • 为0,说明锁全部释放,删除key:DEL lock
  • 大于0,说明锁还在使用,重置有效时间:EXPIRE lock 20

 

 

集群模式的Redis分布式锁

 

RedLock算法

这个场景是假设有一个redis cluster,有5个redis master实例。然后执行如下步骤获取一把锁:

  1. 获取当前时间戳,单位是毫秒
  2. 轮流尝试在每个master节点上创建锁,过期时间较短,一般就几十毫秒
  3. 尝试在大多数节点上建立一个锁,比如5个节点就要求是3个节点(n / 2 +1)
  4. 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了
  5. 要是锁建立失败了,那么就依次删除这个锁
  6. 只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁

 

Redission

虽然我们已经实现了分布式锁,能够满足大多数情况下的需求,不过我们的代码并不是万无一失。

某些场景下,可能需要实现分布式的不同类型锁,比如:公平锁、互斥锁、可重入锁、读写锁等等。实现起来比较麻烦。而开源框架Redission就帮我们实现了上述的这些 锁功能,而且还有很多其它的强大功能。

 

Redission流程图

 

  • 加锁机制: 如果某个客户端要加锁, 它面对的是Redis Cluster集群, 首先会根据hash节点选择一台机器.
  • 锁互斥机制: 这个时候如果客户端2来尝试家锁, 发现myLock这个锁Key已经存在了, 在Mylock这个锁key的剩余时间内, 客户端2会进入一个while循环, 不停的尝试加锁.
  • watch dog自动延期机制: 客户端1一旦加锁成功, 就会启动一个watch dog看门够, 他是一个后台线程,会每隔10秒检查一下, 如果客户端1还持有锁key, 那么就会不断的延长锁key的生存时间.
  • 可重入加锁机制: 执行可重入锁,会对客户端1的加锁次数, 累加1.
  • 锁释放机制: 执行释放锁, 就会对和护短加锁次数减1. 如果发现锁此时是0, 就从Redis中删除这个key, 另外客户端2就可以尝试加锁了.

 

来源:

参考是和多个大神总结得到自己的笔记

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

松鼠喵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值