一、概述

数据库一般都会并发执行多个事务,多个事务可能会并发的对相同的一批数据进行增删改查操作,可能 就会导致我们说的脏写、脏读、不可重复读、幻读这些问题。 这些问题的本质都是数据库的多事务并发问题,为了解决多事务并发问题,数据库设计了事务隔离机制、锁机 制、MVCC多版本并发控制隔离机制,用一整套机制来解决多事务并发问题

二、事务及ACID属性

2.1、ACID属性

事务是由一组SQL语句组成的逻辑处理单元,事务具有以下4个特性,简称ACID属性

  • 原子性 atomocity - 事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。
  • 隔离性 isolation - 数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独 立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。
  • 持久性 durable - 事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持
  • 一致性 consiistent - 在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规 则都必须应用于事务的修改,以保持数据的完整性

    2.2、事务并发带来的问题

  • 更新丢失或脏写 - 当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存 在,就会发生丢失更新问题–最后的更新覆盖了由其他事务所做的更新

  • 脏读 - 一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致的状态;这 时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此作进一步的 处理,就会产生未提交的数据依赖关系。这种现象被形象的叫做“脏读”。事务A读取到了事务B已经修改但尚未提交的数据,还在这个数据基础上做了操作。此时,如果B 事务回滚,A读取的数据无效,不符合一致性要求。
  • 不可重复读 - 事务A内部的相同查询语句在不同时刻读出的结果不一致,不符合隔离性
  • 幻读 - 事务A读取到了事务B提交的新增数据,不符合隔离性

    三、事务的隔离级别

    image.png
    注意:隔离级别越高,越安全,并发性越差,性能越差,

    1. 常看当前数据库的事务隔离级别: show variables like '%tx_isolation%';
    2. 设置事务隔离级别:set tx_isolation='REPEATABLE-READ';
    3. Mysql默认的事务隔离级别是可重复读,用Spring开发程序时,如果不设置隔离级别默认用Mysql设置的隔 离级别,
    4. 如果Spring设置了就用已经设置的隔离级别
    5. show open tables; 查看是否被加了表锁,in_use1表示被加了表锁

    四、锁

    4.1、锁分类

  • 从性能上分为乐观锁(用版本对比来实现)和悲观锁

  • 从对数据库操作的类型分,分为读锁和写锁(都属于悲观锁)

