一、本地事务
1、事务的基本性质
数据库事务的几个特性:原子性(Atomicity)、一致性(Consistency)、隔离性或独立性(isolation)、持久性(Durability),简称就是 ACID。
- 原子性:一系列的操作整体不可拆分,要么同时成功,要么同时失败。
- 一致性:数据在事务的前后,业务整体一致。 转账:A:1000; B:1000; 转 200 事务成功; A:800; B:1200
- 隔离性:事务之间互相隔离
- 持久性:一旦事务成功,数据一定会落盘在数据库。 在以往的单体应用中,我们多个业务操作使用同一条连接操作不同的数据表,一旦有异常,我们可以很容易的整体回滚。
Business:我们具体的业务代码
Storage:库存业务代码,扣减库存
Order:订单业务代码,保存订单
Account:账号业务代码,减账户余额
比如买东西业务,扣库存,下订单,账户扣款,是一个整体,必须同时成功或失败。
本地事务的适用非常简单,在方法上加上@Transactional
注解就可以了。
2、事务的隔离级别
数据库事务的隔离级别有4种,由低到高分别为Read uncommitted 、Read committed 、Repeatable read 、Serializable 。而且,在事务的并发操作中可能会出现脏读,不可重复读,幻读。下面通过事例一一阐述它们的概念与联系。
Read uncommitted#
读未提交,顾名思义,就是一个事务可以读取另一个未提交事务的数据。 事例:老板要给程序员发工资,程序员的工资是3.6万/月。但是发工资时老板不小心按错了数字,按成3.9万/月,该钱已经打到程序员的户口,但是事务还没有提交,就在这时,程序员去查看自己这个月的工资,发现比往常多了3千元,以为涨工资了非常高兴。但是老板及时发现了不对,马上回滚差点就提交了的事务,将数字改成3.6万再提交。
分析:实际程序员这个月的工资还是3.6万,但是程序员看到的是3.9万。他看到的是老板还没提交事务时的数据。这就是脏读。
那怎么解决脏读呢?Read committed!读提交,能解决脏读问题。
Read committed 读提交,顾名思义,就是一个事务要等另一个事务提交后才能读取数据。
事例:程序员拿着信用卡去享受生活(卡里当然是只有3.6万),当他买单时(程序员事务开启),收费系统事先检测到他的卡里有3.6万,就在这个时候!!程序员的妻子要把钱全部转出充当家用,并提交。当收费系统准备扣款时,再检测卡里的金额,发现已经没钱了(第二次检测金额当然要等待妻子转出金额事务提交完)。程序员就会很郁闷,明明卡里是有钱的…
分析:这就是读提交,若有事务对数据进行更新(UPDATE)操作时,读操作事务要等待这个更新操作事务提交后才能读取数据,可以解决脏读问题。但在这个事例中,出现了一个事务范围内两个相同的查询却返回了不同数据,这就是不可重复读。 那怎么解决可能的不可重复读问题?Repeatable read !
Repeatable read# 重复读,就是在开始读取数据(事务开启)时,不再允许修改操作
事例:程序员拿着信用卡去享受生活(卡里当然是只有3.6万),当他买单时(事务开启,不允许其他事务的UPDATE修改操作),收费系统事先检测到他的卡里有3.6万。这个时候他的妻子不能转出金额了。接下来收费系统就可以扣款了。
分析:重复读可以解决不可重复读问题。写到这里,应该明白的一点就是,不可重复读对应的是修改,即UPDATE操作。但是可能还会有幻读问题。因为幻读问题对应的是插入INSERT操作,而不是UPDATE操作。
什么时候会出现幻读? 事例:程序员某一天去消费,花了2千元,然后他的妻子去查看他今天的消费记录(全表扫描FTS,妻子事务开启),看到确实是花了2千元,就在这个时候,程序员花了1万买了一部电脑,即新增INSERT了一条消费记录,并提交。当妻子打印程序员的消费记录清单时(妻子事务提交),发现花了1.2万元,似乎出现了幻觉,这就是幻读。
那怎么解决幻读问题?Serializable!
Serializable 序列化# Serializable 是最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用。
值得一提的是:大多数数据库默认的事务隔离级别是Read committed,比如Sql Server , Oracle。Mysql的默认隔离级别是Repeatable read。 在代码中我们可以指定事务的隔离级别:
@Transactional(isolation=Isolation.READ_COMMITED)
3、事务的传播行为
1、支持当前事务: TransactionDefinition.PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。 TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。 TransactionDefinition.PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)。
2、不支持当前事务: TransactionDefinition.PROPAGATION_REQUIRED_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。 TransactionDefinition.PROPAGETION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。 TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
3、其他 TransactionDefinition.PROPAGATION_NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。
@Transactional // a事务的所有设置就传播到了和他共用一个事务的方法。
public void a() {
// b,c是否和a共用一个事务?
b(); // b需要一个事务,这时b和a共用一个事务,即在a事务里边
c(); // c是一个新事务
int i = 10 / 0; // 哪个会回滚呢?
}
@Transactional(propagation=Propagation.REQUIRED)
public void b() {
}
@Transactional(propagation=Propagation.REQUIRED_NEW)
public void c() {
}
如果上边的代码 int i = 10 / 0;执行,哪个会回滚呢? 由于b和a是在同一个事务里边,所以,两个都会回滚,而c是一个新事物,所以,不会回滚。
我们还可以给事务添加超时时间 @Transactional(timeout =20),如果共用事务,则子事务的设置就不起作用了
,会用父类的事务。
@Transactional(timeout =20) // a事务的所有设置就传播到了和他共用一个事务的方法。
public void a() {
// b,c是否和a共用一个事务?
b(); // b需要一个事务,这时b和a共用一个事务,即在a事务里边
c(); // c是一个新事务
int i = 10 / 0; // 哪个会回滚呢?
}
// b这里设置的超时时间不起作用,因为它用的是a的事务,所以,超时时间和a的一样
@Transactional(propagation=Propagation.REQUIRED, timeout =2 )
public void b() {
}
@Transactional(propagation=Propagation.REQUIRED_NEW, , timeout =30)
public void c() {
}
4、SpringBoot事务关键点
- 事务的自动配置 TransactionAutoConfiguration
事务的坑 在同一个类里面,编写两个方法,内部调用的时候,会导致事务设置失效。原因是没有用到代理对象的缘故。
@Transactional(timeout =20) // a事务的所有设置就传播到了和他共用一个事务的方法。
public void a() {
// b,c做任何设置都没用,都是和a共用一个事务,事务最大的一个特性就是代理
// b,c是否和a共用一个事务?
b(); // b需要一个事务,这时b和a共用一个事务,即在a事务里边
c(); // c是一个新事务
int i = 10 / 0; // 哪个会回滚呢?
}
// b这里设置的超时时间不起作用,因为它用的是a的事务,所以,超时时间和a的一样
@Transactional(propagation=Propagation.REQUIRED, timeout =2 )
public void b() {
}
@Transactional(propagation=Propagation.REQUIRED_NEW, , timeout =30)
public void c() {
}
事务使用代理对象来控制的,上边的b,c和a是相同的对象,同一个对象内事务方法互调默认失败,原因是绕过了代理对象,
解决: 1)、导入spring-boot-starter-aop,引入了 aspectj 2)、@EnableTransactionManagement(proxyTargetClass=true) 3)、@EnableAspectJAutoProxy(exposeProxy=true);开启 aspectj 动态代理功能。以后所有的动态代理都是 aspectj 创建(即使没有接口也可以创建动态代理),对外暴露代理对象。 4)、本类互调用调用对象(OrderServiceImpl obj = AopContext.currentProxy()) 如果有本类互调的可以使用上边的解决方案。
5、本地事务问题#
在提交订单方法上,我们加了本地事务 @Transactional,当订单创建失败时,会自动回滚相关的数据,但是该方法包含了远程调用锁库存等服务,如何回滚远程的锁库存数据是一个问题;还有一个问题,当远程锁库存成功,但是由于网络等问题,响应超时了,这时还以为锁库存失败了,订单就会自动回滚,这就会出现一个很严重的问题,订单回滚了,而锁定的库存没有回滚。
标签:事务,提交,Transactional,回滚,程序员,本地,void,分布式 From: https://blog.51cto.com/u_15993308/6427792