性能调优 5. MySQL事务原理与优化以及MVCC底层原理剖析

1. 概述


‌‌‌  数据库一般都会并发执行多个事务,多个事务可能会并发的对相同的一批数据进行增删改查操作,就会导致脏写、脏读、不可重复读、幻读这些问题。

‌‌‌  这些问题的本质都是数据库的多事务并发问题,为了解决多事务并发问题,数据库设计了事务隔离机制(级别)、锁机制、MVCC多版本并发控制隔离机制、日志机制,用一整套机制来解决多事务并发问题

2. 事务及其ACID属性


2.1. 事务


‌‌‌  事务是一组操作,要么全部成功,要么全部失败,目的是为了保证数据最终的一致性。


2.2. 事务的属性


‌‌‌  事务具有以下4个属性,通常简称为事务的ACID属性

‌‌‌  原子性(Atomicity) :当前事务的操作要么同时成功,要么同时失败。原子性由undo log日志来实现。

‌‌‌  undo log日志:简单说有个undo log日志链,每次修改数据,会记录修改前的数据,修改后的数据有个回滚指针指向修改前的数据。

‌‌‌  隔离性(Isolation) :在事务并发执行时,它们内部的操作不能互相干扰。隔离性由MySQL的各种锁以及MVCC机制来实现

‌‌‌  持久性(Durable) :一旦提交了事务,它对数据‌‌‌库的改变就应该是永久性的。持久性由redo log日志来实现。

‌‌‌  一致性(Consistent) :使用事务的最终目的由其它3个特性保证以及业务代码正确逻辑来实现。比如Spring框架下,下单成功了,try {订单减库存业务逻辑执行失败}catch捕获的异常不抛出去,则不能回滚事务,导致下单和减库存数据不一致性。

2.3. 并发事务处理带来的问题

2.3.1. 更新丢失(Lost Update)或脏写


‌‌‌  当两个或多个事务选择同一行数据修改,有可能发生更新丢失问题,即最后事务的更新覆盖了由其它事务所做的更新

2.3.2. 脏读(Dirty Reads)


‌‌‌  事务A读取到了事务B已经修改但尚未提交的数据

2.3.3. 不可重读(Non-Repeatable Reads)


‌‌‌  事务A内部的相同查询语句在不同时刻读出的结果不一致,更看重原先查询数据内容的变更。

2.3.4. 幻读(Phantom Reads)


‌‌‌  事务A读取到了事务B提交的新增数据,跟不可重读有些类似,更看重数据增减。

2.4. 事务隔离机制(级别)

2.4.1. 隔离级别


‌‌‌  MySQL的InnoDB引擎中,定义了四种隔离级别,级别越高事务隔离性越好,但性能就越低,而隔离性是由MySQL的各种锁以及MVCC机制来实现的。

‌‌‌  “脏读”、“不可重复读”和“幻读”,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决。

‌‌‌  下面隔离级别从上到下越来越高。

在这里插入图片描述

‌‌‌  数据库的事务隔离越严格,并发副作用越小,干扰性越小,但付出的代价也就越大,因为事务隔离实质上就是使事务在一定程度上“串行化”进行,这显然与“并发”是矛盾的。

‌‌‌  同时,不同的应用对读一致性和事务隔离程度的要求也是不同的,比如许多应用对“不可重复读"和“幻读”并不敏感,可能更关心数据并发访问的能力。

注意

‌‌‌  1. MySQL默认的事务隔离级别是可重复读(orcal是读已提交),用Spring开发程序时,如果不设置隔离级别默认用MySQL设置的隔离级别,如果Spring设置了就用已经设置的隔离级别。

2.4.2. 查看当前数据库的事务隔离级别,仅限当前会话


‌‌‌  show variables like 'tx_isolation';
‌‌‌  Mysql 8.0开始是
‌‌‌  show variables like 'transaction_isolation';

‌‌‌2.4.3. 设置事务隔离级别,仅限当前会话


‌‌‌  set tx_isolation='REPEATABLE-READ';
‌‌‌  Mysql 8.0开始是
‌‌‌  set transaction_isolation='REPEATABLE-READ';

2.4.4. 事务隔离级别案例分析

2.4.4.1. 准备数据

