01_事务基础知识

一. 事务的ACID

  • Atomic: 原子性
    一组SQL放在一起执行,要么全部执行成功,要么就都别执行,不存在部分执行成功的状态。(关注的是数据的状态)
  • Consistency: 一致性
    在事务的执行过程中,中间状态的数据对外不可见,只有最初和最终的状态对外可见。(关注的是数据的可见性)
  • Isolation: 隔离性
    多个事务在运行时不能互相干扰。防止这种情况: 事务A正在操作某个数据,此时事务B也在操作这个数据,并且还导致事务A报错了。
  • Durability: 持久性
    只要事务执行成功了,那么它对数据的影响就是永久性的。别搞得过了一会儿,刚刚更新的数据又自动还原了。

二. 事务的隔离级别

2.1 读,未提交(READ UNCOMMITTED)

在这里插入图片描述
假设服务B正在更新id=1的数据,将name修改成了张三,且尚未提交事务。在读未提交的事务隔离级别下,服务A发起请求,创建的事务A中,能够看到事务B中修改的数据,也就是说,name等于张三。非常危险。

2.2 读,已提交(READ COMMITTED)

在这里插入图片描述

  1. 服务B修改了id=1的数据,将name的值修改成张三,未提交事务。
  2. 服务A查询id=1的数据,发现name等于李四。
  3. 服务B提交了事务。
  4. 服务A在同一个事务中,再次查询id=1的数据,发现name等于张三。

2.3 可重复读(REPEATABLE READ)

在这里插入图片描述

  1. 服务B正在修改id=1的数据,把name的值修改成张三,未提交事务。
  2. 服务A请求Mysql,在事务A-1中查询id=1的数据,此时name是"李四"。
  3. 服务B提交了事务B,
  4. 服务A在事务A-1中再次查询id=1的数据,此时name的值仍然是"李四"。
  5. 当且仅当服务A开辟一个新的事务,再次查询id=1的数据,才能查询到name的值是"张三"。

commit操作后的数据只能对新的Session可见,在旧的Session(比如事务A-1)中无论你执行多少次查询语句,都不可能看的到事务B commit之后的数据。Mysql默认的事务隔离级别是可重复度。

我们用MVCC的概念,可以很容易的理解上述结论。

  1. 为什么某个事务commit后的数据只能对新的Session可见,对旧的Session不可见?
    答: 因为这个事务的SessionId比新的SessionId小,比旧的SessionId大,Mysql的MVCC机制告诉我们,事务能看到的数据,必须满足,这条数据的创建事务id小于等于当前事务。

2.4 幻读

  1. 数据库中存储了id=1,name=张三,这一条数据。
  2. 服务A创建事务,查询数据库,发现只有id等于1的这一条数据。
  3. 服务B向数据库中新增了2条数据,并提交了事务。
  4. 服务A在尚未关闭的事务中,再次查询数据库,发现查到了3条数据,这与之前的查询结果不一样,站在自己的角度,就好像"凭空"多出了2条数据,这就是幻读。

可重复读,可以缓解部分幻读问题,它可以做到在同一个Session中,无论你查多少次,只要自己不做增删改,那么查到的结果一定能保持相同,不管其它Session有没有增删改数据。但是这有一个大前提,那就是只能一直做查询操作。假如说,我用了可重复度,在Session1内首先做了查询并得到结果集A,接着在Session2中新增了两条数据并提交Session2,然后在Session1中对所有的数据进行了更新(比如说不带条件的更新),提交Session1,此时会惊讶的发现,包括Session2新增的两条数据在内,所有的数据全都被Session1的更新操作影响了。

换句话说,虽然在Session1中,我们看不到Session2提交的数据,但却能更新它们,这是为什么呢?

这是因为Select走的是快照读,在执行Select操作时,Mysql会为本次执行的结果生成一份快照,后续多次的Select其实都是在查询这份快照,Session2中新产生的数据当然就没办法看到了。一旦某一行数据执行了增删改,这个事情就发生了本质的变化,因为Mysql会为这一行数据生成新的快照点,再次执行Select显然就能看到这一行数据的最新数据了,但是值得注意的是,其它行的数据仍然走之前生成的快照。

所以,想要彻底解决幻读,需要比可重复度更加严格的隔离机制,那就是串行化。

