数据库事物

一、什么是事务

1.1 事务的概念

事务是数据库管理系统的最小逻辑工作单元,这个逻辑工作单元包含一系列操作;这些操作作为一个整体一起向数据库系统提交,要么都执行、要么都不执行;事务是一组不可再分割的操作集合(工作逻辑单元);

通俗的讲,就是为了达到某个目的,而做的一系列的操作,要么一起成功(事务提交),要么一起失败(事务回滚)。

最常见的例子就是转账:

【举例】

小明给如花转账:

开启事务(begin;)-------

  1. 从小明的账户扣除1000块
  2. 给如花的账户增加1000块

事务提交(commit;)-------

img

上面例子在任务开启后一旦出现问题,都会导致事务回滚。

1.2 事务的四大特性

一原持久隔离

1.原子性(Atomicity):同一个事务中的多个操作是不可分割的原子单位。事务中所有操作要么全部执行成功,要么全部执行失败。

2.一致性(Consistency):事务提交后,数据库状态与其它业务规则保持一致。如转账业务,无论事务执行成功与否,参与转账的两个账号余额之和应该是不变的。

3.隔离性(Isolation):隔离性是指在并发操作中,不同事务之间应该隔离开来,使每个并发中的事务不会相互干扰。

4.持久性(Durability):一旦事务提交成功,事务中所有的数据操作都必须被持久化到数据库中,即使提交事务后,数据库马上崩溃,在数据库重启时,也必须你能保证通过某种机制恢复数据。

1.3 原生的JDBC事务处理

try {
    connection.setAutoCommit(false);
    // 数据库操作... insert  /  update  /   delete
    connection.commit();
} catch (Exception ex) {
    connection.rollback();
} finally {
    connection.setAutoCommit(true);
}

二、事务的隔离级别

  • 不同的事务隔离级别,在并发环境会存在不同的并发问题
√:可能出现的情况 ×:不会出现该情况 脏读 不可重复读 幻读
SERIALIZABLE(读序列化) × × ×
REPEATABLE READ(可重复读)(mysql默认) × ×
READ COMMITTED(读已提交)(oracle,sql server默认) ×
READ UNCOMMITTED(读未提交) (无法容忍)

串行:我的事务尚未提交,别人就别想改数据。
可重复读:别人改数据的事务已经提交,我在我的事务中也不去读。
读已提交:别人改数据的事务已经提交,我在我的事务中才能读到。
读未提交:别人改数据的事务尚未提交,我在我的事务中也能读到。
这4种隔离级别,并行性能依次增高,安全性依次降低。

  • 查看mysql当前隔离级别
SHOW VARIABLES;
SHOW VARIABLES LIKE "%transaction%";

-- 当前会话
select @@session.transaction_isolation;
select @@session.tx_isolation; -- 不常用


-- 全局
select @@global.transaction_isolation;
select @@global.tx_isolation;  -- 不常用
  • 设置隔离级别
-- 当前会话
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- 全局
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;

2.1 现象演示

脏读

事务A读取了事务B中尚未提交的数据。如果事务B回滚,则A读取使用了错误的数据称为脏读

img

如花和小明两个事务,当前小明5000,如花3000,小明事务中小明给如花转账1000,此时,小明4000,如花4000,如花事务中如花查询余额4000,并提交事务,但是,小明事务还未提交,中途因为某些原因,给回滚了,这时,如花事务读到的就是错误数据,程序中这种情况是无法容忍的

脏读:指事务A正在访问数据,并且对数据进行了修改(事务未提交),这时,事务B也使用这个数据,后来事务A撤销回滚,并把修改后的数据恢复原值,B读到的数据就与数据库中的数据不一致,即B读到的数据是脏数据。

不可重复读

img

刚开始如花事务,查询余额100元,随后小明事务花掉了50元并提交,然后如花事务再次查询余额变为了50元然后提交事务。特指更新操作

不可重复读:在一个事务内,多次读取同一个数据,但是由于另一个事务在此期间对这个数据做了修改并提交,导致前后读取到的数据不一致;

