Redis系列之事务

概述

Redis事务提供一种将多个命令打包,然后一次性、按顺序地执行的机制,在事务执行的期间不会主动中断,服务器在执行完事务中的所有命令之后,才会继续处理其他客户端的其他命令。

三个重要的保证:

  • 批量操作在发送EXEC命令前被放入队列缓存
  • 收到EXEC命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行
  • 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中

Redis中的事务是一组命令的集合,事务也是Redis最小的执行单位,一个事务中的命令要么都执行,要么都不执行。

Redis事务由5个命令来实现:

  1. MULTI:标记一个事务块的开始。开启Redis事务,置客户端为事务态
  2. EXEC:提交事务,执行从MULTI到此命令前的命令队列,置客户端为非事务态
  3. DISCARD:取消事务,置客户端为非事务态
  4. WATCH:监视键值对,作用时如果事务提交EXEC时发现监视的监视对发生变化,事务将被取消
  5. UNWATCH:取消WATCH命令对所有Key的监视

事务从开始到执行会经历以下三个阶段:

  • 开始事务
  • 命令入队
  • 执行事务

MULTI标记事务的开始,将客户端状态的flags属性的REDIS_MULTI选项打开,让客户端从非事务状态切换到事务状态。

命令入队

Redis客户端处理非事务状态时,命令会立即被服务端执行。但是事务状态下,如果客户端发送的命令是上面四个命令其一,则服务器立即执行;除此之外,服务器并不立即执行命令,而是将命令放入事务队列里面,向客户端返回QUEUED回复。

WATCH

Redis使用WATCH命令监视给定的Key,当EXEC时如果监视的Key从调用WATCH后发生过变化,则整个事务会失败。也可以调用WATCH多次监视多个Key,这样就可以对指定的Key加乐观锁。注意WATCH的Key是对整个连接有效的,事务也一样。如果连接断开,监视和事务都会被自动清除。当然EXEC,DISCARD,UNWATCH命令都会清除连接中的所有监视。

实战

本文使用的Redis版本为Windows下7.4.0版本。

empty array

执行MULTI命令后没有其他命令,直接输入EXEC命令,不报错:

sh 复制代码
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> exec
(empty array)

WATCH

可用于实现类似于乐观锁效果,即CAS,compare and set。

WATCH用于监视Key是否被改动过,支持同时监视多个Key,只要还没真正触发事务,WATCH都会尽职尽责的监视。当多个线程更新同一个Key值时,会跟原值做比较,一旦发现它被修改过,则拒绝执行命令,执行EXEC时就会返回nil,表示事务无法触发。

双击redis-cli.exe打开一个命令行窗口,依次执行如下命令:

sh 复制代码
127.0.0.1:6379> watch age
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set age 31
QUEUED

双击redis-cli.exe打开另一个命令行窗口,执行如下命令:

sh 复制代码
127.0.0.1:6379> set age 32
OK

回到第一个窗口(客户端),提交EXEC命令:

sh 复制代码
127.0.0.1:6379(TX)> exec
(nil)

命令返回(nil),表示事务无法触发。因为另外一个客户端通过set命令更新过Key

哪怕是值不变,有过set key动作就会更新更新时间戳字段(猜测),和MVCC机制比较类似,Redis源码待调研。

客户端一执行:

sh 复制代码
127.0.0.1:6379> set age 11
OK
127.0.0.1:6379> watch age
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set age 12
QUEUED

客户端二执行:

sh 复制代码
127.0.0.1:6379> get age
"11" # 在客户端一执行set后,可get获取
127.0.0.1:6379> set age 11
OK # 在客户端一执行watch后,尝试set更新到相同的年龄

客户端一执行:

sh 复制代码
127.0.0.1:6379(TX)> exec
(nil)

执行结果也是(nil)。

事务错误

两类错误:

  • 调用EXEC之前的错误
  • 调用EXEC之后的错误

EXEC执行前出错:如语法有误,内存不足导致。只要出现某个命令无法成功写入缓冲队列的情况,Redis都会进行记录,在客户端调用EXEC时,Redis会拒绝执行这一事务。

sh 复制代码
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> he
(error) ERR unknown command 'he', with args beginning with:
127.0.0.1:6379(TX)> set he he
QUEUED
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.

EXEC执行后出错:Redis不会理睬这些错误,而是继续向下执行事务中的其他命令。对于应用层面的错误,并不是Redis需要考虑和处理的问题,所以事务中如果某一条命令执行失败,并不会影响接下来的其他命令的执行。

sh 复制代码
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set age 34
QUEUED
127.0.0.1:6379(TX)> sadd age 35
QUEUED
127.0.0.1:6379(TX)> set age 35
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) OK
(0.53s)
127.0.0.1:6379> get age
"35"

