其实和Mysql一样,Redis虽然作为一个非关系的K-V结构数据库,也是存在事务的,当然事务并不会有Mysql那么强,关于Mysql的数据库事务,可以看下我三年前一篇blog的介绍【数据库策略 二】数据库事务,关于事务的ACID四大特性、并发的常见问题和事务的隔离级别
事务简介
当多个客户端对同一个Key执行set操作的时候,客户端的get预期是会有偏差的,那么依赖于Redis的单线程特性,我们处理Redis的问题比Mysql的要简单一些。
Redis 事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令
事务基本操作
事务有三个基本操作,创建事务队列multi;执行事务exec;取消事务discard。
事务的工作流程
执行的流程如下,当一条指令到来的时候要判断它是普通指令还是事务指令,
- 如果是普通指令,则判断当前是否存在事务队列,如果不存在直接执行,如果存在则入队
- 如果是事务指令,那么分为三种,multi为开启事务队列,exec为执行事务队列中的指令,执行完成后销毁队列,descrad为不执行队列中的指令,直接销毁队列
执行的时候按照先进先出的队列模式进行执行。
事务操作的注意事项
事务操作时出错的情况分为两种:指令书写错误和语法性错误(例如让list实现自增),这种情况下处理机制是什么呢?
指令书写错误
若在事务队列中存在命令书写错误(类似于java编译性错误),则执行EXEC命令时,所有命令都不会执行:
[root@192 redis-6.0.8]# redis-server config/redis-6379.conf [root@192 redis-6.0.8]# redis-cli 127.0.0.1:6379> keys * 1) "age" 127.0.0.1:6379> flushdb OK 127.0.0.1:6379> keys * (empty array) 127.0.0.1:6379> set name tml OK 127.0.0.1:6379> multi OK 127.0.0.1:6379> set age 32 QUEUED 127.0.0.1:6379> set sex girl QUEUED 127.0.0.1:6379> est color red (error) ERR unknown command `est`, with args beginning with: `color`, `red`, 127.0.0.1:6379> exec (error) EXECABORT Transaction discarded because of previous errors. 127.0.0.1:6379> get name "tml" 127.0.0.1:6379> get age (nil) 127.0.0.1:6379>
可以看到,这种情况下age也没有被set成功,也就是如果一条指令书写错误,则事务队列中的所有指令均不执行。
指令语法错误
若在事务队列中存在语法性错误(类似于java的1/0的运行时异常),则执行EXEC命令时,其他正确命令会被执行,错误命令抛出异常。
127.0.0.1:6379> multi OK 127.0.0.1:6379> set name tml QUEUED 127.0.0.1:6379> set name guochengyu QUEUED 127.0.0.1:6379> set age 37 QUEUED 127.0.0.1:6379> incr name QUEUED 127.0.0.1:6379> exec 1) OK 2) OK 3) OK 4) (error) ERR value is not an integer or out of range 127.0.0.1:6379> get name "guochengyu" 127.0.0.1:6379> get age "37" 127.0.0.1:6379>
可以看到当让string类型进行自增时出现报错,但不影响其它指令的执行。
Redis事务无法回滚
通过上边两种异常的了解我们知道,当指令出现语法错误的时候,redis是不支持事务的回滚机制的。能做的只能通过持久化的备份去恢复,以及写代码的时候小心小心再小心。
锁的使用
锁可以用来控制多客户端的同时操作对事务的干预问题,同时也能控制分布式并发场景下的事务正常执行。
事务的watch监控锁
想象一个场景,如果多个客户端都想对同一个Key进行操作,如果我们只使用事务去限制,不一定能达到效果。例如我想让一个num自增1,客户端1和客户端2都接到了这个任务**【商品补货】**:客户端1创建了一个事务,并且让num自增1
127.0.0.1:6379> flushdb OK 127.0.0.1:6379> set age 50 OK 127.0.0.1:6379> multi OK 127.0.0.1:6379> incr age QUEUED 127.0.0.1:6379> exec 1) (integer) 52 127.0.0.1:6379>
结果在客户端1事务队列执行的时候,客户端2对num进行了自增得到了51,此时客户端1再执行事务的时候发现结果变为了52,事务怎么不具备原子性了呢?这都是并发导致的问题
127.0.0.1:6379> incr age (integer) 51 127.0.0.1:6379>
为了解决这个问题,我们可以使用锁来监控key的状态,只要监控的key变化了,那么事务就取消执行,防止执行过程中的非原子性。还是上一个例子:客户端1开启锁并开启事务
127.0.0.1:6379> flushdb OK 127.0.0.1:6379> set age 50 OK 127.0.0.1:6379> watch age OK 127.0.0.1:6379> multi OK 127.0.0.1:6379> incr age QUEUED 127.0.0.1:6379> set name tml QUEUED 127.0.0.1:6379> exec (nil) 127.0.0.1:6379> get name (nil) 127.0.0.1:6379>
同时客户端2在事务执行的过程中incr了num:
127.0.0.1:6379> flushdb OK 127.0.0.1:6379> incr age (integer) 51 127.0.0.1:6379>
那么执行事务的时候就返回了nil,事务执行失败,并且这个操作相当于销毁队列,连name的值都取不到,还有一点需要注意,当watch事务的时候,即使事务队列没有watch的key,如果key发生变化也会销毁队列,redis不会识别队列的key:
127.0.0.1:6379> flushdb OK 127.0.0.1:6379> set age 50 OK 127.0.0.1:6379> watch age OK 127.0.0.1:6379> multi OK 127.0.0.1:6379> set name guochengyu QUEUED 127.0.0.1:6379> exec (nil) 127.0.0.1:6379> get name (nil) 127.0.0.1:6379>
例如这里,watch
的是age,但是我事务里对name操作,当另一个客户端对age操作变化后,事务队列name的操作也是无效的。如果我们不想监控的话,可以使用unwatch
命令,unwatch之后事务队列可以正常执行了。
分布式锁
处理补货问题的时候,可以使用watch来监控添加数量,防止重复添加,但使用watch的时候只能监控到要改变的key是否改变了,在超卖问题下,不仅要监控key是否改变了还要求各个客户端不能进行操作。
这里我们需要使用setnx这个分布式锁来操作:
setnx key value
有值返回设置失败【无控制权】,无值返回设置成功【有控制权】
127.0.0.1:6379> flushdb OK 127.0.0.1:6379> setnx lock-num 1 //拿到锁 OK 127.0.0.1:6379> expire lock-num 10 (integer) 1 127.0.0.1:6379> set num 10 OK 127.0.0.1:6379> incr num (integer) 11 127.0.0.1:6379> del lock-num //删除锁 (integer) 1 127.0.0.1:6379> get num "11" 127.0.0.1:6379>
为了防止客户端拿到锁后宕机,我们一般需要给该锁设置一个过期时间,防止发生死锁。关于分布式锁的具体实践可以看我的另一篇实践的blog:【Redis实战系列 一】分布式锁实战,是基于公司工作的业务场景做的,介绍的比较全面,感兴趣的可以看下。
以上就是redis事务的全部内容,相比于Mysql可以说简单不少,而因为其事务不怎么强,一般用的也比较少,我感觉这里最重要的一个概念就是分布式锁吧,而且做过一次不错的实践,大家感兴趣可以看看。