事务是什么

事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。

事务的ACID属性

  • 原子性(Atomicity): 事务是数据库的逻辑工作单位,事务中包括的诸操作要么全做,要么全不做。
  • 一致性(Consistency): 事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的
  • 隔离性(Isolation): 一个事务的执行不能被其他事务干扰。由并发事务所作的修改必须与任何其他并发事务所作的修改隔离。事务识别数据时数据所处的状态,要么是另一并发事务修改它之前的状态,要么是第二个事务修改它之后的状态,事务不会识别中间状态的数据。这称为可串行性,因为它能够重新装载起始数据,并且重播一系列事务,以使数据结束时的状态与原始事务执行的状态相同。
  • 持久性(Durability): 一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。

事务的4个隔离级别

事务并发可能出现的情况
脏读

一个事务读到了另外一个事务未提交的内容

脏读示意图

会话B开启一个事务,把id=1name小米12修改成苹果13,此时另外一个会话A也开启一个事务,读取id=1name,此时的查询结果为苹果13,会话B的事务最后回滚了刚才修改的记录,这样会话A读到的数据是不存在的,这个现象就是脏读。(脏读只在读未提交隔离级别才会出现

不可重复读

一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值。(不可重复读在读未提交和读已提交隔离级别都可能会出现)

会话A开启一个事务,查询id=1的结果,此时查询的结果name小米12。接着会话B把id=1name修改为苹果13(隐式事务,因为此时的autocommit为1,每条SQL语句执行完自动提交),此时会话A的事务再一次查询id=1的结果,读取的结果name苹果13。会话B再此修改id=1name洛基亚,会话A的事务再次查询id=1,结果name的值为洛基亚,这种现象就是不可重复读。

幻读

一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来。(幻读在读未提交、读已提交、可重复读隔离级别都可能会出现)

幻读示意图

会话A开启一个事务,查询id>0的记录,此时会查到name=小米12的记录。接着会话B插入一条name=苹果13的数据(隐式事务,因为此时的autocommit为1,每条SQL语句执行完自动提交),这时会话A的事务再以刚才的查询条件(id>0)再一次查询,此时会出现两条记录(name小米12苹果13的记录),这种现象就是幻读。

事务隔离级别

数据库事务的隔离级别有4个,读未提交、读已提交、可重复读、可串行读

隔离级别比较:读未提交<读已提交<可重复读<可串行读

隔离级别对性能的影响比较:读未提交<读已提交<可重复读<可串行读

有次可以看出,隔离级别越高,所需要消耗的性能越大,为了平衡二者,一般建议设置的隔离级别为可重复读,MySQL默认为此

读未提交

事务A可以读取到事务B修改过但未提交的数据。

可能发生脏读、不可重复读和幻读问题,一般很少使用此隔离级别。

读未提交隔离级别

读已提交

事务B只能在事务A修改过并且已提交后才能读取到事务B修改的数据。

读已提交隔离级别解决了脏读的问题,但可能发生不可重复读和幻读问题,一般很少使用此隔离级别

读已提交隔离级别

可重复读

事务B只能在事务A修改过数据并提交后,自己也提交事务后,才能读取事务B修改的数据

可重复读解决了脏读和不可重复读问题,但可能发送幻读

可重复读隔离级别

可串行读

各种问题(脏读、不可重复读、幻读)都不会发生,通过加锁实现(读写锁)

可串行读隔离级别(读读操作不会阻塞)

可串行读隔离级别(读写操作阻塞)

可串行读隔离级别(写读操作阻塞)

可串行读隔离级别(写写操作阻塞)

隔离级别比较:

脏读 不接重复读 幻读
读未提交 × × ×
读已提交 × ×
可重复读 ×
串行读

MySQL中事务的实际操作

创建一个测试用的表

1
2
3
4
5
6
7
8
create table tb_demo
(
id int not null,
name varchar(20) not null,
amount int not null,
constraint tb_demo_pk
primary key (id)
);

MySQL原生

事务正常提交:
1
2
3
4
5
begin; # 开始事务
insert into tb_demo value (1, "InkDP", 999);
insert into tb_demo value (2, "LFP", 999);
commit; # 提交
select * from tb_demo;
id name amount
1 InkDP 999
2 LPF 999

开始事务后正确的插入了两条数据,然后提交,最后查询可以看到两条数据是正常插入了的

ps: 如果此时有一条数据插入失败会怎样?

事务回滚:
1
2
3
4
begin; # 开始事务
insert into tb_demo value (3, "HDF", 999);
rollback; # 回滚
select * from tb_demo;
id name amount
1 InkDP 999
2 LPF 999

开始事务后插入一条数据,然后回滚,最后可以看到表记录里是没有刚刚插入的数据的。

使用Gorm操作事务

会话事务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func Transaction(db *gorm.DB) {
err := db.Transaction(func(tx *gorm.DB) error {
// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
if err := tx.Create(&Demo{Id: 1, Name: "InkDP", Amount: 999}).Error; err != nil {
// 返回任何错误都会回滚事务
return err
}

if err := tx.Create(&Demo{Id: 2, Name: "LPF", Amount: 999}).Error; err != nil {
return err
}

// 返回 nil 提交事务
return nil
})
log.Println("Error:", err)
}
手动事务(不建议使用)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func Transaction(db *gorm.DB) {
// 开始事务
tx := db.Begin()
result := tx.Model(&Demo{}).Where("id = ?", 1).UpdateColumn("amount", gorm.Expr("amount - ?", 500))
if result.Error != nil || result.RowsAffected == 0 {
// 事务回滚
tx.Rollback()
return
}
result = tx.Model(&Demo{}).Where("id = ?", 5).UpdateColumn("amount", gorm.Expr("amount + ?", 500))
if result.Error != nil || result.RowsAffected == 0 {
// 事务回滚
tx.Rollback()
return
}
// 提交事务
tx.Commit()
}

手动事务的时候一定要检查最终是否有调用Rollback或者Commit,否则它会一直占用这个连接,直到程序退出。

Kratos中使用Gorm完成事务

参考在 在 Go-Kratos 框架中优雅的使用 GORM 完成事务

主要的思路就是将gorm事务会话添加到上下文中

进阶

  • 隔离级别与事务的实现原理

  • 分布式事务

  • NoSql的事务