本地事务-以mysql为例原创
事务特性
事务一般是指逻辑上的一组操作,或者作为单个逻辑执行单元的一系列操作。同属于一个事务的操作会作为一个整体交给系统,这些操作要么全部执行成功要么全部执行失败。
事务存在的意义是为了保证系统中所有的数据都是符合期望的,且相互关联的数据之间不会产生矛盾,即数据状态之间的一致性(Consistency)。
按照数据库的经典理论,要达成一致性的这个目标,需要三方面的共同努力来保障。
- 原子性(Atomic):在同一项业务的处理过程中,所有的操作要么全部成功,要么全部失败。
- 隔离性(Isolation):不同的业务处理过程的读写操作不会互相干扰。
- 持久性(Durability):一旦事务执行成功,事务的操作结果就被保存下来了,不会丢失。
mysql事务基础
mqsql是一种典型的支持事务的关系型数据库,因此我们从mysql为研究对象,研究本地事务在mysql上的现象。
并发下事务会产生的问题
在并发的场景下,多个事务之间执行的读写操作存在相互影响的问题。我们常说并发场景下事务存在幻读、不可重复度、脏读这三种问题,这其实是锁的组合以及锁的生命周期导致的。
在介绍幻读、不可重复读以及脏读之前,我们有必要了解一下锁。
锁,是为了保证共享资源按照我们想要的方式被读写的一种策略。
现代数据库均提供了以下三种锁。
读锁(read lock,也叫做共享锁,S锁):数据被一个事务上了读锁之后,允许其他事务上读锁,但是不允许其他事务上写锁。当前仅当该数据只有当前事务上读锁时,读锁可以被当前事务升级为写锁。
写锁(write lock,也叫做排他锁,X锁):数据被一个事务上了写锁之后,不允许其他事务上写锁或者读锁。
范围锁(range lock):对于某个范围直接加排他锁,在这个范围内的数据不能被写入。如下语句是典型的加范围锁的例子
select * from account where id < 5 for update;
请注意,不要把范围锁理解成一组排他锁的集合。加了范围锁之后,不仅无法修改该范围内已有的数据,也不能在该范围内新增或者删除任何数据,后者是一组排他锁的集合无法做到的。
按照 ANSI/ISO SQL-92的定义,事务的隔离级别按照强度从大到小排序可以分为四种
- 串行化(serializable)
- 可重复读(repeatable read)
- 读已提交(read committed)
- 读未提交(read uncommitted)
按照隔离级别以及他们可能出现的问题,我们可以绘制出如下表格。
事务的隔离级别 | 存在的问题 |
---|---|
串行化 | 无 |
可重复读 | 幻读 |
读已提交 | 幻读、不可重复读 |
读未提交 | 幻读、不可重复读、读未提交 |
在开始阐述事务的隔离级别之前,我们做一下案例的准备工作。
create database test;
use test;
# 创建数据表account
create table account(
id int not null auto_increment,
name varchar(30) not null default '',
balance int not null default 0,
primary key(id)
) engine=InnoDB default charset=utf8mb4;
# 插入初始化数据。
INSERT INTO
test.account(NAME, balance)
VALUES
('张三', 300),
('李四', 350),
('王五', 500);
# 设置mysql不自动提交事务
show variables like 'autocommit' # 此时为ON
set autocommit = 0;
show variables like 'autocommit' # 此时为OFF
接下来我们按照事务的隔离级别,从强到弱进行讲解。
串行化
在串行化这个隔离级别下,所有的操作都是按照顺序执行的,不存在并发的情况。完全符合普通程序员对数据竞争加锁的理解,如果不考虑性能优化的话,对事务的所有读、写数据全部加上读锁、写锁和范围锁即可做到可串行化(简化理解,实际上还是很复杂的)但是数据库不考虑性能肯定是不行的。现代数据库一定会提供可串行话以外 的其他隔离级别提供给用户使用,让用户调节隔离级别的选项,根本目的是让用户可以调节数据库的加锁方式,趣得隔离性与吞吐量之间的平衡。
可重复读
相较于可串行化,可重复读存在幻读的问题。
在任何数据库客户端打开两个查询窗口,这里使用sqlyog进行演示。
第一步:在事务T1执行如下语句
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT * FROM account WHERE id < 5;
得到如下结果
第二步:事务T2执行如下语句
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
INSERT INTO account (`name`, `balance`) VALUES ('李四', 100);
第三步:在事务T1执行如下语句
SELECT * FROM account WHERE id < 5;
得到如下结果
此时我们发现事务T1读取到了事务T2的数据,这证明事务T1、T2之间的隔离性受到了破坏。而这种破坏的问题我们叫做幻读。指的是一个事务读到了另一个事务新增的数据。
出现幻读的本质是,该事务隔离级别下没有加范围锁来禁止在该范围内插入新的数据。
读已提交
第一步:事务T1执行如下语句
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT * FROM account WHERE id = 1;
此时可以看到张三的余额为300
第二步:在事务T2执行如下语句
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
UPDATE account SET balance = balance + 100 WHERE id = 1;
有些朋友会报错
Cannot execute statement: impossible to write to binary log since BINLOG_FORMAT = STATEMENT and at least one table uses a storage engine limited to row-based logging. InnoDB is limited to row-logging when transaction isolation level is READ COMMITTED or READ UNCOMMITTED.
找到my.ini 或者my.cnf文件
添加如下配置,即可解决该问题。
binlog_format=row
第三步:在事务T1执行如下语句
select * from account;
发现张三的存款仍然为300元,这解决了脏读问题
第四步:在事务T2执行如下语句
commit;
select * from account;
发现读到了更新后的数据,但是本次读取和第一次读取得到的数据不一致,这就出现了不可重复读问题。
读未提交
第一步:事务T1执行如下语句
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT * FROM account WHERE id = 1;
此时可以看到张三的余额为300
第二步:在事务T2执行如下语句
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
UPDATE account SET balance = balance + 100 WHERE id = 1;
第三步:在事务T1执行如下语句
select * from account;
读到了未提交的数据,出现了脏读。
参考文献
《凤凰架构》-- 周志明
《深入理解分布式事务:原理与实战》 – 肖宇;冰河