事务特性 ?

原子性: 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;

一致性: 执行事务前后,数据保持一致;

隔离性: 并发访问数据库时,一个用户的事物不被其他事物所干扰,各并发事务之间数据库是独立的;

持久性: 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响

事务隔离级别?

SQL 标准定义了四个隔离级别:

READ-UNCOMMITTED(读取未提交):

最低的隔离级别,允许读取尚未提交的数据变更, 可能会导致脏读、幻读或不可重复读

READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据, 可以阻止脏读,但是幻读或不可重复读仍有可能发生

REPEATABLE-READ(可重读): 一个事务内多次读取结果都是一致的,除非数据是被本身事务自己所修改, 可以阻止脏读和不可重复读,但幻读仍有可能发生。

SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说, 该级别可以防止脏读、不可重复读以及幻读

MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)

注: 与 SQL 标准不同的地方在于InnoDB 存储引擎在 REPEATABLE-READ(可重读) 事务隔离级别下使用的是Next-Key Lock 锁算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server)是不同的。所以说InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)  已经可以完全保证事务的隔离性要求,即达到了 SQL标准的 SERIALIZABLE(可串行化) 隔离级别。

并发事务的问题

不可重复度和幻读区别:

不可重复读的重点是修改,幻读的重点在于新增或者删除。

InnoDB的七种锁

总的来说,InnoDB共有七种类型的锁:

  • 共享/排它锁(Shared and Exclusive Locks)
  • 意向锁(Intention Locks)
  • 记录锁(Record Locks)
  • 间隙锁(Gap Locks)
  • 临键锁(Next-key Locks)
  • 插入意向锁(Insert Intention Locks)
  • 自增锁(Auto-inc Locks)

并发控制的场景在于对临界资源的访问,为了并发情况下保证数据一致性一般采取的手段:

  • 加锁
  • 多版本并发控制(MVCC)

如果对并发中任何操作都加锁,完全串行,吞吐量严重受限,不过讨论一些并发的场景,对于 临界资源的

读读操作 可以在读的时候加上 共享锁,可以读读并行;

读写操作 可以使用 MVCC,做到读写并行;

写写操作 使用 排他锁

redo undo

理解这两种日志,就能理解 事务实现MVCC实现

每次数据库commit操作后为了保证事务特性中的 持久性,应该将提交的事务数据刷到磁盘, 频繁的磁盘IO 操作性能会很差并且还是 磁盘随机写,性能更差。所以其实commit操作只是临时把数据刷到内存buffer(Page Cache)中,并将page cache的内存结构的物理改变以二进制写入redo log,此时变成了 磁盘顺序写,其buffer数据定期刷盘。即使某一刻数据库宕机,重启后,已提交的且在宕机前尚未刷盘事务数据会从redo log重做回来。

其实这里面涉及的 存储设计 非常通用,消息中间件为了确保消息不丢失一般都先落盘再投递,落盘尽量转化为磁盘顺序IO,并且一般都批量异步刷盘。以kafka和rocketmq存储设计举例

Kafka 会在 Broker 上为每一个 topic 创建一个独立的 partiton 文件,Broker 接受到消息后,会按主题在对应的 partition 文件中顺序的追加消息内容。而 RocketMQ 则会创建一个 commitlog 的文件来保存分片上所有主题的消息。

Broker 接收到任意主题的消息后,都会将消息的 topic 信息,消息大小,校验和等信息以及消息体的内容顺序追加到 Commitlog 文件中,Commitlog 文件一般为固定大小,当前文件达到限定大小时,会创建一个新的文件,文件以起始便宜位置命名。

同时,Broker 会为每一个主题维护各自的 ConsumerQueue 文件,文件中记录了该主题消息的索引,包括在 Commitlog 中的偏移位置,消息大小及校验和,以便于在消费时快速的定位到消息位置。ConsumerQueue 的维护是异步进行的,不影响消息生产的主流程,即使 ConsumerQueue 没有及时更新的 情况下,服务异常终止,下次启动时也可以根据 Commitlog 文件中的内容对 ConsumerQueue 进行恢复。

数据库事务未提交时,会将事务内的修改前的记录在undo 日志里面,用于事务执行失败的回滚,比如对于 insert操作,undo日志记录新数据的PK(ROW_ID),回滚时直接删除;

对于 delete/update操作,undo日志记录旧数据row,回滚时直接恢复;

所以说undo 日志保证了事务的 原子性

InnoDB的内核,会对所有row数据增加三个内部属性:

(1) DB_TRX_ID,6字节,记录每一行最近一次修改它的事务ID;

(2) DB_ROLL_PTR,7字节,记录指向回滚段undo日志的指针;

(3) DB_ROW_ID,6字节,单调递增的行ID;

所以说MVCC中旧版本数据都存在于undo 日志中,当对临界资源有写操作时,读操作可以不加锁的读取旧版本数据,进而提高读写性能。

MVCC 在 读提交 和 可重复读 隔离级别下的差异

在mvcc中读操作

  • 总是能读到本事务内操作

  • RC下,快照读总是能读到最新的行数据快照,当然,必须是已提交事务写入的

  • RR下,某个事务首次read记录的时间为T,未来不会读取到T时间之后已提交事务写入的记录,以保证连续相同的read读到相同的结果集

事务隔离级别的实现

InnoDB使用不同的锁策略来实现不同的隔离级别。

\\ 读未提交**

该场景下select操作不加锁,可能出现脏读

串行化

这种事务的隔离级别下,所有select语句都会被隐式的转化为select … in share mode.这可能导致,如果有未提交的事务正在修改某些行,所有读取这些行的select都会被阻塞住。这是一致性最好的,但并发性最差的隔离级别。

可重复读

  • 普通的select 使用快照读(snapshot read),这是一种不加锁的一致性读(Consistent Nonlocking Read),底层使用MVCC来实现

  • 加锁的select(select … in share mode / select … for update), update, delete等语句,它们的锁,依赖于它们是否在唯一索引(unique index)上使用了唯一的查询条件(unique search condition),或者范围查询条件(range-type search condition):

    • 在唯一索引上使用唯一的查询条件,会使用记录锁(record lock),而不会封锁记录之间的间隔,即不会使用间隙锁(gap lock)与临键锁(next-key lock)

    • 范围查询条件,会使用间隙锁与临键锁,锁住索引记录之间的范围,避免范围间插入记录,以避免产生幻影行记录,以及避免不可重复的读

\\ 读提交 **

  • 普通读是快照读;

  • 加锁的select, update, delete等语句,除了在外键约束检查(foreign-key constraint checking)以及重复键检查(duplicate-key checking)时会封锁区间,其他时刻都只使用记录锁;

此时,其他事务的插入依然可以执行,就可能导致,读