数据库事务原理详解
一、 从Spring事务配置说起
1.1、Spring事务的基础配置:
<aop:aspectj-autoproxy proxy-target-class="true"/>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
<!-- 配置事务传播特性 -->
<tx:advice id="transactionAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="add*" propagation="REQUIRED" rollback-for="Exception,RuntimeException,SQLException"/>
<tx:method name="remove*" propagation="REQUIRED" rollback-for="Exception,RuntimeException,SQLException"/>
<tx:method name="modify*" propagation="REQUIRED" rollback-for="Exception,RuntimeException,SQLException"/>
<tx:method name="login" propagation="NOT_SUPPORTED"/>
<tx:method name="query*" read-only="true"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="transactionPointcut" expression="execution(public * com..*.service..*Service.*(..))"/>
<aop:advisor advice-ref="transactionAdvice" pointcut-ref="transactionPointcut"/>
</aop:config>
Spring事务管理基于AOP实现,主要作用是统一封装非功能性需求。
1.2、通过 PlatformTransactionManager使用
不推荐使用
1.3、通过TransactionTemplate 使用事务
1.4、声明式事务
二、 事务的基本概念
事务(Transaction)是访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。
特点:事务是恢复和并发控制的基本单位。事务应该具有4个属性:原子性、一致性、隔离性、持久性。这4个属性通常称为ACID特性。
- 原子性:一个事务是一个不可分割的工作单位,事务中包括的诸多操作,要么都做,要么都不做。
- 一致性:事务必须使数据库从一个一致性状态变到另一个一致性状态。
- 隔离性:并发执行的各个事务之间不能互相干扰。
- 持久性:事务一旦提交,它对数据库中数据的改变就应该是永久性的。
三、 事务的基本原理
Spring事务的本质其实就是数据库对事务的支持。对于纯JDBC操作数据库,想要用到事务,可以按照以下步骤进行:
- 获取连接:Connection con = DriverManager.getConnection();
- 开启事务:con.setAutoCommit(true/false);
- 执行CRUD操作
- 提交事务/回滚事务:con.commit(); / con.rollback();
- 关闭连接:con.close()。
问题1:Spring是如何在我们书写CRUD操作之前和之后开启事务和关闭事务的呢?解决了这个问题,也就可以从整体上理解Spring的事务管理实现原理。下面简单地介绍下,以注解方式为例:
- 在配置文件中开启注解驱动,在相关的类和方法上通过注解@Transactional标识。
- Spring在启动的时候会解析生成相关的Bean,这时候会查看拥有相关注解的类和方法,并且为这些类和方法生成代理,根据@Transactional的相关参数进行相关配置注入,这样就在代理中为我们把相关的事务处理掉了(开启正常提交事务、异常回滚事务)。
- 真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。
四、 Spring事务的传播属性
所谓Spring事务的传播属性,就是定义在多个事务同时存在的时候,Spring应该如何处理这些事务的行为。这些属性在TransactionDefinition中定义,具体常量的解释如下表所示:
NO. | 事务传播行为类型 | 说明 |
---|---|---|
1 | PROPAGATION_REQUIRED(默认使用) | 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。 |
2 | PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 |
3 | PROPAGATION_MANDATORY | 使用当前的事务,如果当前没有事务,就抛出异常。 |
4 | PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起。 |
5 | PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 |
6 | PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 |
7 | PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。 |
五、 数据库事务隔离级别
数据库事务隔离级别如下表所示:
NO. | 隔离级别 | 值 | 导致的问题 |
---|---|---|---|
1 | Read-Uncommitted | 0 | 导致脏读 |
2 | Read-Committed | 1 | 避免脏读,允许不可重复读和幻读 |
3 | Repeatable-Read | 2 | 避免脏读,不可重复读,允许幻读 |
4 | Serializable | 3 | 串行化读,事务只能一个一个执行,避免了脏读、不可重复读、幻读。执行速度慢 |
- 脏读:一个事务对数据进行了增、删、改,但未提交,另一个事务可以读取到未提交的数据。如果第一个事务这时候回滚了,那么第二个事务就读到了脏数据。
- 不可重复读:一个事务中发生了两次读操作,在第一次读操作和第二次读操作之间,另外一事务对数据进行了修改,这时候两次读取的数据是不一致的。
- 幻读:第一个事务对一定范围的数据进行了批量修改,第二个事务在这个范围内增加了一条数据,这时候第一个事务就会丢失对新增数据的修改。
数据库事务隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。大多数数据库(比如SQLServer和Oracle)事务默认隔离级别的Read-Commited,少数数据库(比如MySQL InnoDB)事务默认隔离级别为Repeatable-Read。
六、 Spring中的事务隔离级别
Spring中的事务隔离级别如下表所示。
NO. | 常量 | 解释 |
---|---|---|
1 | ISOLATION_DEFAULT | 这是PlatformTransactionManager默认的事务隔离级别(数据库默认的) |
2 | ISOLATION_READ_UNCOMMITTED | 这是最低的事务隔离级别,它允许另外一个事务看到这个事务未提交的数据。这种隔离级别会产生脏读、不可重复读和幻读。 |
3 | ISOLATION_READ_COMMITTED | 保证一个事务修改的数据提交后才能被另一个事务读取。另一个事务不能读取该事务未提交的数据。 |
4 | ISOLATION_REPEATABLE_READ | 可防止脏读、不可重复读,但是可能出现幻读。 |
5 | ISOLATION_SERIALIZABLE | 这是花费最高代价但是最可靠的事务隔离级别,事务被处理为顺序执行 |
七、 事务的嵌套
分析嵌套事务场景来深入理解Spring事务传播机制。
假设外部事务ServiceA的Method A()调用内部事务ServiceB的Method B()。
- PROPAGATION_REQUIRED:没事务创建事务,有事务就使用当前事务。
- PROPAGATION_REQUIRES_NEW:挂起当前事务后创建新事务,当创建的事务执行完后挂起的事务才会继续执行。
- PROPAGATION_SUPPORTS:有事务就使用事务,没事务就以没事务方式执行。
- PROPAGATION_NESTED:嵌套事务最有价值的地方,它起到了分支执行的效果。
void MethodA() {
try{
ServiceB.MethodB();
}catch (SomeException) {
// 执行其他事务,如ServiceC.MethodC();
}
}
- 捕获异常,执行异常分支逻辑
- 外部事务回滚/提交
- 另外三种事务传播属性基本用不到,在此不做分析。
八、 Spring事务API架构图
Spring事务API架构图如下图所示:
九、 浅谈分布式事务
理论依据就是CAP理论,即Consistency(一致性)、Availability(可用性)和Partition Tolerance(分区容错性)。为了可用性和分区容错性,往往放弃强一致性,转而追求最终一致性。
理解数据的一致性:
- 强一致性:当更新操作完成之后,任何多个后续进程或者线程的访问都会返回最新的值。
- 弱一致性:系统并不保证后续进行或者线程的访问都会返回最新的值。在数据写入成功之后,系统不承诺立即可以读到最新写入的值,也不会承诺多久之后可以读到。
- 最终一致性:弱一致性的特定形式。系统保证在没有后续更新的前提下,最终返回上一次更新操作的值。在没有故障发生的前提下,不一致窗口的时间主要受通信延迟、系统负载和副本个数影响。
事务失效的根本原因:动态代理(AOP)
@Log4j2
@Service
public class TestService {
@Autowired
private OrderMapper mapper;
private TestService proxy;
@Autowired
private ApplicationContext ac;
//被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。
// PostConstruct在构造函数之后执行,init()方法之前执行。PreDestroy()方法在destroy()方法知性之后执行
@PostConstruct
public void init(){
proxy = ac.getBean(TestService.class);
}
@Transactional
public void parent(){
log.info("====调用parent()==========");
try{
//this.child();
//解决方案1:从AOP上下文中获取当前代理对象
TestService service = (TestService)AopContext.currentProxy();
service.child();
//解决方案2:从Spring上下文中获取代理对象
proxy.child();
}catch (Exception e){
e.printStackTrace();
}
Order order = new Order();
order.setTitle("测试定单:parent");
order.setOrderno("parent");
order.setAmount(100);
order.setStatus(0);
mapper.save(order);
//log.info("保存成功 id:[{}]",order.getId());
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void child(){
log.info("====调用child()==========");
Order order = new Order();
order.setTitle("测试定单:child");
order.setOrderno("child");
order.setAmount(100);
order.setStatus(0);
mapper.save(order);
//log.info("保存成功 id:[{}]",order.getId());
int a = 1/0;
}
}