从8个问题出发带你梳理数据库事务| 青训营笔记

145 阅读7分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第4篇笔记

这是我在做抖音项目过程中遇到的一些数据库事务问题,我把这些问题总结成了以下8个问题,希望对大家在开发过程中有一定的帮助。

1.MYSQL的隔离级别(大致了解mysql数据库事务)

mysql的隔离级别分别为:

  1. 读未提交 在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。

  2. 读提交

这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。

  1. 可重复读

这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。

  1. 串行化

即读加读锁,写加写锁。

2. 如何避免长事务对业务的影响?(在项目中避免出现长事务)

在应用开发端:

  1. 确认是否使用了set autocommit=0。 这个确认工作可以在测试环境中开展, 把MySQL的general_log开起来, 然后随便跑一个业务逻辑, 通过general_log的日志来确认。 一般框架如果会设置这个值, 也就会提供参数来控制行为, 你的目标就是把它改成1。
  2. 确认是否有不必要的只读事务。 有些框架会习惯不管什么语句先用begin/commit框起来。 我见过有些是业务并没有这个需要, 但是也把好几个select语句放到了事务中。 这种只读事务可以去掉。
  3. 业务连接数据库的时候, 根据业务本身的预估, 通过SETMAX_EXECUTION_TIME命令,来控制每个语句执行的最长时间, 避免单个语句意外执行太长时间。

在数据库端:

  1. 监控 information_schema.Innodb_trx表, 设置长事务阈值, 超过就报警/或者kill;
  2. 在业务功能测试阶段要求输出所有的general_log, 分析日志行为提前发现问题;

3. mysql中锁的种类?(了解数据库锁)

MySQL里面的锁大致可以分成全局锁、 表级锁和行锁三类。

  1. MySQL提供了一个加全局读锁的方法, 命令是Flush tables with read lock (FTWRL)。 当你需要让整个库处于只读状态的时候, 可以使用这个命令, 之后其他线程的以下语句会被阻塞: 数据更新语句(数据的增删改) 、 数据定义语句(包括建表、 修改表结构等) 和更新类事务的提交语句。

    全局锁的典型使用场景是, 做全库逻辑备份。 也就是把整库每个表都select出来存成文本。当mysqldump使用参数–single-transaction的时候, 导数据之前就会启动一个事务, 来确保拿到一致性视图。(MVCC innoDB)

  2. MySQL里面表级别的锁有两种: 一种是表锁, 一种是元数据锁(meta data lock, MDL)表锁的语法是 lock tables …read/write。 与FTWRL类似, 可以用unlock tables主动释放锁,也可以在客户端断开的时候自动释放。

  3. MDL不需要显式使用,(MySQL server层使用的一种表级别锁,并不是InnoDB引擎中实现的。) 在访问一个表的时候会被自动加上。 MDL的作用是, 保证读写的正确性。 你可以想象一下, 如果一个查询正在遍历一个表中的数据, 而执行期间另一个线程对这个表结构做变更, 删了一列, 那么查询线程拿到的结果跟表结构对不上, 肯定是不行的。(MDL会直到事务提交才释放)

  4. 在InnoDB事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。

4.死锁和死锁检测(项目中遇到的效率问题)

出现死锁应该怎么办?

  1. 一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数innodb_lock_wait_timeout来设置。
  2. 另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数innodb_deadlock_detect设置为on,表示开启这个逻辑。

但是存在效率问题,尤其是热点行更新所带来的性能问题。

如果你能确保这个业务一定不会出现死锁,可以临时把死锁检测关掉。

在中间件进行设置:对于相同行的更新,在进入引擎之前排队。这样在InnoDB内部就不会有大量的死锁检测工作了。

5.事务隔离

InnoDB的行数据有多个版本,每个数据版本有自己的row trx_id,每个事务或者语句有自己的一致性视图。普通查询语句是一致性读,一致性读会根据row trx_id和一致性视图确定数据版本的可见性。

对于可重复读,查询只承认在事务启动前就已经提交完成的数据;

对于读提交,查询只承认在语句启动前就已经提交完成的数据; 而当前读,总是读取已经提交完成的最新版本。

6. 什么情况会引发数据库的flush过程呢?

  1. InnoDB的redo log写满了。这时候系统会停止所有更新操作,把checkpoint往前推进,redo log留出空间可以继续写。 注意!!此时的更新数会跌到0!

  2. 系统内存不足。当需要新的内存页,而内存不够用的时候,就要淘汰一些数据页,空出内存给别的数据页使用。如果淘汰的是“脏页”,就要先将脏页写到磁盘。

你一定会说,这时候难道不能直接把内存淘汰掉,下次需要请求的时候,从磁盘读入数据页,然后拿redo log出来应用不就行了?这里其实是从性能考虑的。如果刷脏页一定会写盘,就保证了每个数据页有两种状态:

一种是内存里存在,内存里就肯定是正确的结果,直接返回; 另一种是内存里没有数据,就可以肯定数据文件上是正确的结果,读入内存后返回。 这样的效率最高

  1. MySQL认为系统“空闲”的时候

  2. 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;