事务

事务就是用户定义的一系列数据库操作

1. 事务四大特性(ACID)

  • 原子性(Atomicity)

原子性是指事务是不可分割的, 包含的所有操作要么全部成功,要么全部失败回滚,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。

  • 一致性(Consistency)

事务开始前和结束后,数据库的完整性约束没有被破坏。
比如A向B转账,不可能A扣了钱,B却没收到。

  • 隔离性(Isolation)

隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离
同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。
比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。

  • 持久性(Durability)

持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作

2. 事务隔离级别

查看数据库版本 select version() ;
mysql: 查看会话隔离级别
show variables like 'tx_isolation';
select @@transaction_isolation;
会话和系统隔离级别可能不同
设置隔离级别
set sesstion tx_isolation='read-committed';
session transaction isolation level read uncommitted ;

MySQL/InnoDB中,定义了四种隔离级别:

image.png

事务隔离级别(升高) 脏读 不可重复读 幻读 并发性能逐渐降低
读未提交(read-uncommitted) 可以读取未提交记录,也被称为脏读
此隔离级别很少用
读已提交 RC
(read-committed)
(大多数数据库) (互联网项目)
可重复读 RR
(repeatable-read)
(MySQL默认)重点🦖
串行化(serializable) 最高的隔离级别

MySQL配置 那Mysql在5.0这个版本以前,binlog只支持STATEMENT这种格式! 而这种格式在读已提交(Read Commited)这个隔离级别下主从复制是有bug的,因此Mysql将可重复读(Repeatable Read)作为默认的隔离级别!

  • 事务的隔离级别要得到底层数据库引擎的支持, 而不是应用程序或者框架的支持.
  • Oracle 支持的 2 种事务隔离级别:READ_COMMITED , SERIALIZABLE

隔离级别越高越能保证数据的完整性和一致性,对并发性能的影响也越大。
尽管它会导致不可重复读、幻读这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。

3. 并发事务在每个级别问题

更新丢失:两个事务操作相同数据,后提交的事务会覆盖先提交, 通过乐观锁解决

3.1 读未提交

脏读:所有事务都可以看到其他未提交事务的数据,产生脏读。

3.2 读已提交 RC

不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果因此本事务先后两次读到的数据结果会不一致。

解决不可重复读
每个SELECT都会获取最新的read view,因此你能读到其他事务提交的数据。
因为,这个View生成时机不同,所以实现了可重复读。

3.3 可重复读 RR

幻读:事务A读到了事务B新增的数据, 不符合隔离性

  • 幻读是指当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,之前的事务再次读取该范围的记录时,会产生幻行
  • 不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。
  • 解决不可重复读的问题只需锁住满足条件的行,解决幻读需要MVCC+间隙锁

解决不可重复读
具体是通过Readview实现。记录了未提交事务的表,事务会依据该表选择合适的行版本号进行快照读操作,具体选择版本号的方法是挑选除readview表的版本号外的最新版本号
当隔离级别为RR,事务执行一条语句时才生成readview,且之后不会再变动,就克服了不可重复读的问题。

快照读情况下,mysql通过mvcc来避免幻读

当前读情况下,mysql通过next-key来避免幻读

next-key 锁 (当前读)
next-key 锁包含两部分

  • 记录锁(行锁)
  • 间隙锁

记录锁是加在索引上的锁,间隙锁是加在索引之间的。(思考:如果列上没有索引会发生什么?)
select from T where number = 1 for update;
select
from T where number = 1 lock in share mode;
insert update delete
原理:对读取到的记录加锁(记录锁),同时保证对读取的范围加锁,新的满足查询条件的记录不能够插入(间隙锁)。

3.4 串行化

事务是在一个个按照顺序执行,不会产生任何异常。
从MVCC并发控制退化为基于锁的并发控制。不区别快照读和当前读,所有的读操作都是当前读,读加读锁(S锁),写加写锁(X锁)。
在该隔离级别下,读写冲突,因此并发性能急剧下降,在MySQL/InnoDB中不建议使用。

6. 事务启动语句

MySQL的事务启动方式有以下几种:

  1. 显式启动事务语句, beginstart transaction
  2. 提交语句是commit
  3. 回滚语句是rollback
  4. set autocommit=0,这个命令会将这个线程的自动提交关掉。意味着如果你只执行一个select语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行commitrollback 语句,或者断开连接。

