原子性( atomicty)
事务中的全部操作在数据库中是不可分割的,要么全部完成,要么全部不执行。
一致性(consistency)
事务的执行不能破坏数据库数据的完整性和一致性。一致性指数据满足所有数据库的条件,比如字段约束、外键约束、触发器等,事务从一致性开始,以一致性结束。
隔离性( isolation)
事务的执行不受其他事务的干扰,事务执行的中间结果对其他事务是透明的。
持久性(durability)
对于提交事务,系统必须保证该事务对数据库的改变不被丢失,即使数据库出现故障。
注: DBMS一般采用日志来保证事务的原子性、一致性和持久性。
SQL92标准定义了4种隔离级别的事务
大多数数据库系统如oracle的默认隔离级别都是 Read committed,mysql默认为可重复读,InnoDB 和 XtraDB 存储引擎通过多版并发控制(MVCC,Multivesion Concurrency Control)解决了幻读问题,Repeatable read 是 Mysql 默认的事务隔离级别,其中 InnoDB主 要通过使用 MVVC 获得高并发,使用一种被称为 next-key-locking 的策略来避免幻读。
本地事务模型: 事务由本地资源管理器来管理。简单理解就是直接使用JDBC的事务API。
connection.setAutoCommit(false);// 自动提交关闭
//XXXX数据库的增删改查操作
connection.commit(); //提交事务
编程式事务模型: 事务通过JTA以及底层的JTS实现来管理,对于开发人员而言,管理的是“事务”,而非“连接”。简单理解就是使用事务的API写代码控制事务。
示例一、JTA的API编程
UserTransaction txn = sessionCtx.getUserTransaction();
txn.begin();
txn.commit();
示例二、Spring的事务模版
transactionTemplate.execute(
new TransactionCallback
声明式事务: 事务由容器进行管理,对于开发人员而言,几乎不管理事务。简单理解就是加个事务注解或做个AOP切面。
@Transactional(rollbackFor = Exception.class)
public void updateStatus(String applyNo){
cashierApplyMapper.updateStatus(applyNo, CANCEL_STATUS, CANCEL_STATUS_DESC);
}
比较
以Spring+Mybatis+JDBC+Mysql为例,常见的事务类请求的调用链路如下图。请求调用应用服务,应用服务中开启事务并进行业务操作,操作过程中调用Mybatis进行数据库类操作,Mybatis通过JDBC驱动与底层数据库交互。
因此接下来先按Mysql、JDBC、Mybatis、Spring来介绍各层的事务相关知识;最后进行全链路的调用分析。
架构图如下(InnoDB存储引擎):
MySQL事务是由存储引擎实现的,MySQL支持事务的存储引擎有InnoDB、NDB Cluster等,其中InnoDB的使用最为广泛,其他存储引擎如MyIsam、Memory等不支持事务。
Mysql的4个特性中有3个与 WAL(Write-Ahead Logging,先写日志,再写磁盘)有关系,需要通过 Redo、Undo 日志来保证等,而一致性需要通过DBMS的功能逻辑及原子性、隔离性、持久性共同来保证。
假设 F1~F6 是表中字段的名字,1~6 是其对应的数据。后面三个隐含字段分别对应该行的隐含ID、事务号和回滚指针,如下图所示。
具体的更新过程如下:
假如一条数据是刚 INSERT 的,DB_ROW_ID 为 1,其他两个字段为空。当事务 1 更改该行的数据值时,会进行如下操作,如下图所示。
用排他锁锁定该行,记录 Redo log;
把该行修改前的值复制到 Undo log,即图中下面的行;
修改当前行的值,填写事务编号,并回滚指针指向 Undo log 中修改前的行。
如果再有事务2操作,过程与事务 1 相同,此时 Undo log 中会有两行记录,并且通过回滚指针连在一起,通过当前记录的回滚指针回溯到该行创建时的初始内容,如下图所示,这里的undolog不会一直增加,purge thread在后面会进行undo page的回收,也就是清理undo log。
java定义了统一的JDBC驱动API,各数据库厂商按规范实现。jdbc驱动相关包在java.sql包下:
使用示例:
// 创建数据库连接
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/easyflow", "root", "12345678");
// 自动提交设置
connection.setAutoCommit(false);
// 只读设置
connection.setReadOnly(false);
// 事务隔离级别设置
connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
// 创建查询语句
PreparedStatement statement = connection.prepareStatement("update config set cfg_value='1' where id=11111");
// 执行SQL
int num = statement.executeUpdate();
System.out.println("更新行数:" + num);
// 事务提交
connection.commit();
JDBC驱动注册机制
之前需要调用Class.forName或其他方式显式加载驱动,现在有了SPI机制后可不写。
public class DriverManager {
// List of registered JDBC drivers
private final static CopyOnWriteArrayList registeredDrivers = new CopyOnWriteArrayList<>();
private static volatile int loginTimeout = 0;
private static volatile java.io.PrintWriter logWriter = null;
private static volatile java.io.PrintStream logStream = null;
// Used in println() to synchronize logWriter
private final static Object logSync = new Object();
/* Prevent the DriverManager class from being instantiated. */
private DriverManager(){}
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
……
Mybatis核心是提供了sql查询方法、结果集与应用方法及对象之间的映射关系,便于开发人员进行数据库操作。
整体模块如下:
各模块与下面的各子包一一对应:
Mybatis执行的核心类如下:
Mysql的核心入口类为SqlSession,事务相关的操作通过TransactionFactory来处理,可选择使用Spring事务(SpringManagedTransaction)还是内置事务管理。
事务相关的控制处理可见SqlSessionInterceptor类,主要逻辑如下:
源码见下:
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
try {
Object result = method.invoke(sqlSession, args);
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
Throwable unwrapped = unwrapThrowable(t);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
// release the connection to avoid a deadlock if the translator is no loaded. See issue #22
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator
.translateExceptionIfPossible((PersistenceException) unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw unwrapped;
} finally {
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}
常用类为TransacitonTemplate,执行逻辑为:获取事务状态->在事务中执行业务->提交或回滚,源码见下:
@Override
@Nullable
public T execute(TransactionCallback action) throws TransactionException {
Assert.state(this.transactionManager != null, "No PlatformTransactionManager set");
if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);
}
else {
TransactionStatus status = this.transactionManager.getTransaction(this);
T result;
try {
result = action.doInTransaction(status);
}
catch (RuntimeException | Error ex) {
// Transactional code threw application exception -> rollback
rollbackOnException(status, ex);
throw ex;
}
catch (Throwable ex) {
// Transactional code threw unexpected exception -> rollback
rollbackOnException(status, ex);
throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
}
this.transactionManager.commit(status);
return result;
}
}
声明式事务实现原理就是通过AOP/动态代理。
在Bean初始化阶段创建代理对象:Spring容器在初始化每个单例bean的时候,会遍历容器中的所有BeanPostProcessor实现类,并执行其postProcessAfterInitialization方法,在执行AbstractAutoProxyCreator类的postProcessAfterInitialization方法时会遍历容器中所有的切面,查找与当前实例化bean匹配的切面,这里会获取事务属性切面,查找@Transactional注解及其属性值,然后根据得到的切面创建一个代理对象,默认是使用JDK动态代理创建代理,如果目标类是接口,则使用JDK动态代理,否则使用Cglib。
在执行目标方法时进行事务增强操作:当通过代理对象调用Bean方法的时候,会触发对应的AOP增强拦截器,声明式事务是一种环绕增强,对应接口为MethodInterceptor,事务增强对该接口的实现为TransactionInterceptor,类图如下:
事务拦截器TransactionInterceptor在invoke方法中,通过调用父类TransactionAspectSupport的invokeWithinTransaction方法进行事务处理,包括开启事务、事务提交、异常回滚 。
声明式事务有5个配置项,说明如下:
事务传播属性是spring事务模块的一个重要属性。简单理解,他控制一个方法在进入事务时,在外层方法有无事务的场景下,自己的事务的处理策略,如是复用已有事务还是创建新事务。
spring支持的传播属性有7种,如下:
同一个数据源的操作在一个事务内可保证一致,但实际场景中会因为不同事务或不同数据源(不同关系数据库、缓存或远端服务)而导致数据不能强一致。在CAP理论框架下,我们一般是保证可用性、分区容错性,基于BASE理论达到最终一致性。但如何达到数据的最终一致性需要合理设计。
数据库的提交、缓存的更新、RPC的执行、消息的发送的先后顺序
一般我们以数据库数据为准,先数据库提交,再更新缓存或发送消息,通过异步轮询补偿的方式保证异常情况下的最终一致性。
不建议用法:
事务回滚会导致缓存和数据库不一致
事务回滚会导致消息接收方收到的数据状态错误
建议用法:
先更新数据库,事务提交后再更新缓存或发送消息
通过异步异常重试或批处理同步来保证数据的最终一致性
核心交易以数据库数据为准
系统健壮性增强,但编程模型复杂一些
如果事务中有耗时长的SQL或有RPC操作可能会导致事务时间变长,会导致并发量大的情况下数据库连接池被占满,应用无法获取连接资源,在主从架构中会导致主从延时变大。
建议事务粒度尽量小,事务中尽量少包含RPC操作。事务尽量放在下层。
不建议用法
建议用法
这种方式需要应用程序保证多个事务操作的最终一致性,一般可通过异常重试来实现。