jdbctemplate 开启事务_SpringBoot 系列教程之事务隔离级别知识点小结
上一篇博文介绍了声明式事务@Transactional的简单使用姿势,最文章的最后给出了这个注解的多个属性,本文将着重放在事务隔离级别的知识点上,并通过实例演示不同的事务隔离级别下,脏读、不可重复读、幻读的具体场景
I. 基础知识
在进入正文之前,先介绍一下事务隔离级别的一些基础知识点,详细内容,推荐参考博文
mysql 之锁与事务[1]
1. 基本概念
以下基本概念源于个人理解之后,通过简单的 case 进行描述,如有问题,欢迎拍砖
更新丢失
简单来讲,两个事务 A,B 分别更新一条记录的 filedA, filedB 字段,其中事务 B 异常,导致回滚,将这条记录的恢复为修改之前的状态,导致事务 A 的修改丢失了,这就是更新丢失
脏读
读取到另外一个事务未提交的修改,所以当另外一个事务是失败导致回滚的时候,这个读取的数据其实是不准确的,这就是脏读
不可重复读
简单来讲,就是一个事务内,多次查询同一个数据,返回的结果居然不一样,这就是不可重复度(重复读取的结果不一样)
幻读
同样是多次查询,但是后面查询时,发现多了或者少了一些记录
比如:查询 id 在[1,10]之间的记录,第一次返回了 1,2,3 三条记录;但是另外一个事务新增了一个 id 为 4 的记录,导致再次查询时,返回了 1,2,3,4 四条记录,第二次查询时多了一条记录,这就是幻读
幻读和不可重复读的主要区别在于:
- 幻读针对的是查询结果为多个的场景,出现了数据的增加 or 减少
- 不可重复读对的是某些特定的记录,这些记录的数据与之前不一致
2. 隔离级别
后面测试的数据库为 mysql,引擎为 innodb,对应有四个隔离级别
隔离级别说明fixnot fixRU(read uncommitted)未授权读,读事务允许其他读写事务;未提交写事务禁止其他写事务(读事务 ok)更新丢失脏读,不可重复读,幻读RC(read committed)授权读,读事务允许其他读写事务;未提交写事务,禁止其他读写事务更新丢失,脏读不可重复读,幻读RR(repeatable read)可重复度,读事务禁止其他写事务;未提交写事务,禁止其他读写事务更新丢失,脏读,不可重复度幻读serializable序列化读,所有事务依次执行更新丢失,脏读,不可重复度,幻读-
说明,下面纯为个人观点,不代表权威,谨慎理解和引用
- 我个人的观点,rr 级别在 mysql 的 innodb 引擎上,配合 mvvc + gap 锁,已经解决了幻读问题
- 下面这个 case 是幻读问题么?
- 从锁的角度来看,步骤 1、2 虽然开启事务,但是属于快照读;而 9 属于当前读;他们读取的源不同,应该不算在幻读定义中的同一查询条件中
II. 配置
接下来进入实例演示环节,首先需要准备环境,创建测试项目
创建一个 SpringBoot 项目,版本为2.2.1.RELEASE,使用 mysql 作为目标数据库,存储引擎选择Innodb,事务隔离级别为 RR
1. 项目配置
在项目pom.xml文件中,加上spring-boot-starter-jdbc,会注入一个DataSourceTransactionManager的 bean,提供了事务支持
mysql mysql-connector-javaorg.springframework.boot spring-boot-starter-jdbc
2. 数据库配置
进入 spring 配置文件application.properties,设置一下 db 相关的信息
## DataSourcespring.datasource.url=jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=falsespring.datasource.username=rootspring.datasource.password=
3. 数据库
新建一个简单的表结构,用于测试
CREATETABLE`money` ( `id`int(11) unsignedNOTNULL AUTO_INCREMENT, `name`varchar(20) NOTNULLDEFAULT''COMMENT'用户名', `money`int(26) NOTNULLDEFAULT'0'COMMENT'钱', `is_deleted`tinyint(1) NOTNULLDEFAULT'0', `create_at`timestampNOTNULLDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间', `update_at`timestampNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'更新时间', PRIMARY KEY (`id`), KEY`name` (`name`)) ENGINE=InnoDB AUTO_INCREMENT=1DEFAULTCHARSET=utf8mb4;
III. 实例演示
1. 初始化数据
准备一些用于后续操作的数据
@Componentpublicclass DetailDemo { @Autowired private JdbcTemplate jdbcTemplate; @PostConstruct public void init() { String sql = "replace into money (id, name, money) values (320, '初始化', 200)," + "(330, '初始化', 200)," + "(340, '初始化', 200)," + "(350, '初始化', 200)"; jdbcTemplate.execute(sql); }}
提供一些基本的查询和修改方法
private boolean updateName(int id) { String sql = "update money set `name`='更新' where id=" + id; jdbcTemplate.execute(sql); returntrue;}public void query(String tag, int id) { String sql = "select * from money where id=" + id; Map map = jdbcTemplate.queryForMap(sql); System.out.println(tag + " >>>> " + map);}private boolean updateMoney(int id) { String sql = "update money set `money`= `money` + 10 where id=" + id; jdbcTemplate.execute(sql); returnfalse;}
2. RU 隔离级别
我们先来测试 RU 隔离级别,通过指定@Transactional注解的isolation属性来设置事务的隔离级别
通过前面的描述,我们知道 RU 会有脏读问题,接下来设计一个 case,进行演示
事务一,修改数据
/** * ru隔离级别的事务,可能出现脏读,不可避免不可重复读,幻读 * * @param id */@Transactional(isolation = Isolation.READ_UNCOMMITTED, rollbackFor = Exception.class)public boolean ruTransaction(int id) throws InterruptedException { if (this.updateName(id)) { this.query("ru: after updateMoney name", id); Thread.sleep(2000); if (this.updateMoney(id)) { returntrue; } } this.query("ru: after updateMoney money", id); returnfalse;}
只读事务二(设置 readOnly 为 true,则事务为只读)多次读取相同的数据,我们希望在事务二的第一次读取中,能获取到事务一的中间修改结果(所以请注意两个方法中的 sleep 使用)
@Transactional(readOnly = true, isolation = Isolation.READ_UNCOMMITTED, rollbackFor = Exception.class)public boolean readRuTransaction(int id) throws InterruptedException { this.query("ru read only", id); Thread.sleep(1000); this.query("ru read only", id); returntrue;}
接下来属于测试的 case,用两个线程来调用只读事务,和读写事务
@Componentpublicclass DetailTransactionalSample { @Autowired private DetailDemo detailDemo; /** * ru 隔离级别 */ public void testRuIsolation() throws InterruptedException { int id = 330; new Thread(new Runnable() { @Override public void run() { call("ru: 只读事务 - read", id, detailDemo::readRuTransaction); } }).start(); call("ru 读写事务", id, detailDemo::ruTransaction); }}private void call(String tag, int id, CallFunc func) { System.out.println("============ " + tag + " start ========== "); try { func.apply(id); } catch (Exception e) { } System.out.println("============ " + tag + " end ========== ");}@FunctionalInterfacepublicinterface CallFunc { R apply(T t) throws Exception;}
输出结果如下
============ ru 读写事务 start ====================== ru: 只读事务 - read start ==========ru read only >>>> {id=330, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 11:37:51.0, update_at=2020-01-20 11:37:51.0}ru: after updateMoney name >>>> {id=330, name=更新, money=200, is_deleted=false, create_at=2020-01-20 11:37:51.0, update_at=2020-01-20 11:37:52.0}ru read only >>>> {id=330, name=更新, money=200, is_deleted=false, create_at=2020-01-20 11:37:51.0, update_at=2020-01-20 11:37:52.0}============ ru: 只读事务 - read end ==========ru: after updateMoney money >>>> {id=330, name=更新, money=210, is_deleted=false, create_at=2020-01-20 11:37:51.0, update_at=2020-01-20 11:37:54.0}============ ru 读写事务 end ==========
关注一下上面结果中ru read only >>>>开头的记录,首先两次输出结果不一致,所以不可重复读问题是存在的
其次,第二次读取的数据与读写事务中的中间结果一致,即读取到了未提交的结果,即为脏读
3. RC 事务隔离级别
rc 隔离级别,可以解决脏读,但是不可重复读问题无法避免,所以我们需要设计一个 case,看一下是否可以读取另外一个事务提交后的结果
在前面的测试 case 上,稍微改一改
// ---------- rc 事物隔离级别// 测试不可重复读,一个事务内,两次读取的结果不一样@Transactional(readOnly = true, isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class)public boolean readRcTransaction(int id) throws InterruptedException { this.query("rc read only", id); Thread.sleep(1000); this.query("rc read only", id); Thread.sleep(3000); this.query("rc read only", id); returntrue;}/** * rc隔离级别事务,未提交的写事务,会挂起其他的读写事务;可避免脏读,更新丢失;但不能防止不可重复读、幻读 * * @param id * @return */@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class)public boolean rcTranaction(int id) throws InterruptedException { if (this.updateName(id)) { this.query("rc: after updateMoney name", id); Thread.sleep(2000); if (this.updateMoney(id)) { returntrue; } } returnfalse;}
测试用例
/** * rc 隔离级别 */private void testRcIsolation() throws InterruptedException { int id = 340; new Thread(new Runnable() { @Override public void run() { call("rc: 只读事务 - read", id, detailDemo::readRcTransaction); } }).start(); Thread.sleep(1000); call("rc 读写事务 - read", id, detailDemo::rcTranaction);}
输出结果如下
============ rc: 只读事务 - read start ==========rc read only >>>> {id=340, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:17.0}============ rc 读写事务 - read start ==========rc: after updateMoney name >>>> {id=340, name=更新, money=200, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:23.0}rc read only >>>> {id=340, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:17.0}============ rc 读写事务 - read end ==========rc read only >>>> {id=340, name=更新, money=210, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:25.0}============ rc: 只读事务 - read end ==========
从上面的输出中,在只读事务,前面两次查询,结果一致,虽然第二次查询时,读写事务修改了这个记录,但是并没有读取到这个中间记录状态,所以这里没有脏读问题;
当读写事务完毕之后,只读事务的第三次查询中,返回的是读写事务提交之后的结果,导致了不可重复读
4. RR 事务隔离级别
针对 rr,我们主要测试一下不可重复读的解决情况,设计 case 相对简单
/** * 只读事务,主要目的是为了隔离其他事务的修改,对本次操作的影响; * * 比如在某些耗时的涉及多次表的读取操作中,为了保证数据一致性,这个就有用了;开启只读事务之后,不支持修改数据 */@Transactional(readOnly = true, isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class)public boolean readRrTransaction(int id) throws InterruptedException { this.query("rr read only", id); Thread.sleep(3000); this.query("rr read only", id); returntrue;}/** * rr隔离级别事务,读事务禁止其他的写事务,未提交写事务,会挂起其他读写事务;可避免脏读,不可重复读,(我个人认为,innodb引擎可通过mvvc+gap锁避免幻读) * * @param id * @return */@Transactional(isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class)public boolean rrTransaction(int id) { if (this.updateName(id)) { this.query("rr: after updateMoney name", id); if (this.updateMoney(id)) { returntrue; } } returnfalse;}
我们希望读写事务的执行周期在只读事务的两次查询之内,所有测试代码如下
/** * rr * 测试只读事务 */private void testReadOnlyCase() throws InterruptedException { // 子线程开启只读事务,主线程执行修改 int id = 320; new Thread(new Runnable() { @Override public void run() { call("rr 只读事务 - read", id, detailDemo::readRrTransaction); } }).start(); Thread.sleep(1000); call("rr 读写事务", id, detailDemo::rrTransaction);}
输出结果
============ rr 只读事务 - read start ==========rr read only >>>> {id=320, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:17.0}============ rr 读写事务 start ==========rr: after updateMoney name >>>> {id=320, name=更新, money=200, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:28.0}============ rr 读写事务 end ==========rr read only >>>> {id=320, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:17.0}============ rr 只读事务 - read end ==========
两次只读事务的输出一致,并没有出现上面的不可重复读问题
说明
- @Transactional注解的默认隔离级别为Isolation#DEFAULT,也就是采用数据源的隔离级别,mysql innodb 引擎默认隔离级别为 RR(所有不额外指定时,相当于 RR)
5. SERIALIZABLE 事务隔离级别
串行事务隔离级别,所有的事务串行执行,实际的业务场景中,我没用过... 也不太能想像,什么场景下需要这种
@Transactional(readOnly = true, isolation = Isolation.SERIALIZABLE, rollbackFor = Exception.class)public boolean readSerializeTransaction(int id) throws InterruptedException { this.query("serialize read only", id); Thread.sleep(3000); this.query("serialize read only", id); returntrue;}/** * serialize,事务串行执行,fix所有问题,但是性能低 * * @param id * @return */@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = Exception.class)public boolean serializeTransaction(int id) { if (this.updateName(id)) { this.query("serialize: after updateMoney name", id); if (this.updateMoney(id)) { returntrue; } } returnfalse;}
测试 case
/** * Serialize 隔离级别 */private void testSerializeIsolation() throws InterruptedException { int id = 350; new Thread(new Runnable() { @Override public void run() { call("Serialize: 只读事务 - read", id, detailDemo::readSerializeTransaction); } }).start(); Thread.sleep(1000); call("Serialize 读写事务 - read", id, detailDemo::serializeTransaction);}
输出结果如下
============ Serialize: 只读事务 - read start ==========serialize read only >>>> {id=350, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 12:10:23.0, update_at=2020-01-20 12:10:23.0}============ Serialize 读写事务 - read start ==========serialize read only >>>> {id=350, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 12:10:23.0, update_at=2020-01-20 12:10:23.0}============ Serialize: 只读事务 - read end ==========serialize: after updateMoney name >>>> {id=350, name=更新, money=200, is_deleted=false, create_at=2020-01-20 12:10:23.0, update_at=2020-01-20 12:10:39.0}============ Serialize 读写事务 - read end ==========
只读事务的查询输出之后,才输出读写事务的日志,简单来讲就是读写事务中的操作被 delay 了
6. 小结
本文主要介绍了事务的几种隔离级别,已经不同干的隔离级别对应的场景,可能出现的问题;
隔离级别说明
级别fixnot fixRU更新丢失脏读,不可重复读,幻读RC更新丢失 脏读不可重复读,幻读RR更新丢、脏读,不可重复读,幻读-serialze更新丢失、 脏读,不可重复读,幻读-
使用说明
- mysql innodb 引擎默认为 RR 隔离级别;@Transactinoal注解使用数据库的隔离级别,即 RR
- 通过指定Transactional#isolation来设置事务的事务级别
IV. 其他
0. 系列博文&源码
源码
- 工程:https://github.com/liuyueyi/spring-boot-demo[8]
- 实例源码: https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-boot/101-jdbctemplate-transaction[9]
jdbctemplate 开启事务_SpringBoot 系列教程之事务隔离级别知识点小结相关推荐
- sprinboot中编程式事务_SpringBoot系列教程之事务传递属性
200202-SpringBoot系列教程之事务传递属性 对于mysql而言,关于事务的主要知识点可能几种在隔离级别上:在Spring体系中,使用事务的时候,还有一个知识点事务的传递属性同样重要,本文 ...
- Spring的AOP和IOC是什么?使用场景有哪些?Spring事务与数据库事务,传播行为,数据库隔离级别
Spring的AOP和IOC是什么?使用场景有哪些?Spring事务与数据库事务,传播行为,数据库隔离级别 AOP:面向切面编程. 即在一个功能模块中新增其他功能,比方说你要下楼取个快递,你同事对你说 ...
- 一天十道Java面试题----第四天(线程池复用的原理------>spring事务的实现方式原理以及隔离级别)
这里是参考B站上的大佬做的面试题笔记.大家也可以去看视频讲解!!! 文章目录 31.线程池复用的原理 32.spring是什么? 33.对Aop的理解 34.对IOC的理解 35.BeanFactor ...
- 别看是面试问烂的题目,一面试你照样还是不会系列MySQL四种隔离级别,看完吊打面试官!
别看是面试问烂的题目,一面试你照样还是不会系列MySQL四种隔离级别,看完吊打面试官! 什么是事务 事务是应用程序中一系列严密的操作,所有操作必须成功完成,否则在每个操作中所作的所有更改都会被撤消.也 ...
- spring boot 跨域请求_SpringBoot 系列教程 web 篇之自定义请求匹配条件 RequestCondition...
191222-SpringBoot 系列教程 web 篇之自定义请求匹配条件 RequestCondition 在 spring mvc 中,我们知道用户发起的请求可以通过 url 匹配到我们通过@R ...
- springboot banner在线生成_SpringBoot系列教程10--小花样之SpringBoot配置自定义Banner
SpringBoot系列教程10--小花样之SpringBoot配置自定义Banner 作者:一一哥 一. Spring Boot 常用配置 本章节主要介绍一下 Spring Boot 中的一些常用配 ...
- mysql分布式事务wcf_WCF系列_分布式事务(下)
1.WCF分布式事务例子 这里也用转账的例子说事. 用户在系统A和系统B都有账户,账户间的资金可以互转,系统A的资金减少多少,系统B的相应账户的资金就增加多少. 系统A机器上有数据库AccountA, ...
- 数据库事务的4大特性与隔离级别
本篇讲诉数据库中事务的四大特性(ACID),并且将会详细地说明事务的隔离级别. 如果一个数据库声称支持事务的操作,那么该数据库必须要具备以下四个特性: ⑴ 原子性(Atomicity) 原子性是指事务 ...
- 同一个事务里面对同一条数据做2次修改_MySQL事务与MVCC如何实现的隔离级别
有情怀,有干货,微信搜索[三太子敖丙]关注这个不一样的程序员. 本文 GitHub https://github.com/JavaFamily 已收录,有一线大厂面试完整考点.资料以及我的系列文章. ...
最新文章
- split命令用法(shell)
- normalize()方法
- c/c++标准库中的文件操作总结
- 吞吐量、速率、背板带宽
- GNS3 1.5.2 无法上传文件的解决办法
- Android SharedPreferences的简单使用
- Ubuntu下很给力的下载工具
- Codeforces Round #412 (rated, Div. 2, base on VK Cup 2017 Round 3) A	 Is it rated?
- mysql增删改查sql语句_sql增删改查语句是什么?
- Java制作一个更加真实的按钮
- SVN版本管理的回滚(SmartSVN)
- WhatsApp选择了便利而不是隐私,这是解决问题的方法
- Cortex-M的M0,M+,M3,M4,M7几种内核的简单区别
- 怎么更改计算机物理地址,修改MAC地址,教您怎么修改MAC地址
- 地理信息系统矢量数据的组织形式 第二章:数字表面模型
- 郎咸平:用友和金蝶的长板在哪儿
- Nature:细菌增长和扩张的平衡机制
- 三级数据库技术|重要知识点(一)
- 只要活着,我愿意一辈子都做程序员
- 英语邮件撰写 | WpEmail笔记 + Additional Resources
热门文章
- sharepoint安装心得_过程
- quill一些插件的配置 注意事项
- Win8 Consumer Preview 8250 + VS11 体验
- java 计算工具类_java精确计算工具类
- tftp ping 不通。无法访问目标主机
- pandas用均值填充nan_python – 如何用pandas中的滚动平均值填充nan值
- linux中的文件,文件夹,链接的权限划分
- windows 2502 2503 错误解决
- 实验项目 3-4:一元多项式的乘法与加法运算
- 应用软件使计算机的内存分配更合理,应用软件使计算机的内存分配更合理,运行更加稳定()...