查询长事务

  1. select *
  2. from information_schema.innodb_trx
  3. where TIME_TO_SEC(timediff(now(),trx_started))>60

MVCC

InnoDB引擎中 RR隔离级别和读已提交,是基于多版本的并发控制协议——MVCC(Multi-Version Concurrency Control), 解决了幻读的问题。

与MVVC相对的,是基于锁的并发控制,Lock-Based Concurrency Control。

一句话总结:同一份数据临时保存多个版本的一种方式,进而实现并发控制。

MVCC最大的优势:读不加锁,读写不冲突。在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能

多数数据库都实现了多版本并发控制,并且都是靠保存数据快照来实现的。

文章: 链接
链接2

1. 当前读和快照读

在MVCC并发控制中,读操作可以分为两类:
MVCC是为了实现读-写冲突不加锁,(读指的就是快照读, 而非当前读) 当前读实际上是一种加锁的操作,是悲观锁的实现

1)快照读 (snapshot read):
select

  • 非阻塞读 读取的是记录的可见版本 (有可能是历史版本),不用加锁(共享读锁s锁也不加,所以不会阻塞其他事务的写)。
  • 串行级别下的快照读会退化成当前读
  • 快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本

2)当前读 (current read):
select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁)

  • 读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。

    2. 隐式字段

  • DB_TRX_ID

6byte, 创建这条记录时可能为空, 记录最后一次修改该记录的事务id

  • DB_ROLL_PTR

7byte, 回滚指针, 指向这条记录的上一个版本

  • DB_ROW_ID

6Byte, 隐藏的主键id,
如果不存在主键,会以这个字段作为自增的聚簇索引

  • 创建版本号
  • 删除版本号

    3. 读视图Read-view

    RR可重复读级别, 在首次快照读时生成读视图
    RC读已提交级别, 在每次快照读时生成读视图
    image.png
    image.png

    4. MVCC怎么实现的

以 InnoDB 为例。可以理解为每一行中都冗余了两个字段,一个是行的创建版本,一个是行的删除(过期)版本。
具体的版本号(trx_id)存在 information_schema.INNODB_TRX 表中。
版本号(trx_id)随着每次事务的开启自增。
事务每次取数据的时候都会取创建版本小于当前事务版本的数据,以及过期版本大于当前版本的数据。

普通的 select 就是快照读。
select * from T where number = 1;
原理:将历史数据存一份快照,所以其他事务增加与删除数据,对于当前事务来说是不可见的。

MVCC的实现是通过undo logread view来实现的

在innodb引擎下的表,每个数据行都有隐藏的两列,一列是trx_id,也就是更新(insert、update、delete)这条记录的事务ID;一列是roll_pointer,指向上次修改的指针,如果是新增的则为null;如果不存在主键的话,还会有第三列row_id,在没有主键的情况下默认生成的主键;

我们都知道在mysql的事务日志中有redo log和undo log,redo log记录的是真实改变的值,而undo log记录的是和操作相反的操作,由于一条记录可能会被修改多次,这些修改连在一起就形成了一个版本链,这个版本链就是MVCC实现的基础。
image.png
其中最后两列一个是trx_id,一个是roll_pointer。有了版本链,还有一个read view,看这是什么概念,翻译过来叫一致性视图,一致性视图中有以下几个属性比较重要,
m_ids,在生成read view时当前活跃的读写事务的列表

min_trx_id,m_ids中最小的

max_trx_id,m_ids中最大的+1

版本链中的trx_id是否对当前事务可见通过以下的规则进行判断,

trx_id<min_trx_id 表示数据中的事务ID比当前活跃的事务id最小的还小,代表该记录在生成readview的时候已经提交,那么是可见的;

trx_id>=max_trx_id 表示数据中的事务ID比当前活跃的事务id最大的还大,代表该记录在生成readview后提交的,那么是不可见的;

min_trx_id<=trx_id<max_trx_id 当trx_id在m_ids中表示,该事务还未提交,那么是不可见的;当trx_id不在m_ids中,说明已经提交了,那么是可见的;

如果某个版本的数据对当前事务是不可见的,那么就要顺着版本链继续查找下个版本,直到找到可见的版本。

