第三章、Redis的事务概述
1、关系型数据库中事务定义
1.1、Mysql事务概述
事务指的是可以一次执行多个命令,本质是一组命令集合,一个事务中的所有命令都会序列化,按顺序的串行化执行而不会被其他命令插入。银行转账就是最经典的事务场景之一。
传统的关系型数据库如mysql,oracle中的事务需要满足ACID四个特性。即:
- 原子性(atomicity): 事务是数据库的逻辑工作单位,而且是必须是原子工作单位,对于其数据修改,要么全部执行,要么全部不执行。
- 一致性(consistency): 事务在完成时,必须是所有的数据都保持一致状态。在相关数据库中,所有规则都必须应用于事务的修改,以保持所有数据的完整性。
- 隔离性(isolation) :一个事务的执行不能被其他事务所影响。
- 持久性(durability) :一个事务一旦提交,事物的操作便永久性的保存在DB中。即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
1.2、Redis事务定义
redis事务中使用的命令用于一个队列中,一次性,顺序性,排他性的执行一系列命令。且redis数据属于典型的Nosql数据库。不需要满足ACID四个特性。Nosql数据库满足CAP三个特性其中之二。CAP即:
- 强一致性(Consistency): 在任意时刻,所有的分布式节点中的数据是一样的。
- 可用性(Availability): 分布式系统中某一个服务在某台或者多台台服务器出问题后,在其他服务器上依然能够完成用户的操作。
- 分区容错性(Partition torerance):在出现网络分区(比如断网)的情况下,分离的系统也能正常运行。
Redis事务是一个单独的隔离操作:本质上也是一组命令的集合! 事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断,Redis事务的主要作用就是串联多个命令防止别的命令插队。
Redis 事务可以一次执行多个命令, 并且带有以下三个重要的保证:
- 批量操作命令在发送 EXEC 命令前被放入队列缓存【命令入队】。
- 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行【执行事务】。
- 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。
Redis事务没有隔离级别的概念!所有的命令在事务中,并没有直接被执行!只有发起执行命令(Exec)的时候才会执行!Redis单条命令是保证原子性的,但是事务不保证原子性!
一个事务从开始到执行会经历以下三个阶段:
- 开启事务(multi)
- 命令入队(.....)
- 执行事务(exec)
1.3、Redis事务三特性
单独的隔离操作
事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
没有隔离级别的概念
队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
不保证原子性
事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
2、redis事务基本使用
2.1、MULTI、EXEC、DISCARD基本使用
下表列出了 redis 事务的相关命令:
命令 | 描述 | 用法 |
---|---|---|
MULTI | 标记一个事务块的开始 | MULTI标记一个事务开始,后面是一系列事务操作。 |
EXEC | 执行所有事务块内的命令 | EXEC在一系列事务操作后使用该命令,执行所有事务块内的命令。 |
DISCARD | 取消事务,放弃执行事务块内的所有命令 | DISCARD在一系列事务操作后使用该命令,放弃执行事务块内的所有命令 |
WATCH | 监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。 | WATCH key **[**key ...] |
UNWATCH | 取消 WATCH 命令对所有 key 的监视。 | UNWATCH |
从输入Multi命令开始,输入的批量操作命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。
组队【命令入队】的过程中可以通过discard来放弃组队(discard类似事务回滚)。
图解:
演示: 以下是一个事务的例子, 它先以 MULTI 开始一个事务, 然后将多个命令入队到事务中, 最后由 EXEC 命令触发事务, 一并顺序的执行事务中的所有命令:
redis 127.0.0.1:6379> MULTI
OK
redis 127.0.0.1:6379(TX)> SET book-name1 "Mastering C++ in 21 days"
QUEUED #命令1放到队列中进行组队
redis 127.0.0.1:6379(TX)> SET book-name2 "PHP python"
QUEUED #命令2放到队列中进行组队
redis 127.0.0.1:6379(TX)> EXEC
1) OK
2) ok
演示discard:
redis 127.0.0.1:6379> MULTI
OK
redis 127.0.0.1:6379(TX)> SET a1 v1
QUEUED #命令1放到队列中进行组队
redis 127.0.0.1:6379(TX)> SET a2 v2
QUEUED #命令2放到队列中进行组队
redis 127.0.0.1:6379(TX)> discard
1) OK #表示目前放弃组队,上面的命令不执行
2.2、事务的错误处理演示
情况一:组队中某个命令出现了报告错误,执行时整个的操作命令队列都会被取消。
演示:
redis 127.0.0.1:7000> multi
OK
redis 127.0.0.1:7000> set a aaa
QUEUED
redis 127.0.0.1:7000> set b bbb
QUEUED
redis 127.0.0.1:7000> set c #模拟组队过程中的错误输入操作命令
(error) ERR wrong number of arguments for 'set' command
redis 127.0.0.1:7000> exec
#组队过程中有失败的命令,则队列中所有命令都不会成功执行
(error) EXECABORT Translation discard because of previous errors
情况二: 如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
演示:
redis 127.0.0.1:7000> multi
OK
redis 127.0.0.1:7000> set a aaa
QUEUED
redis 127.0.0.1:7000> incr a #首先这个命令是正确的,但是组队阶段不会提示错误
QUEUED
redis 127.0.0.1:7000> set c ccc
QUEUED
redis 127.0.0.1:7000> exec
#第二个命令执行失败,因为我们的a对应的value不是数字类型,但是其他命令正常执行
1) OK
2) (error)ERR value is not an integer or out of range
3) OK
单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。事务可以理解为一个打包的批量命令执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已执行指令的回滚,也不会造成后续的指令不执行。
3、Redis事务冲突
3.1、冲突问题演示
为什么要采用事务? 主要是因为共享数据可能在操作过程中会出现冲突,类似于Java
当中的线程安全问题,即并发带来的问题。
一个简单的应用场景:多个线程(事务)对同一个账户进行取款操作 例:马上618了、假如一个场景:有很多人有你的账户并且该账户只有10000,同时去参加618抢购,同时发送了三个请求:
- 一个请求想给金额减8000
- 一个请求想给金额减5000
- 一个请求想给金额减1000
这三个请求同时发出,假如都执行成功了,那么此时账户的余额就有可能变成了10000-8000-5000-1000 = -4000,此时账户余额变成负的了,显然这在实际生活中是很不合理的。
这时我们能想到的就是加锁了,Redis中提供了两种锁--悲观锁和乐观锁
3.2、悲观锁和乐观锁
3.2.1、悲观锁(Pessimistic Lock)
顾名思义,就是很悲观的那种,喜欢胡思乱想每次去拿数据的时候总认为别人会改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。缺点就是效率低,只能一个人一个人的操作
3.2.2、乐观锁(Optimistic Lock)
和悲观锁相反,它在拿到数据时不会加锁,因为他相信这世界是美好的,别人不会修改,但出于严谨还是会在改数据的那一刻会检查一下有没有被人修改。可以使用版本号(Version)或CAS算法等机制检查。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。
乐观锁比悲观锁的效率要高,因为悲观锁每次都会加锁而乐观锁只是在修改数据时检查一下版本号是不是刚拿到的版本号不是就不能执行,是就执行。
抢票就是一个乐观锁的例子:所有人都能同时去抢票,但是最终只能一个人支付成功有票,因为只要一个人支付了,这个状态,版本就变化了,其他人就不能支付了
3.3、redis中的WATCH监控
在执行multi
之前,先执行watch key1 [key2]
,可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
返回值:
- 事务块内所有命令的返回值,按命令执行的先后顺序排列。
- 当操作被打断时,返回空值
nil
。
实例:
设置一个键为 balance 值为100的字符串类型数据。使用watch监控。开启两个客户端,分别监控后再开启事务对balance进行+100操作,客户端一先提交执行会成功,客户端二提交执行会失败,这就是WAYCH对事务监控,防止事务冲突,就类似于版本机制的乐观锁
客户端一:执行事务成功
# 监视 key ,且事务成功执行
redis 127.0.0.1:7000> set balance 100
ok
redis 127.0.0.1:7000> watch balance #监视key
redis 127.0.0.1:7000> multi
ok
redis 127.0.0.1:7000>incrby balance 100
QUEUED
redis 127.0.0.1:7000>exec
110
客户端二:执行事务失败
# 监视 key ,且事务被打断
redis 127.0.0.1:7000> get balance
100
redis 127.0.0.1:7000> watch balance #监视key
redis 127.0.0.1:7000>multi
ok
redis 127.0.0.1:7000>incrby balance 100
QUEUED
redis 127.0.0.1:7000>exec
nil #balance这个key 在客户端1被命令所改动,那么事务将被打断,所以都不会执行,为空
3.4、总结
在实际应用中很容易出现并发问题、所以我们要在redis事务中加锁(乐观锁,开启监控)解决事务的冲突问题
在redis中使用的check-and-set乐观锁机制实现事务的
redis中的乐观锁使用watch命令来实现的【版本控制】
4、unwatch
unwatch
:取消 WATCH 命令对所有 key 的监视,即解锁,类似自旋锁
redis 127.0.0.1:7000> get balance
100
redis 127.0.0.1:7000> watch balance #监视key
redis 127.0.0.1:7000>multi
ok
redis 127.0.0.1:7000>incrby balance 100
QUEUED
redis 127.0.0.1:7000>exec
(nil) #balance这个key 在客户端1被命令所改动,那么事务将被打断,所以都不会执行,为空
#解决:事务执行失败:进行解锁,获取最新的版本,重新进行监视
redis 127.0.0.1:7000> unwatch
ok
redis 127.0.0.1:7000>watch balance #监视key
redis 127.0.0.1:7000>multi
ok
redis 127.0.0.1:7000>incrby balance 100
QUEUED
redis 127.0.0.1:7000>exec
210 #执行成功,
如果在执行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了。