一、事务的概念
事务是最小的逻辑操作单元,它是由用户定义的一个数据库操作序列,
这些操作要么全做,要么全不做,是一组不可再分割的工作执行单元;
二、事务的特性(ACID)
事务的ACID特性取决于事务的隔离级别的设置。
事务特性(ACID)
Atomic(原子性):
事务是最小的逻辑操作单元,事务中包含的各操作要么都做,要么都不做
Consistent(一致性):
在一个事务执行之前和执行之后数据库都必须处于一致
性状态。
【事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统 运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是 不一致的状态。】
Isolate(隔离性):
一个事务的执行不能其它事务干扰。
Durable(持久性):
一旦事务提交,则其所做的修改会永久保存到数据库。
三、事务的属性
3.1 事务的传播属性
即当前的事务方法被另外一个事务方法调用时如何使用事务,
默认取值为required,即使用调用方法的事务。
@Transactional( propagation = Propagation.REQUIRED )
事务的传播行为:
Propagation.REQUIRED
如果有事务在运行,当前的方法就在这个事务内运行;否则,就启动一个新的事务,并在自己的事务内运行;
Propagation.REQUIRES_NEW
当前的方法必须启动新事务,并在它自己的事务内运行;如果有事务正在运行,应该将它挂起。
Propagation.MANDATORY
当前的方法必须运行在事务内部,如果没有正在运行的事务,将抛出异常。
Propagation.SUPPORTS
如果有事务在运行,当前的方法就在这个事务内运行;否则它可以不运行在事务中。
Propagation.NOT_SUPPORTED
当前的方法不应该运行在事务中,如果有运行的事务,将它挂起。
Propagation.NEVER
当前的方法不应该运行在事务中,如果有运行的事务,就抛出异常。
Propagation.NESTED
如果有事务在运行,当前的方法就应该在这个事务的嵌套事务内运行。否则,就启动一个新的事务,并在它自己的事务内运行。
3.2 事务的隔离级别
3.2.1 事务的隔离级别
事务的ACID特性取决于事务的隔离级别的设置。
五种隔离级别:DEFAULT、
READ_UNCOMMITTED、
READ_COMMITTED、
REPEATABLE_READ
SERIALIZABLE。
① DEFAULT(读提交)
这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别。另外四个与JDBC的隔离级别相对应。大部分数据库的默认级别都是READ_COMMITTED(读取已提交)。
Oracle数据库默认隔离级别的是READ_COMMITTED,MySQL的是REPEATABLE_READ。
② READ_UNCOMMITTED(读取未提交)
这是事务最低的隔离级别,它充许令外一个事务可以看到这个事务未提交的数据(这是很可怕的事情)。
这种隔离级别会产生脏读,不可重复读和幻读。
举例子:对于一个数A原来50修改为100,但是我还没有提交修改,另一个事务看到这个修改,而这个时候原事务发生了回滚,这时候A还是50,但是另一个事务看到的A是100,这就是未提交读
产生脏读场景:A事务读取一个字段,但是这个字段被另外一个事务更新却未提交,再次读取该字段时如果另外一个事务回滚则出现了脏读现象(读到的数据与第一次,数据库中的数据都不同)。
产生不可重复读场景:A事务读取一个字段,但是这个字段被另外一个事务更新并提交,再次读取该字段值不一样则出现了不可重复读现象(同一个事务中,不能保证读取的字段值相同)。
产生幻读场景:A事务读取一个字段集合,但是这个表被另外一个事务更新并提交(如插入了几行),再次读取该表可能会多几行则出现了幻读现象。
3.READ_COMMITTED(读取已提交)
保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。
可以避免脏读,但不可重复读和幻读的现象仍然可能出现。
举例就是对于一个数A原来是50,然后提交修改成100,这个时候另一个事务在A提交修改之前,读取到了A是50,刚读取完,A就被修改成100了,这个时候另一个事务再进行读取发现A就突然变成100了
A事务读取一个字段,但是这个字段被另外一个事务更新并提交,再次读取该字段值不一样则出现了不可重复读现象(同一个事务中,不能保证读取的字段值相同)。
读取一个字段,但是这个表被另外一个事务更新并提交(如插入了几行),再次读取该表可能会多几行则出现了幻读现象。
4.REPEATABLE_READ:(可重复读)
就是对于一个记录读取多次的记录是相同的。这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻读。
它除了保证一个事务不能读取另一个事务未提交的数据外,还保证了在一个事务过程,读取的数据不会发生变化(即使数据库中的数据在该事务过程中发生了变化)。
//如下代码所示,可重复读意味着两次读取字段A值相同!
public void test(){
//开启事务
//读取字段A;
//此时数据库中A发生了变化;
//读取字段A;
//提交事务;
//关闭事务;
}
读取一个字段集合,但是这个表被另外一个事务更新并提交(如插入了几行),再次读取该表可能会多几行则出现了幻读现象。
5.SERIALIZABLE :(可串行化)
在并发情况下和串行化的读取的结果是一致的,没有什么不同。这是花费最高代价但是最可靠的事务隔离级别,事务被处理为顺序执行。
除了防止脏读,不可重复读外,还避免了幻读。但性能十分低下!
3.2.2 什么是脏读、不可重复读和幻读
脏读: 对于两个事务 T1, T2, T1 读取了已经被 T2 更新但还没有被提交的字段。之后, 若 T2 回滚, T1读取的内容就是临时且无效的,也就是脏数据。
不可重复读:对于两个事务 T1, T2, T1 读取了一个字段, 然后 T2 更新了该字段。之后, T1再次读取同一个字段, 值就不同了。
幻读:事务T1读取一条指定where条件的语句,返回结果集。此时事务T2插入一行新记录,恰好满足T1的where条件。然后T1使用相同的条件再次查询,结果集中可以看到T2插入的记录,这条新纪录就是幻读。
或者说事务A开启事务,查询当前表;事务B开启事务插入一条新数据(id为主键);事务A 插入和事务B一样的数据,会出现锁等待超时现象。事务B提交后,事务A再次尝试插入,出现Duplicate entry…事务B很诧异,明明查询出来不存在将要插入的数据┭┮﹏┭┮
不可重复读重点是在update,即事务前后对比特定数据内容的修改。而幻读是insert和delete,即事务前后数据结果集的对比。
3.2.3 事务隔离级别与存在问题
READ UNCOMMITTED 幻读、不可重复读和脏读都允许。
READ COMMITTED 允许幻读、不可重复读,不允许脏读
REPEATABLE READ 允许幻读,不允许不可重复读和脏读
SERIALIZABLE 幻读、不可重复读和脏读都不允许
Oracle数据库支持READ COMMITTED(默认) 和 SERIALIZABLE这两种事务隔离级别。所以Oracle不支持脏读。
MySQL 支持 后4 种事务隔离级别:READ_UNCOMMITTED(读取未提交),READ_COMMITTED(读取已提交),REPEATABLE_READ(可重复读-默认)和SERIALIZABLE (可串行化)。
SQL标准所定义的默认事务隔离级别是SERIALIZABLE,但是Oracle 默认使用的是READ COMMITTED。MySQL默认事务隔离级别为REPEATABLE_READ。
事务的隔离级别要得到底层数据库引擎的支持, 而不是应用程序或者框架的支持。
注意:事务的隔离级别和数据库并发性是成反比的,隔离级别越高,并发性越低。
3.3 事务的其他属性
3.3.1 回滚
noRollbackFor = ArithmeticException.class,//遇到某种异常不会滚
noRollbackForClassName = "java.lang.ArithmeticException" ,//和上述用法一致
rollbackFor = ArithmeticException.class,//只有遇到这种异常才会滚
rollbackForClassName = "java.lang.ArithmeticException",//和上述用法一致
3.3.2 事务的超时和只读属性
由于事务可以在行和表上获得锁, 因此长事务会占用资源, 并对整体性能产生影响.
如果一个事物只读取数据但不做修改, 数据库引擎可以对这个事务进行优化.
超时事务属性: ( timeout=millons )
事务在强制回滚之前可以保持多久. 这样可以防止长期运行的事务占用资源.
只读事务属性:(readOnly=true/false)
表示这个事务只读取数据但不更新数据, 这样可以帮助数据库引擎优化事务.
三、事务控制
3.1 事务控制在程序中的位置
事务控制为什么放在service层?
用户访问 -》action-》service -》dao
一个业务的成功,调用的service肯定是执行成功,
意味着service中调用的所有dao肯定也是执行成功的。
所以,事务控制应该放在service层。
程序中的事务控制,可以用aop实现。
将事务控制的代码抽离出来,运行的时候动态织入到业务代码中。
基于此,spring已经提供了对事务的管理技术。
开发者只需按照spring的规则进行配置即可。
3.2 事务控制的分类
事务管理可以分为两种方式,一种是编程式事务控制,另一种是声明式事务控制。
Spring 既支持编程式事务管理, 也支持声明式的事务管理.
3.2.1 编程式事务控制
>>>>>> 概念
概念:
将事务管理代码嵌入到业务方法中来控制事务的提交和回滚.
在编程式管理事务时, 必须在每个事务操作中包含额外的事务
管理代码.
编程式事务管理
通过代码实现事务控制,可以实现细粒度的事务控制。
比如你可以通过程序代码来控制你的事务何时开始,何时结束等。
例如jdbc,hibernate,spring中不提倡使用。
>>>>>> 实例
实例:
自己通过写代码去控制事务,这就叫做编程式事务控制。
+++ jdbc事务控制代码:
Conn.setAutoCommite(false); // 设置手动控制事务
conn.commit();提交事务
conn.rollback(point);事务回滚
+++ Hibernate事务控制代码:
Session.beginTransaction(); // 开启一个事务
session.getTransaction().commit();//提交事务
>>>>>> 优点和缺点
优点和缺点:
+++ 优点:
1.可以实现细粒度的事务控制,比较灵活
比如你可以通过程序代码来控制你的事务何时开始,何时结束。
【细粒度事务控制,可以对指定的方法、指定的方法的某几行添加事务控制】
+++ 缺点:
1.开发起来比较繁琐,每次都要开启、提交、回滚。
+++ 整体评价:
编程式事务控制是细粒度的事务控制,比较灵活,但开发起来比较繁
琐: 每次都要开启、提交、回滚
3.2.2 声明式事务管理
>>>>>> 概念
概念:
它将事务管理代码与业务代码分离, 以声明的方式来实现事务管理.
事务管理作为一种横切关注点, 可以通过 AOP 方法模块化.
Spring 通过 Spring AOP 框架支持声明式事务管理.
声明式事务管理
只需通过配置即可实现事务控制。它只能实现粗粒度的事务控制。
如spring提供的声明式事务控制技术。
1. Spring提供了对事务控制的管理技术,你只需配置即可。这个就叫声明式事务控制。
(声明一个变量,拿来就用;定义一个事务,拿来就用)
2. spring声明式事务控制,核心实现是基于AOP。
>>>>>> 优缺点
优缺点:
+++ 优点:
1.spring提供的声明式事务控制做到了事务控制的最大程度的解耦。
开发者如果想使用事务,只需在配置文件中配置即可。如果不想使用,
移除配置即可。
2. spring提供的声明式事务控制是粗粒度的事务控制,只能给方法应用事务,
而不能对方法的某几行应用事务。
但是作为企业级开发,已经足够满足一般场景。
+++ 整体评价:
1.spring提供的声明式事务管理。 可以实现对事务控制的最大程度的解耦。
用户想要使用声明式事务管理,只需配置即可。
2.声明式事务控制是粗粒度的事务控制, 只能给整个方法应用事务,
不可以对方法的某几行应用事务。(因为aop拦截的是方法。)
3.尽管不能实现细粒度的事务控制,但是在企业级开发中,已经满足了一般场景。
3.3 编程式事务控制(自己手动写代码控制事务)
JDBC事务控制、以及Hibernate事务控制,其事务控制代码是不同的。即事务控制的API是不同的
但他们最终还是通过硬编码的方式实现事务控制。
3.3.1 JDBC事务操作
/**
* 模拟两次新增部门操作,出现异常不会回滚【事务回滚】
*/
@Test
public void save2() {
Connection conn = null;
PreparedStatement pst=null;
try {
conn = JdbcUtils.getConn();
//开启事务
conn.setAutoCommit(false);
String sql="insert into t_dept(t_id,t_name) values(?,?)";
pst= conn.prepareStatement(sql);
//第一次插入
pst.setInt(1, 222);
pst.setString(2, "xxxx");
boolean f1 = pst.execute();
System.out.println(f1?"成功":"失败");
//模拟异常
int i=1/0;
//第二次插入
pst.setInt(1, 333);
pst.setString(2, "xxxx");
boolean f2 = pst.execute();
System.out.println(f2?"成功":"失败");
//提交事务
conn.commit();
} catch (SQLException e) {
try {
//事务回滚
conn.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
}
3.3.2 Hibernate事务操作
public void save(){
Session session = sessionFactory.getCurrentSession();
session.beginTransaction();
Info info = new Info("xx");
info.setContent("嘻嘻嘻嘻嘻");
session.save(info );
session.getTransaction().commit();
}
3.4 声明式事务控制(spring提供的声明式事务管理)
Spring 通过 Spring AOP 框架支持声明式事务管理.
3.4.1 声明式事务控制的实质
+++ 声明式事务控制的实质
声明式事务控制,实质上就是spring将事务控制的代码与业务代码分离。在业务执行过程中,根据配置动态植入事务控制代码。
它的本质就是AOP。
+++ 事务管理器类的实质
1.由于jdbc事务控制、hibernate事务控制,其代码肯定是不同的,
所以spring根据持久层的不同实现,封装了多个事务管理器类。
2.spring没有直接管理事务,而是提供了事务管理器类。
利用aop技术,拦截方法,动态植入事务管理代码。
3.4.2 事务管理器类
>>>>>> 事务管理器的分类
+++ 为什么提供不同的事务管理器类?
利用jdbc技术、利用hibernate技术,事务的管理代码不同。
所以spring针对这两种技术提供了两个声明式事务管理器类。
Spring声明式事务管理器类:
Jdbc技术:DataSourceTransactionManager
Hibernate技术:HibernateTransactionManager
下面是比较常见的事务管理器类:
>>>>>> 事务管理器的底层原理分析
Spring并不直接管理事务,而是提供了多种事务管理器,
他们将事务管理的职责委托给Hibernate或者JTA等持久化机制所提供的相关平台框架的事务来实现。
Spring事务管理器的接口是
org.springframework.transaction.PlatformTransactionManager,
通过这个接口,Spring为各个平台如JDBC、Hibernate等都提供了对应的事务管
理器,但是具体的实现就是各个平台自己的事情了。
此接口的内容如下:
Public interface PlatformTransactionManager() {
// 由TransactionDefinition得到TransactionStatus对象
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
// 提交
Void commit(TransactionStatus status) throws TransactionException;
// 回滚
Void rollback(TransactionStatus status) throws TransactionException;
}
从这里可知具体的具体的事务管理机制对Spring来说是透明的,
它并不关心那些,那些是对应各个平台需要关心的,所以Spring事务管理的
一个优点就是为不同的事务API提供一致的编程模型,
如JTA、JDBC、Hibernate、JPA。
1.Class DataSourceTransactionManager
在应用程序中只需要处理一个数据源,而且通过JDBC存取。
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
实际上,DataSourceTransactionManager是通过调用java.sql.Connection来管理事务,而后者是通过DataSource获取到的。通过调用连接的commit()方法来提交事务,同样,事务失败则通过调用rollback()方法进行回滚。
2.Class JtaTransactionManager
在JAVAEE应用服务器上用JTA(Java Transaction API)进行事务管理。
3.Class HibernateTransactionManager
如果应用程序的持久化是通过Hibernate实习的,那么你需要使用HibernateTransactionManager。
对于Hibernate3,需要在Spring上下文定义中添加如下的声明:
<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
3.4.3 spring事务配置
>>>>>> 使用注解配置事务
@Transactional(propagation=Propagation.REQUIRES_NEW,
isolation=Isolation.READ_COMMITTED,
rollbackFor={UserAccountException.class},
readOnly=false,
timeout=3)
public void purchase(String userName, int isbn) {
}
>>>>>> 使用xml配置事务
<!-- *************1.配置事务管理器******************* -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- *************2.配置事务属性 *********************-->
<tx:advice id="txadvice" transaction-manager="transactionManager" >
<tx:attributes>
<!-- 根据方法名指定事务的属性 -->
<tx:method name="purchase" propagation="REQUIRES_NEW" isolation="READ_COMMITTED" />
<tx:method name="checkout" propagation="REQUIRED" isolation="READ_COMMITTED" />
<tx:method name="get*" read-only="true"/>
<tx:method name="find*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!-- ***3.配置事务切入点,以及把事务切入点和事务属性关联起来 -->
<aop:config>
<aop:pointcut expression="execution(* com.web.xml.service.*.*(..))" id="txPointCut"/>
<!-- 配置增强通知,关联advice与pointcut -->
<aop:advisor advice-ref="txadvice" pointcut-ref="txPointCut" />
</aop:config>
四、其他【暂时记录】
https://blog.youkuaiyun.com/J080624/article/details/53995591
4.1 MySQL的锁机制
MySQL的InnoDB锁机制分为表级锁和行级锁,官网文档:
https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html
① 行级锁
行级锁中有共享锁和排它锁。
共享锁又称为读锁,简称S锁,顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
select * from t_user where id =10 lock in share mode;
排他锁又称为写锁(独占锁),简称X锁,顾名思义,排他锁就是不能与其他所并存。
如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据就行读取和修改。
select * from t_user where id =10 for update;
MySQL InnoDB引擎默认的修改数据语句,update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型。
select * from t_user where id=10 for update; # 错误
select * from t_user where id=10 lock in share mode; # 错误
select * from t_user where id=1 # 正常获取数据
另外,为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。
意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。
InnoDB行锁模式兼容性列表:
② InnoDB行锁实现方式
InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。
InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!
③ 间隙锁(Next-Key锁)
当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的 索引项加锁。
对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁 (Next-Key锁)。
InnoDB除了通过范围条件加锁时使用间隙锁外,如果使用相等条件请求给一个不存在的记录加锁,InnoDB也会使用间隙锁!
总结:MySQL避免幻读是需要分情形的。
在快照读读情况下,mysql通过mvcc来避免幻读。
在当前读读情况下,mysql通过next-key来避免幻读
MVCC
mvcc全称是multi version concurrent control(多版本并发控制)。mysql把每个操作都定义成一个事务,每开启一个事务,系统的事务版本号自动递增。每行记录都有两个隐藏列:创建版本号和删除版本号
select:事务每次只能读到创建版本号小于等于此次系统版本号的记录,同时行的删除版本号不存在或者大于当前事务的版本号。
update:插入一条新记录,并把当前系统版本号作为行记录的版本号,同时保存当前系统版本号到原有的行作为删除版本号。
delete:把当前系统版本号作为行记录的删除版本号
insert:把当前系统版本号作为行记录的版本号
4.2 测试
第一个测试示例如下(不手动加锁):
① 事务A查询表中id为9的数据
没有没有还是没有!
② 事务B向表中插入id为9的数据,暂不提交
start TRANSACTION;
insert into t_user(id,name,age)VALUES(9,'jane1',18);
SELECT * from t_user;
没提交的数据在日志中,没有持久化到数据库!!
③ 事务A去尝试更新id为9的数据
mysql> update t_user set name='jane00'where id=9;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
锁等待超时,什么鬼,谁加锁了(事务B的insert 添加了排它锁)?
④ 事务B将事务提交,此时数据持久化到数据库
⑤ 事务A再次尝试更新id为9的数据
mysql> update t_user set name='jane00'where id=9;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
发生了什么?怎么成功了,不是没有该条数据么?让我再次查一下看看:
我擦我擦,见鬼了,凭空出现了数据!!!
update 时使用了当前读(读取最新数据),再次查询的时候会查询最新数据。
第二个测试实例如下(手动加共享锁):
① 事务A查询id为10的数据并使用共享锁
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t_user where id =10 lock in share mode;
Empty set (0.00 sec)
② 事务B尝试插入id为10的数据
start TRANSACTION;
SELECT * from t_user;
insert into t_user(id,name,age)VALUES(10,'jane1',18);
被阻塞了,等待然后到来的是锁等待超时(事务A已经加了行级锁中的共享锁,事务B只能读,不能写):
Err] 1205 - Lock wait timeout exceeded; try restarting transaction
③ 事务A尝试更新id为10的数据
mysql> update t_user set name='janei' where id =10;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0 Changed: 0 Warnings: 0
# 很显然 空语句,那就提交吧。
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
④ 事务B再次插入数据并提交
insert into t_user(id,name,age)VALUES(10,'jane1',18);
SELECT * from t_user;
COMMIT;
此时数据表中有了id为10的数据:
唔,使用共享锁好像可以了,就是等待超时会抛异常。
第三个测试示例如下(使用排它锁/独占锁/互斥锁):
① 事务A查询当前表
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t_user for update;
# 没有使用索引,将会加表锁
唔,很好,没有id为11的数据。
② 事务B尝试插入id为11的数据,插入前先使用排它锁查一下吧
start TRANSACTION;
select * from t_user for update;
# 直接爆异常--表被事务加表锁了,不能再加其他锁
[SQL]select * from t_user for update;
[Err] 1205 - Lock wait timeout exceeded; try restarting transaction
事务A不提交,事务B是没法执行的。使用共享锁和排它锁貌似挺好用的,应该没有其他问题了吧?
第四个测试示例如下(同样使用for update):
此时数据表数据如下:
① 事务A对max(id)进行加锁
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select max(id) from t_user for update;
+---------+
| max(id) |
+---------+
| 10 |
+---------+
1 row in set (0.00 sec)
# [10,+∞)范围内的id都被加了间隙锁+X锁。
② 事务B尝试插入id 4 5 和20的 数据并提交事务。
start TRANSACTION;
insert into t_user(id,name,age)VALUES(4,'jane1',18);
insert into t_user(id,name,age)VALUES(5,'jane1',18);
insert into t_user(id,name,age)VALUES(20,'jane1',18);
COMMIT;
id为4和5数据插入正常,id为20数据插入失败。
[SQL]
insert into t_user(id,name,age)VALUES(20,'jane1',18);
[Err] 1205 - Lock wait timeout exceeded; try restarting transaction
③ 事务A尝试更新id为4的记录并查询表记录数
mysql> update t_user set name='januie'where id=4;
Query OK, 1 row affected (42.56 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from t_user ;
+----+--------+------+
| id | name | age |
+----+--------+------+
| 1 | 小明 | 18 |
| 2 | janus | 18 |
| 3 | 明天 | 18 |
| 4 | januie | 18 |
| 5 | jane1 | 18 |
| 8 | jane1 | 18 |
| 9 | jane00 | 18 |
| 10 | jane1 | 18 |
+----+--------+------+
8 rows in set (0.00 sec)
什么情况?事务A已经使用了for update,怎么事务B还能插进去?为什么插入 id 为4和 5 正常,插入id为20失败?
事务A不光update成功了,而且查询记录还多出来两条!!!
第五个测试示例(不加锁,事务A只做普通查询)
① 事务A查询表记录
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t_user;
+----+--------+------+
| id | name | age |
+----+--------+------+
| 1 | 小明 | 18 |
| 2 | janus | 18 |
| 3 | 明天 | 18 |
| 4 | januie | 18 |
| 5 | jane1 | 18 |
| 8 | jane1 | 18 |
| 9 | jane00 | 18 |
| 10 | januie | 18 |
+----+--------+------+
8 rows in set (0.00 sec)
8条记录。
② 事务B插入id为6的记录并提交
start TRANSACTION;
insert into t_user(id,name,age)VALUES(6,'jane1',18);
SELECT * from t_user;
COMMIT;
此时数据库实际有9条数据。
③ 事务A再次查询数据表记录
mysql> select * from t_user;
+----+--------+------+
| id | name | age |
+----+--------+------+
| 1 | 小明 | 18 |
| 2 | janus | 18 |
| 3 | 明天 | 18 |
| 4 | januie | 18 |
| 5 | jane1 | 18 |
| 8 | jane1 | 18 |
| 9 | jane00 | 18 |
| 10 | januie | 18 |
+----+--------+------+
8 rows in set (0.00 sec)
嗯,很好的保证了可重复读,还是8条数据。
④ 事务A提交事务后再次查询表记录
mysql> select * from t_user;
+----+--------+------+
| id | name | age |
+----+--------+------+
| 1 | 小明 | 18 |
| 2 | janus | 18 |
| 3 | 明天 | 18 |
| 4 | januie | 18 |
| 5 | jane1 | 18 |
| 6 | jane1 | 18 |
| 8 | jane1 | 18 |
| 9 | jane00 | 18 |
| 10 | januie | 18 |
+----+--------+------+
9 rows in set (0.00 sec)
(ÒωÓױ),数据库有9条啊,事务A刚才看的不是最新数据,是历史数据!!!