Redis 事务机制和分布式锁以及解决死锁

什么是redis的事务?

简单理解,可以认为redis事务是一些列redis命令的集合,并且有如下两个特点:

  • 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
  • 事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。

一个事务从开始到执行会经历以下三个阶段:
开始事务。
命令入队。
执行事务。

redis事务的错误

使用事务时可能会遇上以下两种错误:
a)入队错误:事务在执行 EXEC 之前,入队的命令可能会出错。比如说,命令可能会产生语法错误(参数数量错误,参数名错误,等等),或者其他更严重的错误,比如内存不足(如果服务器使用 maxmemory 设置了最大内存限制的话)。
b)执行错误:命令可能在 EXEC 调用之后失败。举个例子,事务中的命令可能处理了错误类型的键,比如将列表命令用在了字符串键上面,诸如此类
注:第三种错误,redis进程终结,本文并没有讨论这种错误。

redis事务的用法

redis事务是通过MULTI,EXEC,DISCARD和WATCH四个原语实现的。

MULTI命令用于开启一个事务,它总是返回OK。

MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。

另一方面,通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务

  • 正常执行
127.0.0.1:6379>multi
"OK"
127.0.0.1:6379>set tran1 hello
"QUEUED"
127.0.0.1:6379>set tran2 world
"QUEUED"
127.0.0.1:6379>exec
 1)  "OK"
 2)  "OK"

EXEC 命令的回复是一个数组,数组中的每个元素都是执行事务中的命令所产生的回复。 其中,回复元素的先后顺序和命令发送的先后顺序一致。

当客户端处于事务状态时,所有传入的命令都会返回一个内容为 QUEUED的状态回复(status reply),这些被入队的命令将在 EXEC命令被调用时执行。

  • 放弃事务

当执行 DISCARD 命令时,事务会被放弃,事务队列会被清空,并且客户端会从事务状态中退出:

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET key1 1
QUEUED
127.0.0.1:6379> DISCARD
OK
127.0.0.1:6379> EXEC
(error) ERR EXEC without MULTI
  • 使用WATCH
    WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。

被 WATCH 的键会被监视,并会发觉这些键是否被改动过了。 如果有至少一个被监视的键在 EXEC 执行之前被修改了, 那么整个事务都会被取消, EXEC 返回空多条批量回复(null multi-bulk reply)来表示事务已经失败。

127.0.0.1:6379> WATCH key1
OK
127.0.0.1:6379> set key1 2
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set key1 3
QUEUED
127.0.0.1:6379> set key2 3
QUEUED
127.0.0.1:6379> EXEC
(nil)

使用上面的代码, 如果在 WATCH 执行之后, EXEC 执行之前, 有其他客户端修改了 key1 的值, 那么当前客户端的事务就会失败。 程序需要做的, 就是不断重试这个操作, 直到没有发生碰撞为止。

这种形式的锁被称作乐观锁, 它是一种非常强大的锁机制。并且因为大多数情况下, 不同的客户端会访问不同的键, 碰撞的情况一般都很少, 所以通常并不需要进行重试。

事务对异常的处理机制

Redis执行命令的错误主要分为两种:

  • 1.命令错误:执行命令语法错误

开启事务之后,往事务中添加的命令如果有命令错误(语法错误),那么整个事务中的命令都不会执行。

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set key1 1
QUEUED
127.0.0.1:6379> HSET key2 1
(error) ERR wrong number of arguments for 'hset' command
127.0.0.1:6379> SADD key3 1
QUEUED
127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
  • 2.运行时错误:命令语法正确,但是执行错误,比如说对 List 集合执行 sadd 命令

如果语法没有错误,而执行过程中发生了运行时错误Redis不仅不会回滚事务,还会跳过这个运行时错误,继续向下执行命令

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> HSET key1 field1 1
QUEUED
127.0.0.1:6379> HSET key2 field1 1
QUEUED
127.0.0.1:6379> EXEC
1) (error) WRONGTYPE Operation against a key holding the wrong kind of value
2) (integer) 1

tip:事务不会回滚,所以出现错误的时候,要根据实际情况恢复数据。

分布式锁

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。

实现分布式锁有很多实现方式和工具,如Zookeeper、Redis等。

使用Redis实现分布式锁原理:

Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系,基于此,Redis中可以使用SETNX命令实现分布式锁。

SETNX——SET if Not eXists(如果不存在,则设置):

setnx key value

将 key 的值设为 value ,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。

如果需要解锁,使用 del key 命令就能释放锁.首先A使用setnx对键加锁成功返回1,B使用setnx命令对键加锁失败返回0,说明有客户端持有锁。A使用del释放锁以后,B就可以使用setnx命令对键加锁。

解决死锁

如果一个持有锁的客户端失败或崩溃了不能释放锁,该怎么解决?

答:给锁设置一个过期时间,可以通过两种方法实现:通过命令 setnx 键名 过期时间;或者通过设置锁的expire时间,让Redis去删除锁。(淘汰机制)

  • 使用 setnx key “当前系统时间+锁持有的时间”和getset key “当前系统时间+锁持有的时间”组合的命令就可以实现。

1.客户端2发送SETNX lock.test 想要获得锁,由于之前的客户端1还持有锁,所以Redis返回一个0
2.客户端2发送GET lock.test以检查锁是否超时了,如果没超时,则等待或重试。
反之,如果已超时,客户端2通过下面的操作来尝试获得锁: GETSET lock.test 过期的时间
3.通过GETSET,客户端2拿到的时间戳如果仍然是超时的,那就说明,客户端2如愿以偿拿到锁了。
4.如果在客户端2之前,有个客户端3比客户端2快一步执行了上面的操作,那么客户端2拿到的时间戳是个未超时的值,这时,说明客户端2没有如期获得锁,需要再次等待或重试。
5.尽管客户端2没拿到锁,但它改写了客户端3设置的锁的超时值,不过这一点非常微小的误差带来的影响可以忽略不计。

  • 通过Redis中expire()给锁设定最大持有时间,如果超过,则Redis来帮我们释放锁。

1.客户端1使用setnx获得了锁,并且使用expire设定一个过期时间,假定是10ms
2.过了4ms后,客户端1不幸运的宕机了,此时客户端2想要通过setnx尝试获得锁,但是锁还没有过期,任然被客户端1所持有。
3.到了11ms时,锁过期了,Redis帮我们删除了锁,此时客户端2想要通过setnx尝试获得锁,此时就能成功获得锁。

redis 127.0.0.1:6379> EXPIRE runooobkey 60
(integer) 1
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值