读锁(共享锁,S锁(Shared)):针对同一份数据,多个读操作可以同时进行而不会互相影响
写锁(排它锁,X锁(eXclusive)):当前写操作没有完成前,它会阻断其他写锁和读锁

  • 从对数据操作的粒度分,分为表锁和行锁

    4.2、表锁与行锁

    4.2.1、表锁

    每次操作锁住整张表。开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低; 一般用在整表数据迁移的场景``sql CREATE TABLEmylock(idINT (11) NOT NULL AUTO_INCREMENT,NAMEVARCHAR (20) DEFAULT NULL, PRIMARY KEY (id) ) ENGINE = MyISAM DEFAULT CHARSET = utf8; INSERT INTOtest.mylock(id,NAME) VALUES ('1', 'a'); INSERT INTOtest.mylock(id,NAME) VALUES ('2', 'b'); INSERT INTOtest.mylock(id,NAME) VALUES ('3', 'c'); INSERT INTOtest.mylock(id,NAME`) VALUES (‘4’, ‘d’); 手动增加表锁 - lock table 表名称 read(write),表名称2 read(write); 查看表上加过的锁 - show open tables; 删除表锁 - unlock tables; — lock table mylock read; 读锁 当前session和其他session都可以读该表 当前session中插入或者更新锁定的表都会报错,其他session插入或更新则会等待(等待锁释放) — lock table mylock write; 写锁 当前session对该表的增删改查都没有问题,其他session对该表的所有操作被阻塞
mysiam存储引擎只支持表锁

- 对MyISAM表的读操作(加读锁) ,不会阻寒其他进程对同一表的读请求,但会阻赛对同一表的写请求。只有当 读锁释放后,才会执行其它进程的写操作。 
- 2、对MylSAM表的写操作(加写锁) ,会阻塞其他进程对同一表的读和写操作,只有当写锁释放后,才会执行其它进 程的读写操作
<a name="oeHRg"></a>
### 4.2.2、行锁
行锁 开销大 加锁慢 因为需要先去找到这一行,会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度最 高。 <br />**innodb支持事务,支持行锁(锁是加载索引上的)**<br />**一个session开启事务更新不提交,另一个session更新同一条记录会阻塞,更新不同记录不会阻塞(如下图,由于session - 1更改但未提交事务,所以session - 2会被阻塞),mysql的默认隔离级别**<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1161651/1604470359186-50b0d1fe-10d8-4b95-bba9-d1d9b49bb979.png#crop=0&crop=0&crop=1&crop=1&height=117&id=qI10E&name=image.png&originHeight=219&originWidth=1395&originalType=binary&ratio=1&rotation=0&showTitle=false&size=31408&status=done&style=none&title=&width=746)<br />**总结:**

- MyISAM在执行查询语句SELECT前,会自动给涉及的所有表加读锁,在执行update、insert、delete操作会自 动给涉及的表加写锁。
- **InnoDB在执行查询语句SELECT时(非串行隔离级别),不会加锁。但是update、insert、delete操作会加行 锁**。

**************读锁会阻塞写,但是不会阻塞读,而写锁则会把读和写都阻塞*****************<br />锁索引记录<br />锁间隙<br />锁索引+间隙
<a name="aXYZR"></a>
# 五、 行锁与事务隔离级别演示
mysql的事务隔离级别是针对session的
<a name="C2WmO"></a>
## 5.1、隔离级别 - 读未提交
**-- 设置隔离级别 set tx_isolation='read-uncommitted';**<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1161651/1604472154782-46599cfb-3d1d-4c47-b409-d0aed6e6c77c.png#crop=0&crop=0&crop=1&crop=1&height=433&id=BSfRi&name=image.png&originHeight=803&originWidth=1314&originalType=binary&ratio=1&rotation=0&showTitle=false&size=170036&status=done&style=none&title=&width=709)<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1161651/1604472758739-a357a6ae-a0bc-49e6-a923-ecdffe500a41.png#crop=0&crop=0&crop=1&crop=1&height=264&id=H7XUo&margin=%5Bobject%20Object%5D&name=image.png&originHeight=438&originWidth=1225&originalType=binary&ratio=1&rotation=0&showTitle=false&size=56243&status=done&style=none&title=&width=739)<br />A事务读到了B事务未提交的事务 - 脏读<br />A事务在同一个事务下统一sql语句读取到的值不相同 - 不可重复读<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1161651/1604473226421-cd7ac974-459d-4807-8a8a-cbed1b4ef8e7.png#crop=0&crop=0&crop=1&crop=1&height=282&id=vQVwS&margin=%5Bobject%20Object%5D&name=image.png&originHeight=485&originWidth=1241&originalType=binary&ratio=1&rotation=0&showTitle=false&size=59626&status=done&style=none&title=&width=722)<br />事务A在同一个事务下同一语句读取的结果不同 - 幻读
<a name="rRZuF"></a>
## 5.2、隔离级别 - 读已提交
**-- 设置隔离级别 set tx_isolation='read-committed';**<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1161651/1604479412859-4ff45342-d05e-40fd-8b92-cc37661ec5df.png#crop=0&crop=0&crop=1&crop=1&height=318&id=HAm5T&margin=%5Bobject%20Object%5D&name=image.png&originHeight=514&originWidth=1207&originalType=binary&ratio=1&rotation=0&showTitle=false&size=69160&status=done&style=none&title=&width=746)<br />A事务并未读到B事务未提交的事务 - 解决了脏读问题<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1161651/1604480617903-ca07c7e4-a3af-4030-9d27-6064c69b1ab8.png#crop=0&crop=0&crop=1&crop=1&height=252&id=M6ltW&margin=%5Bobject%20Object%5D&name=image.png&originHeight=395&originWidth=1170&originalType=binary&ratio=1&rotation=0&showTitle=false&size=53007&status=done&style=none&title=&width=746)<br />A事务同一条sql读取到的数据依然不一致 - 未解决可重复读问题<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1161651/1604480889144-5ff80de9-38d3-45a5-9f79-0dad20636010.png#crop=0&crop=0&crop=1&crop=1&height=347&id=LEkoz&margin=%5Bobject%20Object%5D&name=image.png&originHeight=543&originWidth=1168&originalType=binary&ratio=1&rotation=0&showTitle=false&size=70275&status=done&style=none&title=&width=746)<br />A事务同一事务下读到的数据条数不一样 - 未解决幻读问题
<a name="DBWQd"></a>
## 5.3、隔离级别 - 可重复读
**set tx_isolation='repeatable-read';**<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1161651/1604481616206-79ee57de-05ab-4d72-98d2-fd8bad19f22f.png#crop=0&crop=0&crop=1&crop=1&height=335&id=EyoqU&margin=%5Bobject%20Object%5D&name=image.png&originHeight=568&originWidth=1265&originalType=binary&ratio=1&rotation=0&showTitle=false&size=90781&status=done&style=none&title=&width=746)<br />虽然B事务已经更改了值并且提交,数据库值也做了更新,但是A事务多次读取的值依然是相同的(其实这是不对的) -- 解决了可重复读问题;   (此时A事务也不会读到B事务新增或者删除的数据,幻读也不会出现)<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1161651/1604544400056-22ab483a-697e-47af-bc96-76ee77769684.png#crop=0&crop=0&crop=1&crop=1&height=353&id=nwzm0&margin=%5Bobject%20Object%5D&name=image.png&originHeight=572&originWidth=1210&originalType=binary&ratio=1&rotation=0&showTitle=false&size=74651&status=done&style=none&title=&width=746)<br />A事务多次读取数据库,读取到的值不相同,发生了幻读(其实事务A是可以感知到其他事务对数据库进行了更改)

| id | balance |
| --- | --- |
| 1 | 200 |

<a name="A5yBV"></a>
### 5.3.1、可重复读的问题
客户端A<br />1、打开事务<br />2、select * from account where id = 1  -- balace结果为200<br />6、select * from account where id = 1  -- balace结果为200<br />9、select * from account where id = 1  -- balace结果为200  --注释客户端B的事务已提交<br />10、update account balace = balance - 50 where id = 1<br />11、select * from account where id = 1  -- balace结果为100  -- 10、11两步为正确的步骤和结果<br />12、如果在java客户端操作,balance = balance - 50  -- 则balace =  150<br />13、update account balace = java计算的结果 where id = 1   --结果为150 是错误的结果

-- java中操作要加锁

客户端B<br />3、打开事务<br />4、select * from account where id = 1  -- balace结果为200<br />5、update account set balance = balance - 50 where id = 1<br />7、select * from account where id = 1  -- balace结果为 150<br />8、commit

串行化在读也加一把锁,其他隔离级别读不加锁

<a name="RpagL"></a>
### 5.3.2、锁
<a name="rGkCW"></a>
#### 5.3.2.1、锁分类
行锁 - 锁定索引行<br />间隙锁 - 锁定范围<br />临间锁 - 行锁与间隙锁的组合<br />**行锁演示 - innoDB的行锁是针对索引加的锁,不是针对记录加的锁。并且该索引不能失效,否则都会从行锁升级为表锁**<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1161651/1604545745273-6b834203-daf2-4def-8c00-41781dc2fd0c.png#crop=0&crop=0&crop=1&crop=1&height=155&id=lVZD6&margin=%5Bobject%20Object%5D&name=image.png&originHeight=273&originWidth=1317&originalType=binary&ratio=1&rotation=0&showTitle=false&size=45819&status=done&style=none&title=&width=746)<br />事务A更新 id=5 的记录但是并未提交,事务B也开始更新 id=5的记录但是被卡主,果断时间报了锁等待异常,而id=1的记录可正常进行。<br />**间隙锁演示 - 间隙锁是在可重复读隔离级别下才会生效**<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1161651/1604546561498-e487c4d6-83c5-42b4-aca6-af5940251c20.png#crop=0&crop=0&crop=1&crop=1&height=204&id=vu5ck&margin=%5Bobject%20Object%5D&name=image.png&originHeight=375&originWidth=1372&originalType=binary&ratio=1&rotation=0&showTitle=false&size=65100&status=done&style=none&title=&width=746)<br />客户端A<br />1、开启事务,<br />2、update account set balance = 666 where id > 2  回车<br />客户端B<br />3、insert into account values(3, 'zhaoliu',  999)    回车,sql并不会执行,卡主,过段时间会报锁等待超时<br />4、insert into account values(10, 'zhaoliu',  999)    回车,sql并不会执行,卡主,过段时间会报锁等待超时<br />锁的间隙为:(2,5),(5-正无穷)
<a name="PT7BZ"></a>
#### 5.3.2.2、行锁分析
```sql
show status like 'innodb_row_lock%';
Innodb_row_lock_current_waits 当前正在等待锁的数量
Innodb_row_lock_time 从系统启动到现在锁定总时间长度
Innodb_row_lock_time_avg 每次等待锁平均时间
Innodb_row_lock_time_max 从系统启动到现在等待最长的一次时间
Innodb_row_lock_waits 系统启动后到现在总共等待的次数
 select * from INFORMATION_SCHEMA.INNODB_TRX;   -- 查看事务
 select * from INFORMATION_SCHEMA.INNODB_LOCKS;  -- 查看锁
 select * from INFORMATION_SCHEMA.INNODB_LOCK_WAITS; -- 查看锁等待
 kill trx_mysql_thread_id -- 释放锁
 show engine innodb status\G; -- 查看锁等待详细信息