2.5 串行化(SERIALIZABLE)

在这里插入图片描述
服务A的事务A在执行的过程中,其它的客户端都无法开启事务。上图中,服务A开启了事务A,先是查询表中的数据,再做了CRUD操作,最后提交事务,这个过程内其他的客户端都无法创建事务,只能等着,等到事务A提交后,才能开启和执行自己的事务。事务A做完,事务B才能执行,这就是事务的串行化执行。

2.6 MVCC机制

上面说了4种事务的隔离机制,默认使用的就是可重复读(REPETABLE READ),那么这是怎么实现的呢?Mysql是通过MVCC机制实现的,MVCC(multi-version concurrency control)是多版本并发控制的意思。

概念1: 任何一个服务接入到Mysql中创建事务时,Mysql都会为其分配一个事务id,并且事务id全局唯一,自增长。
概念2: 每一行数据,除了自身的字段外,还会维护两个隐藏字段,分别是创建这行数据的事务id和删除这行数据的事务id。
概念3: 每一个事务只能看到以下数据: 1. 当前事务id >= 创建数据的事务id 2.当前事务id < 删除数据的事务id

比如说,现在有一张student表,包含了三个字段,分别是 id, name和age。

① 事务(id=100) 新增了id=1的数据和id=2的数据

idnameage创建数据的事务id删除数据的事务id
1张三27100空着
2李四26100空着

事务(id=101)执行select * from student where id = 1时,能看到 id=1,name=张三,age=27,这是因为101 >= 100

② 事务(id=102)删除了id=1的数据。

idnameage创建数据的事务id删除数据的事务id
1张三27100102
2李四26100空着

事务(id=101)仍然能看到id=1的数据,这是因为101 < 102

③ 事务(id=102)修改了id=2的数据,把name改成了"王五",age改成了28

idnameage创建数据的事务id删除数据的事务id
1张三27100102
2李四26100空着
2王五28102空着

事务(id=101)select * from student where id = 2时,只能看到旧数据,也就是name=李四,age=26,这是因为101 < 102。

注意: 当数据表中存在id相同的多条数据时(并非真的有多条数据,毕竟id不可能重复,这里只是因为在事务里面),我们查询到的一定是满足MVCC条件的,且"创建数据的事务id"最大的那条数据。比如说,假设事务(id=102)在执行全表查询时,表中有创建SessionId=100、101以及103的三条数据,它们的id都等于2,那么事务(id=102)查询到的id=2的数据,应该是SessionId=101的。

这里有一个问题,假如事务100创建了id=2的数据,并且提交了事务,此时先后开启了两个事务101和102,接着,102修改了数据,提交了事务。根据之前的分析可知,101看不到102修改后的数据内容,只能看到事务100,但这还没完,101修改了这条数据,并且提交了事务。最后开启了事务103,请问103能看到哪个事务提交的数据?

有人说,这不是废话吗?最后修改id=2数据的事务是101,那么103看到的当然是101修改的数据。但是你难道不觉得很奇怪吗?按照之前的逻辑,id=2的数据应该会"存在"3条,分别对应的创建事务id=100,101,102,为什么103看到的不是102,而是101呢?

我的推测如下: 提交事务时,如果对某个数据做出了修改,那么当前事务会把原先实际存储在表中的数据删掉,接着写入一条新的数据,虽然数据id保持不变,但是创建数据的事务id会写成自己。

这样一来,虽然102比101先提交了事务,但是没关系啊,101提交事务时,会把102提交事务时产生的那条数据给覆盖了。所以,103就只能看到101创建的数据了。

2.7 经典小案例

假设数据表内有id,name,age这三个字段

idnameage创建数据的事务id删除数据的事务id
1张三2799

事务A(SessionId=100)和事务B(SessionId=101)依次开启事务,事务A的SessionId < 事务B的SessionId。

事务A新增了两条数据,并且修改了id=1的数据,将age改成了35,并且提交了事务。(下方id=1,age=27的数据被我画了删除线,这是因为当事务A提交事务后,会重新更新实际存储在表中的数据)

idnameage创建数据的事务id删除数据的事务id
1张三2799空着
1张三35100空着
2李四26100空着
3王五28100空着

