**高并发、微服务 、性能调优实战案例100讲，所有案例均源于个人工作实战，均配合代码落地**

加我微信：itsoku，所有案例均提供在线答疑。



# 第7节 接口性能调优之大事务优化

本文内容非常硬核，请大伙打起精神，坚持看完。

接口性能不是谁都会优化的，如果面试的时候，能够说出优化接口的一些方案，会让面试官对你刮目相看。



# 先来看一段代码

```java
@Transactional
public void bigTransaction() throws InterruptedException {
    // 1、getData()方法模拟一个比较耗时的获取数据的操作，这个方法内部会休眠5秒
    String data = this.getData();

    //2、将上面获取到的数据写入到db中
    Lesson007PO po = new Lesson007PO();
    po.setId(UUID.randomUUID().toString());
    po.setData(data);
    this.lesson007Mapper.insert(po);
}

public String getData() throws InterruptedException {
    //休眠5秒
    TimeUnit.SECONDS.sleep(5);
    return UUID.randomUUID().toString();
}
```



## 上面这段代码没有没有什么问题？

明眼人可能已经看出来了，方法上加了`@Transactional`注解，加了这个注解，说明这个方法会交给Spring来自动管理这个方法的事务，那么这个方法的逻辑就变成了下面这样

```java
1、Spring去数据库连接池拿到一个数据库连接
2、开启事务
3、执行bigTransaction()中的代码
4、提交事务
5、将数据库连接还给数据库连接池中
```

这整个过程中，这个连接都会被占用，数据库连接都是有上限的，是非常稀缺的资源，如果所有人都把连接拿去使用很久而没有释放，那么当连接池里面的连接都被拿走了去使用，此时其他请求就没有数据库连接可以使用了，从而导致无法从连接池中获取有效的连接，会导致获取连接超时，而导致请求失败。



## 有没有什么优化方案？（小事务化）

上面的写法也就是我们常说的大事务，有很大的优化空间。

优化方案：将事务最小化，再来看看这段代码，如下，其实getData()方法中是用不到数据库操作的，这个方法里面并没有数据库操作，只有最后的insert才会用到数据库操作，会向db中写入数据，这个时候才会用到数据库的连接，那么我们能不能把这个代码优化下呢，只让最后写数据的时候才用到事务。

```java
@Transactional
public void bigTransaction() throws InterruptedException {
    // 1、getData()方法模拟一个比较耗时的获取数据的操作，这个方法内部会休眠5秒
    String data = this.getData();

    //2、将上面获取到的数据写入到db中
    Lesson007PO po = new Lesson007PO();
    po.setId(UUID.randomUUID().toString());
    po.setData(data);
    this.lesson007Mapper.insert(po);
}

public String getData() throws InterruptedException {
    //休眠5秒
    TimeUnit.SECONDS.sleep(5);
    return UUID.randomUUID().toString();
}
```



## 使用TransactionTemplate优化代码

Spring为我们提供了一个工具类`TransactionTemplate`，通过这个类，我们可以灵活的控制事务的粒度，这个类就是我们常说的编程式事务。

下面将上面大事务的代码优化下，为了和上面方法区分，我们将优化后的代码放到一个新的方法中了，稍后方便测试者两个方法的效果，如下

主要有2点改动

- 将方法上面的 @Transactional  去掉了
- 将最后需要事务操作的insert代码丢到`this.transactionTemplate.executeWithoutResult`，被这个方法包裹起来的代码才会使用spring事务

```java
/**
 * 使用 TransactionTemplate 编程式事务，可以灵活的控制事务的范围
 *
 * @throws InterruptedException
 */
public void smallTransaction() throws InterruptedException {
    // 1、调用getData()方法，讲获取的数据写到db中，假设 getData方法比较耗时，比如耗时 5秒
    String data = this.getData();

    //2、将上面获取到的数据写入到db中
    Lesson007PO po = new Lesson007PO();
    po.setId(UUID.randomUUID().toString());
    po.setData(data);

    // this.transactionTemplate.executeWithoutResult可以传入一个Consumer，这个Consumer表述需要在事务中执行的业务操作
    this.transactionTemplate.executeWithoutResult(action -> {
        this.lesson007Mapper.insert(po);
    });
}
```





## 测试两种方案的效果，让大伙大吃一惊

大家先不要划走，看完，我会写个测试用例，测试这两种写法的效果，这块效果还挺难测试的，结果会让大伙吃一惊。

我们会分别对这2中写法模拟200个并发请求，统计成功和失败的数量。

大家重点关注着两个指标，以及失败的原因（主要是获取不到数据库连接导致失败）。



## 为了方便看到测试效果，需要做下面准备

1. TransactionController中提供2个接口分别调用上面2个方法，方便稍后测试

2. tomcat 连接池配置，配置200，这样可以支持200个请求同时过来，测试更真实，（application.yml）中添加下面配置

   ```yaml
   server:
     tomcat:
       threads:
         max: 200
         min-spare: 200
   ```

3. 将连接池最大数量设置为20个，获取连接超时时间为3秒，（application.yml）中添加下面配置

   ```yaml
       # 这里我们把数据库连接池的最大量配置为20，最小10，获取链接超时时间为3秒，这样方便看到效果
       hikari:
         maximum-pool-size: 20
         minimum-idle: 10
         connection-timeout: 3000
   ```



## 运行测试用例 

对这两种事务的接口进行测试，分别对他们进行模拟200个并发请求，然后输出成功数量和失败的数量

