Redis面试题之Redis事务,Redis分布式锁

1. Redis事务简介

为了避免Redis执行指令过程中, 多条连续执行的指令被干扰, 打断, 插队这种情况, 就需要开启事务.

Redis事务就是一个命令执行的队列, 将一系列预定义命令包装成一个整体(一个队列), 当执行时, 一次性按照添加顺序依次执行, 中间不会被打断或者干扰.

或者简单的理解, 就是一个队列中, 一次性,顺序性, 排他性的执行一系列命令.

2. Redis事务操作

2.1 Redis事务的基本操作

  • 开启事务multi
    作用: 设定事务的开启位置, 次指令后, 后续的所有指定均加入到事务中.
  • 执行事务exec
    作用: 设定事务的结束位置, 同时执行事务, 与multi成对出现, 成对使用.

注意: 加入事务的命令暂时进入到任务队列中, 并没有立即执行, 只有与执行exec命令才开始执行.

  • 取消事务discard
    作用: 终止当前事务的定义, 发生在multi之后, exec之前.

2.2 事务的工作流程

在这里插入图片描述

  • 执行指令
  • 服务器判断指令是否是事务指令
    • 普通指令就普通执行
    • 如果是multi就创建队列, 并返回ok
      • 继续执行指令
      • 服务器继续判断指令是否是事务指令
        • 如果是exec, 则执行事务, 并返回队列中所有结果
        • 如果是普通指令, 则加入队列中, 返回queued
        • 如果是discard, 则销毁队列, 返回ok

2.3 使用事务的注意事项

  • 如果开启事务后, 命令中出现了语法错误, 则导致该事务队列失效(之前书写正确的也失效了), 这就是都会整体失败.
  • 如果开始事务后, 命令执行时出现了错误, 则提交事务后, 正确的被执行了, 错误的不被执行, 也就是说不会整体回滚, 那只能手动回滚数据.
  • 如何手动进行事务回滚呢?
  1. 记录操作过程中被影响的数据之前的状态
    单数据: String
    多数据:hash, list, set, zset
  2. 设置指令恢复所有的被修改的项
    单数据: 直接set回去,注意该值是否有时效, 如有也有设置回去
    多数据: 修改对应值或者整体克隆复制.

2.4 基于特定条件的事务执行-锁

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

  • 场景分析
    天猫双11热卖过程中, 对已经售罄的货物追加补货, 4个业务员都有权限补货, 只能被一个业务员连续操作, 不能被多个业务员重复补货, 如何解决呢?
    1. 多个客户端有可能同时操作同一个key, 且该key只能被修改一次.
    2. 如果修改后, 后面再被修改则要终止当前的操作.
    3. 肯定也要添加事务, 涉及增删改问题肯定是要添加事务的.
  • 解决方案
    1. 对同一个要修改的key添加监听锁
    2. 开启事务
    3. 一旦该key发生改变, 当前客户端操作的事务则失败
      watch key1 [key2, key3......] # 监听指定的key
      multi # 开启事务
      ... # 进行一些列操作
      exec # 提交事务
      
    4. 如何取消监视呢? 可以取消对所有key的监视
      unwatch
      

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

3. 分布式锁

3.1 分布式锁是什么

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

  • 场景分析
    还是天猫双11热卖过程中, 怎么避免最后一件商品不被多人同时购买(超卖问题)
    1. watch监听能监听特定的key是否被修改, 但是无法监听被修改的值, 此处要监控的是具体的数据.
    2. 虽然Redis是单线程的, 但是多个客户端对同一数据同时进行操作时, 如何避免不被同时修改呢?
  • 解决方案
    1. 使用setnx设置一个公共锁 setnx lock-key value, value可以为随机任意值.
    2. setnx命令能返回value值.只有第一次执行的才会成功并返回1,其它情况返回0:
      如果返回是1, 说明没有人持有锁, 当前客户端设置锁成功,可以进行下一步的具体业务操作.
      如果返回是0, 说明有人持有了锁, 当前客户端设置锁失败, 那么需要排队或等待锁的释放.
    3. 操作完毕通过del操作释放锁.

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

3.2 分布式锁版本1

3.2.1 业务场景

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

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

    1. 使用expire为锁key添加时间限定, 到时不释放锁, 则放弃锁.
      setnx lock-key 001 # 设置分布式锁
      expire lock-key second # 设置单位为秒
      pexpire lock-key milliseconds # 设置单位为毫秒
      
    2. 或者直接设置的时候添加时间限制
      set 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

3.5 集群模式的Redis分布式锁

3.5.1 RedLock算法

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

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

3.5.2 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就可以尝试加锁了.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值