性能文章>本地事务-以mysql为例>

本地事务-以mysql为例原创

2年前
319012

事务特性

事务一般是指逻辑上的一组操作,或者作为单个逻辑执行单元的一系列操作。同属于一个事务的操作会作为一个整体交给系统,这些操作要么全部执行成功要么全部执行失败。

事务存在的意义是为了保证系统中所有的数据都是符合期望的,且相互关联的数据之间不会产生矛盾,即数据状态之间的一致性(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的定义,事务的隔离级别按照强度从大到小排序可以分为四种

  1. 串行化(serializable)
  2. 可重复读(repeatable read)
  3. 读已提交(read committed)
  4. 读未提交(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;

得到如下结果

image-20221022144705159

第二步:事务T2执行如下语句

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
INSERT INTO account (`name`, `balance`) VALUES ('李四', 100);

第三步:在事务T1执行如下语句

SELECT * FROM account WHERE id < 5;

得到如下结果

image-20221022145125424

此时我们发现事务T1读取到了事务T2的数据,这证明事务T1、T2之间的隔离性受到了破坏。而这种破坏的问题我们叫做幻读。指的是一个事务读到了另一个事务新增的数据。

出现幻读的本质是,该事务隔离级别下没有加范围锁来禁止在该范围内插入新的数据。

读已提交

第一步:事务T1执行如下语句

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT * FROM account WHERE id = 1;

此时可以看到张三的余额为300

image-20221022155222828

第二步:在事务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元,这解决了脏读问题

image-20221022155222828

第四步:在事务T2执行如下语句

commit;
select * from account;

发现读到了更新后的数据,但是本次读取和第一次读取得到的数据不一致,这就出现了不可重复读问题。

image-20221022154523219

读未提交

第一步:事务T1执行如下语句

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT * FROM account WHERE id = 1;

此时可以看到张三的余额为300

image-20221022155222828

第二步:在事务T2执行如下语句

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
UPDATE account SET balance = balance + 100 WHERE id = 1;

第三步:在事务T1执行如下语句

select * from account;

image-20221022154523219

读到了未提交的数据,出现了脏读。

参考文献
《凤凰架构》-- 周志明
《深入理解分布式事务:原理与实战》 – 肖宇;冰河

点赞收藏
分类:标签:
1763391
请先登录,查看1条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

日常Bug排查-偶发性读数据不一致

日常Bug排查-偶发性读数据不一致

2
1