数据库事务隔离级别

Concurrency Control: Transaction Isolation

本次分享内容

  1. 事务的特点
  2. 常见的事务并发错误以及解决方案
  3. 事务隔离级别 是什么/为什么/怎么实现

事务提供的安全性保证:ACID

持久性 Durability

其实,并不存在完美的持久性

事务提交后,即使存在故障,事务写入的任何数据不会丢失

原子性 Atomicity

原子代表不可分割

包含了多个写操作的事务,如果事务终止,则会撤销所有的修改。 要不全部成功,要么全部失败。

原子性可以简化事务的重试

隔离性 Isolation

隔离性其实含义更接近 可序列化 隔离级别

并发执行的多个事务相互隔离,不能相互交叉。(可串行化)

通俗讲: 就是多个事务不能相互影响,就像一个接一个执行一样。

一致性 Consistency

对数据有特定的预期状态,任何数据必须满足需求

注意:

  1. 一致性要求应用层来维护
  2. 应用层可能借助 原子性 隔离性,达到一致性

比如: 充值 = 余额 + 消费

为什么需要事务?

  1. 需要事务提供的原子性保障,简化错误处理与重试
  2. 需要提供的隔离性,预防并发问题

常见的事务隔离级别

常见的错误并发现象

多个事务同时并发执行时,发生的错误

脏读 (dirty read)

一个事务读取了另一个并行未提交事务写入的数据。

不可重复读 (nonrepeatable read)

一个事务重新读取之前读取过的数据,发现该数据已经被另一个事务(在初始读之后提 交)修改。

幻读 (phantom read)

一个事务重新执行一个返回符合一个搜索条件的行集合的查询, 发现满足条件的行集合 因为另一个最近提交的事务而发生了改变。

序列化异常 (serialization anomaly)

成功提交一组事务的结果与这些事务所有可能的串行执行结果都不一致。

事务隔离级别表格

隔离级别脏读不可重复读幻读序列化异常
读未提交允许可能可能可能
读已提交不可能可能可能可能
可重复读不可能不可能允许可能
可序列化不可能不可能不可能不可能

备注

问题: 事务隔离级别是全局设置吗?

设置语法

set [global | session] transaction isolation level xxxxx;

set global transaction isolation level serializable;
set session transaction isolation level read committed;
set transaction isolation level repeatable read;

注意

读已提交

脏读

并发事务,事务B 读到了 事务A,已写入 未提交 的数据

举个例子

begin;
	update users set age = 18;
	--- 这里发生脏读,会出现 有一个18岁,但是18岁统计值 为0的矛盾数据
	update state set age_18_count = age_18_count + 1;
commit;

脏写

两个事务,都没有提交时,修改了相同数据,后写入的操作,会覆盖早的写入

会带来的问题

如果两个事务,需要同时更新多行数据,会产生错误结果

举个例子

begin;                                             begin;
	update car set user = 'dalei' where id = 2;
	                                                   update `key` set user = 'mifa' where id=1;
	                                                   update car set user = 'mifa' where id=2;
	update `key` set user = 'dalei' where id = 1;
commit;                                            commit;

结果是:dalei有🔑,mifa有🚗

对于脏读

策略

只读已经提交的数据

实现

(开始有点MVCC的味道了)

对于脏写

策略

为了预防脏写,一般推迟第二个写操作,直到前面的事务完成

( 卡住 )

实现

数据库会自动加上行锁

可重复读/快照隔离级别

读不阻塞写,写不阻塞读

不可重复读

一个事务重新读取之前读取过的数据,发现该数据已经被另一个事务(在初始读之后提 交)修改。

例子: mifa给dalei转账5元, 开始均有10块钱

begin;
  select balance from account where name='dalei'; -- 10
                                                        begin;
                                                          update account set balance = balance + 5 where name = 'dalei';
                                                          update account set balance = balance - 5 where name = 'mifa';
                                                        commit;
  select balance from account where name='mifa'; -- 5
commit;

奇怪🤔:怎么只有15 = 10 + 5

可重复读/快照隔离级别

策略

每个事务,一开始只能看到最近提交的数据, 即使后来被改了,还是只能看到老数据

实现

写操作需要加写锁, 防止脏写 读数据不加锁,数据库保留了对象的不同版本

与读已提交不同点

更新丢失问题

一般都是 读-修改-写回 引发的丢失问题

常用的方法

注意:

对于并发更新

常见并发更新的例子

可重读隔离级别下, 过年增加年龄

begin;
  select age from users where name='dalei' for update; -- 18
                                                                 begin;
                                                                   select age from users where name='dalei'; -- 18
                                                                   select age from users where name='dalei' for update; -- pg 会中止事物,mysql?
                                                                   select age from users where name='dalei'; -- 值是多少?
                                                                   update users set age = age + 1 where name='dalei';
                                                                 commit;
	update users set age = age + 1 where name='dalei';
commit;

问题

上面👆pg会报:

could not serialize access due to concurrent update

可串行化

读阻塞写,写阻塞读

写倾斜和幻读

两个事务同时更新不同对象

例子,两个人同时想退出admin, 系统至少有一个admin (注意pg可重读没这个问题)

begin;
  select count(*) from users where role='admin';
                                                                  begin;
                                                                  	select count(*) from users where role='admin';
                                                                  	update users set role='normal' where name='enlai'; -- count > 1
                                                                  commit;
  update users set role='normal' where name='dalei'; -- count > 1
commit;

fuck, 没有管理员了!

备注:

写倾斜模式

  1. select 查询一些条件
  2. 业务层 判断查询结果
  3. 根据结果,写入数据库,并提交事务 (写入的数据会影响步骤1)

解决办法

写倾斜与幻读

有些情况,加锁不能解决问题

东亮的问题, 需要插入数据

BEGIN;
  -- 步骤1: 检查会议室有空吗
  SELECT COUNT(*) FROM bookings WHERE room_id = 123 AND end_time > '2015-01-01 12:00' AND start_time < '2015-01-01 13:00';

  -- 步骤2: 如果👆返回0

  -- 步骤3: 插入记录
  INSERT INTO bookings (room_id, start_time, end_time, user_id) VALUES (123, '2015-01-01 12:00', '2015-01-01 13:00', 666);

COMMIT;

串行化的实现方式

2PL

面试问题:next-key locking?

后续分享