数据库并发控制
当有多个查询需要在同一时刻修改数据,都会产生并发控制问题。解决并发控制问题需要借助锁。大多数情景,mysql内部的锁管理都是透明的。
读写锁
在处理并发读或者写时,可以通过实现一个由两种类型的锁组成的锁系统来解决问题。这两种类型的锁通常被称为共享锁和排他锁,也叫读锁和写锁。读锁是共享的,多个客户端可以同一时刻读取同一个资源;写锁是排他的,一个写锁会阻塞其它的写锁和读锁,确保在既定时间内,只有一个用户能执行写入,并防止其它用户读取正在写入的同一资源。
锁粒度
表锁(table lock)
表锁是mysql中最基本、开销最小的锁策略。它会锁定整张表。一个用户在对表进行写操作前,需要先获得写锁,这会阻止其它用户对该表的所有读写操作。
行锁(row lock)
行级锁可以最大程度地支持并发,同时也引入最大的锁开销。
事务的定义
事务是指一组原子性的sql查询,如果数据库引擎能够成功地对数据库应用该组查询的全部语句,那么就执行该组查询。如果其中任意一条语句因某种原因无法执行,那么所有语句都不会执行。也就是说,事务中的sql语句,要么全部执行成功,要么全部执行失败。
以mysql为例,有两种途径开启事务
- 显式地开启事务。START TRANSACTION; do something; COMMIT;
- 关闭自动提交。set AUTOCOMMIT=0; 在自动提交模式下,每一条sql都被当作一个事务来执行提交。如果关闭了自动提交,那么所有查询都处于一个事务中,直到显式地执行COMMIT或ROLLBACK,事务结束。
JDBC开启数据库事务通常是这样
Connection conn = DriverManager.getConnection(...);
try{
con.setAutoCommit(false);
Statement stmt = con.createStatement();
//do some query
con.commit();
}catch(Exception e){
con.rollback();
}finally{
con.close();
}
有的驱动支持设置保存点,可以更好地控制回滚操作。创建一个保存点意味着稍后只需要返回到这个点,而非你事务的开头。
conn.setAutoCommit(false);
Statement stmt = conn.createStatement();
stmt.executeUpdate(command1);
Savepoint svpt = conn.setSavepoint();
stmt.executeUpdate(command2);
if (...) conn.rollback; // 回滚commond2的影响
...
conn.commit;
事务的属性
原子性
事务是不可分割的单元,要么全部执行成功提交,要么全部失败回滚,不存在部分成功。
一致性
数据库始终从一个一致性状态转换到另一个一致性状态。典型的场景是银行转账,从A账户转账给B账户,包含两步操作:A扣款,B增款。一致性是指,如果这两步操作在同一个事务中,那么就不存在A扣款成功,而B增款失败的情况,也即系统的总余额保持不变。
隔离性
事务在提交之前,对其它事务的可见性程度。详见后面对事务隔离级别的介绍。
持久性
事务一旦提交,其所做更改永久保存在数据库中。
事务隔离级别
SQL标准定义了事务隔离级别
READ UNCOMMITTED
一个事务能够读取到另一个事务中未提交的修改。这被称为脏读。
READ COMMITTED
oracle的默认隔离级别。一个事务,只能看到其它事务已提交的修改。能够避免脏读,但是无法避免不可重复读。也即同一个事务中多次查询同样的记录,查询结果不一样,因为其它事务在期间提交了更新操作被此事务感知到。
REPEATED READ
mysql的默认隔离级别。保证同一条记录在一个事务的多次查询结果中一致。避免脏读和不可重复读,但是不能避免幻读。因为事务依然能感知到其它事务提交的插入操作,导致此事务在多次执行同一个范围查询时,产生幻行。
SERIALIZABLE
串行化,最高隔离级别,所有事务都串行执行,避免脏读、不可重复读、幻读。很少用。
jdbc可以通过java.sql.Connection#setTransactionIsolation设置事务隔离级别。
mysql设置事务隔离级别的方式如下
SET [GLOBAL | SESSION] TRANSACTION ISOLATION LEVEL
{
READ UNCOMMITTED
| READ COMMITTED
| REPEATABLE READ
| SERIALIZABLE
}
-- 不带[GLOBAL | SESSION]:为下一个事务设置隔离级别
-- GLOBAL:设置全局的后续事务隔离级别,
-- SESSION:设置当前会话后续事务的隔离级别
隐式和显式锁定
InnoDB存储引擎会根据事务隔离级别在需要的时候自动加锁,不需要用户干预,这称为隐式锁定。InnoDB也支持通过特定的语句显式锁定
select ... lock in share mode
select ... for update
mysql也支持lock tables 和 unlock tables语句,这是在数据库服务层实现的,和存储引擎无关。它们有各自的用途,但是并不能替代事务处理。如果应用需要用到事务,还是应该选择事务性存储引擎。
死锁
死锁是指两个或多个事务互相持有对方想锁定的资源的情形。
# 事务1
start transaction;
update consumer set name = 'John' where id = 1;
update consumer set name = 'Jim' where id = 2;
# 事务2
start transaction;
update consumer set name = 'Jim' where id = 2;
update consumer set name = 'John' where id = 1;
如果两个事务都执行了各自的第一条update语句,更新了一行数据,同时锁定了改行数据。接着,两个事务开始执行第二条update语句,发现已经被对方上锁了,于是两者都在等待对方释放锁,却又持有对方需要的锁,这就陷入了僵局。
为了避免死锁,数据库系统实现了各种死锁检测和死锁超时机制。InnoDB引擎处理死锁的方法是,将持有最少行级排他锁的事务进行回滚。
事务日志
事务日志可以提高事务的执行效率。存储引擎在修改表数据的时候,只需修改其内存拷贝,再把修改行为持久化到磁盘的事务日志上,不用每次都把修改的数据本身持久化到磁盘。事务日志采用的是追加的方式,因此写日志的操作是磁盘上一小块区域的顺序I/O,而不像随机I/O需要在磁盘的多个地方移动磁头,所以采用事务日志相对来说快得多。事务日志持久化之后,内存中被修改的数据在后台可以慢慢地刷回到磁盘。大多数存储引擎都是这么实现的。
多版本并发控制(MVCC)
可以认为MVCC是行级锁的一个变种,它在很多情况下都避免了加锁操作,因此开销更低。实现非阻塞的读操作,写操作也只锁定必要的行。
MVCC的实现是通过保存数据在某个时间点的快照来实现的。也就是说,不管需要执行多长时间,一个事务内看到的数据都是一致的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。
不同存储引擎的MVCC实现不同,典型的有乐观和悲观并发控制。
InnoDB的MVCC通过在每行记录后面保存两个隐藏的列实现。这两个列,一个保存了行的创建时间,另一个保存行的过期时间(或删除时间)。当然,存储的并不是实际的时间值,而是系统版本号(system version number)。每开始一个事务,系统的版本号都会自动递增。事务开始时刻的系统版本号会被当作事务的版本号,用来和查询到的每行记录的版本号进行比较。在repeatable read隔离级别下,MVCC具体流程是这样的:
- SELECT
InnoDB会根据以下两个条件检查每行记录:
a. InnoDB只查找版本早于当前事务版本的数据行,这样可以保证,事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或修改的。
b. 行的删除版本要么未定义,要么大于当前事务版本号。这样可以保证,事务读取到的行,在事务开始之前未被删除。
只有符合这两个条件的记录,才能作为查询结果。 - INSERT
InnoDB为新插入的每一行保存当前系统版本号作为行版本号。 - DELETE
InnoDB为删除的每一行保存当前系统版本号作为行删除标识。 - UPDATE
InnoDB插入一条新纪录,保存当前系统版本号为行版本号,同时保存当前系统版本号到原来的行作为行删除标识。
保存这两个额外的系统版本号,使大多数读操作都可以不用加锁。这样设计使得读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行。不足之处是需要额外的存储空间,需要做更多的行检查和额外的维护工作。MVCC只在repeatable read和read committed两个隔离级别下工作。其它两个隔离级别都和MVCC不兼容,因为READ UNCOMMITTED总是读取最新的数据行,而不是符合当前事务版本的数据行。而SERIALIZABLE则会对所有读取的行都加锁。
Spring事务传播属性
- REQUIRED 支持当前事务,如果当前没有事务,则创建新事务。
- SUPPORTS 支持当前事务,如果当前没有事务,则在非事务状态下执行。
- MANDATORY 支持当前事务,如果当前不存在事务,则抛出异常。
- REQUIRES_NEW 创建新事务,如果当前存在事务,则将其挂起。
- NOT_SUPPORTED 非事务状态执行,如果当前存在事务,将其挂起。
- NEVER 非事务状态下执行,如果当前存在事务,则抛出异常。
- NESTED 如果当前存在事务,则在嵌套事务中执行。
Spring事务管理器
Spring声明式事务管理的基本原理是,使用spring aop,将TransactionInterceptor中的advice织入声明了事务的代理bean中。advice织入的过程可以参考以下流程图。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XfogyrJA-1578035334505)(assets/Spring-AOP织入advice.png)]
我们接着看TransactionInterceptor是如何创建、提交和回滚事务的:
protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation)
throws Throwable {
// If the transaction attribute is null, the method is non-transactional.
// 获取事务属性,即@Transactional注解标注的属性
final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);
// 通常是DataSourceTransactionManager
final PlatformTransactionManager tm = determineTransactionManager(txAttr);
// 切面方法签名
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
// Standard transaction demarcation with getTransaction and commit/rollback calls.
// 1. 创建事务
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// target invocation exception
// 2. 方法执行出现异常后,如何继续事务:回滚还是提交?
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
/// 3. 清除事务信息
cleanupTransactionInfo(txInfo);
}
// 4. 方法执行成功,提交事务
commitTransactionAfterReturning(txInfo);
return retVal;
}
else {
......
}
}
创建事务
org.springframework.transaction.interceptor.TransactionAspectSupport#createTransactionIfNecessary
protected TransactionInfo createTransactionIfNecessary(
PlatformTransactionManager tm, TransactionAttribute txAttr, final String joinpointIdentification) {
// If no name specified, apply method identification as transaction name.
// 如果没有设置事务名称,那么使用方法签名作为事务名
if (txAttr != null && txAttr.getName() == null) {
txAttr = new DelegatingTransactionAttribute(txAttr) {
@Override
public String getName() {
return joinpointIdentification;
}
};
}
TransactionStatus status = null;
if (txAttr != null) {
if (tm != null) {
// 1. 根据事务属性,通过transaction manager获取事务
status = tm.getTransaction(txAttr);
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Skipping transactional joinpoint [" + joinpointIdentification +
"] because no transaction manager has been configured");
}
}
}
// 创建TransactionInfo实例,包含PlatformTransactionManager,TransactionAttribute和TransactionStatus;绑定到ThreadLocal
return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
}
org.springframework.transaction.support.AbstractPlatformTransactionManager#getTransaction
public final TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {
// 创建DataSourceTransactionObject,尝试从TransactionSynchronizationManager获取ConnectionHolder,放进DataSourceTransactionObject
Object transaction = doGetTransaction();
// Cache debug flag to avoid repeated checks.
boolean debugEnabled = logger.isDebugEnabled();
if (definition == null) {
// 如果入参为null,则使用默认的事务属性定义
definition = new DefaultTransactionDefinition();
}
// 如果DataSourceTransactionObject包含ConnectionHolder,且事务已激活,则根据传播属性做相应处理
if (isExistingTransaction(transaction)) {
// Existing transaction found -> check propagation behavior to find out how to behave.
return handleExistingTransaction(definition, transaction, debugEnabled);
}
...
else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
// 将当前事务持有的资源从ThreadLocal中取出,挂起。
SuspendedResourcesHolder suspendedResources = suspend(null);
if (debugEnabled) {
logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition);
}
try {
boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
// 创建DefaultTransactionStatus实例,其维护了TransactionDefinition、
// DataSourceTransactionObject等信息
DefaultTransactionStatus status = newTransactionStatus(
definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
// 获取连接,设置autoCommit为false,设置ConnectionHolder为事务激活状态,根据事务定义设置超时,
// 将ConnectionHolder注册到TransactionSynchronizationManager,key为datasource。
doBegin(transaction, definition);
prepareSynchronization(status, definition);
return status;
}
catch (RuntimeException ex) {
resume(null, suspendedResources);
throw ex;
}
catch (Error err) {
resume(null, suspendedResources);
throw err;
}
}
else {
// Create "empty" transaction: no actual transaction, but potentially synchronization.
if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) {
logger.warn("Custom isolation level specified but no actual transaction initiated; " +
"isolation level will effectively be ignored: " + definition);
}
boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null);
}
}
mybatis如何接入Spring事务管理器
mybatis接入Spring事务管理器的原理非常简单。为了保证mybatis进行jdbc操作以及Spring创建、回滚事务使用的是同一个connection对象,mybatis在获取数据库连接时,会从TransactionSynchronizationManager获取,具体代码如下:
org.mybatis.spring.transaction.SpringManagedTransaction#openConnection
private void openConnection() throws SQLException {
// 调用Spring的DataSourceUtils获取连接,而DataSourceUtils会从TransactionSynchronizationManager获取,key为数据源对象。而连接为Spring在开始事务时注册到TransactionSynchronizationManager的
this.connection = DataSourceUtils.getConnection(this.dataSource);
this.autoCommit = this.connection.getAutoCommit();
this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(
"JDBC Connection ["
+ this.connection
+ "] will"
+ (this.isConnectionTransactional ? " " : " not ")
+ "be managed by Spring");
}
}