幻读

在事务A多次读取构成中,事务B对数据进行了新增操作,导致事务A多次读取的数据行数不一致。

幻读:在一个事务中,先后两次进行读取相同的数据(一般是范围查询),但由于另一个事务新增或者删除了数据,导致前后两次结果不一致。

① 不可重复读和幻读的区别:

不可重复读侧重于读取到其他事务修改的数据,幻读侧重于读取到其他事务新增或者删除的数据。

② 可以采用锁机制来解决不可重复读和幻读:

对于不可重复读,只需对操作的数据添加行级锁,防止操作的数据发生变化;而对于幻读,需要添加表级锁,将整张表锁定,防止新增或者删除数据。

2.2 事务丢失演示

回滚丢失

比如A和B同时在执行一个数据,然后B事务已经提交了,然后A事务回滚了,这样B事务的操作就因A事务回滚而丢失了。

img

现余额100元,小明事务花掉50元,这时如花事务放了50元并提交,而小明事务,因为某些原因回滚到了原来的100元,导致如花事务直接给丢失了。违反一致性,如花事物放进去的50元凭空消失了,是无法容忍的

提交覆盖丢失演示

A和B一起执行一个数据,两个同时取到一个数据,然后,B事务首先提交,但是,A事务接下来又提交,这样就覆盖了B事务。

img

小明事务开始时账户是100元,小明准备放50元,随后如花事务很快的放了50元并提交,此时存钱罐是150元,这时小明事务还未结束(不知道余额被修改了)就提交事务150,结果导致原本应该是200元的最后缺变成了150元,如花事务被覆盖。

三、MVCC

一、前言

本文使用的mysql版本为5.7

1、MVCC是什么

  MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。百度百科解释

  多版本控制:指的是一种提高并发的技术。最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的可并发程度。在内部实现中,与Postgres在数据行上实现多版本不同,InnoDB是在 undo log 中实现的,通过 undo log 可以找回数据的历史版本。找回的数据历史版本可以提供给用户读(按照隔离级别的定义,有些读请求只能看到比较老的数据版本),也可以在回滚的时候覆盖数据页上的数据。在 InnoDB 内部中,会记录一个全局的活跃读写事务操作记录链表,其主要用来判断事务的可见性。

  MVCC 在 InnoDB 中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读

2、当前读与快照读

  在学习MVCC多版本并发控制之前,必须先了解一下,什么是MySQL InnoDB 下的当前读快照读

  • 当前读

  像 select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁

  • 快照读

  像不加锁的select操作就是快照读,即不加锁的非阻塞读。快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读。之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以将 MVCC 认为是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销。既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本

    说白了MVCC就是为了实现读-写冲突不加锁,而这个读指的就是快照读, 而非当前读,当前读实际上是一种加锁的操作,是悲观锁的实现

3、当前读与快照读和MVCC的关系

  • 准确的说,MVCC 多版本并发控制指的是 “维持一个数据的多个版本,使得读写操作没有冲突” 这么一个概念。仅仅是一个理想概念
  • 而在MySQL中,实现这么一个 MVCC 理想概念,就需要MySQL提供具体的功能去实现它,而快照读就是MySQL为实现 MVCC 理想模型的其中一个具体非阻塞读功能。而相对而言,当前读就是悲观锁的具体功能实现
  • 要说的再细致一些,快照读本身也是一个抽象概念,再深入研究。MVCC模型在MySQL中的具体实现则是由 3个隐式字段undo logRead View 等去完成的,具体可以看下面的MVCC实现原理

4、MVCC解决的问题

数据库并发场景有三种,分别为:

  • 读-读:不存在任何问题,也不需要并发控制
  • 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
  • 写-写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失

备注:
第1类丢失更新:事务A撤销时,把已经提交的事务B的更新数据覆盖了
第2类丢失更新:事务A覆盖事务B已经提交的数据,造成事务B所做的操作丢失

5、MVCC带来的好处

  多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 所以MVCC可以为数据库解决以下问题

  • 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
  • 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题

