admin管理员组

文章数量:1570208

目录

事务 ACID 属性

用户如何开启Redis的事务?

使用redis-cli客户端来展示

​Go语言编码使用事务

Redis 的事务机制能保证哪些属性?

1. 原子性

语法错误

运行错误

执行EXEC时,Redis发生故障

Redis对事务原子性属性的保证情况

 2. 一致性

命令入队就错误

命令在实际执行时才报错

EXEC命令执行时,Redis发生故障

3. 隔离性 

4. 持久性

总结


为什么要把这批操作 称为 事务呢?是因为一批操作在执行过程中想获得一些保证。而事务就提供了这些保证。

事务在执行的时候,会提供专门的属性保护,包括原子性、一致性、隔离性和持久性。

事务 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两个命令来完成这三个步骤。

  1. 客户端使用 MULTI 命令显式表示开启一个事务
  2. 客户端吧事务中要执行的操作(比如增删改数据)发送给服务端。注意:这些命令虽然被客户端发送到了服务端,但Redis实例只是把这些命令暂存到一个命令队列中,并不会立即执行。
  3. 客户端向服务器发送提交事务的命令 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的事务机制可以保证一致性隔离性一定程度的原子性(不提供回滚),无法保证持久性。 

原子性的情况比较复杂,只有当事务中使用的命令执行时有误(即是入队无误,执行时刻有误),原子性得不到保证,其他情况下,事务都可以原子性执行。

本文标签: 属性机制事务RedisACID