目录
一、初识事务
二、mysql数据库的事务处理
三、JDBC封装mysql的事务处理
四、spring中的事务处理
五、分布式事务
一、初识事务
事务其实是针对的具体的数据库操作,以电商平台中的提交订单、扣减库存为例,这里会同时操作两张表写订单表、修改库存表:如果提交订单成功,但扣减库存失败,就会导致订单中的商品数大于库存的实际商品数,导致部分订单因为没有库存而无法出库;如果提交订单失败,但扣减库存成功,会导致实际库存大于0,但库存表显示为0的情况,导致实际有库存,但用户无法下单。这时就必须借助事务控制,把提交订单、扣减库存两个操作封装为一个原子操作,即要么两个操作都成功,否则对其中一个执行成功的操作进行回滚。
事务控制的本质是“并发控制”的最小单位,在“同一个数据库中”,要求对一批记录进行 insert、update、delete操作,要么全部成功,否则回滚已经执行的insert、update、delete操作,是一个不可分割的工作单位。
上述内容描述了事务的两个注意点:
1、事务处理是针对的数据库,或者说传统的关系型数据库,如msyql、oracle等。对于nosql数据的事务处理,只能通过自己写代码实现类似的“rollback”操作。
2、事务处理是在同一个数据库中。跨数据库的事务处理,其实就是我们平常说的“分布式事务”,归根结底还是要具体到每个数据库中分别进行事务处理。
事务的4大特性-ACID:
A、原子性(Atomicity):事务是数据库的逻辑工作单位,事务中包括的所有操作要么全做,要么全不做。
B、一致性(Consistency):事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。
C、隔离性(Isolation):一个事务的执行被其他事务干扰的程度,可以设置不同的隔离级别来完成。4种隔离级别:
RU(Read Uncommitted最低,什么都不做,事务之间相互干扰);
RC(Read Committed 只有在事务提交后,其更新结果才会被其他事务看见,解决脏读);
RR(Repeated Read 在一个事务中,对于同一份数据的读取结果总是相同的,解决脏读、不可重复读)mysql事务的默认隔离级别(通过执行select @@tx_isolation;查看,也可以更改);
Serialization(所有事务都按照串行化执行,隔离级别最高,但并发性最差)。
D、持久性(Durability)一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。
事务与锁的关系:事务的隔离级性,是通过对表或记录进行加锁实现的,通过控制锁的粒度来实现不同的隔离级别(RU不加锁,Serialization对整个过程进行加锁)。
事务与线程的关系:一个事务属于一个线程,一个线程里可以有多个事务,所谓事务的传播性,指的是采用spring管理事务时,在同一个线程中如果遇到多个事务切面,是否新开事务的策略,spring事务处理部分再详细讲解。也就是说所谓的事务传播性,其实是spring对数据库事务处理的一种封装。
二、mysql数据库的事务处理
这里以msyql数据库的事务处理为例进行分析。
首先mysql默认情况下是 事务是“自动提交的”,可以通过执行set autocommit = 0;改为手动提交,也就是开启事务,当然也可以使用start transction或者begin 开启事务,如果事务中中一条语句执行失败,则执行rollback命令 进行回滚,否则执行commit提交事务:
1、begin;//开启事务
2、执行多条insert、update、delete语句
3、判断结果执行commit或者rollback
If(error)
commit;
else
rollback;
对于rollback还可以设置回滚点,默认是全部回滚,这里就不详细讲解。
在java中,其实是对这个过程的一个封装,有一个try catch 对整个过程进行包裹,如果出现异常,就执行rollback,否则执行commit。下面来看下jdbc事务处理:
三、JDBC封装mysql的事务处理
JDBC的事务处理,实际上是对mysql事务处理的封装,本质上最终还是会解释成上述mysql事务处理语句,在msyql服务端执行。上述msyql对应的事务处理命令,会被一些java代码代替:
public class JDBCTrans { public void test1(){ Connection conn = null; PreparedStatement ps1 = null; PreparedStatement ps2 = null; Savepoint sp = null; try{ Class.forName("com.MySQL.jdbc.Driver"); conn = DriverManager.getConnection("jdbc:mysql:///xxxxx", "root", "root"); conn.setAutoCommit(false);//开启事务 ps1 = conn.prepareStatement("update xxxx set xxxx where id=?"); ps1.setInt(1, 1); ps1.executeUpdate(); int i = 1/0;//模拟异常 ps2 = conn.prepareStatement("update xxxx set xxxx where id=?"); ps2.setInt(1, 2); ps2.executeUpdate(); conn.commit(); //统一提交事务 }catch (Exception e) { e.printStackTrace(); try { conn.rollback(); //事务回滚 } catch (SQLException e1) { e1.printStackTrace(); } }finally{ // close connect source } } }
Jdbc事务处理代码:
1、conn.setAutoCommit(false); 开启事务类似 最终翻译成mysql中的 “set autocommit = 0;”命令。
2、conn.commit();统一提交事务,最终翻译成mysql中的 “commit;”命令。
3、conn.rollback();事务回滚,最终翻译成msyql中的“rollback;”命令。
四、spring中的事务处理
我们可以看到JDBC的事务处理方式非常繁琐,开启事务、提交事务、回滚事务这些代码与数据库连接、数据库操作、异常处理等代码逻辑混在一起,难以复用和维护。Spring为了简化这些操作,提供了统一的事务处理解决方案。
Spring中的事务处理有两种方式:声明式事务处理、编程式事务处理。其本质是对jdbc事务处理的再一次封装。相对于编程式事务处理,声明式事务处理的好处是,业务代码与事务处理配置完全分离。
Spring事务处理演进过程:
1、编程式:直接通过TransactionDefinition、PlatformTransactionManager、TransactionStatus 通过编程的方式完成事务处理。这种代码的写法有点类似直接使用jdbc做事务处理:
private PlatformTransactionManager txManager; public void testPlatformTransactionManagerForLowLevel1() { DefaultTransactionDefinition def = new DefaultTransactionDefinition(); def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED); def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); TransactionStatus status = txManager.getTransaction(def); Connection conn = xxxxxx;//jdbc Connection try { PreparedStatement pstmt = conn.prepareStatement("update xxxxxx"); pstmt.setString(1, "test"); pstmt.execute(); conn.prepareStatement("DELETE xxxxxx").execute(); txManager.commit(status); } catch (Exception e) { status.setRollbackOnly(); txManager.rollback(status); } finally { //释放资源; } }
Xml 配置里需要定义txManager对象,可以使用DataSourceTransactionManager(是PlatformTransactionManager的孙子实现类)。这种方式几乎不会有人去使用。
2、编程式:使用TransactionTemplate,其实是对第一种方式的改进,通过过观察第一种方式,可以发现事务处理,都是类似的模式(开启事务,提交事务,回滚事务,异常处理,资源释放),可以把这些内容抽象成一个模板,而不用每次都去编写类似的代码。TransactionTemplate使用示例:
private PlatformTransactionManager txManager; public TransactionTemplate getDataSourceTransactionManager() { return new TransactionTemplate(this.txManager); } public void doTrans(){ TransactionTemplate template = getDataSourceTransactionManager(); template.execute(new TransactionCallback() { @Override public Object doInTransaction(TransactionStatus status) { try { //一系列需要在同一个事务中处理的操作 //插入表 //修改表 } catch (Exception e) { status.setRollbackOnly(); throw new RuntimeException(e); } return status; } }); }
Xml配置跟第一种方式一样,需要定义txManager对象,可以使用DataSourceTransactionManager(是PlatformTransactionManager的孙子实现类):
<bean id=" txManager " class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource"> <ref bean="dataSource" /> //数据源配置省略 </property> </bean>
Java代码相对于第一种方式简洁了很多,目前在一些老系统中,还有很多事物处理是采用的TransactionTemplate。
可以看到编程式事务处理,事务代码与业务代码严重耦合。接下来的声明式事务处理,主要就是解决代码耦合性问题,整体上更加简洁。
3、声明式:TransactionInterceptor,对每个需要事务处理的对象,通过配置的方式创建代理对象:
<bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor"> <property name="transactionManager" ref="txManager"/> <property name="transactionAttributes"> <props> <prop key="transfer">PROPAGATION_REQUIRED</prop> //事务传播性配置 </props> </property> </bean> <bean id="userService" class="com.xxx.UserServiceImpl"> </bean> <bean id="userServiceTrans" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="target" ref="userService"/> <property name="interceptorNames"> <list> <idref bean="transactionInterceptor"/> </list> </property> </bean>
4、声明式:TransactionProxyFactoryBean,这种方式是对第三种方式的优化,第三种方式需要为每一个需要做事务处理的对象创建三个bean(transactionInterceptor、userService、userServiceTrans)。TransactionProxyFactoryBean可以把transactionInterceptor和userServiceTrans合二为一:
<bean id="userService" class="com.xxx.UserServiceImpl"> </bean> <bean id="userServiceTrans" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> <property name="target" ref="userService"/> <property name="transactionManager" ref="txManager"/> <property name="transactionAttributes"> <props> <prop key="transfer">PROPAGATION_REQUIRED</prop> </props> </property> </bean>
第三、四种都是声明式事务处理,这两种方式本质上是一样的,基于拦截器 责任链模式只是这里的责任链只有1个,在责任链只有1个的时候,其实就退化为代理模式。总所周知,spring里处理代理模式有个高大上的名字-----spring aop。接下来讲解的两种事务处理,都是基于spring aop的方式,也是目前最为推荐的事务处理方式。
5、声明式:spring aop <tx>命名空间,xml配置方式如下:
<tx:advice id="txAdvice" transaction-manager="txManager"> <tx:attributes> <tx:method name="add*" propagation="REQUIRED" isolation="READ_COMMITTED"/> <tx:method name="*" propagation="REQUIRED" isolation="READ_COMMITTED" read-only="true"/> </tx:attributes> </tx:advice> <aop:config> <aop:pointcut id="myPointcut" expression="execution(* com.xxx.xxx.*.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="bankPointcut"/> </aop:config>
<tx:advice>用于定义通知,并在aop配置<aop:config>中引用这个通知,<aop:config>是标准的Spring aop配置方式,详细了解spring aop请点击这里http://moon-walker.iteye.com/blog/2381532。
这里重点说下<tx:method name="*">,这里的name属性用于指定方法名,可以使用”*”进行前缀、后缀匹配。另外<tx:method>还有几个重要的属性:
propagation:指定spring事务的传播性,可选配置见本节末尾。
isolation:指定事务的隔离级别,文章开头已经讲解。
read-only:表示是否为只读事务。
<tx>命名空间的配置方式,可以实现与业务代码完全解耦,没有任何侵入性。而且配置简单,在事务处理类比较集中的情况下(比如事务处理类都在同一个目录下),强烈推荐使用。
6、声明式:@Transactional注解,这种方式本质上也是基于spring aop,这种在方法上加注解的方式,可以更加细粒度的控制事务,如果需要事务处理的类比较分散,采用第五种方式不是很好统一制定切面的时候,采用@Transactional注解,也不失为一种好方式,xml配置很简单,启动注解即可:
<tx:annotation-driven transaction-manager="txManager"/>
在需要进行事务处理的方法上进行注解:
@Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.READ_COMMITTED, readOnly = false,timeout = 100) public void transfer(Object bean) { //一系列的在同一个事务中进行的操作 //修改 //插入 //删除 }
@Transactional注解支持的参数与前面<tx:method>的支持的属性完全相同,除了这里提到的propagation、Isolation、readOnly、timeout 还有rollbackFor、noRollbackFor(指定回滚异常)等,具体可以打开Transactional的注解定义看下源码。
这里把spring支持的propagation传播行为,及说明列举如下:
a、PROPAGATION_REQUIRED 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。
b、PROPAGATION_SUPPORTS 支持当前事务,如果当前没有事务,就以非事务方式执行。
c、PROPAGATION_MANDATORY 使用当前的事务,如果当前没有事务,就抛出异常。
d、PROPAGATION_REQUIRES_NEW 新建事务,如果当前存在事务,把当前事务挂起。
e、PROPAGATION_NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
f、PROPAGATION_NEVER 以非事务方式执行,如果当前存在事务,则抛出异常。
g、PROPAGATION_NESTED 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与 PROPAGATION_REQUIRED 类似的操作。
spring默认为PROPAGATION_REQUIRED,假设m1、m2、m3三个方法都存在事务处理,在同一个线程内先执行m1调用m2再调用m3,采用PROPAGATION_REQUIRED配置,会在同一个事务中进行。
tips:建议使用第五、六两种spring声明式事务处理方式,这里对前面四种的讲解只是为了描述spring事务处理演进过程。其实第五、六两种事务处理的区别,就是spring aop基于xml配置与基于注解的区别。
五、分布式事务
最后简单说下分布式事务(不是本次总结的重点),spring支持的jta的分布式事务处理(不要跟spring jpa搞混了),它包括事务管理器(Transaction Manager)和一个或多个支持 XA 协议的资源管理器 ( Resource Manager ) 两部分,本质上,是把事务处理分配到各个数据库节点上,有一个统一的调度者同步各个节点的数据库操作状态,再统一通知各个节点进行事务提交。
基于XA协议的分布式实现,又分为两阶段提交和三阶段提交。但都无法很好的解决一致性问题,同时由于存在等待所有节点同步状态等问题,资源开销也比较大。
另外也可以基于zookeeper实现分布式式事务处理,可以很好的解决一直性问题,zookeeper底层主要通过实现Paxos算法来实现分布式的一直性问题。但也存在开销大的问题。
目前在高并发环境下,主流的做法是通过各种补偿机制实现最终一致性,比如通过mq消息通知延迟处理,通过加日志,手工处理,或者定时worker处理等。其实就是根据具体业务对一致性的容忍程度,采取补偿措施,这些操作已经不是传统意义上的事务处理了,也就是所谓的“柔性事务”处理。如果继续扩展的话还会扯到比较火热的BASE理论、CAP原则,其实都是一些理论上的东西,这里就不再进行深入扩展。