这是我参与「第三届青训营 -后端场」笔记创作活动的第4篇笔记
这是我在做抖音项目过程中遇到的一些数据库事务问题,我把这些问题总结成了以下8个问题,希望对大家在开发过程中有一定的帮助。
1.MYSQL的隔离级别(大致了解mysql数据库事务)
mysql的隔离级别分别为:
-
读未提交 在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。
-
读提交
这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。
- 可重复读
这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。
- 串行化
即读加读锁,写加写锁。
2. 如何避免长事务对业务的影响?(在项目中避免出现长事务)
在应用开发端:
- 确认是否使用了set autocommit=0。 这个确认工作可以在测试环境中开展, 把MySQL的general_log开起来, 然后随便跑一个业务逻辑, 通过general_log的日志来确认。 一般框架如果会设置这个值, 也就会提供参数来控制行为, 你的目标就是把它改成1。
- 确认是否有不必要的只读事务。 有些框架会习惯不管什么语句先用begin/commit框起来。 我见过有些是业务并没有这个需要, 但是也把好几个select语句放到了事务中。 这种只读事务可以去掉。
- 业务连接数据库的时候, 根据业务本身的预估, 通过SETMAX_EXECUTION_TIME命令,来控制每个语句执行的最长时间, 避免单个语句意外执行太长时间。
在数据库端:
- 监控 information_schema.Innodb_trx表, 设置长事务阈值, 超过就报警/或者kill;
- 在业务功能测试阶段要求输出所有的general_log, 分析日志行为提前发现问题;
3. mysql中锁的种类?(了解数据库锁)
MySQL里面的锁大致可以分成全局锁、 表级锁和行锁三类。
-
MySQL提供了一个加全局读锁的方法, 命令是Flush tables with read lock (FTWRL)。 当你需要让整个库处于只读状态的时候, 可以使用这个命令, 之后其他线程的以下语句会被阻塞: 数据更新语句(数据的增删改) 、 数据定义语句(包括建表、 修改表结构等) 和更新类事务的提交语句。
全局锁的典型使用场景是, 做全库逻辑备份。 也就是把整库每个表都select出来存成文本。当mysqldump使用参数–single-transaction的时候, 导数据之前就会启动一个事务, 来确保拿到一致性视图。(MVCC innoDB)
-
MySQL里面表级别的锁有两种: 一种是表锁, 一种是元数据锁(meta data lock, MDL)表锁的语法是 lock tables …read/write。 与FTWRL类似, 可以用unlock tables主动释放锁,也可以在客户端断开的时候自动释放。
-
MDL不需要显式使用,(MySQL server层使用的一种表级别锁,并不是InnoDB引擎中实现的。) 在访问一个表的时候会被自动加上。 MDL的作用是, 保证读写的正确性。 你可以想象一下, 如果一个查询正在遍历一个表中的数据, 而执行期间另一个线程对这个表结构做变更, 删了一列, 那么查询线程拿到的结果跟表结构对不上, 肯定是不行的。(MDL会直到事务提交才释放)
-
在InnoDB事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。
4.死锁和死锁检测(项目中遇到的效率问题)
出现死锁应该怎么办?
- 一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数innodb_lock_wait_timeout来设置。
- 另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数innodb_deadlock_detect设置为on,表示开启这个逻辑。
但是存在效率问题,尤其是热点行更新所带来的性能问题。
如果你能确保这个业务一定不会出现死锁,可以临时把死锁检测关掉。
在中间件进行设置:对于相同行的更新,在进入引擎之前排队。这样在InnoDB内部就不会有大量的死锁检测工作了。
5.事务隔离
InnoDB的行数据有多个版本,每个数据版本有自己的row trx_id,每个事务或者语句有自己的一致性视图。普通查询语句是一致性读,一致性读会根据row trx_id和一致性视图确定数据版本的可见性。
对于可重复读,查询只承认在事务启动前就已经提交完成的数据;
对于读提交,查询只承认在语句启动前就已经提交完成的数据; 而当前读,总是读取已经提交完成的最新版本。
6. 什么情况会引发数据库的flush过程呢?
-
InnoDB的redo log写满了。这时候系统会停止所有更新操作,把checkpoint往前推进,redo log留出空间可以继续写。 注意!!此时的更新数会跌到0!
-
系统内存不足。当需要新的内存页,而内存不够用的时候,就要淘汰一些数据页,空出内存给别的数据页使用。如果淘汰的是“脏页”,就要先将脏页写到磁盘。
你一定会说,这时候难道不能直接把内存淘汰掉,下次需要请求的时候,从磁盘读入数据页,然后拿redo log出来应用不就行了?这里其实是从性能考虑的。如果刷脏页一定会写盘,就保证了每个数据页有两种状态:
一种是内存里存在,内存里就肯定是正确的结果,直接返回; 另一种是内存里没有数据,就可以肯定数据文件上是正确的结果,读入内存后返回。 这样的效率最高
-
MySQL认为系统“空闲”的时候
-
MySQL正常关闭的情况。这时候,MySQL会把内存的脏页都flush到磁盘上,这样下次MySQL启动的时候,就可以直接从磁盘上读数据,启动速度会很快。
综上所述:InnoDB的刷盘速度就是要参考这两个因素:一个是脏页比例,一个是redo log写盘速度。
7. 如何解决幻读问题?
为了解决幻读问题,InnoDB只好引入新的锁,也就是间隙锁(Gap Lock)。
间隙锁和行锁合称next-key lock,每个next-key lock是前开后闭区间。
注意:跟行锁有冲突关系的是“另外一个行锁”。
但是间隙锁不一样,跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作。间隙锁之间都不存在冲突关系
8.MVCC能解决幻读吗?
不能。需要加next-key lock。 如果事务中的语句都是快照读,则也可以避免幻读;但是如果存在快照读和当前读,则需要加next-key lock。每个next-keylock是前开后闭区间。
如:
事务2:
start transaction;
select * from student;
select * from student;
(事物3已结commit了)
//新添加语句
update student set name='Tian' where id=4;
select * from student;
commit;
事务3:
start transaction;
insert into student values(NULL,'tian');
commit;