引言
在之前的文章中,对explain用法以及利用mysql的索引,在实际项目中进行各种优化的方式。说完优化后,不得不提到mysql另一大块功能了,那就是事务。在实际开发中,可能有很多人对事务的感知度不高,可能就是在代码中写一个@Transaction,或者在oracle的plsql中,点一下解锁,然后修改完数据点一下提交[无辜笑]。具体mysql数据库有什么事务,或者在项目中是具体怎么应用的,估计一下说不完整。此文章就是针对mysql的各种事务以及其使用进行介绍。
事务的作用
事务主要是为了保证一组数据库操作执行后,整组操作只能一起成功,或者一起失败,保证数据一致性。通俗点也就是在银行取100块,从你账户扣100块和吐100块这两个操作必须一起成功或失败,否则可能就会出现你账户钱扣了,却没有拿到钱的尴尬情况。其实从官方文档上,事务有以下四个属性,也就是我们常挂嘴边的ACID:
- 原子性:表示一个事务操作只能一起成功和一起失败,也即一个事务就是最细粒度的操作,不可再拆分。
- 一致性:这个是事务要达到的最终效果,mysql事务也就是为了保持读取数据的一致性
- 隔离性:当mysql存在多个事务并发操作时,保证事务的内部不互相干扰,MySQL是通过各种锁以及内部机制保证的。当然不同的事物隔离性是不一样的,这个是本文的重点。
- 持久性:也即事务一旦提交,提交的数据是永久生效的,同时mysql会把数据写道redo log,来保证其持久性
并发事务带来的常见问题以及常见的事务隔离级别
在实际项目中,对数据库并发操作是很常见的,多个线程对一个表操作导致的死锁、脏写、脏读等都是我们问题,接下来先介绍这些问题,以及mysql四种事务隔离级别可能存在的事务问题。首先事务常见问题如下:
- 脏写:多个事务之间,对数据库的修改出现了丢失或者被错误覆盖
- 脏读:一个事务读取到另一个事务未commit的数据
- 不可重复度:一个事务内,同一查询语句,多次查询结果不相同
- 幻读:一个事务内,读取到其他事务新增提交的数据
接下来是mysql事务隔离级别:
- 读未提交(read-uncommitted):可以读取到其他事务未提交的数据。可能出现脏读、不可重复读、幻读的问题。因为问题太多,所以也基本不会使用。
- 读已提交(read-committed):简称RC,可以读取到其他事务已提交的数据。可能出现不可重复读、幻读的问题。一般在并发要求比较高的场景,使用此级别提高效率,不过得在java层面做做控制。
- 可重复度(repeatable‐read):简称RR,保证一个事务内,同一查询语句,查询的结果相同。可能出现幻读问题。这也是mysql默认的一个事务隔离级别,做政府类型的项目,用这个级别基本够用。
- 可串行化(serializable):事务隔离的最高级别,不会出现事务问题,不过这也会使得mysql性能降到最低,基本上表的并发操作会因为数据库变成串行等待。基本不会使用。
这里提一下,mysql默认设置的隔离级别是RR,利用@Transactional注解时,可以通过isolation设置spring的隔离级别,整体设置方式为 @Transactional(isolation = Isolation.READ_COMMITTED)。如果不设置隔离级别,则mysql会默认使用RR。
对隔离级别做了基本了解后,就针对工作中可能会用到的RR以及RC进行实际验证下,读未提交和可串行化本文就不继续谈了,我们还是以实战解决问题为主,不专做学术研究[去污粉]。
- RC(读已提交),验证方式为:首先打开会话1,设置此会话隔离级别为RC,并开始事务,查询sys_user表数据。打开另一个会话2,开启事务,对sys_user的部门数据进行修改,修改后先不提交.在会话1中再次查询sys_user表数据,具体操作如下图,这样就可以解决脏读问题了
接下来,在会话2中进行提交,操作,再回到会话1中查询sys_user表数据,发现虽然仍在一个事务中,两次一模一样的sql,事务内对数据也没修改,查询的结果数据却不一样,如下图。这就是不可重复读问题。
- RR(可重复读),验证方式为,先在会话1中设置查询级别为RR,开启事务后,查询sys_user表数据。在会二中开启事务,并对sys_user表的数据进行更新,更新后提交事务。此时查看数据库中sys_user的数据,发现库表数据已更新。最后在会话1中查询sys_user表数据,发现会话1中的数据并未修改,成功解决了可重复读的问题。如下图。
RR事务隔离级别还是比较特殊,有一些特殊的规则,这里列一下,别搞混了。首先RR事务级别是通过MVCC机制保证了数据的可重复度,内部是通过一个readview来实现历史版本的读取(具体下篇文章单独拉出来讲讲吧[吃瓜群众]比较复杂),因此RR隔离级别,select操作读取的历史版本,不过insert、update、delete是当前版本。
接下来是RR存在幻读情况验证。①先在会话1中设置查询级别为RR,开启事务后,查询sys_user表数据。②在会二中开启事务,并对sys_user表的数据进行插入,插入后提交事务。③在会话1中查询sys_user数据,发现会话2中的数据没有查询来。此处幻读看似解决。④在会话1对新增的数据执行update操作,⑤重新查询sys_user数据,发现新增了一条数据,幻读问题又来了[打脸],所以说RR没有办法彻底解决幻读问题。具体步骤如下图,方便直观感受[奸笑]
mysql的锁
mysql数据一致性,其实很多时候是靠锁来解决的,分为读锁和写锁,具体作用如下:
- 读锁(共享锁、S):相当于在select 语句后面加上 lock in share mode,读锁为什么说是共享锁呢?因为加了读锁,是不影响其他事务读取此数据的,只是不允许其他事务修改
- 写锁(排他锁,X):相当于在select 语句后面加for update,写锁是排他锁,其他事务的读取、和写入操作都要被阻塞,在mysql中,如果你在事务中写update、select、insert都会对数据加上写锁
关于事务的简单优化方案
了解完mysql事务后,那在实际业务中,就有一些要注意的事项了,我整理如下事项:
- 做好数据库连接池的配置,在并发条件下,如果多个事务被卡住,可能会导致数据库连接池被撑爆,从而导致系统异常
- 按需锁定数据,尤其是mysql有个间隙锁机制,如果一个表大量的数据区间被锁定,很可能会导致大量阻塞,最终应用内存溢出或者大量服务超时
- 严格避免死锁,死锁会导致资源一直不可用,最后导致排列的线程过多,内存占用越来越高
- 单个事务尽量小,不要一个事务内做过多操作,否则极其容易出现undolog过大、回滚耗时超长、连接池被撑爆等问题
- 部分数据一致性的操作可以在java解决,mysql的事务能避免还是避免下,毕竟数据库资源过于珍贵[狗头]
- 事务的超时时间一定要合理设置,不然有些事务等待过长,也是很容易导致内存溢出