数据库事务

数据库并发控制

当有多个查询需要在同一时刻修改数据,都会产生并发控制问题。解决并发控制问题需要借助锁。大多数情景,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");
    }
  }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值