目录
为什么要把这批操作 称为 事务呢?是因为一批操作在执行过程中想获得一些保证。而事务就提供了这些保证。
事务在执行的时候,会提供专门的属性保护,包括原子性、一致性、隔离性和持久性。
事务 ACID 属性
原子性(Atomicity)
事务的整个过程如原子操作一样,最终要么全部成功,或者全部失败,这个原子性是从最终结果来看的,从最终结果来看这个过程是不可分割的。业务应用使用事务时,原子性也是最被看重的一个属性。
一致性(Consistency)
一个事务必须使数据库从一个一致性状态变换到另一个一致性状态,是指数据库中的数据在事务执行前后是一致的。
比如说转账, 用户A余额200元,用户B余额100元,A给B转账100元,事务结束后,用于A余额是100元,用户B余额是200元,这个就是一致性。如果A上的钱减少了,而B上的钱却没有增加,那么我们认为此时数据处于不一致的状态。
隔离性(Isolation)
一个事务的执行不能被其他事务干扰。它要求数据库在执行一个事务时,其它操作无法存取到正在执行事务访问的数据。
比如某件商品库存10件,在一个事务过程中,用户A对该商品下单6件,而另外,用户B也对该商品下单5件,那累加后就超过库存数了,不符合业务要求。所以是,在事务的修改某数据时候,其他的不能修改该数据,但是可以访问该数据,但只能访问到事务开始前的值。这样就是两个进行了隔离。
持久性(Durability)
一个事务一旦提交,他对数据库中数据的改变就应该是永久性的。当事务提交之后,数据会持久化到硬盘,修改是永久性的。
用户如何开启Redis的事务?
事务的执行过程包含3个步骤,Redis提供了MULTI、EXEC两个命令来完成这三个步骤。
- 客户端使用 MULTI 命令显式表示开启一个事务
- 客户端吧事务中要执行的操作(比如增删改数据)发送给服务端。注意:这些命令虽然被客户端发送到了服务端,但Redis实例只是把这些命令暂存到一个命令队列中,并不会立即执行。
- 客户端向服务器发送提交事务的命令 EXEC,让数据库实际执行第二步中发送的具体操作。当服务端收到该命令后,才会真正执行命令队列中的所有命令。
使用redis-cli客户端来展示
事务过程中它们执行后的返回结果都是 QUEUED,这就表示,这些操作都被暂存到了命令队列,还没有实际执行。等到执行了 EXEC 命令后,可以看到返回了结果,这就表明,事务中的操作已经成功地执行了。
Go语言编码使用事务
func main() {
client := redis.NewClient(&redis.Options{
Addr: "127.0.0.0.1:6379",
Password: "", //若是设置了密码,就需要填写
DB: 0,
})
ping, _ := client.Ping().Result() //测试是否网络通
fmt.Println(ping)
//该方法注解//TxPipeline acts like Pipeline, but wraps queued commands with MULTI/EXEC.
pipe := client.TxPipeline()
val, _ := pipe.Set("key1", 100, 0).Result()
fmt.Println("set key1 ", val) //该结果要在执行exec后才会有
pipe.Set("key2", "200", 0)
pipe.Get("key2")
//执行命令队列中的命令
_, err := pipe.Exec()
if err != nil {
fmt.Println(err)
return
}
fmt.Println("执行exec 后 set key1 ", val)
value1, err := client.Get("key1").Result()
if err != nil {
fmt.Println(err)
return
}
fmt.Println("key1: ", value1)
}
Redis 的事务机制能保证哪些属性?
1. 原子性
要求是批操作是全部成功或者全部失败。要是在事务过程中,出现了错误,Redis还能保证原子性吗?这个要分三种情况来分析。
语法错误
语法错误指的是命令的参数个数不正确,命令本身拼写错误等情况。
在执行EXEC命令时,客户端发送的操作命令本身就有错误(比如使用了不存在的命令),在命令入队时候就被Redis判断出来。
在命令入队时候,Redis就会报错并记录下该错误。而此时,我们还能继续提交命令。等到执行EXEC之后,Redis就会拒绝执行所有提交的命令操作,返回错误的结果。
这样,事务中的所有命令都不被执行,保证了原子性。
运行错误
运行错误是指输入的命令格式正确,但是在命令执行期间出现了错误。典型的及时命令和操作的数据类型不服务的情况。比如添加一个string类型的key,之后却对该key进行列表操作。
127.0.0.1:6379> set skey ss
OK
127.0.0.1:6379> lpush skey value
(error) WRONGTYPE Operation against a key holding the wrong kind of value
那么,事务操作入队时候,命令和操作的数据类型不匹配,但是Redis没有检查出错误。但是在执行完EXEC之后,Redis实际执行这些操作时候,会报错。但是,注意:Redis对错误命令报错,但还是会把正确的命令执行完。在这种情况下,事务的原子性就无法得到保证。
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set score 100
QUEUED
127.0.0.1:6379(TX)> lpush score 200
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
上面的例子,命令和操作的数据类型不匹配的时候,set命令成功了,而lpush失败了。这个事务的原子性就没有得到保证。
DISCARD命令
到这里,你可能会疑惑,传统数据库(比如MySQL)在z执行事务的时候,会提供回滚机制,当事务执行发生错误的时候,事务中的所有操作都会被撤销,已经被修改的数据也会被恢复到事务执行前的状态。那么,在上面的例子中,若命令实际执行时报错了,是不是可以用回滚机制来恢复原来的数据呢?
其实,Redis没有提供回滚机制。Redis提供了DISCARD命令,但是,这个命令只能用来主动放弃事务执行,把暂存的命令队列情况,没起到回滚的效果。
执行EXEC时,Redis发生故障
事务的命令都是放入到一个命令队列中的,那么执行exec时候,执行了该事务的一半命令,而Redis出现故障,那这一半命令是执行成功的了。但是Redis出故障重启后,就需要通过RDB或者AOF文件去恢复数据。
- AOF模式:现在一般都是开启AOF的,若开启了AOF的, Redis会首先去找AOF文件恢复数据。首先,这个事务的一半命令的操作记录是被记录到AOF日志中的了。但是这个事务是未完成的。使用
redis-check-aof
工具检测 AOF 日志文件,可以把未完成的事务操作从 AOF 文件中去除。这样一来,使用 AOF 文件恢复实例后,这个事务不会被再执行,从而保证了原子性。 - RDB模式:若是只使用RDB模式,在执行事务时,Redis 不会中断事务去执行保存 RDB 的工作,只有在事务执行之后,保存 RDB 的工作才有可能开始。即是最新的 RDB 快照是在当前 EXEC 执行之前生成的。所以当 RDB 模式下的 Redis 服务器进程在事务中途出故障时,事务内执行的命令,不管成功了多少,都不会被保存到 RDB 文件里。通过RDB文件去恢复,这样就不会再次执行该事务的命令语句,从而保证了原子性。
- 不开启AOF和RDB:重启后内存中的数据就会全部丢失,也就谈不上原子性了。
Redis对事务原子性属性的保证情况
- 命令入队时就报错,会放弃事务执行,保证原子性;
- 命令入队时没有报错,在实际执行时报错,不保证原子性;
- exec命令执行时,Redis实例故障,若开启了AOF或者RDB,可以保证原子性。
2. 一致性
其会受到错误命令、实例故障的影响。有三种情况:
命令入队就错误
在这种情况下,事务本身就会被放弃执行,那么就可以保证数据库的一致性。
命令在实际执行时才报错
在这种情况下,有错误的命令不会被执行,正确的命令可以正常执行,所以也不会改变数据库的一致性。
EXEC命令执行时,Redis发生故障
故障后进行重启,就会使用RDB或AOF文件来恢复数据。
- 纯内存模式。即是没有开启RDB或者AOF,,那实例故障重启后,数据就都没有了,数据库是一致的。
- 使用RDB快照模式。因为RDB快照不会在事务执行时执行,所以,事务命令的操作不会被保存到RDB快照中。使用RDB恢复时候,数据库里的数据也是一样的。
- 使用AOF。而事务操作还没有被记录到AOF日志时(即是命令已经有执行一条的,当时该记录还没有写入到磁盘中),实例就发生故障。所以使用AOF日志恢复的数据库日志时一致的。若是只有部分操作被记录到了AOF日志,我们可以使用 redis-check-aof 清除事务中已经完成的操作,数据库恢复后也是一致的。
所以,总结来说,在命令执行错误或 Redis 发生故障的情况下,Redis 事务机制对一致性属性是有保证的。
3. 隔离性
这个属性有讲究。一开始时候,我认为Redis的执行命令部分是单线程的,那就是串行化,那肯定是有隔离性保障的。但是却不是这么简单的。
看一个场景,商品库存是10,用户A开启事务,把库存-5的操作存入到命令队列中,这时用户B把库存-5,这个操作是即刻执行的。那在真正执行事务命令前,库存就是3了。之后exec,那库存再-5,那就不符合业务要求的,那就是没有隔离性。
所以, 不是说是单线程执行命令就是有隔离性保证的了。
Redis的隔离性保证,会受到和事务一起执行的并发操作的影响。而事务执行又可以分成 命令入队(exec命令执行前)和命令实际执行(exec命令执行后) 两个阶段。
- 并发操作在exec命令执行前,隔离性的保证要使用watch机制来实现,否则无隔离性(就是上面的例子)。
- 并发操作在exec命令后,此时,隔离性可以保证。
那要讲讲watch机制。其作用是,在事务真正执行前,监控该事务会操作的一个或多个键的变化情况,当事务调用exec执行时,watch机制会先检查监控的键是否被其他客户端修改了,若是修改了,就放弃事务执行,避免事务的隔离性被破坏。然后,客户可以再次执行事务,若此时没有并发修改数据的操作了,事务就能正常执行,隔离性也得到了保证。
watch机制,对于用户来说,就是使用watch命令,当时需要用户主动显性使用watch监听想要操作的键。比如上面的事务是想要操作键shop,所以就需要在mutli之前执行 watch shop。
执行exec时候,返回nil,表示放弃事务执行。这样一来,事务的隔离性就可以得到保证了。
而另一种情况:并发操作在 EXEC 命令之后被服务器端接收并执行。
这个就简单多了。因为Redis执行命令部分是单线程的,而且,exec命令执行后,Redis会保证先把命令队列中的命令执行完,之后才执行并发收到的命令。所以这时并发操作,不会影响到事务命令执行。所以,在该情况下,并发操作并不会破坏事务的隔离性。
4. 持久性
Redis是内存数据库,所以,其数据是否持久化保存是完全取决于Redis的持久化配置模式。
- 没有使用RDB或者AOF,事务的持久性肯定是得不到保证的。
- 使用RDB模式。在一个事务执行后,下一次的 RDB 快照还未执行前,如果发生了实例宕机,事务的持久性同样无法保证;
- 使用 AOF 模式。AOF 模式的三种配置选项 no 、everysec 都会存在数据丢失的情况 。always 可以保证事务的持久性,但因为性能太差,在生产环境一般不推荐使用。
综上,redis 事务的持久性是无法保证的 。
总结
Redis使用 multi、exec、discard、watch 四个命令来支持事务机制。
- multi :开启一个事务
- exec :提交事务,从命令队列中取出提交的操作命令,进行实际执行
- discard : 放弃一个事务,清空命令队列
- watch :检测一个或多个键的值在事务执行过程中是否发生变化,若发生变化,当前事务就放弃执行
Redis的事务机制可以保证一致性和隔离性和一定程度的原子性(不提供回滚),无法保证持久性。
原子性的情况比较复杂,只有当事务中使用的命令执行时有误(即是入队无误,执行时刻有误),原子性得不到保证,其他情况下,事务都可以原子性执行。