网站制作价格是多少元,网站时间轴,公司网站荣誉墙怎么做,济南网络品牌推广事务的基本原理
在学习 Spring 的事务之前#xff0c;你首先要了解数据库的事务原理#xff0c;我们以 MySQL 5.7 为例#xff0c;讲解一下数据库事务的基础知识。
我们都知道 当 MySQL 使用 InnoDB 数据库引擎的时候#xff0c;数据库是对事务有支持的。而事务最主要的作…事务的基本原理
在学习 Spring 的事务之前你首先要了解数据库的事务原理我们以 MySQL 5.7 为例讲解一下数据库事务的基础知识。
我们都知道 当 MySQL 使用 InnoDB 数据库引擎的时候数据库是对事务有支持的。而事务最主要的作用是保证数据 ACID 的特性即原子性Atomicity、一致性Consistency、隔离性Isolation、持久性Durability下面来一一解释。
原子性 是指一个事务Transaction中的所有操作要么全部完成要么全部回滚而不会有中间某个数据单独更新的操作。事务在执行过程中一旦发生错误会被回滚Rollback到此次事务开始之前的状态就像这个事务从来没有执行过一样。
一致性 是指事务操作开始之前和操作异常回滚以后数据库的完整性没有被破坏。数据库事务 Commit 之后数据也是按照我们预期正确执行的。即要通过事务保证数据的正确性。
持久性 是指事务处理结束后对数据的修改进行了持久化的永久保存即便系统故障也不会丢失其实就是保存到硬盘。
隔离性 是指数据库允许多个连接同时并发多个事务又对同一个数据进行读写和修改的能力隔离性可以防止多个事务并发执行时由于交叉执行而导致数据不一致的现象。而 MySQL 里面就是我们经常说的事务的四种隔离级别即读未提交Read Uncommitted、读提交Read Committed、可重复读Repeatable Read和串行化Serializable。
由于隔离级别是事务知识点中最基础的部分我们就简单介绍一下四种隔离级别。但是它特别重要你要好好掌握。
四种 MySQL 事务的隔离级别
Read Uncommitted读取未提交内容此隔离级别表示所有正在进行的事务都可以看到其他未提交事务的执行结果。不同的事务之间读取到其他事务中未提交的数据通常这种情况也被称之为脏读Dirty Read会造成数据的逻辑处理错误也就是我们在多线程里面经常说的数据不安全了。在业务开发中几乎很少见到使用的因为它的性能也不比其他级别要好多少。
Read Committed读取提交内容 此隔离级别是指在一个事务相同的两次查询可能产生的结果会不一样也就是第二次查询能读取到其他事务已经提交的最新数据。也就是我们常说的不可重复读Nonrepeatable Read的事务隔离级别。因为同一事务的其他实例在该实例处理期间可能会对其他事务进行新的 commit所以在同一个事务中的同一 select 上多次执行可能返回不同结果。这是大多数数据库系统的默认隔离级别但不是 MySQL 默认的隔离级别。
Repeatable Read可重读 这是 MySQL 的默认事务隔离级别它确保同一个事务多次查询相同的数据能读到相同的数据。即使多个事务的修改已经 commit本事务如果没有结束永远读到的是相同数据要注意它与Read Committed 的隔离级别的区别是正好相反的。这会导致另一个棘手的问题幻读 Phantom Read即读到的数据可能不是最新的。这个是最常见的我们举个例子来说明。
第一步用工具打开一个数据库的 DB 连接如图所示。 查看一下数据库的事务隔离级别。 然后开启一个事务查看一下 user_info 的数据我们在 user_info 表里面插入了三条数据如下图所示。 第二步我们打开另外一个相同数据库的 DB 连接删除一条数据SQL 如下所示。 当删除执行成功之后我们可以开启第三个连接看一下数据库里面确实少了一条 ID1 的数据。那么这个时候我们再返回第一个连接第二次执行 select * from user_info如下图所示查到的还是三条数据。这就是我们经常说的可重复读。 Serializable可串行化这是最高的隔离级别它保证了每个事务是串行执行的即强制事务排序所有事务之间不可能产生冲突从而解决幻读问题。如果配置在这个级别的事务处理时间比较长并发比较大的时候就会导致大量的 db 连接超时现象和锁竞争从而降低了数据处理的吞吐量。也就是这个性能比较低所以除了某些财务系统之外用的人不是特别多。
数据库的隔离级别我们了解完了并不复杂这四种类型中你能清楚地知道Read Uncommitted 和 Read Committed就可以了一般这两个用得是最多的。
下面看一下数据的事务和连接是什么关系呢
MySQL 事务与连接的关系
我们要搞清楚事务和连接池的关系必须要先知道二者存在的前提条件。
事务必须在同一个连接里面的离开连接没有事务可言MySQL 数据库默认 autocommit1即每一条 SQL 执行完自动提交事务数据库里面的每一条 SQL 执行的时候必须有事务环境MySQL 创建连接的时候默认开启事务关闭连接的时候如果存在事务没有 commit 的情况则自动执行 rollback 操作不同的 connect 之间的事务是相互隔离的。
知道了这些条件我们就可以继续探索二者的关系了。在 connection 当中操作事务的方式只有两种。
MySQL 事务的两种操作方式
第一种用 BEGIN、ROLLBACK、COMMIT 来实现。
BEGIN开始一个事务ROLLBACK事务回滚COMMIT事务确认
第二种直接用 SET 来改变 MySQL 的自动提交模式。
SET AUTOCOMMIT0禁止自动提交SET AUTOCOMMIT1开启自动提交
MySQL 数据库的最大连接数是什么
而任何数据库的连接数都是有限的受内存和 CPU 限制你可以通过
show variables like ‘max_connections’ 查看此数据库的最大连接数、通过 show global status like ‘Max_used_connections’ 查看正在使用的连接数还可以通过 set global max_connections1500 来设置数据库的最大连接数。
除此之外你可以在观察数据库的连接数的同时通过观察 CPU 和内存的使用来判断你自己的数据库中 server 的连接数最佳大小是多少。而既然是连接那么肯定会有超时时间默认是 8 小时。
这里我只是列举了 MySQL 数据库的事务处理原理你可以用相同的思考方式看一下你在用的数据源的事务是什么机制的。
那么学习完了数据库事务的基础知识我们再看一下 Spring 中事务的用法和配置是什么样的。
Spring 里面事务的配置方法
由于我们使用的是 Spring Boot所以会通过 TransactionAutoConfiguration.java 加载 EnableTransactionManagement 注解帮我们默认开启事务关键代码如下图所示。 Spring 里面的事务有两种使用方式常见的是直接通过 Transaction 的方式进行配置而我们打开 SimpleJpaRepository 源码类的话会看到如下代码。
复制代码
Repository
Transactional(readOnly true)
public class SimpleJpaRepositoryT, ID implements JpaRepositoryImplementationT, ID {
...
Transactional
Override
public void deleteAll(Iterable? extends T entities) {
.....我们仔细看源码的时候就会发现默认情况下所有 SimpleJpaRepository 里面的方法都是只读事务而一些更新的方法都是读写事务。
所以每个 Respository 的方法是都是有事务的即使我们没有使用任何加 Transactional 注解的方法按照上面所讲的 MySQL 的 Transactional 开启原理也会有数据库的事务。那么我们就来看下 Transactional 的具体用法。
默认 Transactional 注解式事务
注解式事务又称显式事务需要手动显式注解声明那么我们看看如何使用。
按照惯例我们打开 Transactional 的源码如下所示。
复制代码
Target({ElementType.METHOD, ElementType.TYPE})
Retention(RetentionPolicy.RUNTIME)
Inherited
Documented
public interface Transactional {AliasFor(transactionManager)String value() default ;AliasFor(value)String transactionManager() default ;Propagation propagation() default Propagation.REQUIRED;Isolation isolation() default Isolation.DEFAULT;int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;boolean readOnly() default false;Class? extends Throwable[] rollbackFor() default {};String[] rollbackForClassName() default {};Class? extends Throwable[] noRollbackFor() default {};String[] noRollbackForClassName() default {};
}针对 Transactional 注解中常用的参数我列了一个表格方便你查看。 其他属性你基本上都可以知道是什么意思下面重点说一下隔离级别和事务的传播机制。
隔离级别 Isolation isolation() default Isolation.DEFAULT默认采用数据库的事务隔离级别。其中Isolation 是个枚举值基本和我们上面讲解的数据库隔离级别是一样的如下图所示。 propagation代表的是事务的传播机制这个是 Spring 事务的核心业务逻辑是 Spring 框架独有的它和 MySQL 数据库没有一点关系。所谓事务的传播行为是指在同一线程中在开始当前事务之前需要判断一下当前线程中是否有另外一个事务存在如果存在提供了七个选项来指定当前事务的发生行为。我们可以看 org.springframework.transaction.annotation.Propagation 这类的枚举值来确定有哪些传播行为。7 个表示传播行为的枚举值如下所示。
复制代码
public enum Propagation {REQUIRED(0),SUPPORTS(1),MANDATORY(2),REQUIRES_NEW(3),NOT_SUPPORTED(4),NEVER(5),NESTED(6);
}REQUIRED如果当前存在事务则加入该事务如果当前没有事务则创建一个新的事务。这个值是默认的。SUPPORTS如果当前存在事务则加入该事务如果当前没有事务则以非事务的方式继续运行。MANDATORY如果当前存在事务则加入该事务如果当前没有事务则抛出异常。REQUIRES_NEW创建一个新的事务如果当前存在事务则把当前事务挂起。NOT_SUPPORTED以非事务方式运行如果当前存在事务则把当前事务挂起。NEVER以非事务方式运行如果当前存在事务则抛出异常。NESTED如果当前存在事务则创建一个事务作为当前事务的嵌套事务来运行如果当前没有事务则该取值等价于 REQUIRED。
设置方法通过使用 propagation 属性设置例如下面这行代码。
复制代码
Transactional(propagation Propagation.REQUIRES_NEW)虽然用法很简单但是也有使用 Transactional 不生效的时候那么在哪些场景中是不可用的呢
Transactional 的局限性
这里列举的是一个当前对象调用对象自己里面的方法不起作用的场景。
我们在 UserInfoServiceImpl 的 save 方法中调用了带事务的 calculate 方法代码如下。
复制代码
Component
public class UserInfoServiceImpl implements UserInfoService {Autowiredprivate UserInfoRepository userInfoRepository;/*** 根据UserId产生的一些业务计算逻辑*/OverrideTransactional(transactionManager db2TransactionManager)public UserInfo calculate(Long userId) {UserInfo userInfo userInfoRepository.findById(userId).get();userInfo.setAges(userInfo.getAges()1);//.....等等一些复杂事务内的操作userInfo.setTelephone(Instant.now().toString());return userInfoRepository.saveAndFlush(userInfo);}/*** 此方法调用自身对象的方法就会发现calculate方法上面的事务是失效的*/public UserInfo save(Long userId) {return this.calculate(userId);}
}当在 UserInfoServiceImpl 类的外部调用 save 方法的时候此时 save 方法里面调用了自身的 calculate 方法你就会发现 calculate 方法上面的事务是没有效果的这个是 Spring 的代理机制的问题。那么我们应该如何解决这个问题呢可以引入一个类 TransactionTemplate我们看下它的用法。
TransactionTemplate 的用法
此类是通过 TransactionAutoConfiguration 加载配置进去的如下图所示。 我们通过源码可以看到此类提供了一个关键 execute 方法如下图所示。 这里面会帮我们处理事务开始、rollback、commit 的逻辑所以我们用的时候就非常简单把上面的方法做如下改动。
复制代码
public UserInfo save(Long userId) {return transactionTemplate.execute(status - this.calculate(userId));
}此时外部再调用我们的 save 方法的时候calculate 就会进入事务管理里面去了。当然了我这里举的例子很简单你也可以通过下面代码中的方法设置隔离级别和传播机制以及超时时间和是否只读。
复制代码
transactionTemplate new TransactionTemplate(transactionManager);
//设置隔离级别
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
//设置传播机制
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
//设置超时时间
transactionTemplate.setTimeout(1000);
//设置是否只读
transactionTemplate.setReadOnly(true);我们也可以根据 transactionTemplate 的实现原理自己实现一个 TransactionHelper一起来看一下。
自定义 TransactionHelper
第一步新建一个 TransactionHelper 类进行事务管理代码如下。
复制代码
/*** 利用spring进行管理*/
Component
public class TransactionHelper {/*** 利用spring 的机制和jdk8的function机制实现事务*/Transactional(rollbackFor Exception.class) //可以根据实际业务情况指定明确的回滚异常public T, R R transactional(FunctionT, R function, T t) {return function.apply(t);}
}第二步直接在 service 中就可以使用了代码如下。
复制代码 Autowiredprivate TransactionHelper transactionHelper;/*** 调用外部的transactionHelper类利用transactionHelper方法上面的Transaction注解使事务生效*/public UserInfo save(Long userId) {return transactionHelper.transactional((uid)-this.calculate(uid),userId);}上面我介绍了显式事务都是围绕 Transactional 的显式指定的事务我们也可以利用 AspectJ 进行隐式的事务配置。
隐式事务 / AspectJ 事务配置
只需要在我们的项目中新增一个类 AspectjTransactionConfig 即可代码如下。
复制代码
Configuration
EnableTransactionManagement
public class AspectjTransactionConfig {public static final String transactionExecution execution (* com.example..service.*.*(..));//指定拦截器作用的包路径Autowiredprivate PlatformTransactionManager transactionManager;Beanpublic DefaultPointcutAdvisor defaultPointcutAdvisor() {//指定一般要拦截哪些类AspectJExpressionPointcut pointcut new AspectJExpressionPointcut();pointcut.setExpression(transactionExecution);//配置advisorDefaultPointcutAdvisor advisor new DefaultPointcutAdvisor();advisor.setPointcut(pointcut);//根据正则表达式指定上面的包路径里面的方法的事务策略Properties attributes new Properties();attributes.setProperty(get*, PROPAGATION_REQUIRED,-Exception);attributes.setProperty(add*, PROPAGATION_REQUIRED,-Exception);attributes.setProperty(save*, PROPAGATION_REQUIRED,-Exception);attributes.setProperty(update*, PROPAGATION_REQUIRED,-Exception);attributes.setProperty(delete*, PROPAGATION_REQUIRED,-Exception);//创建InterceptorTransactionInterceptor txAdvice new TransactionInterceptor(transactionManager, attributes);advisor.setAdvice(txAdvice);return advisor;}
}这种方式只要符合我们上面的正则表达规则的 service 方法就会自动添加事务了如果我们在方法上添加 Transactional也可以覆盖上面的默认规则。
不过这种方法近两年使用的团队越来越少了因为注解的方式其实很方便并且注解 Transactional 的方式更容易让人理解代码也更简单你了解一下就好了。
上面的方法介绍完了那么一个方法经历的 SQL 和过程都有哪些呢我们通过日志分析一下。
通过日志分析配置方法的过程
大致可以分为以下几个步骤。
第一步我们在数据连接中加上 loggerSlf4JLoggerprofileSQLtrue用来显示 MySQL 执行的 SQL 日志如图所示。 第二步打开 Spring 的事务处理日志用来观察事务的执行过程代码如下。
复制代码
# Log Transactions Details
logging.level.org.springframework.orm.jpaDEBUG
logging.level.org.springframework.transactionTRACE
logging.level.org.hibernate.engine.transaction.internal.TransactionImplDEBUG
# 监控连接的情况
logging.level.org.hibernate.resource.jdbctrace
logging.level.com.zaxxer.hikariDEBUG第三步我们执行一个 saveOrUpdate 的操作详细的执行日志如下所示。 通过日志可以发现我们执行一个 saveUserInfo 的动作由于在其中配置了一个事务所以可以看到 JpaTransactionManager 获得事务的过程图上黄色的部分是同一个连接里面执行的 SQL 语句其执行的整体过程如下所示。
get connection从事务管理里面获得连接就 begin 开始事务了。我们没有看到显示的 begin 的 SQL基本上可以断定它利用了 MySQL 的 connection 初始化事务的特性。set autocommit0关闭自动提交模式这个时候必须要在程序里面 commit 或者 rollback。select user_info看看 user_info 数据库里面是否存在我们要保存的数据。update user_info发现数据库里面存在执行更新操作。commit执行提交事务。set autocommit1事务执行完改回 autocommit 的默认值每条 SQL 是独立的事务。
我们这里采用的是数据库默认的隔离级别如果我们通过下面这行代码改变默认隔离级别的话再观察我们的日志。
复制代码
Transactional(isolation Isolation.READ_COMMITTED)你会发现在开始事务之前它会先改变默认的事务隔离级别如图所示。 而在事务结束之后它还会还原此链接的事务隔离级别又如下图所示。 如果你明白了 MySQL 的事务原理的话再通过日志分析可以很容易地理解 Spring 的事务原理。我们在日志里面能看到 MySQL 的事务执行过程同样也能看到 Spring 的 TransactionImpl 的事务执行过程。这是什么原理呢我们来详细分析一下。
Spring 事务的实现原理
这里我重点介绍一下 Transactional 的工作机制这个主要是利用 Spring 的 AOP 原理在加载所有类的时候容器就会知道某些类需要对应地进行哪些 Interceptor 的处理。
例如我们所讲的 TransactionInterceptor在启动的时候是怎么设置事务的、是什么样的处理机制默认的代理机制又是什么样的呢
Spring 事务源码分析
我们在 TransactionManagementConfigurationSelector 里面设置一个断点就会知道代理的加载类 ProxyTransactionManagementConfiguration 对事务的处理机制。关键源码如下图所示。 而我们打开 ProxyTransactionManagementConfiguration 的话就会加载 TransactionInterceptor 的处理类关键源码如下图所示。 如果继续加载的话里面就会加载带有 Transactional 注解的类或者方法。关键源码如下图所示。 加载期间通过 Trnsactional 注解来确定哪些方法需要进行事务处理。
复制代码
o.s.orm.jpa.JpaTransactionManager : Creating new transaction with name而运行期间通过上面这条日志就可以找到 JpaTransactionManager 里面通过 getTransaction 方法创建的事务然后再通过 debuger 模式的 IDEA 线程栈进行分析就能知道创建事务的整个过程。你可以一步一步地去断点进行查看如下图所示。 如上图我们可以知道 createTransactionIfNecessary 是用来判断是否需要创建事务的有兴趣的话你可以点击进去看看如下图所示。 我们继续往下面 debug 的话就会找到创建事务的关键代码它会通过调用 AbstractPlatformTransactionManager 里面的 startTransaction 方法开启事务如下图所示。 然后我们就可以继续往下断点进行分析了。断点走到最后的时候你就可以看到开启事务的时候必须要从我们的数据源里面获得连接。看一下断点的栈信息这里有几个关键的 debug 点。如下图所示。 其中
第一处是处理带 Transactional 的注解的方法利用 CGLIB 进行事务拦截处理
第二处是根据 Spring 的事务传播机制来判断是用现有的事务还是创建新的事务
第七处是用来判断是否现有连接如果有直接用如果没有就从第八处的数据源里面的连接池中获取连接第七处的关键代码如下。 到这里我们介绍完了事务获得连接的关键时机那么还需要知道它是在什么时间释放连接到连接池里面的。我们在 LogicalConnectionManagedImpl 的 releaseConnection 方法中设置一个断点如下图所示。 然后观察断点线性的执行方法你会发现在事务执行之后它会将连接释放到连接池里面。
我们通过上面的 saveOrUpdate 的详细执行日志可以观察出来事务是在什么时机开启的、数据库连接是什么时机开启的、事务是在什么时机关闭的以及数据库连接是在什么时机释放的如果你没看出来可以再仔细看一遍日志。
所以Spring 中的事务和连接的关系是开启事务的同时获取 DB 连接事务完成的时候释放 DB 连接。通过 MySQL 的基础知识可以知道数据库连接是有限的那么当我们给某些方法加事务的时候都需要注意哪些内容呢
事务和连接池在 JPA 中的注意事项
我们在“17 | DataSource 为何物加载过程是怎样的”中对数据源的介绍时说过数据源的连接池不能配置过大否则连接之前切换就会非常耗费应用内部的 CPU 和内存从而降低应用对外提供 API 的吞吐量。
所以当我们使用事务的时候需要注意如下 几个事项
事务内的逻辑不能执行时间太长否则就会导致占用 db 连接的时间过长会造成数据库连接不够用的情况跨应用的操作如 API 调用等尽量不要在有事务的方法里面进行如果在真实业务场景中有耗时的操作也需要带事务时如扣款环节那么请注意增加数据源配置的连接池数我们通过 MVC 的应用请求连接池数量也要根据连接池的数量和事务的耗时情况灵活配置而 tomcat 默认的请求连接池数量是 200 个可以根据实际情况来增加或者减少请求的连接池数量从而减少并发处理对事务的依赖。