这里可以得出一个很重要的结论:在执行事务时某个命令执行失败,并不会影响其他命令的执行,即Redis 的事务并不会回滚。

Lua集成

Lua脚本集成,参考Redis系列之Lua脚本整合

ACID

传统事务四个核心特性,即ACID。为了保持简单,Redis事务保证其中的一致性和隔离性;不满足原子性和持久性;

原子性

在执行事务命令时,在命令入队时,Redis检测事务里的命令是否正确,即是否有语法错误,如果不正确则会产生错误。事务里的命令是批量提交执行的,也就是命令还没执行,当然也就不存在回滚一说;

当命令格式正确,而因为操作数据结构引起的错误,则该命令执行时才会出现错误,而其之前和之后的命令都会被正常执行。

参考You Don't Need Transaction Rollbacks in Redis,以及Redis Transactions

Redis does not support rollbacks of transactions since supporting rollbacks would have a significant impact on the simplicity and performance of Redis.

翻译:支持回滚会对 Redis 的简单性和性能产生重大影响。

一致性

一致性指的就是事务执行前后的数据符合数据库的定义和要求。

Redis符合要求,不论是发生语法错误还是运行时错误,错误的命令均不会被执行。

隔离性

多个事务并发执行,各个事务之间不会互相影响。原因:Redis事务不会中断,且是单线程执行事务。

持久性

对于事务的执行来说,如果Redis开启AOF持久化,那么一旦事务被成功执行,事务中的命令就会通过write命令一次性写到磁盘中去,如果在向磁盘中写的过程中恰好出现断电、硬件故障等问题,可能出现只有部分命令进行AOF持久化,这时AOF文件就会出现不完整的情况,这时可使用redis-check-aof工具将AOF文件中不完整的信息移除,确保AOF文件完整可用。

原理

Redis中每个客户端都有记录当前客户端的事务状态multiState,客户端client定义,在server.h源码里:

c 复制代码
typedef struct client {
	uint64_t id; // 客户端唯一id
	multiState mstate; // MULTI和EXEC状态(即事务状态)
	// 省略其他属性
} client;

multiState定义如下:

c 复制代码
typedef struct multiState {
	multiCmd *commands; // 存储命令的FIFO队列
	int count; // 命令总数
	// 省略其他属性
} multiState;

multiCmd是一个队列,用来接收并存储开启事务之后发送的命令,定义如下:

c 复制代码
typedef struct multiCmd {
	robj **argv; // 用来存储参数的数组
	int argv_len; // 数组长度
	int argc; // 参数的数量
	struct redisCommand *cmd; // 命令指针
} multiCmd;

类结构图:

拓展

Redis事务在真实业务场景中的应用

  • 批量操作:如果你需要对多个键进行原子性修改,Redis事务可以确保这些操作在没有被中断的情况下执行。例如,批量更新用户积分、库存等;
  • 乐观锁场景:在高并发环境下,使用WATCH可以监控某些关键键的变化,防止并发修改带来的数据不一致问题;
  • 复杂多键操作:在需要对多个键进行依赖性操作时,比如从一个账户扣款同时向另一个账户转账,Redis事务可以确保这类操作的完整性。

局限性:

  • 缺乏回滚机制:如果事务中的某个命令执行失败,Redis不会回滚已经执行的命令,这在某些需要严格数据一致性的场景中是个问题;
  • 命令执行顺序无法调整:事务中的命令执行顺序是固定的,无法在运行时调整,如果某些条件改变,无法动态改变执行顺序;
  • 性能开销:由于事务的执行需要在内存中排队,如果队列中的命令较多,可能导致阻塞和性能问题。

参考

相关推荐
掘金-我是哪吒几秒前
微服务mysql,redis,elasticsearch, kibana,cassandra,mongodb, kafka
redis·mysql·mongodb·elasticsearch·微服务
ketil272 小时前
Ubuntu 安装 redis
redis
王佑辉3 小时前
【redis】redis缓存和数据库保证一致性的方案
redis·面试
Karoku0664 小时前
【企业级分布式系统】Zabbix监控系统与部署安装
运维·服务器·数据库·redis·mysql·zabbix
gorgor在码农4 小时前
Redis 热key总结
java·redis·热key
想进大厂的小王4 小时前
项目架构介绍以及Spring cloud、redis、mq 等组件的基本认识
redis·分布式·后端·spring cloud·微服务·架构
Java 第一深情4 小时前
高性能分布式缓存Redis-数据管理与性能提升之道
redis·分布式·缓存
minihuabei9 小时前
linux centos 安装redis
linux·redis·centos
monkey_meng11 小时前
【Rust中多线程同步机制】
开发语言·redis·后端·rust
hlsd#12 小时前
go 集成go-redis 缓存操作
redis·缓存·golang