6、小结

  总之,MVCC就是因为大牛们,不满意只让数据库采用悲观锁这样性能不佳的形式去解决读-写冲突问题,而提出的解决方案,所以在数据库中,因为有了MVCC,所以可以形成两个组合:

  • MVCC + 悲观锁
    MVCC解决读写冲突,悲观锁解决写写冲突
  • MVCC + 乐观锁
    MVCC解决读写冲突,乐观锁解决读写冲突

这种组合的方式就可以最大程度的提高数据库并发性能,并解决读写冲突,和写写冲突导致的问题

二、事务隔离性的实现原理

  为了实现事务隔离,数据库延伸出了数据库锁,其中Innodb事务的隔离级别是由锁机制和MVVC(多版本并发控制)实现的

1、Mysql锁机制

​ MySQL锁机制的基本工作原理就是:事务在修改数据库之前,需要先获得相应的锁,获得锁的事务才可以修改数据;在该事务操作期间,这部分的数据是锁定,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。(锁机制的更多说明可以参考另一篇博客:MySQL数据库:锁机制

​ 通过对InnoDB不同锁类型的特性分析,可以利用锁解决脏读、不可重复读、幻读:

  • 排它锁解决脏读:在读已提交的隔离级别下,事务A只有在对数据修改时才加排它锁,但直到事务 commit 时才释放锁。因此,同时进行的事务B希望读取同一行数据时,会被事务A的排它锁堵塞,所以解决了脏读的问题
  • 共享锁解决不可重复读:在可重复读的隔离级别下,除了执行读已提交的排它锁方式,还会在读取一行数据时,为这行数据添加共享锁直至事务 commit。例如,事务A读取ID=1这一行数据,然后为ID=1添加共享锁,事务B同时希望update ID=1,此时获取写锁失败,因此在事务A执行完之前,没有其他任何事务可以对ID=1这一行做修改,因此解决了重复读的问题
  • 临键锁解决幻读

​ 虽然共享锁和排它锁解决了事务隔离的并发问题,但锁会导致大量的堵塞,性能下降。某些时候会造成死锁,为了解决死锁,还要添加死锁探测机制,性能进一步下降,因此需要更高效的方式实现事务的隔离级别,也就是 MVCC 多版本并发控制。

2、MVCC

​ InnoDB 的 MVCC 的实现思路是:对每一行数据用 undo log 记录多个版本,每个版本的数据可能都不相同,然后根据 事务ID 去寻找适合它的版本数据,从而实现不同事务之间的隔离性。MVCC 具体实现实现方式是在每行记录后面保存两个隐藏的列:

  • DB_TRX_ID:标识当前数据属于哪个事务,每次提交事务,事务ID会自增,事务开始时会把该事务ID放到当前事务影响的行事务ID字段中
  • DB_ROLL_PTR:指向该行数据 undo log 的指针,undo log 日志文件保存了该行记录的所有版本数据,并在日志中通过链表形式组织

img

​ 通过 MVCC,数据库可以使得事务的读取不需要很多读锁,提升了数据库的性能。当一个事务完成时,数据库会删除关于这条事务所有的 undo log,若未完成事务数据库崩溃则根据 undo log回滚,实现原子性。

MVCC只在 可重复度 和 读已提交 两个隔离级别下才会工作,其中,MVCC实质就是通过保存数据在某个时间点的快照来实现的。

  • 读已提交:事务A执行每一条语句时,生成一份活跃事务表,根据这份表去获取数据,因此不会获取到脏数据(未提交事务引起的),但会有重复读问题(因为可以读取到commit 的事务数据)
  • 可重复读:事务A只有在事务开始时才生成一份活跃事务表,因此不会读取到事务A执行中 commit 的其它事务引起的数据变更,也就不存在重复读问题。

​ 在并发访问数据库时,对正在事务中的数据做MVCC多版本的管理,以避免写操作阻塞读操作,并且可以通过比较版本解决快照读方式的幻读问题,但对于当前读的幻读,MVCC并不能解决,需要通过临键锁来解决。

  • 快照读:Innodb快照读,数据的读取将由 cache(原本数据) + undo(事务修改前的数据) 两部分组成
  • 当前读:SQL读取的数据是最新版本。通过锁机制来保证读取的数据无法通过其他事务进行修改

在可重复读的隔离级别下,MVCC具体操作:

(1)SELECT操作:InnoDB遵循以下两个规则:

只查找数据行的事务ID小于或等于当前事务ID的版本,这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的记录。
行的删除版本要未被定义,读取到事务开始之前状态的版本,这可以确保事务读取到的行,在事务开始之前未被删除。只有同时满足的两者的记录,才能返回作为查询结果。
(2)INSERT:InnoDB为新插入的每一行保存当前事务编号作为行版本号。

(3)DELETE:InnoDB为删除的每一行保存当前事务编号作为行删除标识。

(4)UPDATE:InnoDB为插入一行新记录,保存当前事务编号作为行版本号,同时保存当前事务编号到原来的行作为行删除标识。

保存这两个额外系统版本号,使大多数读操作都可以不用加锁。这样设计使得读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行,不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作。

关于MVCC更多详细的内容,推荐读者阅读这篇文章:《聊聊MVCC》

3、持久性

​ 持久性的实现关键在于redo log日志在执行SQL时会保存已执行的SQL语句到一个指定的Log文件,当执行recovery时重新执行redo log记录的SQL操作。

  • redo log日志

​ 当向数据库写入数据时,执行过程会首先写入Buffer Pool,Buffer Pool中修改的数据会定期刷新到磁盘中(这一过程叫做刷盘),这整一过程称为redo log。

redo log 分为:

  • Buffer Pool内存中的日志缓冲(redo log buffer),该部分日志是易失性的
  • 磁盘上的重做日志文件(redo log file),该部分日志是持久的

​ Buffer Pool的使用可以大大提高了读写数据的效率,但是也带了新的问题:如果MySQL宕机,而此时Buffer Pool中修改的数据在内存还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。

​ 为了确保事务的持久性,在当事务提交时,会调用fsync接口对redo log进行刷盘, (即redo log buffer写日志到磁盘的redo log file中 ),刷新频率由 innodb_flush_log_at_trx_commit变量来控制的:

0: 每秒刷新缓冲池中的数据写入到磁盘中的,当系统崩溃,会丢失1秒钟的数据 ;

1: 事务每次提交的时候,就把缓冲池中的数据刷新到磁盘中;

2:提交事务的时候,把缓冲池中的数据写入磁盘文件对应的 os cache 缓存里去,而不是直接进入磁盘文件。可能 1 秒后才会把 os cache 里的数据写入到磁盘文件里去。

4、一致性

一致性指的是事务不能破坏数据的完整性和业务的一致性 :

  • 数据的完整性: 实体完整性、列完整性(如字段的类型、大小、长度要符合要求)、外键约束等
  • 业务的一致性:例如在银行转账时,不管事务成功还是失败,双方钱的总额不变。

那是如何保证数据一致性的?其实数据一致性是通过事务的原子性、持久性和隔离性来保证的:

原子性:语句要么全执行,要么全不执行,是事务最核心的特性,事务本身就是以原子性来定义的;主要基于undo log实现

持久性:保证事务提交后不会因为宕机等原因导致数据丢失;主要基于redo log实现

隔离性:保证事务执行尽可能不受其他事务影响;InnoDB默认的隔离级别是RR,RR的实现主要基于锁机制(包含next-key lock)、MVCC(包括数据的隐藏列、基于undo log的版本链、ReadView)

四、spring事务的传播特性

4.1 传播特性

4.1.1 什么是传播特性?

指当一个事务方法B被另一个事务方法A调用时,这个事务方法B应该如何进行。

【举例】

img

思考:小弟出现异常,老大要不要回滚?

4.1.2 Spring提供的7种事务传播特性

  • propagation_required: 默认事务类型,如果没有,就新建一个事务;如果有,就加入当前事务。适合绝大多数情况。
  • propagation_required_new: 如果没有,就新建一个事务;如果有,就将当前事务挂起。
  • propagation_nested: 如果没有,就新建一个事务;如果有,就在当前事务中嵌套其他事务。
  • propagation_supports: 如果没有,就以非事务方式执行;如果有,就使用当前事务。
  • propagation_not_supported: 如果没有,就以非事务方式执行;如果有,就将当前事务挂起。即无论如何不支持事务。
  • propagation_never: 如果没有,就以非事务方式执行;如果有,就抛出异常。
  • propagation_mandatory: 如果没有,就抛出异常;如果有,就使用当前事务。

总结:

【1】死活不要事务的

propagation_never: 没有就非事务执行,有就抛出异常。

propagation_not_supported: 没有就非事务执行,有就直接挂起,然后非事务执行。

【2】可有可无的

propagation_supports: 有就用,没有就算了

【3】必须有事务的

propagation_requires_new: 有没有都新建事务,如果原来有,就将原来的挂起。里面的事务,和外面事务是完全隔离的,互不影响的,外面事务不影响里面的事务,里面的事务也不影响外面的事务。

propagation_nested: 如果没有,就新建一个事务,如果有,就在当前事务中嵌套其他事务。外面的事务回滚的时候会影响里面的事务,但是,里面的事务不影响外面的事务。

propagation_requered: 如果没有,就新建一个事务,如果有,就加入当前事务。

propagation_mandatory: 如果没有,就抛出异常,如果有,就使用当前事务。

4.2 七种事物代码举例说明

数据库测试数据初始化:Tom 1000、jerry 1000

4.2.1 propagation_never

1、如果没有,就以非事务方式执行;如果有,就抛出异常。

img

img

2、假如有事务的话,老大也报错,小弟也报错,数据一定会回滚为最初的状态。

img

img

3、从上方数据库结果来看,转账成功了,出现异常缺没有回滚,说明两个方法都没有事务处理。

img

4、假如没有配置propagation.never,同时老大有事务,老大调用小弟,那老大和小弟就是在同一个事务中运行的;

而现在的目的就是不想让小弟在事务中运行,所以需要为小弟添加如下配置。

img

5、假如,现在小弟配置了propagation.never事物传播特性,而老大又有事务,就会直接抛出异常。

img

4.2.2 propagation_not_supported

1、没有就非事务执行,有就直接挂起,然后非事务执行。

img

img

2、出异常后

img

老大,值没有变化,说明有事物并且因为执行异常回滚了

img

小弟,因为使用porpagation.not_supported事物传播特性,导致没有事务,所以,出错后没有回滚。

4.2.3 propagation_supports

1、有就用,没有就算了

img

2、propagation.supports有事务,就用,没有事务就算了。目前,老大有事务,没有报错,小弟配置了supports事物传播类型,那它俩应该在同一个事务里面,小弟报错,老大也跟着回滚。

img

如上图所示老大,小弟,都回滚了,老大,没有减100,小弟,也没有加100。

imgimg

3、如上图所示,老大没有事务,小弟也没有事务,报错后,都没有回滚。

4.2.4 propagation_requires_new

1、有没有都新建事务,如果原来有,就将原来的挂起。

img

img

2、由上图结果分析,老大没有事务,报错后,没有回滚,而小弟,新建了一个事务,报错后,回滚了。

img

3、老大,加了事务,没有报错,所以,没有回滚,成功减了100.

img

4、小弟,新建了一个单独的事务,报错了,所以回滚了,没有加100,还是1000。两个事务互不影响。

img

img

老大有事务,出了问题回滚了,所以还是1000

img

小弟也有事务,但跟老大不是一个事务,它也没有出错,所以,加了100。

img

4.2.5 propagation_nested

单向传递回滚操作,外部异常导致的回滚会连带内部事物一起回滚。

相反,如果内部产生异常发生回滚不会导致外部发生回滚

1、如果没有,就新建一个事务,如果有,就在当前事务中嵌套其他事务。

img

2、老大,有事务,没有出现问题,老大成功减100。小弟,也有事务,但出现了问题,所以回滚了,还是1000,不影响老大的事务。

img

img

老大出异常,老大和小弟,都回滚了。

img

4.2.6 propagation_requered

1、如果没有,就新建一个事务,如果有,就加入当前事务。

img

都没有改变,小弟出异常,由于是同一个事务,所以,老大、小弟都回滚了。老大出异常,小弟无异常,同理,也都会回滚。

img

4.2.7 propagation_mandatory

如果没有,就抛出异常,如果有,就使用当前事务。

img

老大没有事务,直接抛出异常

img

五、spring配置事务

5.1 基础代码

img

5.1.1 实体类

@Data
public class User {
    private Integer id;
    private String name;
    private Integer money;
}

5.1.2 Dao

@Repository
public class UserDAO {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    // 转出

    public void out(String fromName, Integer money) {
        String sql = "update user set money = money-? where name =? ";
        jdbcTemplate.update(sql, money,fromName);
    }
    
    // 转入
    public void in(String toName, Integer money) {
        String sql ="update user set money = money+? where name =?";
        jdbcTemplate.update(sql,money,toName);
	}
}

5.1.3 service

@Service
public class UserService {
    
    @Autowired
    private UserDAO userDAO;
    
    // 没有事物的转账业务
    public void transfer(String fromName, String toName, Integer money) {
    userDAO.out(fromName, money); // 出钱
    int x = 10;
    if(x == 10) {
       throw new RuntimeException("出错啦!");
    }
    userDAO.in(toName, money); // 入钱
    }
}

5.1.4 xml文件配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:cache="http://www.springframework.org/schema/cache"
xsi:schemaLocation="
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/jdbc
http://www.springframework.org/schema/jdbc/spring-jdbc-3.1.xsd
http://www.springframework.org/schema/cache
http://www.springframework.org/schema/cache/spring-cache-3.1.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd">
<!--数据源-->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
    <property name="url" value="jdbc:mysql://192.168.75.131:3306/spring-dk"></property>
    <property name="username" value="root"></property>
    <property name="password" value="root"></property>
</bean>
<!-- 模板方法 -->
<bean class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource"/>
</bean>
<!-- bean扫描配置 -->
<context:component-scan base-package="com.dk"/>
</beans>

5.1.5 测试

@Test
public void T1(){
    ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
    UserService bean = ac.getBean(UserService.class);
    bean.transfer("tom","jerry",100);
}

@Test
public void T2(){
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    UserService bean = ac.getBean(UserService.class);
    bean.transfer("tom","jerry",100);
}

5.2 编程式事务

XML方式配置事务:在applicationContext.xml中添加事务管理器和事务管理器模板的配置

<!-- 事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 事务管理的模板 -->
<bean id="transactionTemplate"
class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="transactionManager" />
</bean>

注解方式配置:事务管理器和事务管理器模版

@ComponentScan("com.dk")
public class AppConfig {
    
    @Bean
    public DataSource dataSource(){
        BasicDataSource ds = new BasicDataSource();
        ds.setUrl("jdbc:mysql://192.168.75.131:3306/spring-dk");
        ds.setDriverClassName("com.mysql.jdbc.Driver");
        ds.setUsername("root");
        ds.setPassword("root");
        return ds;
    }
    
    @Bean
	public DataSourceTransactionManager transactionManager(DataSource dataSource){
        DataSourceTransactionManager tm = new DataSourceTransactionManager();
        tm.setDataSource(dataSource);
        return tm;
	}
    
    @Bean
    public TransactionTemplate transactionTemplate(DataSourceTransactionManager
    transactionManager){
        TransactionTemplate transactionTemplate = new TransactionTemplate();
        transactionTemplate.setTransactionManager(transactionManager);
        return transactionTemplate;
    }

    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource){
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }
}