‌‌‌  CREATE TABLE `account` (
‌‌‌  `id` int(11) NOT NULL AUTO_INCREMENT,
‌‌‌  `name` varchar(255) DEFAULT NULL,
‌‌‌  `balance` int(11) DEFAULT NULL,
‌‌‌  PRIMARY KEY (`id`)
‌‌‌  ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
‌‌‌  INSERT INTO `mytest`.`account` (`name`, `balance`) VALUES ('lilei', '450');
‌‌‌  INSERT INTO `mytest`.`account` (`name`, `balance`) VALUES ('hanmei', '16000');
‌‌‌  INSERT INTO `mytest`.`account` (`name`, `balance`) VALUES ('lucy', '2400');


2.4.4.2. 读未提交

‌‌‌  1. 打开一个客户端A,并设置当前事务模式为read uncommitted(未提交读),查询表account的初始值:


‌‌‌  set tx_isolation='read‐uncommitted';

‌‌‌  MySql8.0

‌‌‌  set transaction_isolation='READ-UNCOMMITTED';

‌‌‌  开启事务‌‌‌START TRANSACTION或者begin,查询数据


	‌‌‌START TRANSACTION或者begin
	
‌‌‌  SELECT * FROM account

在这里插入图片描述

‌‌‌  2. 在客户端A的事务提交之前,打开另一个客户端B,更新表account


‌‌‌  START TRANSACTION;

‌‌‌  UPDATE account SET balance=balance-50 WHERE id=1

‌‌‌  3. 这时,虽然客户端B的事务还没提交,但是客户端A再次查询数据,可以看到B已经更新的数据。


‌‌‌  SELECT * FROM account

在这里插入图片描述

‌‌‌  4. 一旦客户端B的事务因为某种原因回滚,所有的操作都将会被撤销,那客户端A之前,查询到的数据其实就是脏数据,如果用脏数据做操作就会数据不一致性。

‌‌‌  客户端B回滚:


‌‌‌  ROLLBACK

‌‌‌  客户端A再次查询,数据变回之前的:

‌‌‌  SELECT * FROM account

在这里插入图片描述

‌‌‌  要想解决这个问题可以采用读已提交的隔离级别。

‌‌‌  该事务缺点

‌‌‌  1. 脏读,能读到其它事务未提交数据(应该是读对应存储引擎下缓存池的数据)。

‌‌‌  2. 脏写,读到其它事务未提交数据,其它事务回滚,用读到操作进行更新操作

2.4.4.3. 读已提交

‌‌‌  1. 打开一个客户端A,并设置当前事务模式为read committed(读已提交),查询表account的所有记录

‌‌‌  set tx_isolation='read‐committed';

‌‌‌  MySql8.0

‌‌‌  set transaction_isolation='READ-COMMITTED';

‌‌‌  开启事务,查询数据


‌‌‌  START TRANSACTION

‌‌‌  SELECT * FROM account

在这里插入图片描述

‌‌‌  2. 在客户端A的事务提交之前,打开另一个客户端B,更新表account。


‌‌‌  START TRANSACTION;

‌‌‌  UPDATE account SET balance=balance-50 WHERE id=1

‌‌‌  3. 这时,客户端B的事务还没提交,客户端A不能查询到B已经更新的数据,解决了脏读问题

‌‌‌  客户端A查询数据


‌‌‌  SELECT * FROM account

在这里插入图片描述

‌‌‌  4. 客户端B的事务提交


‌‌‌  COMMIT

‌‌‌  5. 客户端B提交事务后,客户端A执行相同的查询,结果数据变更,与之前查询不一致,即产生了不可重复读的问题。

‌‌‌  客户端A查询数据:


‌‌‌  SELECT * FROM account

在这里插入图片描述

‌‌‌  事务缺点

‌‌‌  1. 不可重复读,同样查询,两次查询不一致。

‌‌‌  2. 脏写,别的事务更新了数据,将第一次查询的旧数据,进行更新操作。

‌‌‌  原理

‌‌‌  使用了MySQL的MVCC机制。

2.4.4.4. 可重复读

‌‌‌  1. 打开一个客户端A,并设置当前事务模式为repeatableread,查询表account的所有记录。

‌‌‌  set tx_isolation='repeatableread';

‌‌‌  MySql8.0

‌‌‌  set transaction_isolation='REPEATABLE-READ';

‌‌‌  开启事务,查询数据


‌‌‌  START TRANSACTION

‌‌‌  SELECT * FROM account

在这里插入图片描述

‌‌‌  2. 在客户端A的事务提交之前,打开另一个客户端B,更新表account,然后提交事务。


‌‌‌  START TRANSACTION;

‌‌‌  UPDATE account SET balance=balance-50 WHERE id=1

‌‌‌  COMMIT

‌‌‌  在这里插入图片描述
‌‌‌  3. 在客户端A再次查询表account的所有记录,与步骤1查询结果一致,没有出现不可重复读的问题,也就是不管别的事务怎么改查询的数据,再次查结果不变

‌‌‌  即第一次查询的数据,生成了快照沿用到事务结束。


‌‌‌  SELECT * FROM account

在这里插入图片描述

‌‌‌  4. 在客户端A,接着执行update account set balance=balance-50 where id=1,balance没有变成450-50=400,lbalance值用的是步骤2中的400来算的,所以是350,数据的一致性倒是没有被破坏。

‌‌‌  即写数据用的是当前读(读取最新的数据)。


‌‌‌  UPDATE account SET balance=balance-50 WHERE id=1;

‌‌‌  SELECT * FROM account

在这里插入图片描述

‌‌‌  5. 重新打开客户端B,插入一条新数据后提交。


‌‌‌  START TRANSACTION;

‌‌‌  INSERT INTO account VALUES(4,'LILY',700);

‌‌‌  COMMIT

‌‌‌  6. 在客户端A再次查询表account的所有记录,没有查出新增数据,所以没有出现幻读。


‌‌‌  SELECT * FROM account

在这里插入图片描述

‌‌‌  7. 幻读问题:步骤6后,在客户端A执行 update account set balance=888 where id=4 能更新成功,再次查询能查到客户端B新增的数据(应该是对更新数据,加上快照)。


‌‌‌  UPDATE account SET balance=888 where id=4;

‌‌‌  SELECT * FROM account

在这里插入图片描述

‌‌‌  缺点

‌‌‌  1. 幻读:别的事务插入一条数据,当前事务更新该数据,重新查询能查到新增数据,数据跟第一次查询不一样。

‌‌‌  2. 脏写:拿第一次查询快照的旧数据进行逻辑操作,更新数据。

‌‌‌  原理

‌‌‌  使用了MySQL的MVCC机制。

2.4.4.5. 串行化

‌‌‌  1. 打开一个客户端A,并设置当前事务模式为serializable,查询account表id=1的数据,没有提交事务。


‌‌‌  set tx_isolation='serializable';

‌‌‌  MySql8.0

‌‌‌  set transaction_isolation='serializable';


‌‌‌  SELECT * FROM account WHERE id=1;

在这里插入图片描述

‌‌‌  2. 打开一个客户端B,并设置当前事务模式为serializable,更新相同的id为1的记录会被阻塞等待,更新id为2的记录可以成功。

‌‌‌  客户端B开启事务设置当前事务模式为serializable,并更新id=1的数据。


‌‌‌  START TRANSACTION;

‌‌‌  settx_isolation='serializable';
‌‌‌  MySql8.0
‌‌‌  set transaction_isolation='SERIALIZABLE';

‌‌‌  UPDATE account SET balance=balance-50 WHERE id=1;

‌‌‌  此时更新数据堵塞

在这里插入图片描述

‌‌‌  客户端B更新id=2的数据。


‌‌‌  UPDATE account SET balance=balance-50 WHERE id=2;

‌‌‌  数据正常更新。

在这里插入图片描述

‌‌‌  说明在串行模式下innodb的查询也会被加上行锁(读锁类型即共享锁),如果查询的记录不存在会给这条不存在的记录加上锁(这种也是间隙锁,加的是读锁即共享锁)。

‌‌‌  如果客户端A执行的是一个范围查询,那么该范围内的所有行包括每行记录所在的间隙区间范围都会被加锁。此时如果客户端B在该范围内插入数据都会被阻塞,所以就避免了幻读。

‌‌‌  原理

‌‌‌  以性能上来说锁

‌‌‌  1. 读锁和写锁是互斥

‌‌‌  2. 查询的数据该事务会加上读锁(不管有没有开启事务,正常查询数据都是不会加锁)。更新数据时候加上写锁(任何事务下,修改数据都会自动加写锁),所以用低级别的事务查询时候加上读锁,一样达到串行效果

‌‌‌  注意

‌‌‌  1. 这边涉及的间隙锁也是读锁类型,读读不堵塞。

2.5.5. 脏写问题解决

‌‌‌  1. 在数据已经提交情况下,不在java代码层面,做数据逻辑操作更新数据。直接在数据库做操作即使用当前读(读最新的数据,鸡肋当然不可能这么改数据)。

‌‌‌  2. 悲观锁方式:更新时候会加行锁(写锁的类型),别的数据就不能操作。

‌‌‌  UPDATE account SET balance=balance-50 WHERE id=1;

‌‌‌  2. 乐观锁方式:在数据已经提交情况下,使用乐观锁。对操作数据加个版本号,修改时候就加1。更新数据时候根据查询数据的版本号做条件,版本号不对就不能更新成功。

‌‌‌  可以在java逻辑层面,加上while循环,循环里做逻辑运算后,进行查询,比对再更新(CAS操作),直到更新成功。

‌‌‌  UPDATE account SET balance=balance-50 WHERE id=1 AND version='查出的版本号';

‌‌‌  因为数据修改时候会自动加悲观锁(行锁),读写互斥,所以读时候版本号肯定是最新的,即使后面更新操作前,数据被修改了,因为版本号比对不会更新成功。

2.5.6. 幻读问题解决

‌‌‌  1. 可重复读下事务下,加间隙锁,即对查询数据加读锁。

‌‌‌3. 事务下的锁


‌‌‌  1. 读锁(共享锁、S锁

‌‌‌  lock in share mode。如:select … lock in share mode。

‌‌‌  读锁是共享的,多个事务可以同时读取同一个资源,但不允许其它事务修改。即读读可以,读写互斥。

‌‌‌  写锁(排它锁、X锁):for update,不要理解为只有修改数据时候才能加的锁,查询时候也能加。

‌‌‌  如:select … for update;

‌‌‌  写锁是排他的,会阻塞其他的写锁和读锁,事务下update、delete、insert都会自动加写锁,操作粒度来说一般是行锁

‌‌‌  注意

‌‌‌  1. 不管有没开启事务,一般查询是不会自动加锁的。

‌‌‌  2. 这些锁是基于事务开启加的锁。

4. 事务MVCC多版本并发控制机制


‌‌‌  1. MVCC(Multi-Version Concurrency Control)同一数据多版本(undo日志链)并发控制,就可以做到读写不阻塞,且避免了类似脏读这样的问题。

‌‌‌  MVCC机制的实现就是通过read-view(视图)机制与undo版本链比对机制,使得不同的事务,读取一条数据,根据版本链对比规则,读取同一条数据,在版本链上的不同版本数据。

‌‌‌  MySQL在读已提交(RR)和可重复读(RC)隔离级别下,通过MVCC机制不需要加锁实现,读写不堵塞
‌‌‌  读已提交在每次查询时候都会生成当前视图,可重复读只有在第一次查询时候生成视图,使用该视图到事务结束。两者写都是当前读,即insert、update和delete是读取当前最新数据,不使用视图

4.1. undo日志版本链


‌‌‌  undo日志版本链是指一行数据被多个事务依次修改删除过后(查询不触发),在每个事务修改完后,MySQL会保留修改前的数据undo回滚日志,并且用两个隐藏字段trx_id(事务id),同个事务下修改操作的SQL语句,undo日志的数据事务id都一样。和roll_pointer(回滚指针),指向对应的undo日志地址。把这些undo日志串联起来形成一个历史记录版本链。

在这里插入图片描述

‌‌‌  undo日志

‌‌‌  1. 假设现在往account插入一条数据,生成一条新增的数据的undo日志。这条插入语句是insert id=1,则当前日志数据roll_pointer指向类似del id=1操作的undo日志,回滚用。

在这里插入图片描述

‌‌‌  2. 现在第一条件插入的SQL语句事务提交了,此时commited在该日志数据位置。新的事务开启,SQL语句更新balance=500。MySQL会把原数据复制一份出来进行修改,trx_id变为新的事务id,roll_pointer指向被复制的数据地址,把事务提交了。

在这里插入图片描述

‌‌‌  3. 现在有两个会话,会话A事务进行可重复读,查询select * from account id=1 ,会话B事务进行读已提交,查询select * from account id=1 。此时两者查到都是balance=500

‌‌‌  开启个新事务把数据balance=800提交,此时commited在该日志数据的位置。会话B再查询,可以查到数据内容更新balacne=800,会话A再查询,数据没变balance=500。

在这里插入图片描述

‌‌‌  4. 开启个新事务把数据balance=1000没提交,数据会加到undo日志版本链中。会话A会话B再查询,数据没变balance=500和balance=800。
  
  在这里插入图片描述

‌‌‌  注意

‌‌‌  1. 创建事务id虽然是递增的,但是事务提交顺序是不确定的,可能大事务id会先提交。即组成undo日志链的日志,事务id顺序是不固定的。

‌‌‌4.2. 可重复读和读已提交隔离级别下的undo日志分析


‌‌‌  下面图是一条数据的undo日志分析:

‌‌‌  1. 从左到右执行,从上到下执行。

‌‌‌  2. commit标志的列的事务就是触发提交的事务。

‌‌‌  3. -readview[100,200] 300就表示生成的视图,100-200表示readview视图的未提交事务id数组,100就是min_d,300就是当前操作数据创建事务里头最大的事务id,当做max_id。

‌‌‌  4. 黄色是RR级别的事务,绿色是RC级别的事务。

在这里插入图片描述

4.3. read view视图


‌‌‌  根据情况,执行查询时候,为查询的每条记录都生成一个read view视图,由涉及记录的所有未提交事务id数组(数组里最小的id为min_id)和已创建的最大事务id(max_id)组成。

‌‌‌  如 -readview[100,200] 300就表示生成的视图。100,200表示readview视图的未提交事务id数组,100当做min_id,300表示当前操作数据,创建事务里头最大的事务id,当做max_id。

‌‌‌  可重复读隔离级别的视图:当事务开启,只要第一次查询数据,就会生成当前查询数据的**一致性视图read-view,该视图在事务结束之前永远都不会变化。

‌‌‌  读已提交隔离级别的视图:在每次查询数据时都会重新生成read-view。

4.4. 版本链比对规则


在这里插入图片描述

‌‌‌  版本链比对规则:SQL查询数据,每条数据都要从其undo日志最后的一条数据,按照回滚指针,从下往上一条条取出日志的事务id(注意,同个事务的undo日志,事务id都一样),结合当前事务类型生成的视图,按照下面比对规则,比对这条数据的可见性,得到可查询的数据。

‌‌‌  1. 如果 row 的 trx_id 落在绿色部分( trx_id<min_id,肯定是已提交事务 ),表示这个版本是已提交的事务生成的,这个数据是可见的

‌‌‌  2. 如果 row 的 trx_id 落在红色部分( trx_id>max_id ,肯定是未开始的事务),表示这个版本是由将来启动的事务生成的,是不可见的(若 row 的trx_id 就是当前自己的事务是可见的);

‌‌‌  3. 如果 row 的 trx_id 落在黄色部分(min_id <=trx_id<= max_id,事务可能已提交也可能未提交),那就包括两种情况

‌‌‌  3.1. 若 row 的 trx_id 在视图数组中(都是未提交事务组成),表示这个版本是由还没提交的事务生成的,不可见。

‌‌‌  但若 row 的 trx_id 就是当前自己的事务id,是可见的。因为当前事务,修改了查询的数据,未提交则有行锁,其它事务就没法修改数据会堵塞。不需要用上面规则,从缓存池或者数据库查询自己修改数据就行。

‌‌‌  3.2. 若 row 的 trx_id 不在视图数组中这样的,表示这个版本是已经提交了的事务生成的,可见

‌‌‌  注意

‌‌‌  1. 对于删除的情况可以认为是update的特殊情况,会将版本链上最新的数据复制一份,然后将trx_id修改成删除操作的trx_id,同时在该条记录的头信息(record header)里的(deleted_flag)标记位写上true,来表示当前记录已经被删除,在查询时按照上面的规则查到对应的记录如果delete_flag标记位为true,意味着记录已被删除,则不返回数据

‌‌‌  2. begin/start transaction命令并不是一个事务的起点,在执行到它们之后的第一个修改操作或加排它锁操作(比如select…for update)的语句,事务才真正启动,才会向MySQL申请真正的事务id,只执行查询操作启动的并不是真正事务,事务id不是真正的,MySQL内部是严格按照事务的启动顺序来分配事务id的。

‌‌‌  如:


‌‌‌  begin


‌‌‌  select * from account where id=1;

‌‌‌  select * from INFORMATION_SCHEMA.INNODB_TRX;

在这里插入图片描述

5. 事务持久性


‌‌‌  MySQL引入了redo log,Buffer Pool内存写完了,然后会写一份redo log,这份redo log记载着这次在某个页上做了什么修改,记录物理修改。

‌‌‌  即便MySQL在中途挂了,我们还可以根据redo log来对数据进行恢复。

6. 事务优化


## 6.1. 事务问题定位

# 查询执行时间超过1秒的事务,详细的定位问题方法后面讲完锁课程后会一起讲解
# now()---当前时间 
# trx_started---事务开始时间 
# timediff---相减 
# TIME_TO_SEC--转成秒
# 事务执行中就能从information_schema.innodb_trx查出事务,执行完就删除

‌‌‌  SELECT
‌‌‌  *
‌‌‌  FROM
‌‌‌  information_schema.innodb_trx
‌‌‌  WHERE
‌‌‌  TIME_TO_SEC(timediff(now(),trx_started))>1;

#强制结束事务
‌‌‌  kill 事务对应的线程id(就是上面语句查出结果里的trx_mysql_thread_id字段的值)

6.2. 大事务的影响


‌‌‌  1. 事务下,增删改数据会加锁堵塞,大事务操作比较多执行时间长,则未提交下占用资源堵塞不能释放,同时可能占用连接导致连接池连接不能释放。

‌‌‌  2. 并发情况下,数据库连接池容易被撑爆

‌‌‌  3. 锁定太多的数据,造成大量的阻塞和锁超时

‌‌‌  4. 执行时间长,容易造成主从数据同步延迟

‌‌‌  5. 做了大量更新操作,回滚所需要的时间比较长

‌‌‌  6. undo log膨胀,在当前事务未提交下,比如别的事务进来,进行增删改操作,就会增大对应数据的日志。各事务都提交后undo log总归会进行删除(update操作未必是马上,跟mvcc要使用有关)。

‌‌‌  7. 容易导致死锁 。

6.3. 事务优化实践原则


‌‌‌  1. 将查询等数据准备操作放到事务外,看事务级别比如RC查询等同于不用事务的查询效果可以将查询放到事务外,RR这种就不能

‌‌‌  2. 事务中避免远程调用,远程调用要设置超时,防止事务等待时间太久。比如微服务A调用其它服务接口(事务是可以包含其它业务逻辑操作的,不只是SQL语句操作)。

‌‌‌  3. 事务中避免一次性处理太多数据, 可以拆分成多个事务分次处理。比如更新1000条数据,拆分成10个事务分布更新100条数据。执行时间长,但是减少占用连接等资源。

‌‌‌  4. 更新等涉及加锁的操作尽可能放在事务靠后的位置。比如订单操作,一个事务下。第一种先更新库存,再新增下单记录。第二种先新增下单记录,再更新库存。推荐第二种,因为更新的数据是已存在的。更新操作都是加锁的,别的事务如果需要更新数据,需要等待这个事务提交,如果放到后面,就减少更新时候堵塞等待。

‌‌‌  5. 能异步处理的尽量异步处理,就是不要同步放到事务里头,但是也要加超时时间。

‌‌‌  6. 应用侧(业务代码)保证数据一致性,非事务执行。意思是需要性能非常高,业务很简单下可以不要加事务,可以通过代码保证数据一致性。比如try catch,catch捕获异常,需要自己加回滚操作,很复杂。

7. 面试题

1. 查询操作方法需要使用事务吗?


‌‌‌  在RR事务级别下,多次查询结果一样,是同一时间维度的,如果要做报表什么的业务场景可以加下事务。在RC事务级别下,多次查询结果可能不一样是变动的,跟未开启事务查询一样,适合不是同一时间维度的数据查询。

‌‌‌  Spring下@Transactional修饰方法可以开启事务,表示该注解方法下的SQL语句都在一个事务中。@Transactional(readOnly=true)可以开启只读事务,默认是fasle开启就是读写事务,只是一种优化,性能更高。在只读事务中,进行写操作会报错。 MYSQL开启只读事务 可以运行 start transaction read only。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值