【Redis】 事务

Metadata

title: 【Redis】 事务
date: 2023-07-09 13:04
tags:
  - 行动阶段/完成
  - 主题场景/数据存储
  - 笔记空间/KnowladgeSpace/ProgramSpace/BasicsSpace
  - 细化主题/数据存储/Redis
categories:
  - 数据存储
keywords:
  - 数据存储/Redis
description: 【Redis】 事务

概述

Redis 通过MULTIEXECWATCH等命令实现事务功能。事务是将多个命令打包,然后原子地按顺序地执行的机制,执行期间服务器不会中断事务执行其他客户端的命令请求。下面展示了一次完整事务的执行命令:

redis> MULTI
OK
redis> SET "name" "The Design and Implementation of Redis"
QUEUED
redis> GET "name"
QUEUED
redis> EXEC
1) OK
2) "The Design and Implementation of Redis"
  • 事务提供了一种将多个命令打包,然后一次性、有序地执行的机制。
  • 多个命令会被入队到事务队列中,然后按先进先出(FIFO)的顺序执行。
  • 事务在执行过程中不会被中断,当事务队列中的所有命令都被执行完毕之后,事务才会结束。
  • 带有WATCH命令的事务会将客户端和被监视的键在数据库的watched_keys字典中进行关联,当键被修改时,程序会将所有监视被修改键的客户端的REDIS_DIRTY_CAS标志打开。
  • 只有在客户端的REDIS_DIRTY_CAS标志未被打开时,服务器才会执行客户端提交的事务,否则的话,服务器将拒绝执行客户端提交的事务。
  • Redis的事务总是具有ACID中的原子性、一致性和隔离性,当服务器运行在AOF持久化模式下,并且appendfsy nc选项的值为alway s时,事务也具有耐久性。

事务的实现

可以看出,事务主要有 3 个阶段:事务开始、命令入队、事务执行。

事务开始

MULTI 命令表示事务的开始,将客户端从非事务状态切换为事务状态,在 flags 属性中打开REDIS_MULTI标识

命令入队

当客户端处于事务状态时,命令不会被立即执行(除了 EXEC、DISCARD、WATCH、MULTI),而是加入事务队列。

事务队列

客户端的事务状态保存在 mstate 里:

typedef struct redisCLient{
    //事务状态
    multiState mstate;
    ...
} redisClient;

事务状态包括事务队列入队命令计数器

typedef struct multiState{
    //数组,事务队列
    multiCmd *commands;
    //入队命令计数器
    int count;
} multiState;

事务队列的实例结构:

typedef struct multiCmd {
    //参数
    robj **argv;
    //参数数量
    int argc;
    //命令指针
    struct redisCommand *cmd;
} multiCmd;

先入队的命令先放入数组,后入队的后放入。

执行事务

当收到客户端的 EXEC 命令时,将被立即执行,然后服务器遍历客户端的事务队列,保存命令,执行命令给,返回结果给客户端,移除事务标识。

WATCH 命令的实现

WATCH 命令是一个乐观锁,可以在 EXEC 前监视任意数量的键,如果在 EXEC 执行时,键被修改过,服务器将拒绝执行事务。

使用 WATCH 命令监视数据库键

每个 Redis 数据库都保存着 watched_keys 字典,键是某个被 WATCHED 命令监视的键,值是一个链表,记录所有监视该键的客户端

typedef struct redisDb{
    //正在被WATCHED命令监视的键
    dict *watched_keys;
    ...
} redisDb;

监控机制的触发 & 事务安全

在执行数据库修改命令时,都会调用multi.c/touchWatchKey函数对 watched_keys 字典进行检查,查看是否有客户端正在监视刚被命令修改过的键,如果有,将 watched_keys 该键对应的值,也就是监听的客户端都打开REDIS_DIRTY_CAS标识,表示事务的安全性已经被破坏。此时,服务器拒绝执行事务。

事务的 ACID 性质

Redis 的事务有原子性、一致性和隔离性,当 Redis 运行在特定的持久化模式下时,才具有持久性。

原子性

Redis 事务队列中的命令,要么全部都执行,要么一个都不执行,因此,具有原子性。Redis 进行事务命令入队时,如果命令入队出错,会被拒绝执行。但是命令的语法错误执行错误),不会导致整个命令不被执行,也就是说 Redis 不支持事务的回滚机制

下面例子表示发生入队错误(一致性时将提到入队错误和执行错误)时,事务中的所有命令都不会被执行:

redis> MULTI
OK
redis> SET msg "he1lo"
QUEUED
redis> GET
(error) ERR wrong number of arguments for 'get' command
redis> GET msg
QUEUED 
redis> EXEC
(error) EXECABORT Transaction discarded because of previous errors.

发生执行错误,不影响其他命令的执行:

redis> SET msg "hello" # msg键是一个字符串
OK
redis> MULTI
OK
redis> SADD fruit "apple" "banana" "cherry"
QUEUED
redis> RPUSH msg "good bye" "bye bye" #错误地对字符串键msg执行列表键的命令
QUEUED
redis> SADD alphabet "a" "b" "c"
QUEUED
redis> EXEC 
1) (integer) 3
2)(error) WRONGTYPE Operation against a key holding the wrong kind of value
3)(integer) 3

不支持事务回滚是考虑到了复杂性,与其简单高效的理念不符,并且 Redis 的设计者认为,Redis 事务的执行时错误通常都是编程错误产生的,在开发环境中会有,但生产环境不应该出现,因此,没有设计回滚机制。

一致性

一致性表示在事务的执行前后,成功与否,数据库都是一致的,也就是数据符合数据库本身定义和要求,没有非法或无效错误数据

Redis 通过简单的错误检测来保证一致性。

  1. 入队错误

在 2.6.5 之后的版本,如果一个事务在入队时出现了命令不存在,Redis 则拒绝执行这个事务。

  1. 执行错误

对于命令执行期间发现的错误,不会影响其他命令的执行。服务器会识别出错的命令,并进行相应处理,这些命令不会对数据库做修改,不影响一致性。

  1. 服务器停机

如果 Redis 在执行过程中停机,数据也是一致的。如果没有开启持久化,重启后数据库是空白的。开启持久化后,重启后会还原到一致状态。

隔离性

Redis 是单线程的,并且服务器保证在执行事务期间不会对事务进行中断,因此是具有隔离性的。并且队列中的命令没有提交之前都不会实际的被执行,因为事务提交前任何指令都不会被实际执行,因此没有隔离级别概念

持久性

持久性的意思是,事务执行的结果被永久性地保存,执行事务的结果不会丢失。因为 Redis 没有单独为事务队列提供持久化功能,所以取决于持久化模式,只有 AOF 方式持久化并且 appendsync 的值为 always,而且没有打开no-appendfsync-on-rewrite时,才具有持久性。因为其他方式并不能保证事务的执行结果被第一时间保存到硬盘里。

注:no-appendfsync-on-rewrite打开后,在执行BGSAVEBGREWRITEAOF时会暂停对 AOF 文件的同步。