RDBMS 中的事务
众所周知,关系型数据库的事务拥有四大要素/属性:
- A :原子性(atomicity,或称不可分割性),事务中的操作要么都发生,要么都不发生,是一个不可分割的工作单位;
- C :一致性(consistency),指数据的规则在事务前后是保持一致的
- I :隔离性(isolation,又称独立性),就是指一个事务的执行不会被其他事务干扰。生产环境下可以有多个事务并发执行,它们之间执行的时候互不干扰(需要具体的隔离级别);
- D :持久性(durability),指事务提交后,对数据库的数据修改是永久性的,即使系统故障也不会丢失。
Q:在 MySQL 中,什么是事务?
事务是 MySQL 的一种机制(当然其他的关系型数据库也有),它由一条或者多条 SQL 语句组成,这些成批的 SQL 语句是一个执行单元,它们互相依赖且是一个不可分隔的整体,要么全部执行,要么全部不执行,用来完成某一个业务或事情,保证数据库的完整性或一致性。 如果某一事务执行成功,则在该事务中进行的所有数据修改均会提交,成为数据库中的永久组成部分。 如果事务遇到错误且必须取消或回滚,则所有数据修改均被清除。
在 MySQL 当中,对事务的控制被划分成了一个语言分类,即 TCL(Transaction control language,事务控制语言)。对使用 MySQL 的人员而言,又有 隐式事务 和 显式事务 的划分。
隐式事务
没有明显的开启或结束的标记,事务的自动开启、提交或回滚由 MySQL 内部自动控制。比如一条 DML 语句(insert 、update、delete)就是一个隐式事务。
show variables like "%autocommit%";
显式事务
有明显的开启或结束的标记,如果要开启显式事务 ,则需要首先关闭隐式事务,也就是 set autocommit=0
, 这只在当前连接中临时生效,一旦你退出然后重新连接,则 autocommit 又变成了 ON
在 MySQL 当中,一个显式事务的 SQL 语句形如这样:
set autocommit=0 ; //开启显式事务
start transaction ; // 事务的开始标记
select ... ;
update ... ;
delete ... ;
update ... ;
.... // 多条 SQL 语句组成的一个执行单元
commit; // 事务的结束标记,可以是 commit 关键字,提交后,数据被永久修改且实际落地到磁盘文件中;也可以是 rollback 关键字,此时,所有 SQL 语句执行的修改只临时存在于 内存 当中,数据并未真正被永久修改。
MySQL 的事务还涉及到四种隔离级别以及各种锁机制,这里就不展开。
Redis 当中的事务
Redis 当中的事务和关系型数据库中的事务有些不太一样。在官方文档 https://redis.io/docs/interact/transactions/ 中有这样的一句话——"Redis Transactions allow the execution of a group of commands in a single step, they are centered around the commands MULTI, EXEC, DISCARD and WATCH."
Redis 事务允许您在单个步骤中执行一组命令,事务主要围绕着 MULTI
、EXEC
、DISCARD
、WATCH
等命令展开。
MySQL 等关系型数据库的事务由一条或多条 SQL 语句组成,是为了保证数据的完整性或一致性。Redis 事务本质是一组与 Redis 进行命令交互的集合,这些命令会按照顺序执行且在执行过程中不会被其他客户端的命令打断。在一个 Redis 事务当中,会存在部分命令执行失败的情况,但是其他命令依旧会被正常执行,换言之,Redis 事务没有事务回滚的机制来保证原子性,总结就是:
- 独占排它性
- 没有隔离级别的概念
- 不完全保证原子性(部分支持原子性,后面介绍
watch
)
前面我们介绍了 Redis 的持久化,从头到尾读取 AOF 文件的内容且加载到内存当中的过程本质上就是 Redis 的事务。
与 Redis 事务相关的命令:
multi
– 标记一个事务的开启。执行以后通常返回 OKexec
– 执行事务中的所有命令watch key [key ...]
– 开启事务之前监控一个或多个 key,只针对连接的客户端生效,一旦客户端退出登录或执行了exec
/discard
命令,所有被监控的 key 都失效。如果事务执行时这些 key 变更过,则该事务会被打断,整体执行失败discard
– 标记事务的取消unwatch
– 取消 watch 命令对 key 的监控
基本使用演示
这里以 hash 数据类型来演示。
事务的正常使用
# 写入数据
192.168.100.3:6379> hmset product:redis name redis release 7 age 16
OK
# 让 field 的 value 自增 3
192.168.100.3:6379> HINCRBY product:redis age 3
(integer) 19
上面案例中,使用到了两条命令,我们可以使用 Redis 的事务将这两条命令变成独占的一个命令集合组,如下:
192.168.100.3:6379> del product:redis
(integer) 1
192.168.100.3:6379> multi
OK
192.168.100.3:6379(TX)> hmset product:redis name redis release 7 age 16 ← 终端提示符有了 "(TX)" 的变化
QUEUED ← 放入到队列中
192.168.100.3:6379(TX)> HINCRBY product:redis age 3
QUEUED
192.168.100.3:6379(TX)> exec
1) OK
2) (integer) 19
192.168.100.3:6379> hgetall product:redis
1) "name"
2) "redis"
3) "release"
4) "7"
5) "age"
6) "19"
事务的取消
192.168.100.3:6379> multi
OK
192.168.100.3:6379(TX)> HINCRBY product:redis age -3
QUEUED
192.168.100.3:6379(TX)> HINCRBYFLOAT product:redis release -2.5
QUEUED
192.168.100.3:6379(TX)> discard
OK
# 数据未发生变化
192.168.100.3:6379> hgetall product:redis
1) "name"
2) "redis"
3) "release"
4) "7"
5) "age"
6) "19"
事务的错误
有两种错误的情况可能会出现:
- 在执行
exec
之前,事务中的命令本身有问题,无法放入到队列中,比如不存在的命令或命令的参数错误或可用内存不足等等,这将导致事务中的所有命令无法执行,相当于关系型数据库中的 rollback 回滚 - 执行
exec
后有问题,比如给 hash 数据类型使用了 string 数据类型的命令,这将导致错误的命令被忽略,但是不影响事务中其他命令的正常执行。
这在官方的文档中是有说明的:
接下来我们将模拟这种错误的情况。
第一种错误情况
下面演示的是在执行 exec
命令之前的命令的参数错误。
192.168.100.3:6379> multi
OK
192.168.100.3:6379(TX)> HINCRBY product:redis age -3
QUEUED
192.168.100.3:6379(TX)> hgetall
(error) ERR wrong number of arguments for 'hgetall' command
192.168.100.3:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors. ← 所有命令无法执行,事务被丢弃
# 数据无变化
192.168.100.3:6379> hgetall product:redis
1) "name"
2) "redis"
3) "release"
4) "7"
5) "age"
6) "19" ← 数据无变化
第二种错误情况
下面演示的是在执行 exec
命令之后带来的错误。
192.168.100.3:6379> multi
OK
192.168.100.3:6379(TX)> hdel product:redis age
QUEUED
192.168.100.3:6379(TX)> get product:redis ← 对 hash 数据类型使用不适用的相关命令 ,如您所见,也是被放入到了队列中
QUEUED
192.168.100.3:6379(TX)> hdel product:redis release
QUEUED
192.168.100.3:6379(TX)> exec
1) (integer) 1
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) (integer) 1
# 数据发生变化
192.168.100.3:6379> hgetall product:redis
1) "name"
2) "redis"
简单演示了这两种错误发生的情况,希望对您有帮助。
watch
和 unwatch
MySQL当中,我们提到过各种锁,从锁的机制思想来看,可以划分为:
- 悲观锁:又名 悲观并发控制(Pessimistic Concurrency Control,缩写 "PCC"),它是一种并发控制的方法。如果一个事务执行的操作对某行数据应用了锁,那只有当这个事务把锁释放(即提交事务或者回滚事务后才释放锁),其他事务才能够执行与该锁冲突的操作。简单来说就是事务每次操作这行数据时,很悲观,总觉得其他事务会修改它,于是,在操作数据之前先将其锁定。悲观锁它是一种思想或机制,并不是实际操作的时候特指某一个锁。本质上,排它锁与共享锁都是悲观锁的实现。
- 乐观锁:又名 乐观并发控制(optimistic concurrency control,,缩写 "OCC"),也是一种并发控制的方法。和 悲观锁相反,乐观锁假定认为数据一般情况下不会造成冲突,所以不会对操作的数据进行加锁,只有到数据提交的时候才通过一种机制(常见的是对记录的数据进行版本比对)来验证数据是否冲突。同样的,乐观锁也不是实际操作的时候特指某一个锁,它也是一种思想或机制。
Redis 是一个高性能的基于内存的缓存型数据库,设计的起初就是以性能优先,所以会采用 乐观锁 这种机制思想来最大程度的实现高并发。
Redis 采用了一个叫做 CAS(check-and-set) 的方法实现 乐观锁 这种机制思想,这种方法就体现在具体的 watch
命令上,前面文字提到了 Redis 事务部分支持原子性,就是因为该命令的存在。
正常情况下的 watch
监控
# 监控这个 key
192.168.100.3:6379> watch product:redis
OK
# 开启事务
192.168.100.3:6379> multi
OK
192.168.100.3:6379(TX)> hmset product:redis version 7.2.1
QUEUED
192.168.100.3:6379(TX)> exec
1) OK
192.168.100.3:6379> hgetall product:redis
1) "name"
2) "redis"
3) "version"
4) "7.2.1"
watch
监控后有数据被修改
步骤 | Session 1 | Session 2 |
---|---|---|
1 | 192.168.100.3:6379> watch product:redis OK |
|
2 | 192.168.100.3:6379> multi OK |
|
3 | 192.168.100.3:6379> hmset product:redis web redis.io OK |
|
4 | 192.168.100.3:6379(TX)> hmset product:redis datatype hash QUEUED # 返回 nil,事务被打断,整体执行失败 192.168.100.3:6379(TX)> exec (nil) 192.168.100.3:6379> hgetall product:redis 1) "name" 2) "redis" 3) "version" 4) "7.2.1" 5) "web" 6) "redis.io" |