对于mysql中注重事务优化的就是innodb引擎,我们学习一下innodb事务;
什么是事务?
事务就是一系列的操作,要满足ACID,要么全成功,要么全失败,只满足这还不够,需要ACID;
1. 什么是ACID;
原子性:事务中的所有操作作为一个整体像原子一样不可分割,要么全部成功,要么全部失败。
原子性的实现原理,是基于回滚日志(undo log),当事务回滚时能撤销所有已经成功的SQL语句。 如果事务执行失败或调用了rollback,便可以利用undo log 中信息回滚到之前状态。
一致性:事务开始前和结束后,数据库的完整性约束没有被破坏,都是合法的数据状态。
一致性:保证原子性,持久性,隔离性
隔离性:并发执行的事务不会相互影响,其对数据库的影响和它们串行执行时一样。
严格的隔离性的实现是通过隔离级别,对应了事务间不相互影响,采用锁机制和MVCC(trancation_id,readView,undo log)
持久性:事务一旦提交,其对数据库的更新就是持久的。任何事务或系统故障都不会导致数据丢失。
实现原理:innodb 作为mysql 的存储引擎,数据是存放在磁盘中的,同时innodb提供了buffer pool,作为数据库的缓冲。 当从数据库进行读数据时,会先从buffer pool 中读取,如果没有从磁盘读入放入buffer pool, 当向数据库写数据时,先写buffer pool,buffer pool 会定期刷到磁盘(刷脏)
问题是如果mysql 宕机,而此时buffer pool 中数据,没有刷到磁盘就会丢失。 redo log 记录写到 buffer pool中的操作。
如何避免数据库一致性被破坏 并发控制技术:保证了事务的隔离性,使数据库的一致性不会因为并发执行被操作 日志恢复技术:保证了事务的原子性,使数据库的一致性不会因事务或系统故障被破坏。同时使已提交的对数据库的修改不会因系统崩溃而丢失,保证了事务的持久性。
2. 同时有多个事务在进行会怎么样呢?
多事务的并发进行一般会造成以下几个问题:
脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
丢失修改(Lost to modify): 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。
不可重复读(Unrepeatableread): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
不可重复读和幻读的区别?
不可重复读的重点是修改比如多次读取一条记录发现其中某些列的值被修改,幻读的重点在于新增或者删除比如多次读取一条记录发现记录增多或减少了。
隔离性是由隔离级别,实现原理锁机制,mvcc ,那么具体是如何实现的?
3. 实现隔离性
四大隔离级别:读未提交,读已提交,可重复读,可串行化
默认是可重复读;
1. 读已提交隔离级别
读已提交隔离级别可以解决脏读问题;
innodb 数据行带有三个隐式字段:三个隐藏字段,row_id,transcation_id,rollPointer
当一个事务,对一行数据操作,会在创建一新的拷贝行带有当前事务的id(transcation_id),
image.png使用rollPointer 来指向之前的版本,维护整个版本链;最后形成一个版本的链表;
然后,另一个事务如何读取到原本的数值?
mysql 在select 会生成一个 ReadView 字段数组,里面保存着这条数据没有条件的事务版本号;
这时另一个事务读取版本链,如何在ReadView跳过,最终找到原本的数据;
如果一个事务commit, ReadView 就删除这个版本号;
总之,根据 ReadView判断版本是否提交,如果已提交就可以,提到最前面;那么查询读到的就是当前commit最新版本;
这也就是为啥在读已提交隔离级别会出现不可重复读的问题。
2. 可重复读隔离级别
readView 保存当前活跃的事务,找到第一个不活跃的事务;
可重复的改进很简单,当前事务再次读取表是采用上次读出来的ReadViews,即不更新ReadView,直接用之前已经生成好的ReadView,那么这就是为什么同一个事务里不会出现数据不一致,解决了不可重复读问题。
这也就是,MVCC 多版本控制协议。
自己简单实现mvcc
select (status,status,version) from t_goods where id=#{id}
update t_goods set status=2,version=version+1where id=#{id} and version=#{version};
4. innodb 锁 机制
innodb 锁分为乐观锁和悲观锁;
乐观锁就是上文介绍的MVCC,
乐观锁在数据库上的实现完全是逻辑的,不需要数据库提供特殊的支持。一般的做法是在需要锁的数据上增加一个版本号,或者时间戳,然后按照如下方式实现:
1. SELECT data AS old_data, version AS old_version FROM …;
2. 根据获取的数据进行业务操作,得到new_data和new_version
3. UPDATE SET data = new_data, version = new_version WHERE version = old_version
if (updated row > 0) {
// 乐观锁获取成功,操作完成
} else {
// 乐观锁获取失败,回滚并重试
}
innodb中的悲观锁分为,读锁S和写锁X;S为共享锁,X为排它锁;共享锁可以加多个共享锁,但是不能加排他锁。排他锁不能加任何锁。
但是
select * from table;
上面这种普通的快照读,不管当前加的是读锁还是写锁都不会阻塞;即使这行数据加了读写锁,都不会阻塞;
加锁的方式有两种:
- 显示加锁
读锁, select * from table in share mode;
写锁, select * from table for update;
- 隐式加锁
普通的delete,insert,update都会进行加锁;
delete 默认加 x 锁;
insert 会先加“隐式锁”来保证插入记录在本事务提交前不被访问;隐式锁就是在一个事务插入一条记录后,还未提交,这条记录会保存本事务id,其他事务想访问,发现事务id不会,这时才加x锁;
update: 更新的行前后没有导致存储空间的变化,先加X,再直接修改,如果更新的数据导致存储空间变化,先加X,将记录删除后,再进行insert;
5. InnoDB存储引擎的锁的算法有三种:
- Record lock:单个行记录上的锁
- Gap lock:间隙锁,锁定一个范围,不包括记录本身
- Next-key lock:它是record和gap的结合体, 锁定一个范围,包含记录本身
innodb对于行的查询使用next-key lock解决Phantom Problem幻读问题,innodb可以使用mvcc和next-key解决读的幻读问题;
当查询的索引含有唯一属性时,将next-key lock降级为record key,Gap锁设计的目的是为了阻止多个事务将记录插入到同一范围内,而这会导致幻读问题的产生
1.读已提交:
- 主键或唯一索引的等值情况 a = 1 只会锁住一条数据
- 使用普通索引,会将所有数据加锁,这里可能有多条数据,只是把查出行加锁,插入之间的数据是可以插入;
- 没使用索引,也只是把查出的数据加锁;
- 总结读已提交下只会锁住查询出来的数据,并发度高;
2.可重复读:
- 普通索引,没有查出的记录没加锁;但是插入在查询的
a = 'b' 情况,再插入一条在其中数据,是插入不进去的;附近的间隙加锁,解决幻读; - 没有使用索引,直接使用的表锁;
- 总结,主键索引和唯一索引,在等值查询时只锁查询出来的值,但是普通索引是采用间隙锁,没走索引的直接采用表锁;
当然:范围查询都是使用间隙锁;
6.意向锁
意向共享锁(IS):事务打算给数据行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
意向排他锁(IX):事务打算给数据行加排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。
意向锁主要处理是这类问题,例如要在一个表上加X排他锁,需要判断表行上是否已经加了排他锁,所以需要依次遍历进行判断,显然表太大,效率会很慢;
所以innodb在设计时,在给表加上一个IX意向锁,如果某行加X锁,就在IX表示,那么对表加X,只需要查看IX,防止遍历整个表;
所以意向锁之和表X,S冲突;
7. 读写锁(MyISAM)表锁
一个表加读锁后,只能对当前表进行读,不能更新,更新默认加x锁,在锁期间也不能访问其他表,避免持有并请求;
其他访问加读锁的表,但是更新加读锁表会阻塞,需要加x锁;
一个表加写锁后,可以对当前表读,在一个session内写;
另一个session 查询会阻塞,有时可以查询,是因为从缓存中取出;
8. 行锁
无索引使用不当行锁变表锁;
索引
varchar 必须单引号,否则函数转换索引失效,行锁变成表锁;
网友评论