一、概述
数据库一般都会并发执行多个事务,多个事务可能会并发的对相同的一批数据进行增删改查操作,可能 就会导致我们说的脏写、脏读、不可重复读、幻读这些问题。 这些问题的本质都是数据库的多事务并发问题,为了解决多事务并发问题,数据库设计了事务隔离机制、锁机 制、MVCC多版本并发控制隔离机制,用一整套机制来解决多事务并发问题
二、事务及ACID属性
2.1、ACID属性
事务是由一组SQL语句组成的逻辑处理单元,事务具有以下4个特性,简称ACID属性
- 原子性 atomocity - 事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。
- 隔离性 isolation - 数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独 立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。
- 持久性 durable - 事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持
一致性 consiistent - 在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规 则都必须应用于事务的修改,以保持数据的完整性
2.2、事务并发带来的问题
更新丢失或脏写 - 当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存 在,就会发生丢失更新问题–最后的更新覆盖了由其他事务所做的更新。
- 脏读 - 一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致的状态;这 时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此作进一步的 处理,就会产生未提交的数据依赖关系。这种现象被形象的叫做“脏读”。事务A读取到了事务B已经修改但尚未提交的数据,还在这个数据基础上做了操作。此时,如果B 事务回滚,A读取的数据无效,不符合一致性要求。
- 不可重复读 - 事务A内部的相同查询语句在不同时刻读出的结果不一致,不符合隔离性
-
三、事务的隔离级别

注意:隔离级别越高,越安全,并发性越差,性能越差,常看当前数据库的事务隔离级别: show variables like '%tx_isolation%';设置事务隔离级别:set tx_isolation='REPEATABLE-READ';Mysql默认的事务隔离级别是可重复读,用Spring开发程序时,如果不设置隔离级别默认用Mysql设置的隔 离级别,如果Spring设置了就用已经设置的隔离级别show open tables; 查看是否被加了表锁,in_use为1表示被加了表锁
四、锁
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 /><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 /><br /><br />A事务读到了B事务未提交的事务 - 脏读<br />A事务在同一个事务下统一sql语句读取到的值不相同 - 不可重复读<br /><br />事务A在同一个事务下同一语句读取的结果不同 - 幻读
<a name="rRZuF"></a>
## 5.2、隔离级别 - 读已提交
**-- 设置隔离级别 set tx_isolation='read-committed';**<br /><br />A事务并未读到B事务未提交的事务 - 解决了脏读问题<br /><br />A事务同一条sql读取到的数据依然不一致 - 未解决可重复读问题<br /><br />A事务同一事务下读到的数据条数不一样 - 未解决幻读问题
<a name="DBWQd"></a>
## 5.3、隔离级别 - 可重复读
**set tx_isolation='repeatable-read';**<br /><br />虽然B事务已经更改了值并且提交,数据库值也做了更新,但是A事务多次读取的值依然是相同的(其实这是不对的) -- 解决了可重复读问题; (此时A事务也不会读到B事务新增或者删除的数据,幻读也不会出现)<br /><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 /><br />事务A更新 id=5 的记录但是并未提交,事务B也开始更新 id=5的记录但是被卡主,果断时间报了锁等待异常,而id=1的记录可正常进行。<br />**间隙锁演示 - 间隙锁是在可重复读隔离级别下才会生效**<br /><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; -- 查看锁等待详细信息