数据可见性算法

image.png

大事务怎么排查和解决的

执行时间很长的事务

  1. select *
  2. from information_schema.innodb_trx
  3. where TIME_TO_SEC(timediff(now(),trx_started))>60;

解决:
优化SQL,将一个大事务拆成多个小事务执行,或者缩短事务执行时间即可。

数据库锁

数据的锁定分为两种,第一种叫作悲观锁,第二种叫作乐观锁。

悲观锁

悲观锁,就是对数据的冲突采取一种悲观的态度,也就是说假设数据肯定会冲突,所以在数据开始读取的时候就把数据锁定住。【数据锁定:数据将暂时不会得到修改】

1、排它锁,当事务在操作数据时把这部分数据进行锁定,直到操作完毕后再解锁,其他事务操作才可操作该部分数据。这将防止其他进程读取或修改表中的数据。
实现
大多数情况下依靠数据库的锁机制实现
一般使用 select * from account where name="Max" for update
这条sql 语句锁定了account 表中所有符合检索条件(name=”Max”)的记录。本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。
select for update获取的行锁会在当前事务结束时自动释放,因此必须在事务中使用。
mysql还有个问题是select for update语句执行中所有扫描过的行都会被锁上,这一点很容易造成问题。因此如果在mysql中用悲观锁务必要确定走了索引,而不是全表扫描。

共享锁和排它锁是悲观锁的不同的实现,它俩都属于悲观锁的范畴。

乐观锁

乐观锁,认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让用户返回错误的信息。让用户决定如何去做。

如果有人在你之前更新了,你的更新应当是被拒绝的,可以让用户重新操作。
实现

  1. 大多数基于数据版本(Version)记录机制实现

下单操作包括3步骤:

  1. 查询出商品信息

    1. select (status,status,version) from t_goods where id=#{id}
  2. 根据商品信息生成订单

  3. 修改商品status为2
    1. update t_goods
    2. set status=2,version=version+1
    3. where id=#{id} and version=#{version};
  1. CAS

按照锁的粒度分数据库锁

无锁

主键不存在

行级锁(InnoDB) (排它锁)

推荐阅读! https://www.cnblogs.com/huangfuyuan/p/9510022.html

开销大,加锁慢;会出现死锁;
锁定粒度最小,发生锁冲突的概率最低,并发度也最高;
对表的记录加锁
InnoDB 存储引擎默认情况下是采用行级锁(row-level locking),也支持表级锁。

InnoDB存储引擎的锁的算法有三种

1.Record lock 记录锁
单个行记录上的锁
2.Gap lock
间隙锁,锁定一个范围,不包括记录本身
3.Next-key lock
Next-Key锁时索引记录上的记录锁和在记录之前的间隙锁的组合。

行级锁分为共享锁和排它锁, 都不允许其他事务执行写操作, 但是可以读数据,
排它锁不允许对数据再另行加锁

1. 共享锁(读锁)(S锁)

行级锁
只有在serializable事务隔离级别, 才会给数据读取添加共享锁

  1. select ... from ... LOCK IN SHARE MODE;

事务不提交,共享锁不释放, 添加了共享锁, 其他事务可以读取, 不可以修改数据

2. 排它锁(写锁)(X锁)

行级锁
MySQL默认加排它锁

  1. select ... from ... FOR UPDATE;

共享锁和排它锁
https://zhuanlan.zhihu.com/p/29150809

范围锁(间隙锁)

Next-key lock(左开右闭)
Next-Key锁时索引记录上的记录锁和在记录之前的间隙锁的组合。
解决幻读问题

页面锁

开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
BDB 存储引擎采用的是页面锁(page-level locking),但也支持表级锁

表级锁

主键不明确,没有用到索引
开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低;
MyISAM 和 MEMORY 存储引擎采用的是表级锁(table-level locking)

1. 意向锁

当一个事务带着表锁 去访问行锁资源, 行锁就会升级成意向锁,锁住表

2. 自增锁

事务插入自增类型数据时, 获取自增锁

锁索引数

记录锁

如何减少并发冲突

将复杂的sql语句拆分成简单的SQL


参考文章:
原文链接:https://blog.csdn.net/woshiyeguiren/article/details/80277475