美文网首页
2020-08-16

2020-08-16

作者: 黄榕生Ron | 来源:发表于2020-08-16 17:31 被阅读0次


    I. 基础知识

    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 fix

    RU(read uncommitted)未授权读,读事务允许其他读写事务;未提交写事务禁止其他写事务(读事务 ok)更新丢失脏读,不可重复读,幻读

    RC(read committed)授权读,读事务允许其他读写事务;未提交写事务,禁止其他读写事务更新丢失,脏读不可重复读,幻读

    RR(repeatable read)可重复度,读事务禁止其他写事务;未提交写事务,禁止其他读写事务更新丢失,脏读,不可重复度<del>幻读</del>

    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,提供了事务支持

    mysqlmysql-connector-javaorg.springframework.bootspring-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)unsignedNOTNULLAUTO_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'更新时间',  PRIMARYKEY(`id`),KEY`name`(`name`))ENGINE=InnoDBAUTO_INCREMENT=1DEFAULTCHARSET=utf8mb4;

    III. 实例演示

    1. 初始化数据

    准备一些用于后续操作的数据

    @ComponentpublicclassDetailDemo{@AutowiredprivateJdbcTemplate jdbcTemplate;@PostConstructpublicvoidinit(){        String sql ="replace into money (id, name, money) values (320, '初始化', 200),"+"(330, '初始化', 200),"+"(340, '初始化', 200),"+"(350, '初始化', 200)";        jdbcTemplate.execute(sql);    }}

    提供一些基本的查询和修改方法

    privatebooleanupdateName(intid){    String sql ="update money set `name`='更新' where id="+ id;    jdbcTemplate.execute(sql);returntrue;}publicvoidquery(String tag,intid){    String sql ="select * from money where id="+ id;    Map map = jdbcTemplate.queryForMap(sql);    System.out.println(tag +" >>>> "+ map);}privatebooleanupdateMoney(intid){    String sql ="update money set `money`= `money` + 10 where id="+ id;    jdbcTemplate.execute(sql);returnfalse;}

    2. RU 隔离级别

    我们先来测试 RU 隔离级别,通过指定@Transactional注解的isolation属性来设置事务的隔离级别

    通过前面的描述,我们知道 RU 会有脏读问题,接下来设计一个 case,进行演示

    事务一,修改数据

    /** * ru隔离级别的事务,可能出现脏读,不可避免不可重复读,幻读 * *@paramid */@Transactional(isolation = Isolation.READ_UNCOMMITTED, rollbackFor = Exception.class)publicbooleanruTransaction(intid)throwsInterruptedException{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)publicbooleanreadRuTransaction(intid)throwsInterruptedException{this.query("ru read only", id);    Thread.sleep(1000);this.query("ru read only", id);returntrue;}

    接下来属于测试的 case,用两个线程来调用只读事务,和读写事务

    @ComponentpublicclassDetailTransactionalSample{@AutowiredprivateDetailDemo detailDemo;/**

        * ru 隔离级别

        */publicvoidtestRuIsolation()throwsInterruptedException{intid =330;newThread(newRunnable() {@Overridepublicvoidrun(){                call("ru: 只读事务 - read", id, detailDemo::readRuTransaction);            }        }).start();        call("ru 读写事务", id, detailDemo::ruTransaction);    }}privatevoidcall(String tag,intid, CallFunc func){    System.out.println("============ "+ tag +" start ========== ");try{        func.apply(id);    }catch(Exception e) {    }    System.out.println("============ "+ tag +" end ========== \n");}@FunctionalInterfacepublicinterfaceCallFunc{Rapply(T t)throwsException;}

    输出结果如下

    ============ ru 读写事务start====================== ru: 只读事务 -readstart==========rureadonly>>>> {id=330,name=初始化, money=200, is_deleted=false, create_at=2020-01-2011:37:51.0, update_at=2020-01-2011:37:51.0}ru:afterupdateMoneyname>>>> {id=330,name=更新, money=200, is_deleted=false, create_at=2020-01-2011:37:51.0, update_at=2020-01-2011:37:52.0}rureadonly>>>> {id=330,name=更新, money=200, is_deleted=false, create_at=2020-01-2011:37:51.0, update_at=2020-01-2011:37:52.0}============ ru: 只读事务 -readend==========ru:afterupdateMoney money >>>> {id=330,name=更新, money=210, is_deleted=false, create_at=2020-01-2011:37:51.0, update_at=2020-01-2011:37:54.0}============ ru 读写事务end==========

    关注一下上面结果中ru read only >>>>开头的记录,首先两次输出结果不一致,所以不可重复读问题是存在的

    其次,第二次读取的数据与读写事务中的中间结果一致,即读取到了未提交的结果,即为脏读

    3. RC 事务隔离级别

    rc 隔离级别,可以解决脏读,但是不可重复读问题无法避免,所以我们需要设计一个 case,看一下是否可以读取另外一个事务提交后的结果

    在前面的测试 case 上,稍微改一改

    // ---------- rc 事物隔离级别// 测试不可重复读,一个事务内,两次读取的结果不一样@Transactional(readOnly =true, isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class)publicbooleanreadRcTransaction(intid)throwsInterruptedException{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隔离级别事务,未提交的写事务,会挂起其他的读写事务;可避免脏读,更新丢失;但不能防止不可重复读、幻读 * *@paramid *@return*/@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class)publicbooleanrcTranaction(intid)throwsInterruptedException{if(this.updateName(id)) {this.query("rc: after updateMoney name", id);        Thread.sleep(2000);if(this.updateMoney(id)) {returntrue;        }    }returnfalse;}

    测试用例

    /**

    * rc 隔离级别

    */privatevoidtestRcIsolation()throwsInterruptedException{intid =340;newThread(newRunnable() {@Overridepublicvoidrun(){            call("rc: 只读事务 - read", id, detailDemo::readRcTransaction);        }    }).start();    Thread.sleep(1000);    call("rc 读写事务 - read", id, detailDemo::rcTranaction);}

    输出结果如下

    ============ rc: 只读事务 - readstart==========rcreadonly>>>> {id=340,name=初始化, money=200, is_deleted=false, create_at=2020-01-2011:46:17.0, update_at=2020-01-2011:46:17.0}============ rc 读写事务 -readstart==========rc:afterupdateMoneyname>>>> {id=340,name=更新, money=200, is_deleted=false, create_at=2020-01-2011:46:17.0, update_at=2020-01-2011:46:23.0}rcreadonly>>>> {id=340,name=初始化, money=200, is_deleted=false, create_at=2020-01-2011:46:17.0, update_at=2020-01-2011:46:17.0}============ rc 读写事务 -readend==========rcreadonly>>>> {id=340,name=更新, money=210, is_deleted=false, create_at=2020-01-2011:46:17.0, update_at=2020-01-2011:46:25.0}============ rc: 只读事务 -readend==========

    从上面的输出中,在只读事务,前面两次查询,结果一致,虽然第二次查询时,读写事务修改了这个记录,但是并没有读取到这个中间记录状态,所以这里没有脏读问题;

    当读写事务完毕之后,只读事务的第三次查询中,返回的是读写事务提交之后的结果,导致了不可重复读

    4. RR 事务隔离级别

    针对 rr,我们主要测试一下不可重复读的解决情况,设计 case 相对简单

    /**

    * 只读事务,主要目的是为了隔离其他事务的修改,对本次操作的影响;

    *

    * 比如在某些耗时的涉及多次表的读取操作中,为了保证数据一致性,这个就有用了; 开启只读事务之后,不支持修改数据

    */@Transactional(readOnly =true, isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class)publicbooleanreadRrTransaction(intid)throwsInterruptedException{this.query("rr read only", id);    Thread.sleep(3000);this.query("rr read only", id);returntrue;}/** * rr隔离级别事务,读事务禁止其他的写事务,未提交写事务,会挂起其他读写事务;可避免脏读,不可重复读,(我个人认为,innodb引擎可通过mvvc+gap锁避免幻读) * *@paramid *@return*/@Transactional(isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class)publicbooleanrrTransaction(intid){if(this.updateName(id)) {this.query("rr: after updateMoney name", id);if(this.updateMoney(id)) {returntrue;        }    }returnfalse;}

    我们希望读写事务的执行周期在只读事务的两次查询之内,所有测试代码如下

    /**

    * rr

    * 测试只读事务

    */privatevoidtestReadOnlyCase()throwsInterruptedException{// 子线程开启只读事务,主线程执行修改intid =320;newThread(newRunnable() {@Overridepublicvoidrun(){            call("rr 只读事务 - read", id, detailDemo::readRrTransaction);        }    }).start();    Thread.sleep(1000);    call("rr 读写事务", id, detailDemo::rrTransaction);}

    输出结果

    ============ rr 只读事务 - readstart==========rrreadonly>>>> {id=320,name=初始化, money=200, is_deleted=false, create_at=2020-01-2011:46:17.0, update_at=2020-01-2011:46:17.0}============ rr 读写事务start==========rr:afterupdateMoneyname>>>> {id=320,name=更新, money=200, is_deleted=false, create_at=2020-01-2011:46:17.0, update_at=2020-01-2011:46:28.0}============ rr 读写事务end==========rrreadonly>>>> {id=320,name=初始化, money=200, is_deleted=false, create_at=2020-01-2011:46:17.0, update_at=2020-01-2011:46:17.0}============ rr 只读事务 -readend==========

    两次只读事务的输出一致,并没有出现上面的不可重复读问题

    说明

    @Transactional注解的默认隔离级别为Isolation#DEFAULT,也就是采用数据源的隔离级别,mysql innodb 引擎默认隔离级别为 RR(所有不额外指定时,相当于 RR)

    5. SERIALIZABLE 事务隔离级别

    串行事务隔离级别,所有的事务串行执行,实际的业务场景中,我没用过... 也不太能想像,什么场景下需要这种

    @Transactional(readOnly =true, isolation = Isolation.SERIALIZABLE, rollbackFor = Exception.class)publicbooleanreadSerializeTransaction(intid)throwsInterruptedException{this.query("serialize read only", id);    Thread.sleep(3000);this.query("serialize read only", id);returntrue;}/** * serialize,事务串行执行,fix所有问题,但是性能低 * *@paramid *@return*/@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = Exception.class)publicbooleanserializeTransaction(intid){if(this.updateName(id)) {this.query("serialize: after updateMoney name", id);if(this.updateMoney(id)) {returntrue;        }    }returnfalse;}

    测试 case

    /**

    * Serialize 隔离级别

    */privatevoidtestSerializeIsolation()throwsInterruptedException{intid =350;newThread(newRunnable() {@Overridepublicvoidrun(){            call("Serialize: 只读事务 - read", id, detailDemo::readSerializeTransaction);        }    }).start();    Thread.sleep(1000);    call("Serialize 读写事务 - read", id, detailDemo::serializeTransaction);}

    输出结果如下

    ============ Serialize: 只读事务 - readstart==========serializereadonly>>>> {id=350,name=初始化, money=200, is_deleted=false, create_at=2020-01-2012:10:23.0, update_at=2020-01-2012:10:23.0}============ Serialize 读写事务 -readstart==========serializereadonly>>>> {id=350,name=初始化, money=200, is_deleted=false, create_at=2020-01-2012:10:23.0, update_at=2020-01-2012:10:23.0}============ Serialize: 只读事务 -readend==========serialize:afterupdateMoneyname>>>> {id=350,name=更新, money=200, is_deleted=false, create_at=2020-01-2012:10:23.0, update_at=2020-01-2012:10:39.0}============ Serialize 读写事务 -readend==========

    只读事务的查询输出之后,才输出读写事务的日志,简单来讲就是读写事务中的操作被 delay 了

    6. 小结

    本文主要介绍了事务的几种隔离级别,已经不同干的隔离级别对应的场景,可能出现的问题;

    隔离级别说明

    使用说明

    mysql innodb 引擎默认为 RR 隔离级别;@Transactinoal注解使用数据库的隔离级别,即 RR

    通过指定Transactional#isolation来设置事务的事务级别

    相关文章

      网友评论

          本文标题:2020-08-16

          本文链接:https://www.haomeiwen.com/subject/ivtxjktx.html