修改UserService,使用编程式事务完成事务管理:

@Service
public class UserService {
    
    @Autowired
    private UserDAO userDAO;
    
    @Autowired
    private TransactionTemplate transactionTemplate;

    // 编程式事务 转账业务
    public void transfer(String fromName, String toName, Integer money) {
        transactionTemplate.execute(status -> {
            userDAO.out(fromName, money);// 转出钱
            int x = 10;
            if(x==10){
                throw new RuntimeException("出错啦!!");
            }
            userDAO.in(toName, money);// 收入钱
            return null;
        });
    }
}

再测试:

@Test
public void T1(){
    ApplicationContext ac = new
    ClassPathXmlApplicationContext("applicationContext.xml");
    UserService bean = ac.getBean(UserService.class);
    bean.transfer("tom","jerry",100);
}

@Test
public void T2(){
    AnnotationConfigApplicationContext ac = new
    AnnotationConfigApplicationContext(AppConfig.class);
    UserService bean = ac.getBean(UserService.class);
    bean.transfer("tom","jerry",100);
}

5.3 声明值事务-基于AspectJ XML方式

注:基于TransactionProxyFactoryBean代理的方式是比较古老的方式,在这里就不赘述了。
基于XML方式的配置:
删除applicationContext.xml中的事务管理模版的配置,就是删除下面的配置:

