1 - 事务是个什么鬼?
1.1 - 事务概述
-
在JavaEE企业级开发的应用领域,
为了保证数据的完整性和一致性
,必须引入数据库事务
的概念,所以事务管理是企业级应用程序开发中必不可少的技术。 -
事务就是一组由于逻辑上紧密关联而合并成一个整体(工作单元)的
多个数据库操作
,这些操作要么都执行,要么都不执行
。
举例子,一个事务是买书,需要这样操作数据库:
- 书的数据库:查询这本书的价格。
- 库存的数据库:这本书的数量减1。
- 用户余额数据库:减去这本书的价格。
1.2 - 事务的ACID属性
1、原子性(atomicity)
:“原子”的本意是“不可再分”,事务的原子性表现为一个事务中涉及到的多个操作在逻辑上缺一不可。事务的原子性要求事务中的所有操作要么都执行,要么都不执行
。
要么都执行,要么都不执行。
2、一致性(consistency)
:“一致”指的是数据的一致,具体是指:所有数据都处于满足业务规则的一致性状态。一致性原则要求:一个事务中不管涉及到多少个操作,都必须保证事务执行之前数据是正确的,事务执行之后数据仍然是正确的。如果一个事务在执行的过程中,其中某一个或某几个操作失败了,则必须将其他所有操作撤销,将数据恢复到事务执行之前的状态,这就是回滚。
举例转账,A有5000,B有1000,总共6000。A转给B是2000,A有3000,B有3000,加起来还是6000,不能多不能少。
3、隔离性(isolation)
:在应用程序实际运行过程中,事务往往是并发执行
的,所以很有可能有许多事务同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。隔离性原则要求多个事务在并发执行过程中不会互相干扰。
因为有并发,所以为了防止出错,所以要隔离。
4、持久性(durability)
:持久性原则要求事务执行完成后,对数据的修改永久的保存下来,不会因各种系统错误或其他意外情况而受到影响。通常情况下,事务对数据的修改应该被写入到持久化存储器
中。
这个不用解释。持久化存储器是硬盘。
1.3 - 编程式事务管理
1.3.1 - 使用原生的JDBC API进行事务管理
- 获取数据库连接Connection对象
- 取消事务的自动提交(setAutoCommit设置为false)
- 执行操作
- 正常完成操作时手动提交事务
- 执行失败时回滚事务
- 关闭相关资源
有异常回滚,没有异常提交。
- 使用原生的JDBC API实现事务管理是所有事务管理方式的
基石
,同时也是最典型的编程式事务管理。
不管什么持久层框架,都是对JDBC API的封装。
- 编程式事务管理需要将
事务管理代码
嵌入到业务方法
中来控制事务的提交和回滚。
这个意思就是说,你的事务管理代码要写在service当中,不是写在dao当中。service当中是完整的业务逻辑和功能,dao当中只是对数据库的单个的操作。
- 在使用编程的方式管理事务时,必须在每个事务操作中包含额外的
事务管理代码
。相对于核心业务
而言,事务管理的代码
显然属于非核心业务
,如果多个模块都使用同样模式的代码进行事务管理,显然会造成较大程度的代码冗余
。
每一个service当中的方法,管理事务的过程都是一样的。会造成两个问题。第一个问题,你把非业务代码写到了业务代码当中。第二个问题,你写了了一大堆重复的代码
【事务管理就是Spring当中AOP的最最经典的体现。】
2 - 事务为了解决啥问题
2.1 - 搭建项目
- 创建如下的演示项目:
- 写好配置文件:
2.2 - 准备数据库
- 数据库中创建三个表:
书的表
库存的表
用户余额的表
2.3 - 编写业务逻辑
从dao层往controller层写。
2.3.1 - dao层代码
2.3.2 - service层代码
2.3.3 - controller层代码
2.3.4 - 测试代码
2.3.5 - 测试执行结果
表示执行成功了。
2.4 - 再执行一次
这个时候,库存是9,用户余额是30,不够再买一本50块的书。
再执行一次,就报错了。如下图所示:
但是查看数据库,库存改变了:
用户余额没有改变:
这个就违背了事务的AICD属性中的一致性。没有购买成功,用户的余额还是30,但是为啥库存改变了,这是错误的。
如上图,第三步没有成功,但是第二步执行了,这对事务来说,是不能接受的。要执行成功,你就都要执行成功。有一步执行不成功,你就要全部执行不成功。
为什么是这样?是因为你的项目中根本就还没有引入事务。
这个前奏项目就是为了说明,到底什么TMD是事务,是为了解决什么问题的。
3 - 学一下声明式事务管理
因为我们要讲的是注解式的事务管理,所以必须导入AspectJ相关的三个jar包哦。
其实,最主要的依赖于:com.springsource.org.aopalliance-1.0.0.jar
这个jar包。
3.1 - 选择事务管理器(管理事务的类)
AbstractPlatformTransactionManager
是Spring为我们抽取出来的,事务管理最基本的功能,最基本的实现方式。Spring根据我们在管理事务所使用的不同技术
,又在这个抽象类下分了不同的子类。
比如:DataSourceTransactionManager
是当你使用原生JDBC去管理事务的时候,咱们需要通过使用DataSourceTransactionManager
这个事务管理器。如果你使用的是hibernate,你就需要使用HibernateTransactionManager
这个事务管理器。Jta也是原来的一个持久层框架,你使用Jta,你就需要使用JtaTransactionManager
这个事务管理器。
这说明了一个什么问题?Spring每去管理一个事务,还需要看一下JDBC操作的过程。用最原始的JDBC,用hibernate,用Jta,不同的技术对应不同的事务管理器。
我们用JdbcTemplate,是Spring对JDBC进行的轻量级封装,所以我们需要用:DataSourceTransactionManager
。
3.2 - 配置事务管理器
- 如果你的数据库都没有连接上,那么事务管理器还管理个屁呀。所以,事务管理器管理的是谁?
- 事务管理器管理的是连接对象。
- 连接对象又是由数据源产生的,所以要配置数据源呀。
- 事务管理器依赖于数据源产生的连接对象。
3.3 - 引入tx命名空间、开启注解、指定事务管理器
引入tx命名空间。
开启事务注解,并且制定事务管理器。
这里面有个小点:
第一种写法的配置方式是如下:
第二种写法的配置方式是如下:
建议你还是别用第二种写法了,玩这个花活干啥,不够省心的,还是第一种写法看起来是比较清晰的。
3.4 - 管理事务
上面的项目中,我们事务相关的代码,是在service层中。buybook()这个方法,是一个事务。如下图所示:
这个方法上面写了@Override
,重写了接口当中的抽象方法。
这里面有一个小点:
如果一个方法是重写方法,那么加不加@Override都可以。
如果一个方法不是重写方法,绝对不要加@Override。
这样就可以了,这样就完了,这样就能管理事务了。
3.5 - 验证一下
当前数据库当中的情况,是如下的:
再执行前奏项目,结果如下:
查看数据库的内容是:
说明事务的管理,已经成功了。多简单。
1、配置文件中:配置事务管理器。
2、配置文件中:开启注解,指定配置管理器。
3、service代码中加上@Transactional注解
4 - @Transactional注解
@Transactional注解可以加载方法上,也可以加在类上,也可以同时都加。牛逼大发了。
@Transactional注解里面有这么多的属性。
这里有几个小点:
如果类和方法都加了注解。
都加了注解,还都设置了属性和值。
如果设置的是相同的属性和值,就近原则,方法上的注解生效。
如果设置的是不同的属性和值,叠加效果,共同生效。
打开@Transactional注解的源码,如下所示:
@Transactional注解中可以设置的属性如下:
- propagation
- isolation
- timeout
- readOnly
- rollbackFor
- rollbackForClassName
- noRollbackFor
- noRollbackForClassName
4.1 - @Transactional属性 - propagation
-
前面我们的项目中,写了一个service是BookServiceImpl,这里面是定义了一个方法是buyBook(),表示的是一次买一本书的方法。
-
我们现在再写一个service,是Cashier,表示的是一次买多本书的方法。如图所示:
- 在controller中模拟对一次性买多本书的方法的调用:
- 一次性买多本书的CashireImpl中的方法checkOut是一个事务。
- 一次性买一本书的BookServiceImpl中的方法buyBook也是一个事务。
- 而且checkOut方法中调用了buyBook方法。
-
在被调用者方法上,就需要设置@@Transactional注解的propagation属性。
-
propagation是传播的意思,表示的是事务的传播级别。
-
propagation的属性值是一个枚举类型的。
-
点进源码当中看,是这个样子的:
- propagation可以有下面的取值:
-
对待传递的事务的处理方式,就是所谓的传播性。
-
1 -
Propagation.REQUIRED
- B必须有,必须用
A调用B,A传递给了B事务,B自己不管有没有事务,B必须用A的事务。
A调用B,A没有传递给B事务,B自己有事务,B就用自己的事务。
A调用B,A没有传递给B事务,B自己没有事务,B必须启动一个新事务运行。
- 2 -
Propagation.REQUIRES_NEW
- B必须用自己
A调用B,A传递给了B事务,B不能用,B自己有就用自己的,自己没有就创建一个事务。
A调用B,A没有传递给B事务,B自己有就用自己的,自己没有就创建一个事务。
- 3 -
Propagation.SUPPORTS
- B有就用,没有就不用
A调用B,A传递给了B事务,B自己不管有没有事务,B必须用A的事务。
A调用B,A没有传递给B事务,B自己有事务,B就用自己的事务。
A调用B,A没有传递给B事务,B自己没有事务,B就不运行事务。
相对于REQUIRED来说,对B的要求降低了,主要表现对A支持。
- 4 -
Propagation.NOT_SUPPORTED
- B必须不用
A调用B,A传递给了B事务,B必须不用。
A没有传递给B事务,B自己有事务,B也不用。
A没有传递给B事务,B自己没有事务,B不动。
- 5 -
Propagation.MANDATORY
- A必须传事务
A调用B,A传递给B事务,B必须用A事务。
A调用B,A不传递给B事务,抛出异常。
- 6 -
Propagation.NEVER
- A必须不能传事务
A调用B,A传递给B事务,B抛出异常。
A调用B,A不传递给B事务,正常。
- 7 -
Propagation.NESTED
A调用B,A传递给B事务,B用A的嵌套事务
A调用B,A没有传递给B事务,B启动一个新事务
【这一段,真的很不理解,写的估计也很有毛病。】
【我们经常用的是REQUIRED和REQUIRED_NEW。】
【默认的是REQUIRED,使用调用者的事务。】
4.2 - @Transactional属性 - isolation
isolation表示的是:事务的隔离级别。一般都用在并发中。当有多个请求都去访问数据库的时候,数据应该如何进行操作。
- 1、读未提交:一条数据的一个字段
咱们对数据库进行了一次更改之后,它不会立马将数据进行更新,而必须是等到事务提交之后,才会真正执行更新。
用户在读取数据的过程中,可以读取到别的请求对它进行的还没有提交的操作。
比如说我现在要将数据库中的年龄,由20改成30,修改过了之后,我还没有提交事务。你来读数据的时候,你可以读到30这个数据。
我没有提交,我回滚了。
你读出来的30,这个数据没有意义。
读未提交造成的结果,就是【脏读】。
- 2、读已提交:一条数据中的一个字段
你只能读到,已经提交了事务的数据。
上面那个例子中,我将20改为30,还没有提交的时候。
你来读数据,你只能读到20。
但是也会出现一些问题。
比如说你刚把20读出去了,我这里立马提交了修改。
这个时候,数据库中数据变成了30。
你第二次去读数据库的时候,读出来的就是30。
你两次读出来的数据,是不一致的。(你在操作,你在多次读)
读已提交,造成的问题,叫做【不可重复读】。
- 3、可重复读:一行数据
你正在读一张表中的某些数据,这些数据我不能进行任何的更改。
你每一次读出来的,就是一样的。(你在操作,你在多次读)
你正在读的数据,我不能动,那我就给表中添加了一些新数据。
你读着读着,发现出现了新数据。你疯了。
可重复读造成的问题,叫做【幻读】。
- 4、串行化
串行化就相当于单线程。
就是把你我对数据库的请求,串起来。
只有你操作成功,我才能去操作。
串行化造成的问题是【性能低、消耗大】。
- @Transactional属性的isolation隔离级别可以通过如下图方式设置:
-
@Transactional属性的isolation隔离级别也可以不设置,不设置就是默认。默认的意思就是和数据库的隔离级别保持一致。
-
mysql数据库默认的数据隔离级别是【可重复读】。
-
数据隔离级别也可以用数字来表示:读未提交是1,读已递交是2,可重复度的隔离级别是4,串行化的隔离级别是8。
- linux当中权限管理有:read、write、excute。读、写、执行。读是1、写是2、执行是4,加起来是7,到公司里面就可以说:给我一个7的权限,意思就是读写执行全部都有。
4.3 - @Transactional属性 - timeout
-
timeout是指在事务的
强制回滚
前可以执行(等待)的时间。 -
比如说,双十一的秒杀活动,到时候并发量就非常大,我们实现这个功能之前,一定要做一个测试,先模拟一下情况。
-
如果有10000个并发,数据库处理一个请求要花费多少时间,这个要计算出来。
-
如果不计算出来,不好设置超时时间。
-
不设置超时时间,一旦程序执行时候,线程调度出现了问题,有的请求一直在执行过程中卡住了,这10000个请求都TM卡在了某个位置。
-
这10000个请求难道就一直等着吗?
-
mysql默认最多能够让100个连接同时访问。
- mysql有那么高的性能吗?
- 所以,需要估算mysql处理一个请求所需要多少时间。
- 比如说3s钟,过了3s,请求还没有执行完成,我就强制回滚。
- timeout这个属性,就是为了设置这个超时时间。
- 后面我们用redis模仿秒杀,要用到一个AB工具,模仿并发。
设置timeout的方式,如下图所示:
试验一下:
执行结果如下:
4.4 - @Transactional属性 - readOnly
- 在html的时候,我们学习表单,遇到过readOnly,不让你改,只让你读。
- readOnly指定当前事务中,一系列的操作,是否为只读。
- 事务当中所有的操作,都是读的操作,可以设置为只读,你的Spring会通知你的mysql,这个事务当中,全部都是读的操作。mysql当中有锁的概念。
- mysql当中有
悲观锁
和乐观锁
的概念。mysql在实现的过程中,用的是多线程+锁
,当前如果说有一个用户正在对mysql中数据进行操作,mysql会为你操作的这一条数据加锁,其他的请求不能对这一条数据进行任何操作,当你操作完成后,锁释放了,别的请求就可以对数据进行访问了。下一个请求对数据进行访问的时候,还是会对这一条操作加锁的。 - 如果把事务的只读,设置为true的话,
告诉mysql里面全部都是读的操作,不对数据进行更改,mysql可以不用加锁的,让所有的请求全部都去读这条数据
,提高了性能。 - 当前的事务当中,不仅仅有读的操作,也有写的操作,你还把readOnly设置true的时候,那么不管读还是写,mysql都不会加锁了。那么什么脏读呀、不可重复读呀、幻读呀,全部都来了。
- 当前事务中,如果有写的操作,建议是一定不要设置readOnly为true。
4.4 - @Transactional属性 - rollbackFor
1、rollbackFor
2、rollbackForClassName
3、noRollbackFor
4、noRollbackForClassName
rollbackFor表示的就是,因为什么而回滚。
如果不设置rollbackFor属性,那么只要抛出异常,事务就会回滚。
如果设置了rollbackFor属性,表示就是在抛出某些异常的时候,事务才会回滚。
看一下源码:
这个玩意,是个数组的。你可以定义好几个异常。
自定义了一些异常:
设置事务的rollbackFor或者noRollbackFor。
5 - xml方式配置事务管理
- AOP当中最经典的实现就是两个:日志管理,事务管理
- 事务是作用在方法上的。事务管理是通过AOP来实现的。
- AOP当中就必须定义切面、通知、切入点表达式。
- 事务管理器,相当于AOP当中的切面。
- 对于增删改查来说,对于Service当中一个完整的业务逻辑来说,事务就是公共功能,或者说叫做非核心业务代码。
在spring当中我们写包结构的地方,已经学习了两个,第一个是我们在学习IOC的时候,扫描Spring组件的时候通过
<context:component-scan base-package="">
,设置扫描包的时候,第二个就是在AOP当中写切入点表达式的时候。
使用xml方式配置事务管理的步骤就是三步:
- 配置事务管理器
- 配置事务通知:事务通知要和事务管理器关联起来。
- 配置切入点表达式:事务通知要和切入点表达式关联起来。