一.不同存储引擎支持的锁机制
Mysql数据库有多种数据存储引擎,Mysql中不同的存储引擎支持不同的锁机制
MyISAM和MEMORY存储引擎采用的表级锁
InnoDB存储引擎支持行级锁,也支持表级锁,默认情况下采用行级锁
二.锁类型的划分
按照数据操作的类型分:
读锁(共享锁):针对同一份数据,多个事务可以同时读取
写锁(排他锁):执行写操作未释放锁之前,其他事务不能读取数据或者修改数据
按照数据操作的粒度分:
表级锁:开销小,加锁快,不会出现死锁,锁地粒度较大,发生冲突的概率也越大,并发性能低
行级锁:开销大,加锁慢,会出现死锁,锁定粒度较小,发生冲突的概率小,并发性能越高
行级锁分为共享锁和排它锁两种锁
行锁是Mysql锁中粒度最小的一种锁,虽然锁的粒度小,发生资源争抢的概率也小,但是也会出现死锁的情况
按照操作性能可分为乐观锁和悲观锁
乐观锁:一般的实现方式是对记录数据的版本进行对比,在提交更新的时候进行冲突检测,如果发现错误信息,则提示错误信息
悲观锁:在对一条数据修改前,先加锁,避免同时被其他人修改
三.使用Mysql行级锁的两个前提:
(1)使用innoDB引擎
(2)开启事务(隔离级别为可重复读)
这样确保在同一事务中读取的数据一致,并且在事务结束之前锁定所需的行。
START TRANSACTION;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 执行对数据的操作,包括锁定行级别的操作
-- 提交事务或回滚事务
COMMIT;
-- 或
ROLLBACK;
四.InonoDB行锁的类型:
(1)共享锁:当事务对数据加上共享锁后,其他用户可以读取数据,但是不能修改数据,直到释放锁
(2)排他锁:如果事务A对数据B加上锁之后,在事务A没有释放锁之前,其他事务不能修改数据B和读取数据B。
InnoDB如何实现行锁
在做增删改查的时候加读锁,修改表结构的时候加写锁。
加锁的方式:
1.InnoDB引擎默认更新语句,update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型
2.加共享锁:select*from table where ...lock in share mode
加排他锁:select *from table where...fro update
五.事务的隔离级别和锁的关系
(1)事务的隔离级别是SQL92定制的标准,相当于事务并发控制的整体解决方案,而本质上是对锁和MVCC使用的封装
(2)事务隔离性使用锁来实现的,对响应的操作加不同的锁,可以防止其他事务同时对数据进行读写操作。
(3)当使用隔离级别不能解决并发问题的时候,我们可以用锁
六.行级锁产生死锁的原因:
(1)在一条事务中执行了一条没有索引条件的查询,引发全表扫描,把行级锁上升为全表记录锁定(等价于表级锁),多个这样的事务执行后,就很容易出现死锁和阻塞
如何解决:SQL语句不要有太多的复杂的关联多表查询,使用explain“执行计划”对SQL语句进行分析,对于全表扫描的语句,建立索引
(2)两个事务分别向拿到对方的锁,互相等待,产生了死锁
(3)同一个事务中只有一个SQL,但是有些情况还是会出现锁的情况
(1)事务1从name索引出发,读取到的[hdc,1],[hdc,6]均满足条件,加锁先[1,hdc,100],后[6,hdc,10]
(2)事务2从pubtime索引出发,读取到的[hdc,1],[hdc,6]均满足条件,加锁先[1,hdc,100],后[6,hdc,10]
(3)因为加锁时事务1和事务1加锁顺序正好相反,所以导致了死锁
七.MVCC
MVCC:多版本并发控制,用于实现读已提交和可重复读隔离级别
MVCC:核心是多版本链+Read view。"MV"就是通过Undo log来保存数据的历史版本,实现多版本的管理,"CC"是通过Read-view来实现管理,通过read-view原则决定数据是否显示,同时针对 不同的隔离级别,read-view的生成策略也不同,也就实现了不同的隔离级别
每个数据行当中还会有两个字段,一个是trx_id表示修改当前数据的id,另一个是roll_pointer,指向上一次修改的数据行
当前读:读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁,比如select..lock in share mode(共享锁),select ..for update、update、insert、delete(排他锁)都是当前读
快照读:快照读读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读
(1)Read Committed:每次select,都生成一个快照读
(2)Repeatable read:开启事务第一个select语句才是快照读的地方
undolog回滚日志:
(1)回滚日志:存储老版本数据
(2)版本链:记录不同事务提交事务修改数据的版本,通过roll_pointer指针形成一个链表
当一个事务第一次执行查询sql时,会生成一致性视图read-view(快照),查询时从undo log中最新的一条记录开始跟read-view作对比,如果不符合比较规则,就根据回滚指针回滚到上一条记录继续比较,直到得到符合比较条件的查询结果。
ReadView判断某个记录版本是否可见的规则如下
当执行一次sql时会生成一致性视图read-view,它由执行查询所有未提交事务id数组(数组里最小的id为min_id)和已创建的最大事务(max_id)组成,查询的数据结果需要跟read-view做对比而得到快照结果
版本链对比规则:
1.如果落在绿色部分(trx_id<min_id),表示这个版本是已提交的事务生成的,这个数据是可见的
2.如果落在红色部分(trx_id>max_id)表示这个版本是由将来的事务生成的,是不可见的
3.如果落在(min_id<=trx_id<=max_id)黄色部分包括两种情况
如果当前事务的id在未提交事务的数组中,此条记录不可读
如果当前事务的id在已提交事务的数组中,则此条记录可读
举个例子:
假设当前为可重复读
如图,当我们进行第一次查询的时候
事务100,事务200都没有提交,事务300都没有提交,此时生成的一致性视图为:
[100,200]300
此时的数据版本链为:
我们从最上边这条数据(lilei300)开始对比,这条数据trx_id 为300,落在了黄色区域,且不在
未提交事务的id[100,200]中,那么这个数据版本是可见的,所以最终读到的数据就是name为lilei300这条数据
接下来我们再来进行第二次查询
因为是可重复读此时我们的一致性视图还是[100,200]300,此时因为事务100对数据做了两次修改,此时的数据版本链为
此时生成的一致性视图是:[200]300,此时的数据版本
我们再从上往下一次将数据和视图作比较,lilei2和liei1这两条数据trix_id都为100,此时落在了黄色区域,但是在未提交事务的数组中,属于不可读,接下来又读取lilei300这条数据,显然这条数据是可见的。
我们再开启一个新的事务读取数据:
此时的一致性视图为:[200]300,
此时的数据版本链为:
然后再进行对比,我们发现 lilei4和lilei3这两条数据trx_id都是200,在黄色区域,但是在数组[200】里,此数据版本不可见,单但ile2这条数据trx_id为100,在绿色区域,此数据版本可见,读取的就是lilei2这条数据。如果在这个事务里再次读取,用的还是一致性视图为:[200]300,最终读取到的数据是一样的。
综上我们可以发现,在可重复读的隔离级别下,同一个事务里生成的一致性视图都是这个事务第一次执行快照读生成的视图,因此同一个事务里多次读取数据,得到的结果是一致的。
在不可重复读的隔离级别下,每一次执行快照读都会生成readview