什么是事务?
事务是逻辑上的一组操作,要么都执行,要么都不执行。
Guide 哥:大家应该都能背上面这句话了,下面我结合我们日常的真实开发来谈一谈。
我们系统的每个业务方法可能包括了多个原子性的数据库操作,比如下面的 savePerson()
方法中就有两个原子性的数据库操作。这些原子性的数据库操作是有依赖的,它们要么都执行,要不就都不执行。
public void savePerson() {
personDao.save(person);
personDetailDao.save(personDetail);
}
另外,需要格外注意的是:事务能否生效数据库引擎是否支持事务是关键。比如常用的 MySQL 数据库默认使用支持事务的innodb
引擎。但是,如果把数据库引擎变为 myisam
,那么程序也就不再支持事务了!
事务最经典也经常被拿出来说例子就是转账了。假如小明要给小红转账 1000 元,这个转账会涉及到两个关键操作就是:
- 将小明的余额减少 1000 元
- 将小红的余额增加 1000 元。
万一在这两个操作之间突然出现错误比如银行系统崩溃或者网络故障,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。
public class OrdersService {
private AccountDao accountDao;
public void setOrdersDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
@Transactional(propagation = Propagation.REQUIRED,
isolation = Isolation.DEFAULT, readOnly = false, timeout = -1)
public void accountMoney() {
//小红账户多1000
accountDao.addMoney(1000,xiaohong);
//模拟突然出现的异常,比如银行中可能为突然停电等等
//如果没有配置事务管理的话会造成,小红账户多了1000而小明账户没有少钱
int i = 10 / 0;
//小王账户少1000
accountDao.reduceMoney(1000,xiaoming);
}
}
另外,数据库事务的 ACID 四大特性是事务的基础,下面简单来了解一下。
事物的特性(ACID)了解么?
- 原子性: 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
- 一致性: 执行事务前后,数据保持一致;
- 隔离性: 并发访问数据库时,一个用户的事物不被其他事务所干扰也就是说多个事务并发执行时,一个事务的执行不应影响其他事务的执行;
- 持久性: 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
==事务的ACID是通过InnoDB日志和锁来保证。事务的隔离性是通过数据库锁的机制实现的,持久性通过redo log(重做日志)来实现,原子性和一致性通过回滚日志Undo log来实现。==
UndoLog的原理
很简单,为了满足事务的原子性,在操作任何数据之前,首先将数据备份到一个地方(这个存储数据备份的地方称为UndoLog)。然后进行数据的修改。如果出现了错误或者用户执行了ROLLBACK语句,系统可以利用Undo Log中的备份将数据恢复到事务开始之前的状态。 和Undo Log相反,RedoLog记录
的是新数据的备份。在事务提交前,只要将RedoLog持久化即可,不需要将数据持久化。当系统崩溃时,虽然数据没有持久化,但是RedoLog已经持久化。系统可以根据RedoLog的内容,将所有数据恢复到最新的状态。
事务隔离级别(isolation)
在多线程或并发访问下如何保证访问到的数据具有完整性的?
A)脏读: 修改时允许读取
B) 不可重复读: 读取时允许修改,一次事务读取不一致
C) 幻读: 读取时允许插入
DEFAULT
: 默认值,由底层数据库自动判断应该使用什么隔离界别。READ_UNCOMMITTED
: 可以读取未提交数据,可能出现脏读,不重复读,幻读.READ_COMMITTED
:只能读取其他事务已提交数据.可以防止脏读,可能出现不可重复读和幻读.REPEATABLE_READ
: 读取的数据添加锁,基于mvcc快照机制实现,防止其他事务修改此数据,保证一个事务读取到相同的数据,可以防止不可重复读.脏读,可能出现幻读.mysql和互联网场景下默认的隔离级别SERIALIZABLE
: 排队操作,对整个表添加锁.一个事务在操作数据时,另一个事务等待事务操作完成后才能操作这个表.这里需要注意的是:
与 SQL 标准不同的地方在于 InnoDB 存储引擎在REPEATABLE-READ
(可重读) 事务隔离级别下使用的是 间隙锁(Next-Key算法),因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server)是不同的。所以说 InnoDB 存储引擎的默认支持的隔离级别是REPEATABLE-READ
(可重读) 已经可以完全保证事务的隔离性要求,即达到了 SQL 标准的SERIALIZABLE
(可串行化) 隔离级别。
间隙锁(Next-Key锁)
当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制不是所谓的间隙锁(Next-Key锁)。
举例来说,假如emp表中只有101条记录,其empid的值分别是1,2,…,100,101,下面的SQL:
SELECT * FROM emp WHERE empid > 100 FOR UPDATE
是一个范围条件的检索,InnoDB不仅会对符合条件的empid值为101的记录加锁,也会对empid大于101(这些记录并不存在)的“间隙”加锁。
InnoDB使用间隙锁的目的,一方面是为了防止幻读,以满足相关隔离级别的要求,对于上面的例子,要是不使用间隙锁,如果其他事务插入了empid大于100的任何记录,那么本事务如果再次执行上述语句,就会发生幻读;另一方面,是为了满足其恢复和复制的需要。有关其恢复和复制对机制的影响,以及不同隔离级别下InnoDB使用间隙锁的情况。
很显然,在使用范围条件检索并锁定记录时,InnoDB这种加锁机制会阻塞符合条件范围内键值的并发插入,这往往会造成严重的锁等待。因此,在实际开发中,尤其是并发插入比较多的应用,我们要尽量优化业务逻辑,尽量使用相等条件来访问更新数据,避免使用范围条件。
事务传播属性
REQUIRED
:需要。有事务,我跟你混;没事务我新建个;
SUPPORTS
:支持。有事务,我支持;没有事务我也不新建;
REQUIRES_NEW
:需要自己New个。没有,我新建个;有我也不搭理你,我自己建个,独立回滚。
必须有事务:MANDATORY
没有就报错,NESTED
没有我新建个,有我嵌套一个。
必须没事务:NEVER
有就报错,NOT_SUPPORTED
有我也不搭理你。
详细说明
下面,用一个例子来解释下各个传播属性的不同:
在数据库中新建了一张产品表,两条数据,一条产品id为1,产品名称为IPhone6S,另一条产品id为2,产品名称为MAC PRO。
两个服务serviceA和serviceB,serviceA的methodA方法先减IPhone6S的库存,然后调用serviceB中methodB方法扣减MAC Pro库存,为了观察,methodA最后手动抛出异常,模拟回滚场景。
REQUIRED(默认值)
如果当前有事务,就在事务中执行,如果当前没有事务,新建一个事务.
==需要个事务==:你有我跟着你混,你没有我自己创建个。
外部有事务时,内部继承外部事务,异常时,共同回滚。
外部无事务时,内部新建事务,独立回滚。
在传播属性为PROPAGATION_REQUIRED时,事务加入方会使用当前事务,methodA代码为
public void methodA() {
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
try {
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
transactionTemplate.execute(status -> {
OrdProduct product = ordProductMapper.selectForUpdate(1);
System.out.println("before update " + product.getProductName() + ", inventory is: " + product.getInventory());
product.setInventory(product.getInventory() - 1);
int result = ordProductMapper.updateInventoryByProductId(1, product.getInventory());
serviceB.methodB();
if (result == 1) {
throw new RuntimeException("test");
}
return result;
});
} finally {
OrdProduct product1 = ordProductMapper.selectForUpdate(1);
OrdProduct product2 = ordProductMapper.selectForUpdate(2);
System.out.println("after update " + product1.getProductName() + ", inventory is: " + product1.getInventory());
System.out.println("after update " + product2.getProductName() + ", inventory is: " + product2.getInventory());
}
}
methodB的代码为:
public void methodB(){
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
transactionTemplate.execute(transactionStatus -> {
OrdProduct product = ordProductMapper.selectForUpdate(2);
System.out.println("before update " + product.getProductName() + ", inventory is: " + product.getInventory());
product.setInventory(product.getInventory() -1 );
int result = ordProductMapper.updateInventoryByProductId(2, product.getInventory());
/*if (result == 1) {
throw new RuntimeException("test");
}*/
return result;
});
}
methodA在完成6s的减库存操作后,调用methodB,methodB的传播属性是PROPAGATION_REQUIRED,此时methodA已经起了事务,methodB会使用现存的事务,在methodA抛出异常后,两个方法的减库存操作都会回滚。
当前没有事务,则新建一个事务
methodA代码逻辑不用transactionTemplate.execute()方式执行,methodB方法去除注释抛出异常,methodB会新建一个事务,执行后methodB会回滚,methodA不会,即mac库存不变,6s库存减1;REQUIRES_NEW
必须在事务中执行,如果当前没有事务,新建事务,如果当前有事务,把当前事务挂起.
内部事务和外部事务分离开,独立回滚。
SUPPORTS
如果当前有事务就在事务中执行,如果当前没有事务,就在非事务状态下执行.
- 支持当前事务*和REQUIRED行为相同;
- 没有当前事务,则以非事务的方式执行**(报错后均不进行回滚)
methodA代码逻辑不用transactionTemplate.execute()方式执行,methodB方法去除注释抛出异常,methodB不会新建事务,执行后两个方法都不会回滚,两个产品的库存均减1.
MANDATORY
必须在事务内部执行,如果当前有事务,就在事务中执行,如果没有事务报错.
==必须有事务==:没有我就报错。
methodA代码逻辑不用transactionTemplate.execute()方式执行,执行时则会抛出如下异常.
org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'
NOT_SUPPORTED
必须在非事务下执行,如果当前没有事务,正常执行,如果当前有事务,把当前事务挂起.
==必须没有事务==:有我也不搭理你。
NEVER
必须在非事务状态下执行,如果当前没有事务,正常执行,如果当前有事务,报错.
==必须没有事务==:有我就报错。
NESTED
必须在事务状态下执行**.如果没有事务,新建事务,如果当前有事务,创建一个嵌套事务.
==必须有事务==:没有我新建个,有我嵌套一个。
Spring声明式事务
编程式事务:
由程序员编程事务控制代码.(例如:commit,rollback)
通过 TransactionTemplate
或者TransactionManager
手动管理事务,实际应用中很少使用,但是对于你理解 Spring 事务管理原理有帮助。
使用TransactionTemplate
进行编程式事务管理的示例代码如下:
@Autowired
private TransactionTemplate transactionTemplate;
public void testTransaction() {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
try {
// .... 业务代码
} catch (Exception e){
//回滚
transactionStatus.setRollbackOnly();
}
}
});
}
声明式事务:
- 事务控制代码已经由spring写好.
- 程序员只需要声明出哪些方法需要进行事务控制和如何进行事务控制.
- 声明式事务都是针对于ServiceImpl类下方法的.
- 事务管理器基于通知(advice)的.
- 在spring配置文件中配置声明式事务,配置事务的
传播行为
、只读属性
、隔离级别
、何时回滚
(rollback-for)
两种实现方式
注解方式
@Transactional
的作用范围
- 方法 :推荐将注解使用于方法上,不过需要注意的是:该注解只能应用到 public 方法上,否则不生效。
- 类 :如果这个注解使用在类上的话,表明该注解对该类中所有的 public 方法都生效。
- 接口 :不推荐在接口上使用。
示例代码
使用 @Transactional
注解进行事务管理的示例代码如下:
@Transactional(propagation=propagation.PROPAGATION_REQUIRED)
public void aMethod {
//do something
B b = new B();
C c = new C();
b.bMethod();
c.cMethod();
}
@Test
@Transactional
public void test01() {
Customer dbCustomer = customerService.getCustomerById(2060);
User user = userMapper.selectByPrimaryKey(93);
dbCustomer.setRealName("b");
user.setName("b");
customerMapper.updateByPrimaryKey(dbCustomer);
userMapper.updateByPrimaryKey(user);
throw new NullPointerException();
}
事务管理接口介绍
Spring 框架中,事务管理相关最重要的 3 个接口如下:
PlatformTransactionManager
: (平台)事务管理器,Spring 事务策略的核心。TransactionDefinition
: 事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则)。TransactionStatus
: 事务运行状态。
我们可以把 PlatformTransactionManager
接口可以被看作是事务上层的管理者,而 TransactionDefinition
和 TransactionStatus
这两个接口可以看作是事物的描述。
PlatformTransactionManager
会根据 TransactionDefinition
的定义比如事务超时时间、隔离界别、传播行为等来进行事务管理 ,而 TransactionStatus
接口则提供了一些方法来获取事务相应的状态比如是否新事务、是否可以回滚等等。
配置文件方式
<?xml version="1.0" encoding="UTF-8"?>
<beans >
<!-- 事务管理器 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 数据源 -->
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 通知 -->
<!--配置事务增强,事务如何切入 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 传播行为 -->
<tx:method name="save*" propagation="REQUIRED" />
<tx:method name="insert*" propagation="REQUIRED" />
<tx:method name="add*" propagation="REQUIRED" />
<tx:method name="create*" propagation="REQUIRED" />
<tx:method name="delete*" propagation="REQUIRED" />
<tx:method name="remove*" propagation="REQUIRED" />
<tx:method name="update*" propagation="REQUIRED" />
<tx:method name="find*" propagation="SUPPORTS" read-only="true" />
<tx:method name="select*" propagation="SUPPORTS" read-only="true" />
<tx:method name="get*" propagation="SUPPORTS" read-only="true" />
</tx:attributes>
</tx:advice>
<!-- 切面 -->
<aop:config>
<!-- 配置事务增强以及切入点表达式 -->
<aop:advisor advice-ref="txAdvice"
pointcut="execution(* com.exocr.service..*.*(..))" />
</aop:config>
</beans>
声明式事务中的属性
name=”” 哪些方法需要有事务控制,支持*通配符
readonly=”boolean” 是否是只读事务.
- 如果为 true,告诉数据库此事务为只读事务.数据库优化,会对性能有一定提升,所以只要是查询的方法,建议使用。
- 如果为 false(默认值),事务需要提交的事务.建议新增,删除,修改.
propagation 控制事务传播行为.
isolation=”” 事务隔离级别
rollback-for=”异常类型全限定路径”:当出现什么异常时需要进行回滚
业务示例代码如下:进行插入操作获取返回值,当全部成功后认为新增成功,否则失败。由Spring进行事务回滚。
public int insTbItemDesc(TbItem tbItem, TbItemDesc desc,TbItemParamItem paramItem) throws Exception {
int index =0;
try {
index= tbItemMapper.insertSelective(tbItem);
index+= tbItemDescMapper.insertSelective(desc);
index+=tbItemParamItemMapper.insertSelective(paramItem);
} catch (Exception e) {
e.printStackTrace();
}
if(index==3){
return 1;
}else{
throw new Exception("新增失败,数据还原");
}
}
XA 就是 X/Open DTP 定义的交易中间件与数据库之间的接口规范(即接口函数),交易中间件用它来通知数据库事务的开始、结束以及提交、回滚等。 XA 接口函数由数据库厂商提供。
@Transactional
事务注解原理
面试中在问 AOP 的时候可能会被问到的一个问题。简单说下吧!
我们知道,@Transactional
的工作机制是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理。
多提一嘴:createAopProxy()
方法 决定了是使用 JDK 还是 Cglib 来做动态代理,源码如下:
public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}
.......
}
如果一个类或者一个类中的 public 方法上被标注@Transactional
注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被@Transactional
注解的 public 方法的时候,实际调用的是,TransactionInterceptor
类中的 invoke()
方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务。
Spring AOP 自调用问题
若同一类中的其他没有 @Transactional
注解的方法内部调用有 @Transactional
注解的方法,有@Transactional
注解的方法的事务会失效。
这是由于Spring AOP
代理的原因造成的,因为只有当 @Transactional
注解的方法在==类以外被调用的时候,Spring 事务管理才生效。==
MyService
类中的method1()
调用method2()
就会导致method2()
的事务失效。
@Service
public class MyService {
private void method1() {
method2();
//......
}
@Transactional
public void method2() {
//......
}
}
解决办法就是避免同一类中自调用或者使用 AspectJ 取代 Spring AOP 代理。
@Transactional
的使用注意事项总结
1. `@Transactional` 注解只有作用到 public 方法上事务才生效,不推荐在接口上使用;
2. 避免同一个类中调用 `@Transactional` 注解的方法,这样会导致事务失效;
3. 正确的设置 `@Transactional` 的 rollbackFor 和 propagation 属性,否则事务可能会回滚失败
4. ......