```java
TransactionControllerTest
    
@Test
public void test() throws InterruptedException {
    System.out.println("对这两种事务的接口进行测试，分别对他们进行模拟100个并发请求，然后输出成功数量和失败的数量");
    //对声明式事务的接口进行测试，这个接口内部是大事务
    System.out.println("--------------编程式事务接口压测结果------------------");
    test("http://localhost:8080/bigTransaction");

    //对编程式事务的接口进行测试，这个接口内部是小事务
    System.out.println("--------------编程式事务接口压测结果------------------");
    test("http://localhost:8080/smallTransaction");
}
```



## 测试用例输出

```html
对这两种事务的接口进行测试，分别对他们进行模拟200个并发请求，然后输出成功数量和失败的数量
--------------声明式事务接口压测结果------------------
23:41:41 - 压测开始......
23:41:47 - 压测结束，总耗时(ms):5711
请求成功数:20
请求失败数:180
--------------编程式事务接口压测结果------------------
23:41:47 - 压测开始......
23:41:52 - 压测结束，总耗时(ms):5253
请求成功数:200
请求失败数:0
```

可以看到声明式式事务的接口失败了180次，而编程式事务都成功了，是不是太顶了。



## 声明式事务失败的原因---看下后端异常信息

如下，出现了大量下面这种异常，一眼就可以看出来，主要是获取连接超时弹出了异常，导致接口返回失败。

```java
2024-03-31 23:17:27.723 ERROR 15124 --- [io-8080-exec-28] c.i.l.controller.TransactionController   : 声明式事务 执行异常:Could not open JDBC Connection for transaction; nested exception is java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 3034ms.
2024-03-31 23:17:27.724 ERROR 15124 --- [io-8080-exec-91] c.i.l.controller.TransactionController   : 声明式事务 执行异常:Could not open JDBC Connection for transaction; nested exception is java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 3028ms.
2024-03-31 23:17:27.724 ERROR 15124 --- [io-8080-exec-61] c.i.l.controller.TransactionController   : 声明式事务 执行异常:Could not open JDBC Connection for transaction; nested exception is java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 3030ms.
2024-03-31 23:17:27.724 ERROR 15124 --- [o-8080-exec-188] c.i.l.controller.TransactionController   : 声明式事务 执行异常:Could not open JDBC Connection for transaction; nested exception is java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 3030ms.
```



## 总结

本文主要是通过一个案例介绍了大事务优化后对接口的一个影响。

这里帮大家整理了大事务常见的一些优化点

1. 系统性能要求很高，可以使用TransactionTemplate编程式事务，精准控制事务的粒度，尽量让事务小型化
2. 尽量避免将没有事务的耗时操作放到事务代码中
3. 避免在事务中执行远程操作，远程操作是不需要用到本地事务的，所以没有必要放在事务中
4. 尽量让事务的操作集中在一起执行，比如都放到方法最后，使用TransactionTemplate执行，这样可使事务最小化



## 源码

在高并发系列实战案例的 lesson007 模块中，如果找不到的，留言获取，感谢大家。



# 高并发 & 微服务 & 性能调优实战案例100讲

## 已更新 7 节课

```java
1. 分片上传实战
2. 通用并发处理工具类实战
3. 实现一个好用接口性能压测工具类
4. 超卖问题的4种解决方案，也是防止并发修改数据出错的通用方案
5. Semaphore实现接口限流实战
6. 并行查询，优化接口响应速度实战
7. 使用TransactionTemplate优化接口性能（大事务优化）
```

## 课程部分大纲，连载中。。。。

以下课程均来源于个人多年的实战，均提供原理讲解 && 源码落地

```java
1. 分片上传实战
2. 通用并发处理工具类实战
3. 实现一个好用接口性能压测工具类
4.超卖问题的4种解决方案，也是防止并发修改数据出错的通用方案
5. Semaphore实现接口限流实战
6. 并行查询，优化接口响应速度实战
7. 使用TransactionTemplate优化接口性能（大事务优化）
8. 手写线程池管理器，管理&监控所有线程池
9. 使用SpringBoot实现动态Job管理功能
10. 通用的Excel导出功能实战
11. 通用的幂等性工具类实战
12. 接口返回值通用设计
13. 接口太多，各种dto、vo不计其数，如何命名？
14. 一个业务太复杂了，方法太多，如何传参？
15. 如何统计接口耗时？
16. AOP实战接口日志打印功能
17. AOP实现业务操作日志记录功能
18. AOP实现MyBatis分页功能
19. SpringBoot读写分离实战
20. MQ专题：事务消息实战（防止消息丢失）
21. MQ专题：消息幂等消费通用方案实战
22. MQ专题：延迟消息通用方案实战
23. MQ专题：顺序消息通用方案实战
24. 分布式事务：使用事务消息实现事务最终一致性
25. 分布式事务：通用的TCC分布式事务生产级代码落地实战
26. 分布式锁案例实战
27. 微服务中如何传递上下文？实战
28. 微服务链路日志追踪实战（原理&代码落地）
29. SpringBoot实现租户数据隔离
30. MyBatis进阶：封装MyBatis，实现通用的无SQL版CRUD功能，架构师必备
31. MyBatis进阶：自己实现通用分表功能，架构师必备
32. MyBatis进阶：实现多租户隔离ORM框架
33. SpringBoot中实现自动监听PO的变化，自动生成表结构
34. 分布式专题：其他实战课程等
35. 性能调优：如何排查死锁？
36. 性能调优：如何排查内存溢出？
37. 性能调优：CPU被打满，如何排查？
38. 性能调优：生产代码没生效，如何定位？
39. 性能调优：接口太慢，如何定位？
40. 性能调优：如何查看生产上接口的入参和返回值？
41. 性能调优：远程debug
42. 生产上出现了各种故障，如何定位？
43. 其他等各种实战案例。。。
。。。
```