<!-- 事务管理的模板 -->
<bean id="transactionTemplate"
class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="transactionManager" />
</bean>

添加事务定义的配置和AOP的配置:

<!--基于AspectJ 申明式事务XML配置方式-->
<!-- 定义一个增强 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <!-- 增强(事务)的属性的配置 -->
    <tx:attributes>
    <!-- 
	isolation:DEFAULT ,事务的隔离级别。
    propagation:事务的传播行为.
    read-only:false,不是只读
    timeout:-1
    no-rollback-for:发生哪些异常不回滚
    rollback-for:发生哪些异常回滚事务
    -->
    <tx:method name="transfer" isolation="DEFAULT" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>
<!-- aop配置定义切面和切点的信息 -->
<aop:config>
    <!-- 定义切点:哪些类的哪些方法应用增强 -->
    <aop:pointcut expression="execution(* com.dk.beans.service..*.*(..))" id="mypointcut" />
    <!-- 定义切面: -->
    <aop:advisor advice-ref="txAdvice" pointcut-ref="mypointcut" />
</aop:config>

将业务类的方法改回原来的方式:

删除UserService中的TransactionTemplate声明如下所示

@Service
public class UserService {
    
    @Autowired
    private UserDAO userDAO;
    
    // 没有事物的转账业务
    public void transfer(String fromName, String toName, Integer money) {
    userDAO.out(fromName, money); // 出钱
    int x = 10;
    if(x == 10) {
       throw new RuntimeException("出错啦!");
    }
    userDAO.in(toName, money); // 入钱
    }
}

测试:

...

基于注解的声明式事务:
在配置类上配置@EnableTransactionManagement开启事务。删除注解类中和事务相关的@Bane

@ComponentScan("com.dk")
@EnableTransactionManagement
public class AppConfig {
    
    @Bean
    public DataSource dataSource(){
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setUrl("jdbc:mysql://192.168.75.131:3306/spring-dk");
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUsername("root");
        dataSource.setPassword("root");
        return dataSource;
    }
    
    @Bean
    public DataSourceTransactionManager transactionManager(DataSource dataSource){
        DataSourceTransactionManager transactionManager = new
        DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource);
        return transactionManager;
    }
    
    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource){
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }
}

在UserService类上方或者方法上方通过@Transactional完成事务配置:

@Service
@Transactional
public class UserService {
    
}

六、参考文献

原文地址

参考文章

posted @     阅读(40)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
历史上的今天:
2021-07-11 让MySql支持Emoji表情存储
2021-07-11 jdbcTemplete
2021-07-11 编码与解码的理解
2021-07-11 FTPClient下载文件遇到的坑
2021-07-11 FTP链接的主被动模式:500 Illegal PORT command的问题
点击右上角即可分享
微信分享提示