mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-26 05:06:42 +08:00
2069 lines
49 KiB
HTML
2069 lines
49 KiB
HTML
<!DOCTYPE html>
|
||
|
||
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
|
||
|
||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||
|
||
<head>
|
||
|
||
<head>
|
||
|
||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
|
||
|
||
<link rel="icon" href="/static/favicon.png">
|
||
|
||
<title>06 2成的业务代码的Spring声明式事务,可能都没处理正确.md.html</title>
|
||
|
||
<!-- Spectre.css framework -->
|
||
|
||
<link rel="stylesheet" href="/static/index.css">
|
||
|
||
<!-- theme css & js -->
|
||
|
||
<meta name="generator" content="Hexo 4.2.0">
|
||
|
||
</head>
|
||
|
||
|
||
|
||
<body>
|
||
|
||
|
||
|
||
<div class="book-container">
|
||
|
||
<div class="book-sidebar">
|
||
|
||
<div class="book-brand">
|
||
|
||
<a href="/">
|
||
|
||
<img src="/static/favicon.png">
|
||
|
||
<span>技术文章摘抄</span>
|
||
|
||
</a>
|
||
|
||
</div>
|
||
|
||
<div class="book-menu uncollapsible">
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li><a href="/" class="current-tab">首页</a></li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li><a href="../">上一级</a></li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/00 开篇词 业务代码真的会有这么多坑?.md.html">00 开篇词 业务代码真的会有这么多坑?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/01 使用了并发工具类库,线程安全就高枕无忧了吗?.md.html">01 使用了并发工具类库,线程安全就高枕无忧了吗?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/02 代码加锁:不要让“锁”事成为烦心事.md.html">02 代码加锁:不要让“锁”事成为烦心事.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/03 线程池:业务代码最常用也最容易犯错的组件.md.html">03 线程池:业务代码最常用也最容易犯错的组件.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/04 连接池:别让连接池帮了倒忙.md.html">04 连接池:别让连接池帮了倒忙.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/05 HTTP调用:你考虑到超时、重试、并发了吗?.md.html">05 HTTP调用:你考虑到超时、重试、并发了吗?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
<a class="current-tab" href="/专栏/Java 业务开发常见错误 100 例/06 2成的业务代码的Spring声明式事务,可能都没处理正确.md.html">06 2成的业务代码的Spring声明式事务,可能都没处理正确.md.html</a>
|
||
|
||
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/07 数据库索引:索引并不是万能药.md.html">07 数据库索引:索引并不是万能药.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/08 判等问题:程序里如何确定你就是你?.md.html">08 判等问题:程序里如何确定你就是你?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/09 数值计算:注意精度、舍入和溢出问题.md.html">09 数值计算:注意精度、舍入和溢出问题.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/10 集合类:坑满地的List列表操作.md.html">10 集合类:坑满地的List列表操作.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/11 空值处理:分不清楚的null和恼人的空指针.md.html">11 空值处理:分不清楚的null和恼人的空指针.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/12 异常处理:别让自己在出问题的时候变为瞎子.md.html">12 异常处理:别让自己在出问题的时候变为瞎子.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/13 日志:日志记录真没你想象的那么简单.md.html">13 日志:日志记录真没你想象的那么简单.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/14 文件IO:实现高效正确的文件读写并非易事.md.html">14 文件IO:实现高效正确的文件读写并非易事.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/15 序列化:一来一回你还是原来的你吗?.md.html">15 序列化:一来一回你还是原来的你吗?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/16 用好Java 8的日期时间类,少踩一些“老三样”的坑.md.html">16 用好Java 8的日期时间类,少踩一些“老三样”的坑.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/17 别以为“自动挡”就不可能出现OOM.md.html">17 别以为“自动挡”就不可能出现OOM.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/18 当反射、注解和泛型遇到OOP时,会有哪些坑?.md.html">18 当反射、注解和泛型遇到OOP时,会有哪些坑?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/19 Spring框架:IoC和AOP是扩展的核心.md.html">19 Spring框架:IoC和AOP是扩展的核心.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/20 Spring框架:框架帮我们做了很多工作也带来了复杂度.md.html">20 Spring框架:框架帮我们做了很多工作也带来了复杂度.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/21 代码重复:搞定代码重复的三个绝招.md.html">21 代码重复:搞定代码重复的三个绝招.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/22 接口设计:系统间对话的语言,一定要统一.md.html">22 接口设计:系统间对话的语言,一定要统一.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/23 缓存设计:缓存可以锦上添花也可以落井下石.md.html">23 缓存设计:缓存可以锦上添花也可以落井下石.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/24 业务代码写完,就意味着生产就绪了?.md.html">24 业务代码写完,就意味着生产就绪了?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/25 异步处理好用,但非常容易用错.md.html">25 异步处理好用,但非常容易用错.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/26 数据存储:NoSQL与RDBMS如何取长补短、相辅相成?.md.html">26 数据存储:NoSQL与RDBMS如何取长补短、相辅相成?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/27 数据源头:任何客户端的东西都不可信任.md.html">27 数据源头:任何客户端的东西都不可信任.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/28 安全兜底:涉及钱时,必须考虑防刷、限量和防重.md.html">28 安全兜底:涉及钱时,必须考虑防刷、限量和防重.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/29 数据和代码:数据就是数据,代码就是代码.md.html">29 数据和代码:数据就是数据,代码就是代码.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/30 如何正确保存和传输敏感数据?.md.html">30 如何正确保存和传输敏感数据?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/31 加餐1:带你吃透课程中Java 8的那些重要知识点(一).md.html">31 加餐1:带你吃透课程中Java 8的那些重要知识点(一).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/32 加餐2:带你吃透课程中Java 8的那些重要知识点(二).md.html">32 加餐2:带你吃透课程中Java 8的那些重要知识点(二).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/33 加餐3:定位应用问题,排错套路很重要.md.html">33 加餐3:定位应用问题,排错套路很重要.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/34 加餐4:分析定位Java问题,一定要用好这些工具(一).md.html">34 加餐4:分析定位Java问题,一定要用好这些工具(一).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/35 加餐5:分析定位Java问题,一定要用好这些工具(二).md.html">35 加餐5:分析定位Java问题,一定要用好这些工具(二).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/36 加餐6:这15年来,我是如何在工作中学习技术和英语的?.md.html">36 加餐6:这15年来,我是如何在工作中学习技术和英语的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/37 加餐7:程序员成长28计.md.html">37 加餐7:程序员成长28计.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/38 加餐8:Java程序从虚拟机迁移到Kubernetes的一些坑.md.html">38 加餐8:Java程序从虚拟机迁移到Kubernetes的一些坑.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/答疑篇:代码篇思考题集锦(一).md.html">答疑篇:代码篇思考题集锦(一).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/答疑篇:代码篇思考题集锦(三).md.html">答疑篇:代码篇思考题集锦(三).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/答疑篇:代码篇思考题集锦(二).md.html">答疑篇:代码篇思考题集锦(二).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/答疑篇:加餐篇思考题答案合集.md.html">答疑篇:加餐篇思考题答案合集.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/答疑篇:安全篇思考题答案合集.md.html">答疑篇:安全篇思考题答案合集.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/答疑篇:设计篇思考题答案合集.md.html">答疑篇:设计篇思考题答案合集.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/结束语 写代码时,如何才能尽量避免踩坑?.md.html">结束语 写代码时,如何才能尽量避免踩坑?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
|
||
|
||
<div class="sidebar-toggle-inner"></div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<script>
|
||
|
||
function add_inner() {
|
||
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
|
||
inner.classList.add('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
function remove_inner() {
|
||
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
|
||
inner.classList.remove('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
function sidebar_toggle() {
|
||
|
||
let sidebar_toggle = document.querySelector('.sidebar-toggle')
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let content = document.querySelector('.off-canvas-content')
|
||
|
||
if (sidebar_toggle.classList.contains('extend')) { // show
|
||
|
||
sidebar_toggle.classList.remove('extend')
|
||
|
||
sidebar.classList.remove('hide')
|
||
|
||
content.classList.remove('extend')
|
||
|
||
} else { // hide
|
||
|
||
sidebar_toggle.classList.add('extend')
|
||
|
||
sidebar.classList.add('hide')
|
||
|
||
content.classList.add('extend')
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
function open_sidebar() {
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
|
||
sidebar.classList.add('show')
|
||
|
||
overlay.classList.add('show')
|
||
|
||
}
|
||
|
||
function hide_canvas() {
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
|
||
sidebar.classList.remove('show')
|
||
|
||
overlay.classList.remove('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
</script>
|
||
|
||
|
||
|
||
<div class="off-canvas-content">
|
||
|
||
<div class="columns">
|
||
|
||
<div class="column col-12 col-lg-12">
|
||
|
||
<div class="book-navbar">
|
||
|
||
<!-- For Responsive Layout -->
|
||
|
||
<header class="navbar">
|
||
|
||
<section class="navbar-section">
|
||
|
||
<a onclick="open_sidebar()">
|
||
|
||
<i class="icon icon-menu"></i>
|
||
|
||
</a>
|
||
|
||
</section>
|
||
|
||
</header>
|
||
|
||
</div>
|
||
|
||
<div class="book-content" style="max-width: 960px; margin: 0 auto;
|
||
|
||
overflow-x: auto;
|
||
|
||
overflow-y: hidden;">
|
||
|
||
<div class="book-post">
|
||
|
||
<p id="tip" align="center"></p>
|
||
|
||
<div><h1>06 2成的业务代码的Spring声明式事务,可能都没处理正确</h1>
|
||
|
||
<p>你好,我是朱晔。今天,我来和你聊聊业务代码中与数据库事务相关的坑。</p>
|
||
|
||
<p>Spring 针对 Java Transaction API (JTA)、JDBC、Hibernate 和 Java Persistence API (JPA) 等事务 API,实现了一致的编程模型,而 Spring 的声明式事务功能更是提供了极其方便的事务配置方式,配合 Spring Boot 的自动配置,大多数 Spring Boot 项目只需要在方法上标记 @Transactional 注解,即可一键开启方法的事务性配置。</p>
|
||
|
||
<p>据我观察,大多数业务开发同学都有事务的概念,也知道如果整体考虑多个数据库操作要么成功要么失败时,需要通过数据库事务来实现多个操作的一致性和原子性。但,在使用上大多仅限于为方法标记 @Transactional,不会去关注事务是否有效、出错后事务是否正确回滚,也不会考虑复杂的业务代码中涉及多个子业务逻辑时,怎么正确处理事务。</p>
|
||
|
||
<p>事务没有被正确处理,一般来说不会过于影响正常流程,也不容易在测试阶段被发现。但当系统越来越复杂、压力越来越大之后,就会带来大量的数据不一致问题,随后就是大量的人工介入查看和修复数据。</p>
|
||
|
||
<p>所以说,一个成熟的业务系统和一个基本可用能完成功能的业务系统,在事务处理细节上的差异非常大。要确保事务的配置符合业务功能的需求,往往不仅仅是技术问题,还涉及产品流程和架构设计的问题。今天这一讲的标题“20% 的业务代码的 Spring 声明式事务,可能都没处理正确”中,20% 这个数字在我看来还是比较保守的。</p>
|
||
|
||
<p>我今天要分享的内容,就是帮助你在技术问题上理清思路,避免因为事务处理不当让业务逻辑的实现产生大量偶发 Bug。</p>
|
||
|
||
<h2>小心 Spring 的事务可能没有生效</h2>
|
||
|
||
<p>在使用 @Transactional 注解开启声明式事务时, 第一个最容易忽略的问题是,很可能事务并没有生效。</p>
|
||
|
||
<p>实现下面的 Demo 需要一些基础类,首先定义一个具有 ID 和姓名属性的 UserEntity,也就是一个包含两个字段的用户表:</p>
|
||
|
||
<pre><code>@Entity
|
||
|
||
|
||
|
||
@Data
|
||
|
||
|
||
|
||
public class UserEntity {
|
||
|
||
|
||
|
||
@Id
|
||
|
||
|
||
|
||
@GeneratedValue(strategy = AUTO)
|
||
|
||
|
||
|
||
private Long id;
|
||
|
||
|
||
|
||
private String name;
|
||
|
||
|
||
|
||
public UserEntity() { }
|
||
|
||
|
||
|
||
public UserEntity(String name) {
|
||
|
||
|
||
|
||
this.name = name;
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
<p>为了方便理解,我使用 Spring JPA 做数据库访问,实现这样一个 Repository,新增一个根据用户名查询所有数据的方法:</p>
|
||
|
||
<pre><code>@Repository
|
||
|
||
|
||
|
||
public interface UserRepository extends JpaRepository<UserEntity, Long> {
|
||
|
||
|
||
|
||
List<UserEntity> findByName(String name);
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
<p>定义一个 UserService 类,负责业务逻辑处理。如果不清楚 @Transactional 的实现方式,只考虑代码逻辑的话,这段代码看起来没有问题。</p>
|
||
|
||
<p>定义一个入口方法 createUserWrong1 来调用另一个私有方法 createUserPrivate,私有方法上标记了 @Transactional 注解。当传入的用户名包含 test 关键字时判断为用户名不合法,抛出异常,让用户创建操作失败,期望事务可以回滚:</p>
|
||
|
||
<pre><code>@Service
|
||
|
||
|
||
|
||
@Slf4j
|
||
|
||
|
||
|
||
public class UserService {
|
||
|
||
|
||
|
||
@Autowired
|
||
|
||
|
||
|
||
private UserRepository userRepository;
|
||
|
||
|
||
|
||
//一个公共方法供Controller调用,内部调用事务性的私有方法
|
||
|
||
|
||
|
||
public int createUserWrong1(String name) {
|
||
|
||
|
||
|
||
try {
|
||
|
||
|
||
|
||
this.createUserPrivate(new UserEntity(name));
|
||
|
||
|
||
|
||
} catch (Exception ex) {
|
||
|
||
|
||
|
||
log.error("create user failed because {}", ex.getMessage());
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
return userRepository.findByName(name).size();
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
//标记了@Transactional的private方法
|
||
|
||
|
||
|
||
@Transactional
|
||
|
||
|
||
|
||
private void createUserPrivate(UserEntity entity) {
|
||
|
||
|
||
|
||
userRepository.save(entity);
|
||
|
||
|
||
|
||
if (entity.getName().contains("test"))
|
||
|
||
|
||
|
||
throw new RuntimeException("invalid username!");
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
//根据用户名查询用户数
|
||
|
||
|
||
|
||
public int getUserCount(String name) {
|
||
|
||
|
||
|
||
return userRepository.findByName(name).size();
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
<p>下面是 Controller 的实现,只是调用一下刚才定义的 UserService 中的入口方法 createUserWrong1。</p>
|
||
|
||
<pre><code>@Autowired
|
||
|
||
|
||
|
||
private UserService userService;
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
@GetMapping("wrong1")
|
||
|
||
|
||
|
||
public int wrong1(@RequestParam("name") String name) {
|
||
|
||
|
||
|
||
return userService.createUserWrong1(name);
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
<p>调用接口后发现,即便用户名不合法,用户也能创建成功。刷新浏览器,多次发现有十几个的非法用户注册。</p>
|
||
|
||
<p>这里给出 @Transactional 生效原则 1,除非特殊配置(比如使用 AspectJ 静态织入实现 AOP),否则只有定义在 public 方法上的 @Transactional 才能生效。原因是,Spring 默认通过动态代理的方式实现 AOP,对目标方法进行增强,private 方法无法代理到,Spring 自然也无法动态增强事务处理逻辑。</p>
|
||
|
||
<p>你可能会说,修复方式很简单,把标记了事务注解的 createUserPrivate 方法改为 public 即可。在 UserService 中再建一个入口方法 createUserWrong2,来调用这个 public 方法再次尝试:</p>
|
||
|
||
<pre><code>public int createUserWrong2(String name) {
|
||
|
||
|
||
|
||
try {
|
||
|
||
|
||
|
||
this.createUserPublic(new UserEntity(name));
|
||
|
||
|
||
|
||
} catch (Exception ex) {
|
||
|
||
|
||
|
||
log.error("create user failed because {}", ex.getMessage());
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
return userRepository.findByName(name).size();
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
//标记了@Transactional的public方法
|
||
|
||
|
||
|
||
@Transactional
|
||
|
||
|
||
|
||
public void createUserPublic(UserEntity entity) {
|
||
|
||
|
||
|
||
userRepository.save(entity);
|
||
|
||
|
||
|
||
if (entity.getName().contains("test"))
|
||
|
||
|
||
|
||
throw new RuntimeException("invalid username!");
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
<p>测试发现,调用新的 createUserWrong2 方法事务同样不生效。这里,我给出 @Transactional 生效原则 2,必须通过代理过的类从外部调用目标方法才能生效。</p>
|
||
|
||
<p>Spring 通过 AOP 技术对方法进行增强,要调用增强过的方法必然是调用代理后的对象。我们尝试修改下 UserService 的代码,注入一个 self,然后再通过 self 实例调用标记有 @Transactional 注解的 createUserPublic 方法。设置断点可以看到,self 是由 Spring 通过 CGLIB 方式增强过的类:</p>
|
||
|
||
<p>CGLIB 通过继承方式实现代理类,private 方法在子类不可见,自然也就无法进行事务增强;</p>
|
||
|
||
<p>this 指针代表对象自己,Spring 不可能注入 this,所以通过 this 访问方法必然不是代理。</p>
|
||
|
||
<p><img src="assets/b077c033fa394353309fbb4f8368e46c.png" alt="img" /></p>
|
||
|
||
<p>把 this 改为 self 后测试发现,在 Controller 中调用 createUserRight 方法可以验证事务是生效的,非法的用户注册操作可以回滚。</p>
|
||
|
||
<p>虽然在 UserService 内部注入自己调用自己的 createUserPublic 可以正确实现事务,但更合理的实现方式是,让 Controller 直接调用之前定义的 UserService 的 createUserPublic 方法,因为注入自己调用自己很奇怪,也不符合分层实现的规范:</p>
|
||
|
||
<pre><code>@GetMapping("right2")
|
||
|
||
|
||
|
||
public int right2(@RequestParam("name") String name) {
|
||
|
||
|
||
|
||
try {
|
||
|
||
|
||
|
||
userService.createUserPublic(new UserEntity(name));
|
||
|
||
|
||
|
||
} catch (Exception ex) {
|
||
|
||
|
||
|
||
log.error("create user failed because {}", ex.getMessage());
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
return userService.getUserCount(name);
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
<p>我们再通过一张图来回顾下 this 自调用、通过 self 调用,以及在 Controller 中调用 UserService 三种实现的区别:</p>
|
||
|
||
<p><img src="assets/c43ea620b0b611ae194f8438506d7570.png" alt="img" /></p>
|
||
|
||
<p>通过 this 自调用,没有机会走到 Spring 的代理类;后两种改进方案调用的是 Spring 注入的 UserService,通过代理调用才有机会对 createUserPublic 方法进行动态增强。</p>
|
||
|
||
<p>这里,我还有一个小技巧,强烈建议你在开发时打开相关的 Debug 日志,以方便了解 Spring 事务实现的细节,并及时判断事务的执行情况。</p>
|
||
|
||
<p>我们的 Demo 代码使用 JPA 进行数据库访问,可以这么开启 Debug 日志:</p>
|
||
|
||
<pre><code>logging.level.org.springframework.orm.jpa=DEBUG
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
<p>开启日志后,我们再比较下在 UserService 中通过 this 调用和在 Controller 中通过注入的 UserService Bean 调用 createUserPublic 区别。很明显,this 调用因为没有走代理,事务没有在 createUserPublic 方法上生效,只在 Repository 的 save 方法层面生效:</p>
|
||
|
||
<pre><code>//在UserService中通过this调用public的createUserPublic
|
||
|
||
|
||
|
||
[10:10:19.913] [http-nio-45678-exec-1] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :370 ] - Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
|
||
|
||
|
||
|
||
//在Controller中通过注入的UserService Bean调用createUserPublic
|
||
|
||
|
||
|
||
[10:10:47.750] [http-nio-45678-exec-6] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :370 ] - Creating new transaction with name [org.geekbang.time.commonmistakes.transaction.demo1.UserService.createUserPublic]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
<p>你可能还会考虑一个问题,这种实现在 Controller 里处理了异常显得有点繁琐,还不如直接把 createUserWrong2 方法加上 @Transactional 注解,然后在 Controller 中直接调用这个方法。这样一来,既能从外部(Controller 中)调用 UserService 中的方法,方法又是 public 的能够被动态代理 AOP 增强。</p>
|
||
|
||
<p>你可以试一下这种方法,但很容易就会踩第二个坑,即因为没有正确处理异常,导致事务即便生效也不一定能回滚。</p>
|
||
|
||
<h2>事务即便生效也不一定能回滚</h2>
|
||
|
||
<p>通过 AOP 实现事务处理可以理解为,使用 try…catch…来包裹标记了 @Transactional 注解的方法,当方法出现了异常并且满足一定条件的时候,在 catch 里面我们可以设置事务回滚,没有异常则直接提交事务。</p>
|
||
|
||
<p>这里的“一定条件”,主要包括两点。</p>
|
||
|
||
<p>第一,只有异常传播出了标记了 @Transactional 注解的方法,事务才能回滚。在 Spring 的 TransactionAspectSupport 里有个 invokeWithinTransaction 方法,里面就是处理事务的逻辑。可以看到,只有捕获到异常才能进行后续事务处理:</p>
|
||
|
||
<pre><code>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
|
||
|
||
|
||
|
||
completeTransactionAfterThrowing(txInfo, ex);
|
||
|
||
|
||
|
||
throw ex;
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
finally {
|
||
|
||
|
||
|
||
cleanupTransactionInfo(txInfo);
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
<p>第二,默认情况下,出现 RuntimeException(非受检异常)或 Error 的时候,Spring 才会回滚事务。</p>
|
||
|
||
<p>打开 Spring 的 DefaultTransactionAttribute 类能看到如下代码块,可以发现相关证据,通过注释也能看到 Spring 这么做的原因,大概的意思是受检异常一般是业务异常,或者说是类似另一种方法的返回值,出现这样的异常可能业务还能完成,所以不会主动回滚;而 Error 或 RuntimeException 代表了非预期的结果,应该回滚:</p>
|
||
|
||
<pre><code>/**
|
||
|
||
|
||
|
||
\* The default behavior is as with EJB: rollback on unchecked exception
|
||
|
||
|
||
|
||
\* ({@link RuntimeException}), assuming an unexpected outcome outside of any
|
||
|
||
|
||
|
||
\* business rules. Additionally, we also attempt to rollback on {@link Error} which
|
||
|
||
|
||
|
||
\* is clearly an unexpected outcome as well. By contrast, a checked exception is
|
||
|
||
|
||
|
||
\* considered a business exception and therefore a regular expected outcome of the
|
||
|
||
|
||
|
||
\* transactional business method, i.e. a kind of alternative return value which
|
||
|
||
|
||
|
||
\* still allows for regular completion of resource operations.
|
||
|
||
|
||
|
||
\* <p>This is largely consistent with TransactionTemplate's default behavior,
|
||
|
||
|
||
|
||
\* except that TransactionTemplate also rolls back on undeclared checked exceptions
|
||
|
||
|
||
|
||
\* (a corner case). For declarative transactions, we expect checked exceptions to be
|
||
|
||
|
||
|
||
\* intentionally declared as business exceptions, leading to a commit by default.
|
||
|
||
|
||
|
||
\* @see org.springframework.transaction.support.TransactionTemplate#execute
|
||
|
||
|
||
|
||
*/
|
||
|
||
|
||
|
||
@Override
|
||
|
||
|
||
|
||
public boolean rollbackOn(Throwable ex) {
|
||
|
||
|
||
|
||
return (ex instanceof RuntimeException || ex instanceof Error);
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
<p>接下来,我和你分享 2 个反例。</p>
|
||
|
||
<p>重新实现一下 UserService 中的注册用户操作:</p>
|
||
|
||
<p>在 createUserWrong1 方法中会抛出一个 RuntimeException,但由于方法内 catch 了所有异常,异常无法从方法传播出去,事务自然无法回滚。</p>
|
||
|
||
<p>在 createUserWrong2 方法中,注册用户的同时会有一次 otherTask 文件读取操作,如果文件读取失败,我们希望用户注册的数据库操作回滚。虽然这里没有捕获异常,但因为 otherTask 方法抛出的是受检异常,createUserWrong2 传播出去的也是受检异常,事务同样不会回滚。</p>
|
||
|
||
<pre><code>@Service
|
||
|
||
|
||
|
||
@Slf4j
|
||
|
||
|
||
|
||
public class UserService {
|
||
|
||
|
||
|
||
@Autowired
|
||
|
||
|
||
|
||
private UserRepository userRepository;
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
//异常无法传播出方法,导致事务无法回滚
|
||
|
||
|
||
|
||
@Transactional
|
||
|
||
|
||
|
||
public void createUserWrong1(String name) {
|
||
|
||
|
||
|
||
try {
|
||
|
||
|
||
|
||
userRepository.save(new UserEntity(name));
|
||
|
||
|
||
|
||
throw new RuntimeException("error");
|
||
|
||
|
||
|
||
} catch (Exception ex) {
|
||
|
||
|
||
|
||
log.error("create user failed", ex);
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
//即使出了受检异常也无法让事务回滚
|
||
|
||
|
||
|
||
@Transactional
|
||
|
||
|
||
|
||
public void createUserWrong2(String name) throws IOException {
|
||
|
||
|
||
|
||
userRepository.save(new UserEntity(name));
|
||
|
||
|
||
|
||
otherTask();
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
//因为文件不存在,一定会抛出一个IOException
|
||
|
||
|
||
|
||
private void otherTask() throws IOException {
|
||
|
||
|
||
|
||
Files.readAllLines(Paths.get("file-that-not-exist"));
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
<p>Controller 中的实现,仅仅是调用 UserService 的 createUserWrong1 和 createUserWrong2 方法,这里就贴出实现了。这 2 个方法的实现和调用,虽然完全避开了事务不生效的坑,但因为异常处理不当,导致程序没有如我们期望的文件操作出现异常时回滚事务。</p>
|
||
|
||
<p>现在,我们来看下修复方式,以及如何通过日志来验证是否修复成功。针对这 2 种情况,对应的修复方法如下。</p>
|
||
|
||
<p>第一,如果你希望自己捕获异常进行处理的话,也没关系,可以手动设置让当前事务处于回滚状态:</p>
|
||
|
||
<pre><code>@Transactional
|
||
|
||
|
||
|
||
public void createUserRight1(String name) {
|
||
|
||
|
||
|
||
try {
|
||
|
||
|
||
|
||
userRepository.save(new UserEntity(name));
|
||
|
||
|
||
|
||
throw new RuntimeException("error");
|
||
|
||
|
||
|
||
} catch (Exception ex) {
|
||
|
||
|
||
|
||
log.error("create user failed", ex);
|
||
|
||
|
||
|
||
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>运行后可以在日志中看到 Rolling back 字样,确认事务回滚了。同时,我们还注意到“Transactional code has requested rollback”的提示,表明手动请求回滚:</p>
|
||
|
||
<pre><code>[22:14:49.352] [http-nio-45678-exec-4] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :698 ] - Transactional code has requested rollback
|
||
|
||
|
||
|
||
[22:14:49.353] [http-nio-45678-exec-4] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :834 ] - Initiating transaction rollback
|
||
|
||
|
||
|
||
[22:14:49.353] [http-nio-45678-exec-4] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :555 ] - Rolling back JPA transaction on EntityManager [SessionImpl(1906719643<open>)]
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
<p>第二,在注解中声明,期望遇到所有的 Exception 都回滚事务(来突破默认不回滚受检异常的限制):</p>
|
||
|
||
<pre><code>@Transactional(rollbackFor = Exception.class)
|
||
|
||
|
||
|
||
public void createUserRight2(String name) throws IOException {
|
||
|
||
|
||
|
||
userRepository.save(new UserEntity(name));
|
||
|
||
|
||
|
||
otherTask();
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>运行后,同样可以在日志中看到回滚的提示:</p>
|
||
|
||
<pre><code>[22:10:47.980] [http-nio-45678-exec-4] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :834 ] - Initiating transaction rollback
|
||
|
||
|
||
|
||
[22:10:47.981] [http-nio-45678-exec-4] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :555 ] - Rolling back JPA transaction on EntityManager [SessionImpl(1419329213<open>)]
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
<p>在这个例子中,我们展现的是一个复杂的业务逻辑,其中有数据库操作、IO 操作,在 IO 操作出现问题时希望让数据库事务也回滚,以确保逻辑的一致性。在有些业务逻辑中,可能会包含多次数据库操作,我们不一定希望将两次操作作为一个事务来处理,这时候就需要仔细考虑事务传播的配置了,否则也可能踩坑。</p>
|
||
|
||
<h2>请确认事务传播配置是否符合自己的业务逻辑</h2>
|
||
|
||
<p>有这么一个场景:一个用户注册的操作,会插入一个主用户到用户表,还会注册一个关联的子用户。我们希望将子用户注册的数据库操作作为一个独立事务来处理,即使失败也不会影响主流程,即不影响主用户的注册。</p>
|
||
|
||
<p>接下来,我们模拟一个实现类似业务逻辑的 UserService:</p>
|
||
|
||
<pre><code>@Autowired
|
||
|
||
|
||
|
||
private UserRepository userRepository;
|
||
|
||
|
||
|
||
@Autowired
|
||
|
||
|
||
|
||
private SubUserService subUserService;
|
||
|
||
|
||
|
||
@Transactional
|
||
|
||
|
||
|
||
public void createUserWrong(UserEntity entity) {
|
||
|
||
|
||
|
||
createMainUser(entity);
|
||
|
||
|
||
|
||
subUserService.createSubUserWithExceptionWrong(entity);
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
private void createMainUser(UserEntity entity) {
|
||
|
||
|
||
|
||
userRepository.save(entity);
|
||
|
||
|
||
|
||
log.info("createMainUser finish");
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
<p>SubUserService 的 createSubUserWithExceptionWrong 实现正如其名,因为最后我们抛出了一个运行时异常,错误原因是用户状态无效,所以子用户的注册肯定是失败的。我们期望子用户的注册作为一个事务单独回滚,不影响主用户的注册,这样的逻辑可以实现吗?</p>
|
||
|
||
<pre><code>@Service
|
||
|
||
|
||
|
||
@Slf4j
|
||
|
||
|
||
|
||
public class SubUserService {
|
||
|
||
|
||
|
||
@Autowired
|
||
|
||
|
||
|
||
private UserRepository userRepository;
|
||
|
||
|
||
|
||
@Transactional
|
||
|
||
|
||
|
||
public void createSubUserWithExceptionWrong(UserEntity entity) {
|
||
|
||
|
||
|
||
log.info("createSubUserWithExceptionWrong start");
|
||
|
||
|
||
|
||
userRepository.save(entity);
|
||
|
||
|
||
|
||
throw new RuntimeException("invalid status");
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
<p>我们在 Controller 里实现一段测试代码,调用 UserService:</p>
|
||
|
||
<pre><code>@GetMapping("wrong")
|
||
|
||
|
||
|
||
public int wrong(@RequestParam("name") String name) {
|
||
|
||
|
||
|
||
try {
|
||
|
||
|
||
|
||
userService.createUserWrong(new UserEntity(name));
|
||
|
||
|
||
|
||
} catch (Exception ex) {
|
||
|
||
|
||
|
||
log.error("createUserWrong failed, reason:{}", ex.getMessage());
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
return userService.getUserCount(name);
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
<p>调用后可以在日志中发现如下信息,很明显事务回滚了,最后 Controller 打出了创建子用户抛出的运行时异常:</p>
|
||
|
||
<pre><code>[22:50:42.866] [http-nio-45678-exec-8] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :555 ] - Rolling back JPA transaction on EntityManager [SessionImpl(103972212<open>)]
|
||
|
||
|
||
|
||
[22:50:42.869] [http-nio-45678-exec-8] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :620 ] - Closing JPA EntityManager [SessionImpl(103972212<open>)] after transaction
|
||
|
||
|
||
|
||
[22:50:42.869] [http-nio-45678-exec-8] [ERROR] [t.d.TransactionPropagationController:23 ] - createUserWrong failed, reason:invalid status
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
<p>你马上就会意识到,不对呀,因为运行时异常逃出了 @Transactional 注解标记的 createUserWrong 方法,Spring 当然会回滚事务了。如果我们希望主方法不回滚,应该把子方法抛出的异常捕获了。</p>
|
||
|
||
<p>也就是这么改,把 subUserService.createSubUserWithExceptionWrong 包裹上 catch,这样外层主方法就不会出现异常了:</p>
|
||
|
||
<pre><code>@Transactional
|
||
|
||
|
||
|
||
public void createUserWrong2(UserEntity entity) {
|
||
|
||
|
||
|
||
createMainUser(entity);
|
||
|
||
|
||
|
||
try{
|
||
|
||
|
||
|
||
subUserService.createSubUserWithExceptionWrong(entity);
|
||
|
||
|
||
|
||
} catch (Exception ex) {
|
||
|
||
|
||
|
||
// 虽然捕获了异常,但是因为没有开启新事务,而当前事务因为异常已经被标记为rollback了,所以最终还是会回滚。
|
||
|
||
|
||
|
||
log.error("create sub user error:{}", ex.getMessage());
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
<p>运行程序后可以看到如下日志:</p>
|
||
|
||
<pre><code>[22:57:21.722] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :370 ] - Creating new transaction with name [org.geekbang.time.commonmistakes.transaction.demo3.UserService.createUserWrong2]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
|
||
|
||
|
||
|
||
[22:57:21.739] [http-nio-45678-exec-3] [INFO ] [t.c.transaction.demo3.SubUserService:19 ] - createSubUserWithExceptionWrong start
|
||
|
||
|
||
|
||
[22:57:21.739] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :356 ] - Found thread-bound EntityManager [SessionImpl(1794007607<open>)] for JPA transaction
|
||
|
||
|
||
|
||
[22:57:21.739] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :471 ] - Participating in existing transaction
|
||
|
||
|
||
|
||
[22:57:21.740] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :843 ] - Participating transaction failed - marking existing transaction as rollback-only
|
||
|
||
|
||
|
||
[22:57:21.740] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :580 ] - Setting JPA transaction on EntityManager [SessionImpl(1794007607<open>)] rollback-only
|
||
|
||
|
||
|
||
[22:57:21.740] [http-nio-45678-exec-3] [ERROR] [.g.t.c.transaction.demo3.UserService:37 ] - create sub user error:invalid status
|
||
|
||
|
||
|
||
[22:57:21.740] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :741 ] - Initiating transaction commit
|
||
|
||
|
||
|
||
[22:57:21.740] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :529 ] - Committing JPA transaction on EntityManager [SessionImpl(1794007607<open>)]
|
||
|
||
|
||
|
||
[22:57:21.743] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :620 ] - Closing JPA EntityManager [SessionImpl(1794007607<open>)] after transaction
|
||
|
||
|
||
|
||
[22:57:21.743] [http-nio-45678-exec-3] [ERROR] [t.d.TransactionPropagationController:33 ] - createUserWrong2 failed, reason:Transaction silently rolled back because it has been marked as rollback-only
|
||
|
||
|
||
|
||
org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only
|
||
|
||
|
||
|
||
...
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
<p>需要注意以下几点:</p>
|
||
|
||
<p>如第 1 行所示,对 createUserWrong2 方法开启了异常处理;</p>
|
||
|
||
<p>如第 5 行所示,子方法因为出现了运行时异常,标记当前事务为回滚;</p>
|
||
|
||
<p>如第 7 行所示,主方法的确捕获了异常打印出了 create sub user error 字样;</p>
|
||
|
||
<p>如第 9 行所示,主方法提交了事务;</p>
|
||
|
||
<p>奇怪的是,如第 11 行和 12 行所示,Controller 里出现了一个 UnexpectedRollbackException,异常描述提示最终这个事务回滚了,而且是静默回滚的。之所以说是静默,是因为 createUserWrong2 方法本身并没有出异常,只不过提交后发现子方法已经把当前事务设置为了回滚,无法完成提交。</p>
|
||
|
||
<p>这挺反直觉的。我们之前说,出了异常事务不一定回滚,这里说的却是不出异常,事务也不一定可以提交。原因是,主方法注册主用户的逻辑和子方法注册子用户的逻辑是同一个事务,子逻辑标记了事务需要回滚,主逻辑自然也不能提交了。</p>
|
||
|
||
<p>看到这里,修复方式就很明确了,想办法让子逻辑在独立事务中运行,也就是改一下 SubUserService 注册子用户的方法,为注解加上 propagation = Propagation.REQUIRES_NEW 来设置 REQUIRES_NEW 方式的事务传播策略,也就是执行到这个方法时需要开启新的事务,并挂起当前事务:</p>
|
||
|
||
<pre><code>@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||
|
||
|
||
|
||
public void createSubUserWithExceptionRight(UserEntity entity) {
|
||
|
||
|
||
|
||
log.info("createSubUserWithExceptionRight start");
|
||
|
||
|
||
|
||
userRepository.save(entity);
|
||
|
||
|
||
|
||
throw new RuntimeException("invalid status");
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
<p>主方法没什么变化,同样需要捕获异常,防止异常漏出去导致主事务回滚,重新命名为 createUserRight:</p>
|
||
|
||
<pre><code>@Transactional
|
||
|
||
|
||
|
||
public void createUserRight(UserEntity entity) {
|
||
|
||
|
||
|
||
createMainUser(entity);
|
||
|
||
|
||
|
||
try{
|
||
|
||
|
||
|
||
subUserService.createSubUserWithExceptionRight(entity);
|
||
|
||
|
||
|
||
} catch (Exception ex) {
|
||
|
||
|
||
|
||
// 捕获异常,防止主方法回滚
|
||
|
||
|
||
|
||
log.error("create sub user error:{}", ex.getMessage());
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
<p>改造后,重新运行程序可以看到如下的关键日志:</p>
|
||
|
||
<p>第 1 行日志提示我们针对 createUserRight 方法开启了主方法的事务;</p>
|
||
|
||
<p>第 2 行日志提示创建主用户完成;</p>
|
||
|
||
<p>第 3 行日志可以看到主事务挂起了,开启了一个新的事务,针对 createSubUserWithExceptionRight 方案,也就是我们的创建子用户的逻辑;</p>
|
||
|
||
<p>第 4 行日志提示子方法事务回滚;</p>
|
||
|
||
<p>第 5 行日志提示子方法事务完成,继续主方法之前挂起的事务;</p>
|
||
|
||
<p>第 6 行日志提示主方法捕获到了子方法的异常;</p>
|
||
|
||
<p>第 8 行日志提示主方法的事务提交了,随后我们在 Controller 里没看到静默回滚的异常。</p>
|
||
|
||
<pre><code>[23:17:20.935] [http-nio-45678-exec-1] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :370 ] - Creating new transaction with name [org.geekbang.time.commonmistakes.transaction.demo3.UserService.createUserRight]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
|
||
|
||
|
||
|
||
[23:17:21.079] [http-nio-45678-exec-1] [INFO ] [.g.t.c.transaction.demo3.UserService:55 ] - createMainUser finish
|
||
|
||
|
||
|
||
[23:17:21.082] [http-nio-45678-exec-1] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :420 ] - Suspending current transaction, creating new transaction with name [org.geekbang.time.commonmistakes.transaction.demo3.SubUserService.createSubUserWithExceptionRight]
|
||
|
||
|
||
|
||
[23:17:21.153] [http-nio-45678-exec-1] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :834 ] - Initiating transaction rollback
|
||
|
||
|
||
|
||
[23:17:21.160] [http-nio-45678-exec-1] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :1009] - Resuming suspended transaction after completion of inner transaction
|
||
|
||
|
||
|
||
[23:17:21.161] [http-nio-45678-exec-1] [ERROR] [.g.t.c.transaction.demo3.UserService:49 ] - create sub user error:invalid status
|
||
|
||
|
||
|
||
[23:17:21.161] [http-nio-45678-exec-1] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :741 ] - Initiating transaction commit
|
||
|
||
|
||
|
||
[23:17:21.161] [http-nio-45678-exec-1] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :529 ] - Committing JPA transaction on EntityManager [SessionImpl(396441411<open>)]
|
||
|
||
|
||
|
||
</code></pre>
|
||
|
||
<p>运行测试程序看到如下结果,getUserCount 得到的用户数量为 1,代表只有一个用户也就是主用户注册完成了,符合预期:</p>
|
||
|
||
<p><img src="assets/3bd9c32b5144025f1a2de5b4ec436ff8.png" alt="img" /></p>
|
||
|
||
<h2>重点回顾</h2>
|
||
|
||
<p>今天,我针对业务代码中最常见的使用数据库事务的方式,即 Spring 声明式事务,与你总结了使用上可能遇到的三类坑,包括:</p>
|
||
|
||
<p>第一,因为配置不正确,导致方法上的事务没生效。我们务必确认调用 @Transactional 注解标记的方法是 public 的,并且是通过 Spring 注入的 Bean 进行调用的。</p>
|
||
|
||
<p>第二,因为异常处理不正确,导致事务虽然生效但出现异常时没回滚。Spring 默认只会对标记 @Transactional 注解的方法出现了 RuntimeException 和 Error 的时候回滚,如果我们的方法捕获了异常,那么需要通过手动编码处理事务回滚。如果希望 Spring 针对其他异常也可以回滚,那么可以相应配置 @Transactional 注解的 rollbackFor 和 noRollbackFor 属性来覆盖其默认设置。</p>
|
||
|
||
<p>第三,如果方法涉及多次数据库操作,并希望将它们作为独立的事务进行提交或回滚,那么我们需要考虑进一步细化配置事务传播方式,也就是 @Transactional 注解的 Propagation 属性。</p>
|
||
|
||
<p>可见,正确配置事务可以提高业务项目的健壮性。但,又因为健壮性问题往往体现在异常情况或一些细节处理上,很难在主流程的运行和测试中发现,导致业务代码的事务处理逻辑往往容易被忽略,因此我在代码审查环节一直很关注事务是否正确处理。</p>
|
||
|
||
<p>如果你无法确认事务是否真正生效,是否按照预期的逻辑进行,可以尝试打开 Spring 的部分 Debug 日志,通过事务的运作细节来验证。也建议你在单元测试时尽量覆盖多的异常场景,这样在重构时,也能及时发现因为方法的调用方式、异常处理逻辑的调整,导致的事务失效问题。</p>
|
||
|
||
<p>今天用到的代码,我都放在了 GitHub 上,你可以点击这个链接查看。</p>
|
||
|
||
<h2>思考与讨论</h2>
|
||
|
||
<p>考虑到 Demo 的简洁,文中所有数据访问使用的都是 Spring Data JPA。国内大多数互联网业务项目是使用 MyBatis 进行数据访问的,使用 MyBatis 配合 Spring 的声明式事务也同样需要注意文中提到的这些点。你可以尝试把今天的 Demo 改为 MyBatis 做数据访问实现,看看日志中是否可以体现出这些坑。</p>
|
||
|
||
<p>在第一节中我们提到,如果要针对 private 方法启用事务,动态代理方式的 AOP 不可行,需要使用静态织入方式的 AOP,也就是在编译期间织入事务增强代码,可以配置 Spring 框架使用 AspectJ 来实现 AOP。你能否参阅 Spring 的文档“Using @Transactional with AspectJ”试试呢?注意:AspectJ 配合 lombok 使用,还可能会踩一些坑。</p>
|
||
|
||
<p>有关数据库事务,你还遇到过其他坑吗?我是朱晔,欢迎在评论区与我留言分享,也欢迎你把这篇文章分享给你的朋友或同事,一起交流。</p>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div>
|
||
|
||
<div style="float: left">
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/05 HTTP调用:你考虑到超时、重试、并发了吗?.md.html">上一页</a>
|
||
|
||
</div>
|
||
|
||
<div style="float: right">
|
||
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/07 数据库索引:索引并不是万能药.md.html">下一页</a>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
|
||
|
||
</div>
|
||
|
||
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"7099701d7ce63d60","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
|
||
|
||
</body>
|
||
|
||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||
|
||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
|
||
|
||
<script>
|
||
|
||
window.dataLayer = window.dataLayer || [];
|
||
|
||
|
||
|
||
function gtag() {
|
||
|
||
dataLayer.push(arguments);
|
||
|
||
}
|
||
|
||
|
||
|
||
gtag('js', new Date());
|
||
|
||
gtag('config', 'G-NPSEEVD756');
|
||
|
||
var path = window.location.pathname
|
||
|
||
var cookie = getCookie("lastPath");
|
||
|
||
console.log(path)
|
||
|
||
if (path.replace("/", "") === "") {
|
||
|
||
if (cookie.replace("/", "") !== "") {
|
||
|
||
console.log(cookie)
|
||
|
||
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
|
||
|
||
}
|
||
|
||
} else {
|
||
|
||
setCookie("lastPath", path)
|
||
|
||
}
|
||
|
||
|
||
|
||
function setCookie(cname, cvalue) {
|
||
|
||
var d = new Date();
|
||
|
||
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
|
||
|
||
var expires = "expires=" + d.toGMTString();
|
||
|
||
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
|
||
|
||
}
|
||
|
||
|
||
|
||
function getCookie(cname) {
|
||
|
||
var name = cname + "=";
|
||
|
||
var ca = document.cookie.split(';');
|
||
|
||
for (var i = 0; i < ca.length; i++) {
|
||
|
||
var c = ca[i].trim();
|
||
|
||
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
|
||
|
||
}
|
||
|
||
return "";
|
||
|
||
}
|
||
|
||
|
||
|
||
</script>
|
||
|
||
|
||
|
||
</html>
|
||
|