事务B首先是全表查询,此时只能查到id=1,name=张三,age=27,毕竟读的是快照(别问我为什么age=27明明被删除了,还能查得出来,这里写的清清楚楚,读的是快照,快照)。

接着,事务B更新了id=1的数据,把name修改成了“张三(修改后)”。

idnameage创建数据的事务id删除数据的事务id
1张三2799空着
1张三35100空着
1张三(修改后)35101空着
2李四26100空着
3王五28100空着

现在,事务B全表查询,仍然只能查到id=1的数据,但是name=张三(修改后),并且age=35。
通过更新操作,Mysql为id=1的这条数据重新创建了快照点,所以能看到当前数据库中,id=1的最新的一条数据。但是对于其它id的数据而言,仍然读的是旧的快照,所以查不到数据。

三. Spring的事务支持和传播特性

Spring支持声明式、编程式以及注解这三种方式来操作事务。

编程式事务就是搞一个XML文件,在里面添加数据源,添加事务管理器,添加事务模板,接着在代码中使用事务模板(TransactionTemplate),哪里需要事务,就在哪里使用TransactionTemplate。

transactionTemplate.execute(new TransactionCallbackWithoutResult() {
    @Override
    protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
        String sql = ...
        jdbcTemplate.update(...)
    }
})

声明式事务就是搞一个XML,在里面配置DataSourceTransactionManager,然后搞一堆AOP,配置切点,比如到底要把事务用在哪个类的哪个方法上,好处是对代码没有侵入性。

<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
</bean>

<tx:advice id="txAdvice" transaction-manager="txManager">
        <!-- 事务语义... -->
        <tx:attributes>
            <!-- 所有用'get'开头的方法都是只读的 -->
            <tx:method name="get*" read-only="true"/>
            <!-- 其他的方法使用默认的事务配置(看下面) -->
            <tx:method name="*"/>
        </tx:attributes>
</tx:advice>

 <aop:config>
        <aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.FooService.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>
</aop:config>

不过现在没人去写这么复杂的配置文件了,通常采用注解。注解就是直接使用@Transational,通过为propagation赋值来指定事务的传播行为。

事务一共有7种传播行为:

  • PROPAGATION_REQUIRED (默认)
    假设ServiceA.method()调用了ServiceB.method(),若ServiceA.method()开启了事务,ServiceB.method()也开启了事务,那么ServiceB.method()不会单独开启事务,而是把自己放在ServiceA.method()的事务中。ServiceA.method()和ServiceB.method()任何一个报错,都可能导致整个事务回滚。
  • PROPAGATION_SUPPORTS
    假设ServiceA.method()调用了ServiceB.method(),若ServiceA.method()开启了事务,则ServiceB.method()会把自己加入到前者的事务中。如果ServiceA.method()没有开启事务,则ServiceB.method()也不会开启事务。
  • PROPAGATION_MANDATORY
    必须由一个开启了事务的方法来调用自己,否则报错。
  • PROPAGATION_REQUIRED_NEW
    强制自己开启一个事务,假设ServiceA.method()调用了ServiceB.method(),在调用了ServiceB.method()之后,ServiceA会被卡住,当且仅当ServiceB.method()执行完毕后,ServiceA再去执行自己的操作。所以,如果说我们为Service调用ServiceB的代码加上try/catch,那么就算ServiceB.method()报错了,也仅仅只会回滚ServiceB.method()执行了的操作,与ServiceA没有半毛钱关系。
  • PROPOGATION_NOT_STOPPED
    ServiceB.method()不支持事务,ServiceA.method()在调用ServiceB.method()时,请求将被挂起,等到ServiceB.method()用非事务的方式运行结束之后,ServiceA再执行。这种做法的好处是,ServiceB的代码就算报错了,引起了ServiceA事务回滚,ServiceB执行过的代码也不会被回滚。
  • PROPAGATION_NEVER
    不能被一个开启了事务的方法调用,比如ServiceA.method()开启了事务,ServiceB.method()被标记了PROPAGATION_NEVER,那么ServiceA.method()在调用ServiceB.method()时就会报错。
  • PROPAGATION_NESTED
    开启嵌套事务,ServiceB开启一个子事务,一旦ServiceB执行报错,那么它会回滚到ServiceA调用ServiceB,也就是开启子事务的这个savePoint。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值