MySQL事务
MySQL事务
事务是什么
事务是一个操作序列,这些操作要么都做,要么都不做,是数据库环境中不可分割的逻辑工作单位。事务和程序是两个不同的概念,一般一个程序可包含多个事务。在SQL语言中,事务定义的语句有以下三条:
1)BEGIN TRANSACTION:事务开始。
2)COMMIT:事务提交。该操作表示事务成功地结束,它将通知事务管理器该事务的所有更新操作现在可以被提交或永久地保留。
3)ROLLBACK:事务回滚。该操作表示事务非成功地结束,它将通知事务管理器出故障了,数据库可能处于不一致状态,该事务的所有更新操作必须回滚或撤销。
手动提交事务
-- 查看事务默认提交方式(1代表自动提交,0代表手动提交)
select @@autocommit;
-- 设置事务提交方式(1代表自动提交,0代表手动提交)
set @@autocommit = 1;
-- 事务手动提交操作方式
-- 开启事务
start transaction;
-- 提交事务
commit;
-- 回滚
rollback;
事务的四大特征(ACID)
原子性(Atomicity):事务是原子的,要么都做,要么都不做。
一致性(Consistency):事务执行的结果必须保证数据库从一个一致性状态变到另一个一致性状态。因此,当数据库只包含成功事务提交的结果时,称数据库处于一致性状态。
隔离性(Isolation):事务相互隔离。当多个事务并发执行时,任一事务的更新操作直到其成功提交的整个过程,对其他事务都是不可见的。
持久性(Durability):一旦事务成功提交,即使数据库崩溃,其对数据库的更新操作也将永久有效。
并发影响数据一致性问题
1)脏读(一个事务的数据库操作有部分已经执行,别人这时看见了,后面异常操作进行回滚,看到的就不是准确的):是指在一个事务处理过程里读取了另一个未提交的事务中的数据。当一个事务正在多次修改某个数据,而在这个事务中这多次的修改都还未提交,这时一个并发的事务来访问该数据,就会造成两个事务得到的数据不一致。
例如:用户A向用户B转账100元,当只执行第一条SQL时,A通知B查看账户,B发现确实钱已到账(此时即发生了脏读),而之后无论第二条SQL是否执行,只要该事务不提交,则所有操作都将回滚,那么当B以后再次查看账户时就会发现钱其实并没有转。对应SQL命令如下:
-- A通知B,B看到转入100元
update account set money=money+100 where name=’B’;
-- 同一个事务,下面出错时回滚,转账失败,B没有真正转账成功
update account set money=money-100 where name=’A’;
2)不可重复读(数据库数据修改前后都进行了查询,看到了不一样的结果):是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。
例如事务T1在读取某一数据,而事务T2立马修改了这个数据并且提交事务给数据库,事务T1再次读取该数据就得到了不同的结果,发生了不可重复读。不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。在某些情况下,不可重复读并不是问题,比如我们多次查询某个数据当然以最后查询得到的结果为主。但在另一些情况下就有可能发生问题,例如对于同一个数据A和B依次查询就可能不同,A和B就可能打起来了。
3)幻读(批量修改了某些数据字段,后面又加了一条,结果看到还有一条没有修改):是事务非独立执行时发生的一种现象。
例如事务T1对一个表中所有的行的某个数据项做了从1修改为2的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为1并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。
4)丢失修改(2个人同时进行相同修改操作,前面的事务会丢失):是指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。
例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。
事务的隔离级别
事务的隔离级别有哪些
MySQL数据库为我们提供的四种隔离级别,由低到高:
1)READ-UNCOMMITTED(RU,读未提交):最低级别,任何情况都无法保证。只限制了两个数据不能同时修改,但是修改数据的时候,即使事务未提交,都是可以被别的事务读取到的,这级别的事务隔离有脏读、重复读、幻读的问题。
2)READ-COMMITTED (RC,读已提交):可避免脏读的发生。当前事务只能读取到其他事务提交的数据,所以这种事务的隔离级别解决了脏读问题,但还是会存在重复读、幻读问题。
3)REPEATABLE-READ(RR,可重复读):可避免脏读、不可重复读的发生。可重复读隔离级别,限制了读取数据的时候,不可以进行修改,所以解决了重复读的问题,但是读取范围数据的时候,是可以插入数据,所以还会存在幻读问题。
4)SERIALIZABLE (串行化):可避免脏读、不可重复读、幻读的发生。事务最高的隔离级别,在该级别下,所有事务都是进行串行化顺序执行的。可以避免脏读、不可重复读与幻读所有并发问题。但是这种事务隔离级别下,事务执行很耗性能。
以上四种隔离级别最高的是Serializable级别,最低的是Read uncommitted级别,当然级别越高,执行效率就越低。像Serializable这样的级别,就是以锁表的方式(类似于Java多线程中的锁)使得其他的线程只能在锁外等待,所以平时选用何种隔离级别应该根据实际情况。在MySQL数据库中默认的隔离级别为Repeatable read (可重复读)。而在Oracle数据库中,只支持Serializable(串行化)级别和Read committed(读已提交)这两种级别,其中默认的为Read committed(读已提交)级别。InnoDB存储引擎在分布式事务的情况下一般会用到Serializable (串行化)隔离级别。
设置隔离级别:
-- 查看当前事务的隔离级别
select @@tx_isolation;
-- 设置事务的隔离级别
set tx_isolation='REPEATABLE-READ';
为什么默认是RR隔离级别
我们知道Mysql有四种数据库隔离级别,分别是读未提交、读已提交、可重复读、串行化。而读未提交隔离级别太低了,会有脏读问题,串行化隔离级别太高了,会影响并发读。那么就剩下读已提交(RC)和可重复读(RR)了。那么,Mysql为什么会选择RR作为默认隔离级别呢?我们的MySQL数据库一般都是集群部署的,会有主库、从库。主库负责写,从库负责读。主库写入之后,会进行主从复制,把数据同步到从库。从库是在主库拿到bin log日志,并执行bin log,从而保证从库与主库的数据一致性。实际上,bin log有三种格式,分别是statement,row和mixed。如果是statement格式,bin log记录的是SQL的原文。Mysql早些时候,bin log日志格式只有statement这种,在RC的隔离级别,可能出现数据不一致的问题。MySQL官网上还记录了这个bug。
为什么会数据不一致?
在RC隔离级别下,我们再来看下bin log日志。当两个修改事务执行完后,会先记录后面执行的事务操作(事务先提交),然后再记录之前的事务操作。当bin log日志格式是statement,binlog记录的就是原文。这样的话,当主库把binlog同步到从库,执行SQL回放后,从库的数据就会和第一次修改的一致,而不是最终修改的结果,主数据库和从数据库数据不一致了。而在RR(可重复读的数据库隔离级别)下,因为会有间隙锁的存在,这种情况就不会发生,因此,Mysql默认选择RR作为隔离级别。
很多大厂为什么选择RC数据库隔离级别
互联网大厂和一些传统企业,最明显的特点就是高并发。那么大厂就更倾向提高系统的并发读。RC隔离级别,并发度是会比RR更好的。因为RC隔离级别,加锁过程中,只需要对修改的记录加行锁。而RR隔离级别,还需要加Gap Lock和Next-Key Lock,即RR隔离级别下,出现死锁的概率大很多。并且,RC还支持半一致读,可以大大的减少了更新语句时行锁的冲突;如果对于不满足更新条件的记录,就可以提前释放锁,提升并发度。
隔离级别的实现原理(MVCC)
MySQL的隔离级别是通过MVCC和锁机制来实现的:
1)RU隔离级别最低,没有加锁,存在脏读问题。事务读不加锁,不阻塞其他事务的读和写。
2)RC和RR隔离级别可以通过MVCC来实现。
3)串行化是通过锁机制实现。读加共享锁,写加排他锁,读写互斥。如果有未提交的事务正在修改某些行,所有select这些行的语句都会阻塞。
MVCC,即Multi-Version Concurrency Control(多版本并发控制)。它是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问。通俗的讲,数据库中同时存在多个版本的数据,并不是整个数据库的多个版本,而是某一条记录的多个版本同时存在,在某个事务对其进行操作的时候,需要查看这一条记录的隐藏列事务版本id,对比事务id并根据事物隔离级别去判断读取哪个版本的数据。要了解MVCC的底层原理,需要回顾很多相关知识点:
1)什么是快照读和当前读
一致性读:又称为快照读。快照即当前行数据之前的历史版本。快照读就是使用快照信息显示基于某个时间点的查询结果,而不考虑与此同时运行的其他事务所执行的更改。读取的是记录数据的可见版本(有旧的版本)。不加锁,普通的select语句都是快照读。快照读是MVCC实现的基础。
当前读:当前读的规则,就是要能读到所有已经提交的记录的最新值。读取的是记录数据的最新版本,显式加锁的都是当前读。
半一致性读:一条update语句,如果where条件匹配到的记录已经加锁,那么InnoDB会返回记录最近提交的版本,由MySQL上层判断此是否需要真的加锁。
2)隐式字段
对于InnoDB存储引擎,每一行记录都有两个隐藏列trx_id、roll_pointer,如果表中没有主键和非NULL唯一键时,则还会有第三个隐藏的主键列row_id。
3)什么是Undo Log
undo log,回滚日志,用于记录数据被修改前的信息。在表记录修改之前,会先把数据拷贝到undo log里,如果事务回滚,即可以通过undo log来还原数据。可以这样认为,当delete一条记录时,undo log中会记录一条对应的insert记录,当update一条记录时,它记录一条对应相反的update记录。undo log在事务回滚时,保证原子性和一致性,并且用于MVCC快照读。
4)什么是快照版本链
多个事务并行操作某一行数据时,不同事务对该行数据的修改会产生多个版本,然后通过回滚指针(roll_pointer),连成一个链表,这个链表就称为版本链。
5)事务版本号
事务每次开启前,都会从数据库获得一个自增长的事务ID,可以从事务ID(trx_id)判断事务的执行先后顺序。这就是事务版本号。
6)什么是Read View
它就是事务执行SQL语句时,产生的读视图。实际上在innodb中,每个SQL语句执行前都会得到一个Read View。它主要是用来做可见性判断的,即判断当前事务可见哪个版本的数据。在Read View中,有这几个重要的属性:
m_ids:当前系统中,那些未提交的读写事务ID列表。
min_limit_id:表示在生成Read View时,当前系统中活跃的读写事务中最小的事务id,即m_ids中的最小值。
max_limit_id:表示生成Read View时,系统中应该分配给下一个事务的id值。
creator_trx_id:创建当前Read View的事务ID。
Read view 匹配条件规则如下:
如果数据事务ID trx_id < min_limit_id,表明生成该版本的事务在生成Read View前,已经提交(因为事务ID是递增的),所以该版本可以被当前事务访问。
如果trx_id>= max_limit_id,表明生成该版本的事务在生成Read View后才生成,所以该版本不可以被当前事务访问。
如果min_limit_id=<trx_id<max_limit_id,需要分3种情况讨论:
1、如果m_ids包含trx_id,则代表Read View生成时刻,这个事务还未提交,但是如果数据的trx_id等于creator_trx_id的话,表明数据是自己生成的,因此是可见的。
2、如果m_ids包含trx_id,并且trx_id不等于creator_trx_id,则Read View生成时,事务未提交,并且不是自己生产的,所以当前事务也是看不见的。
3、如果m_ids不包含trx_id,则说明你这个事务在Read View生成之前就已经提交了,修改的结果,当前事务是能看见的。
7)查询一条记录,基于MVCC,是怎样的流程
1、获取事务自己的版本号,即事务ID(trx_id)
2、获取Read View
3、查询得到的数据,然后Read View中的事务版本号进行比较
4、如果不符合Read View的可见性规则, 即就需要Undo log中历史快照
5、最后返回符合规则的数据
InnoDB实现MVCC,是通过Read View + Undo Log实现的,Undo Log保存了历史快照,Read View可见性规则帮助判断当前版本的数据是否可见。
快照版本链示意图:
如何处理大事务和长事务
处理大事务和长事务是数据库设计和优化中非常重要的一部分,以下是一些常用的处理方法:
1)大事务拆分为小事务:将大事务拆分为多个小事务,减少每个事务操作的数据量,可以减少锁竞争和死锁的风险,提高并发性能。
2)优化查询语句:对于长事务中的查询操作,可以通过优化查询语句来提高查询性能,如添加索引、优化SQL结构等。
3)避免长时间占用锁:长事务会占用锁资源,导致其他事务无法访问相应的数据,因此需要尽可能缩短事务的执行时间,避免长时间占用锁。
4)避免长时间的事务等待:长事务可能会导致其他事务的等待时间过长,影响系统的性能和可用性,因此需要尽可能缩短事务的执行时间,避免长时间的事务等待。
5)优化事务日志:长事务会占用大量的事务日志,导致数据库性能下降,因此需要通过优化事务日志的写入和刷盘策略来提高性能。
6)使用定时任务:长时间运行的事务可以通过定时任务来定期执行,以避免长时间占用资源。
7)适当增加硬件资源:如果以上方法不能解决问题,可以适当增加硬件资源,如增加内存、CPU、存储等,以提高系统性能。
如何优化事务的性能
MySQL事务性能优化是提高数据库性能的关键之一,以下是一些常用的优化方法:
1)选择合适的存储引擎:不同的存储引擎具有不同的特性和性能,因此需要根据具体的业务需求选择合适的存储引擎,如 MyISAM、InnoDB、Memory 等。
2)使用合适的索引:合理的索引可以提高查询和更新操作的效率,因此需要根据实际业务情况添加适当的索引,避免全表扫描。
3)避免不必要的锁定:不必要的锁定会降低并发性能,因此需要避免不必要的锁定,如优化查询语句、使用乐观锁等。
4)选择合适的事务隔离级别:不同的事务隔离级别具有不同的特性和性能影响,因此需要根据实际业务情况选择合适的事务隔离级别。
5)减少事务的范围:尽量减小事务的范围,将大事务拆分为多个小事务,可以减少锁竞争和死锁的风险,提高并发性能。
6)使用合适的事务提交方式:对于不需要回滚的事务,可以使用自动提交方式,减少提交操作的次数,提高性能。
7)避免长事务:长时间运行的事务会占用大量的资源,影响并发性能,因此需要尽可能缩短事务的执行时间,避免长时间的事务等待。
8)优化数据库服务器的硬件和配置:优化数据库服务器的硬件和配置可以提高数据库性能,如增加内存、优化磁盘性能、调整缓存大小等。
9)使用分布式数据库:对于高并发的场景,可以使用分布式数据库架构,将数据分布到多个数据库节点上,提高并发性能。
当然,这些方法可能并不适用于所有业务场景,需要根据具体情况进行选择和调整。
Innodb事务实现的基本原理
InnoDB是MySQL中一种常用的存储引擎,支持事务和行级锁等高级特性。以下是InnoDB实现事务的基本原理:
1)InnoDB中,每个事务都拥有一个唯一的事务ID(transaction ID),通过该ID来区分不同的事务。
2)InnoDB使用MVCC(多版本并发控制)来实现事务的隔离性,每次修改会生成一个新版本,查询时只能看到在查询开始之前就已经提交的版本,这样可以避免读取到脏数据。
3)在执行事务中的更新操作时,InnoDB会根据需要对相关的数据行加锁,以保证事务的原子性和一致性。InnoDB中的行级锁是通过在索引节点上加锁来实现的,因此对于相同的数据行,不同的事务可以分别通过不同的索引来访问和修改数据。
4)InnoDB中的事务支持ACID特性,即原子性、一致性、隔离性和持久性。InnoDB通过redo log和undo log来保证事务的原子性和持久性,其中redo log记录了事务的修改操作,而undo log则记录了事务的回滚操作。当系统崩溃或者发生其他故障时,InnoDB可以通过redo log和undo log来恢复数据到事务提交前的状态,以保证数据的一致性和持久性。
5)InnoDB中的事务隔离级别包括读未提交、读已提交、可重复读和串行化。默认的隔离级别是可重复读,使用锁和MVCC机制来实现。在高并发的情况下,如果锁的粒度过大或者锁的竞争过激,可能会导致性能瓶颈或者死锁问题,因此需要针对具体场景进行优化。