spring是如何保证⼀个事务内获取同⼀个Connection?
前⾔
关于Spring的事务,它是Spring Framework中极其重要的⼀块。前⾯⽤了⼤量的篇幅从应⽤层⾯、原理层⾯进⾏了⽐较全⽅位的⼀个讲解。但是因为它过于重要,所以本⽂继续做补充内容:Spring事务的同步机制(后⾯还有Spring事务的监听机制)
Spring事务同步机制?我估摸很多⼩伙伴从来没听过还有这么⼀说法,毕竟它在平时开发中你可能很少遇到(如果你没怎么考虑过系统性能和吞吐量的话)。
让我记录本⽂的源动⼒是忆起两年前⾃⼰在开发、调试过程中遇到这样⼀个诡异异常:
java.sql.SQLException: Connection has already been closed
但是,它不是必现的,重点:它不是必现的。 ⽽⼀旦出现,任何涉及需要使⽤数据库连接的接⼝都有可能报这个错(已经影响正常work 了),重启也解决不了问题的根本。
关于⾮必现问题,我曾经表达了⼀个观点:程序中的“软病(⾮必现问题)”是相对很难解决的,因为定位难度⾼,毕竟只要问题⼀旦定位了,从来不差解决⽅案
这个异常的字⾯意思⾮常简单:数据库连接池连接被关闭了。 可能⼤多数⼈(我当然也不例外)看到此异常都会fuck⼀句:what?我的连接都是交给Spring去管理了,⾃⼰从来不会⼿动close,怎么回事?难道Spring有bug? 敢于质疑“权威”⼀直以来都是件好事,但是有句话这么说:你对⼈家还不了解的情况下不要轻易说⼈家程序有bug。
可能⼤多数⼈对于Spring的事务,只知道怎么使⽤,⽐如加个注解啥的,但是底层原理并不清楚,因此定位此问题就会变得⾮常的困难了~由于我之前有研究过Spring事务的同步机制这块,所以忆起这件事之后就迅速定位了问题所在:这和Spring事务的同步机制有关,并不是Spring事务的bug。
Spring事务极简介绍
关于Spring事务,我推荐⼩伙伴看看上⾯的【相关阅读】,能让你对Spring事务管理有个整体的掌握。但是由于过了有段时间了,此处做个⾮常简单的介绍:
Spring有声明式事务和编程式事务: 声明式事务只需要提供@Transactional的注解,然后事务的开启和提交/回滚、资源的清理就都由spring来管控,我们只需要关注业务代码即可; 编程式事务则需要使⽤spring提供的模板,如TransactionTemplate,或者直接使⽤底层的PlatformTransactionManager⼿动控制提交、回滚。
声明式事务的最⼤优点就是对代码的侵⼊性⼩,只需要在⽅法上加@Transactional的注解就可以实现事务; 编程式事务的最⼤优点就是事务的管控粒度较细,可以实现代码块级别的事务。
前提介绍
Spring把JDBC 的 Connection或者Hibernate的Session等访问数据库的链接(会话)都统⼀称为资源,显然我们知道Connection这种是线程不安全的,同⼀时刻是不能被多个线程共享的。
简单的说:同⼀时刻我们每个线程持有的Connection应该是独⽴的,且都是互不⼲扰和互不相同的
但是Spring管理的Service、Dao等他们都是⽆状态的单例Bean,怎么破?,如何保证单例Bean⾥⾯使⽤的Connection都能够独⽴呢?Spring引⼊了⼀个类:事务同步管理类ansaction.support.TransactionSynchronizationManager来解决这个问题。它的做法是内部使⽤了很多的ThreadLocal为不同的事务线程提供了独⽴的资源副本,并同时维护这些事务的配置属性和运⾏状态信息 (⽐如强⼤的事务嵌套、传播属性和这个强相关)。
这个同步管理器TransactionSynchronizationManager是掌管这⼀切的⼤脑,它管理的TransactionSynchronization是开放给调⽤者⼀个⾮常重要的扩展点,下⾯会有详细介绍~
TransactionSynchronizationManager 将 Dao、Service 类中影响线程安全的所有 “ 状态 ” 都统⼀抽取到
该类中,并⽤ ThreadLocal 进⾏封装,这样⼀来, Dao (基于模板类或资源获取⼯具类创建的 Dao )和 Service (采⽤ Spring 事务管理机制)就不⽤⾃⼰来保存⼀些事务状态了,从⽽就变成了线程安全的单例对象了,优秀~
DataSourceUtils
这⾥有必要提前介绍Spring提供给我们的这个⼯具类。
有些场景⽐如我们使⽤MyBatis的时候,某些场景下,可能⽆法使⽤ Spring 提供的模板类来达到效果,⽽是需要直接操作源⽣API Connection。
那如何拿到这个链接Connection呢(主意此处打⼤前提:必须保证和当前MaBatis线程使⽤的是同⼀个链接,这样才接受本事务控制嘛,否则就脱缰了~)
这个时候DataSourceUtils这个⼯具类就闪亮登场了,它提供了这个能⼒:
public abstract class DataSourceUtils {
...
public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException { ... }
...
// 把definition和connection进⾏⼀些准备⼯作~
public static Integer prepareConnectionForTransaction(Connection con, @Nullable TransactionDefinition definition) throws SQLException { ...}
// Reset the given Connection after a transaction,
// con.setTransactionIsolation(previousIsolationLevel);和con.setReadOnly(false);等等
public static void resetConnectionAfterTransaction(Connection con, @Nullable Integer previousIsolationLevel) { ... }
// 该JDBC Connection 是否是当前事务内的链接~
public static boolean isConnectionTransactional(Connection con, @Nullable DataSource dataSource) { ... }
// Statement 给他设置超时时间不传timeout表⽰不超时
public static void applyTransactionTimeout(Statement stmt, @Nullable DataSource dataSource) throws SQLException { ... }
public static void applyTimeout(Statement stmt, @Nullable DataSource dataSource, int timeout) throws SQLException { ... }
// 此处可能是归还给连接池,也有可能是close~(和连接池参数有关)
public static void releaseConnection(@Nullable Connection con, @Nullable DataSource dataSource) { ... }
public static void doReleaseConnection(@Nullable Connection con, @Nullable DataSource dataSource) throws SQLException { ... }
// 这个是真close
public static void doCloseConnection(Connection con, @Nullable DataSource dataSource) throws SQLException { ... }
// 如果链接是代理,会拿到最底层的connection
public static Connection getTargetConnection(Connection con) { ... }
}
getConnection()这个⽅法就是从TransactionSynchronizationManager⾥拿到⼀个现成的Connection(若没有现成的会⽤DataSource创建⼀个链接然后放进去~~~),所以这个⼯具类还是蛮好⽤的。
其实Spring不仅为JDBC提供了这个⼯具类,还为Hibernate、JPA、JDO等都提供了类似的⼯具类。
Session()
TransactionalEntityManager()
PersistenceManager()
问题场景⼀模拟
为了更好解释和说明,此处我模拟出这样的⼀个场景。
// 此处⽣路⽽关于DataSource、PlatformTransactionManager事务管理器等的配置
@Slf4j
@Service
public class HelloServiceImpl implements HelloService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Transactional
@Override
public Object hello(Integer id) {
// 向数据库插⼊⼀条记录
String sql = "insert into user (id,name,age) values (" + id + ",'fsx',21)";
jdbcTemplate.update(sql);
/
/ 做其余的事情可能抛出异常
System.out.println(1 / 0);
return "service hello";
}
}
如上Demo,这样⼦的因为有事务,所以最终这个插⼊都是不会成功的。(这个应该不⽤解释了吧,初级⼯程师应该必备的“技能”~)
@Transactional
@Override
public Object hello(Integer id) {
// 向数据库插⼊⼀条记录
String sql = "insert into user (id,name,age) values (" + id + ",'fsx',21)";
jdbcTemplate.update(sql);
// 根据id去查询获取总数(若查询到了肯定是count=1)
String query = "select count(1) from user where id = " + id;
Integer count = jdbcTemplate.queryForObject(query, Integer.class);
log.String());
return "service hello";
}
稍微改造⼀下,按照上⾯这么写,我相信想都不⽤想。count永远是返回1的~~这应该也是我们⾯向过程编程时候的经典案例:前⾯insert ⼀条记录,下⾯是可以⽴马去查询出来的
下⾯我把它改造如下:
jdbctemplate是什么@Transactional
@Override
public Object hello(Integer id) {
// 向数据库插⼊⼀条记录
String sql = "insert into user (id,name,age) values (" + id + ",'fsx',21)";
jdbcTemplate.update(sql);
// ⽣产环境⼀般会把些操作交给线程池,此处我只是模拟⼀下效果⽽已~
new Thread(() -> {
String query = "select count(1) from user where id = " + id;
Integer count = jdbcTemplate.queryForObject(query, Integer.class);
log.String());
}).start();
// 把问题放⼤
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "service hello";
}
经过这番改造,这样我们的count的值永远是0(是不是不符合预期了?)。
⼩技巧:此处为了演⽰我使⽤sleep⽅式把问题放⼤了,否则可能有时候好使、有时候不好使 把问题放⼤是debug调试的⼀个基本技巧~
这个现象就是⼀个⾮常严重的问题,它可能会出现:刚插⼊的数据竟然查不到的诡异现象,这个在我们现阶段平时⼯作中也会较为频繁的遇到,若对这块不了解,它会对的业务逻辑、对mysql binlog的顺序
有依赖的相关逻辑全都将会受到影响
解决⽅案
在互联⽹环境编程中,我们经常为了提⾼吞吐量、程序性能,会使⽤到异步的⽅式进⾏优化、消峰等等。因此连接池、线程池被得到了⼤量的应⽤。我们知道异步的提供的好处不⾔⽽喻,能够尽最⼤可能的提升硬件的利⽤率和能⼒,但它带来的缺点只有⼀个:提升系统的复杂性,很多时候需要深⼊的了解它才能运⽤⾃如,毕竟任何⽅案都是⼀把双刃剑,没有完美的~
⽐如⼀个业务处理中,发、发通知、记录操作⽇志等等这些⾮主⼲需求,我们⼀般都希望交给线程池去处理⽽不要⼲扰主要业务流程,所以我觉得现在多线程⽅式处理任务的概率已经越来越⾼了~ 既然如此,我觉得出现上⾯我模拟的这种现象的可能性还是蛮⾼的,所以希望⼩伙伴们能引起重视⼀些。
定位到问题的原因是解决问题的关键,这⾥我先给出直接的解决⽅案,再做理论分析。 我们的诉求是:我们的异步线程的执⾏时,必须确保记录已经持久化到数据库了才ok。因此可以这么来做,⼀招制敌:
@Slf4j
@Service
public class HelloServiceImpl implements HelloService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Transactional
@Override
public Object hello(Integer id) {
// 向数据库插⼊⼀条记录
String sql = "insert into user (id,name,age) values (" + id + ",'fsx',21)";
jdbcTemplate.update(sql);
// 在事务提交之后执⾏的代码块(⽅法)此处使⽤TransactionSynchronizationAdapter,其实在Spring5后直接使⽤接⼝也很⽅便了~
@Override
public void afterCommit() {
new Thread(() -> {
String query = "select count(1) from user where id = " + id;
Integer count = jdbcTemplate.queryForObject(query, Integer.class);
log.String());
}).start();
}
});
// 把问题放⼤
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "service hello";
}
}
我们使⽤TransactionSynchronizationManager注册⼀个TransactionSynchronization然后在afterCommit⾥执⾏我们的后续代码,这样就
能100%确保我们的后续逻辑是在当前事务被commit后才执⾏的,完美的问题解决
它还有个⽅法afterCompletion()有类似的效果,⾄于它和afterCommit()有什么区别,我觉得稍微有点技术敏感性的⼩伙伴都能知晓的~
TransactionSynchronizationManager
对它简单的解释为:使⽤TreadLocal记录事务的⼀些属性,⽤于应⽤扩展同步器的使⽤,在事务的开启,挂起,提交等各个点上回调应⽤的逻辑
// @since 02.06.2003 它是个抽象类,但是没有任何⼦类因为它所有的⽅法都是静态的
public abstract class TransactionSynchronizationManager {
// ======保存着⼀⼤堆的ThreadLocal 这⾥就是它的核⼼存储======
// 应⽤代码随事务的声明周期绑定的对象⽐如:DataSourceTransactionManager有这么做:
//TransactionSynchronizationManager.bindResource(obtainDataSource(), ConnectionHolder());
// TransactionSynchronizationManager.bindResource(obtainDataSource(), suspendedResources);
// 简单理解为当前线程的数据存储中⼼~~~~
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Trans
actional resources");
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论