目录
三、事务和定时任务
1 事务
1.1 事务的概念
事务是由一组SQL语句组成的逻辑处理单元。 也就是单个逻辑单元执行一系列的事件,在这个事务中这一系列的事要么全成功,要么全不成功。
那为什么要使用事务呢?
事务是一系列的动作,一旦其中有一个动作出现错误,必须全部回滚,系统将事务中对数据库的所有已完成的操作全部撤消,滚回到事务开始的状态,避免出现由于数据不一致而导致的接下来一系列的错误。事务的出现是为了确保数据的完整性和一致性,在目前企业级应用开发中,事务管理是必不可少的
1.2 事务的属性(ACID)
事务具有以下4个特性,简称为事务ACID属性。
ACID属性 | 含义 |
---|---|
原子性(Atomicity) | 事务是一个原子操作单元,其对数据的修改,要么全部成功,要么全部失败,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。 |
一致性(Consistent) | 在事务开始和完成时,数据都必须保持一致状态。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。 |
隔离性(Isolation) | 数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的 “独立” 环境下运行。 数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。 |
持久性(Durable) | 事务完成之后,对于数据的修改是永久的,即便系统故障也不会丢失。 |
1.3 MYSQL事务
MySQL事务教程:MySQL 事务 | 菜鸟教程
①MySQL 的数据库的默认隔离级别为 Repeatable read(可重复读) , 查看方式:
show variables like 'tx_isolation';
②MySQL中的事务提价方式是自动提交 ON,查看方式:
show variables like 'autocommit';
关闭自动提交方式:
set autocommit=0;
1.3.1 MySQL事务处理的方法:
① 直接用 SET 来改变 MySQL 的自动提交模式:
SET AUTOCOMMIT=0 禁止自动提交
SET AUTOCOMMIT=1 开启自动提交
mysql默认是自动提交的方式,如果要使用事务,需要手动的设置数据库为手动提交(禁止自动提交)
② 用 BEGIN, ROLLBACK, COMMIT来实现
BEGIN(start transaction) 开始一个事务 ,事务的开始
ROLLBACK 事务回滚,恢复状态到事务开始时
COMMIT 事务确认,提交事务
使用BEGIN, ROLLBACK, COMMIT测试事务:
mysql> use tab_user;
Database changed
mysql> select * from tab_user;
Empty set (0.01 sec)
mysql> begin; # 开始事务
Query OK, 0 rows affected (0.00 sec)
mysql> insert into tab_user value(null,'admin1',200);
Query OK, 1 rows affected (0.01 sec)
mysql> insert into tab_user value(null,'admin2',100);
Query OK, 1 rows affected (0.00 sec)
mysql> commit; # 提交事务
Query OK, 0 rows affected (0.01 sec)
mysql> select * from tab_user ;
2 rows in set (0.01 sec)
mysql> begin; # 开始事务
Query OK, 0 rows affected (0.00 sec)
mysql> insert into tab_user value(null,'admin3',50);
Query OK, 1 rows affected (0.00 sec)
mysql> rollback; # 回滚事务
Query OK, 0 rows affected (0.00 sec)
mysql> select * from tab_user ; # 因为回滚所以数据没有插入
2 rows in set (0.01 sec)
mysql>
在mysql的事务中,假如要执行三条sql语句,如果其中有一条SQL语句是错误的,
如果我们进行commit(手动提交)操作,对应的数据还是会持久化到数据库中
如果我们使用rollback(事务回滚)操作,对应的数据就不会持久化到数据库中
1.4 Spring事务
spring事务的本质就是对数据库事务的支持,没有数据库的事务支持,spring是无法完成事务的。
spring所有的事务管理策略类都继承自org.springframework.transaction.PlatformTransactionManager接口;其中TransactionDefinition接口定义以下特性:事务隔离级别、事务传播行为、事务超时、事务只读属性、spring事务回滚规则等;
1.4.1 Spring事务的使用
1、添加jar包(因为这里要对数据库进行操作,所以也需要有相关数据库的jar包)
<!-- Spring 持久化层支持jar包 -->
<!-- Spring 在执行持久化层操作、与持久化层技术进行整合过程中,需要使用orm、jdbc、tx三个jar包 -->
<!-- 导入 orm 包就可以通过 Maven 的依赖传递性把其他两个也导入 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.3.1</version>
</dependency>
<!--数据库 mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!--数据库连接池 druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.13</version>
</dependency>
2、配置事务管理器到Spring配置文件中
<!--
事务管理器
导入的命名空间xmlns:tx="http://www.springframework.org/schema/tx
spring所有的事务管理策略类都继承自org.springframework.transaction.PlatformTransactionManager接口
-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 数据源的设置要和持久层保持一致-->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--
开启事务的注解驱动
通过注解@Transactional所标识的方法或标识的类中所有的方法,都会被事务管理器管理事务
transaction-manager属性的默认值是transactionManager,如果事务管理器bean的id正好就是这个默认值,则可以省略这个属性
-->
<tx:annotation-driven transaction-manager="transactionManager"/>
1.4.2 事务管理的两种方式
一、编程式事务
类似于BaseDao的那种方式,所有的操作都是由开发者编写来完成
Connection conn = ...;
try {
// 开启事务:关闭事务的自动提交
conn.setAutoCommit(false);
// 核心操作
// 提交事务
conn.commit();
}catch(Exception e){
// 回滚事务
conn.rollBack();
}finally{
// 释放数据库连接
conn.close();
}
Spring的编程式事务
//定义事务管理器,给其指定一个数据源(可以把事务管理器想象为一个人,这个人来负责事务的控制操作)
PlatformTransactionManager txManager
= new DataSourceTransactionManager(dataSource);
// 1、定义事务属性:TransactionDefinition,
//TransactionDefinition可以用来配置事务的属性信息,
//比如事务隔离级别、事务超时时间、事务传播方式、是否是只读事务等等。
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
// 2、根据定义开启事务
TransactionStatus status = .getTransaction(definition);
try {
// 数据库的操作
// 3、提交事务
txManager.commit(status);
} catch (Exception e) {
// 4、异常了,回滚事务
txManager.rollback(status);
throw e;
}
编程式事务使用步骤:
1、定义事务属性信息:TransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
2、定义事务管理器:PlatformTransactionManager txManager= new DataSourceTransactionManager(dataSource);
3、获取事务:TransactionStatus status= txManager.getTransaction(transactionDefinition);
4、执行sql操作
5、提交事务(txManager.commit)或者回滚事务(txManager.rollback(status))
编程式的实现方式存在的缺陷:
细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用。
二、声明式事务
声明式事务就是通过配置让框架实现功能,不需要写过多的代码;
框架将固定模式的代码抽取出来,进行相关的封装。封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作。
声明式事务使用过程:
上面已经配置好了事务管理器在Spring配置文件中,通过注解@Transactional所标识的方法或标识的类中所有的方法,都会被事务管理器管理事务,因为service层表示业务逻辑层,一个方法表示一个完成的功能,因此处理事务一般在service层添加注解@Transactional
声明式事务使用示例:
现在有一张表tab_user表,假如里面的admin1给admin2转钱
建tab_user表:
DROP TABLE IF EXISTS `tab_user`;
CREATE TABLE `tab_user` (
`uid` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) DEFAULT NULL,
`balance` int(11) unsigned DEFAULT NULL,
PRIMARY KEY (`uid`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of tab_user
-- ----------------------------
INSERT INTO `tab_user` VALUES ('1', 'admin1', '400');
INSERT INTO `tab_user` VALUES ('2', 'admin2', '1000');
INSERT INTO `tab_user` VALUES ('3', 'admin3', '300');
服务层接口:
public interface TabUserService {
//第一个人的id,第二个人的id,转账的金额
void trade(Integer uid1,Integer uid2,Integer money);
}
服务层实现类:
/**
* (TabUser)表服务实现类
*
* @ Transactional 注解代表添加了一个事务,
* @ Transactional标识在方法上,只会影响该方法
* @ Transactional标识的类上,会影响类中所有的方法
* 推荐添加在方法上,因为一个类中的方法所需要完成的事务不完全统一
*/
@Service
public class TabUserServiceImpl implements TabUserService {
@Autowired
private TabUserMapper tabUserDao;
@Override
@Transactional //添加了一个事务
public void trade(Integer uid1, Integer uid2, Integer money) {
//第一个人的钱减少
tabUserDao.subMoney(uid1,money);
// 第二个人的钱增加
tabUserDao.addMoney(uid2,money);
}
}
测试类:
/**
* RunWith:运行的时候用的是什么工具
* ContextConfiguration:标明对应的配置文件
* 在这里,数据库中的balance列标明了是unsigned(无符号),
* 所以当id为1的用户balance值小于要转出的金额时,就会产生异常,从而事务回滚到初始状态
* 所以当id为1的用户balance值大于等于要转出的金额时,不会产生异常,事务会成功提交
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring.xml")
public class TestTx {
@Resource
private TabUserService tabUserService;
@Test
public void testTx(){
tabUserService.trade(1,2,500);
}
}
1.5 Spring声明式事务属性
1.5.1、readOnly(只读)
readOnly(只读),默认为true,设置成只读,就能够明确告诉数据库,这个操作不涉及写操作
* 这个属性一般只出现在查询的方法上,但是查询操作也不建议写,如果涉及写操作会报异常
* Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
接口方法:
void tradeReadonly(Integer uid1,Integer uid2,Integer money);
实现类:
/**
* 测试事务的只读属性
*/
@Override
@Transactional(readOnly = true)
public void tradeReadonly(Integer uid1, Integer uid2, Integer money) {
//第一个人的钱减少
tabUserDao.subMoney(uid1,money);
// 第二个人的钱增加
tabUserDao.addMoney(uid2,money);
}
测试类:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring.xml")
public class TestTx {
@Test
public void testTxReadonly(){
tabUserService.tradeReadonly(1,2,500);
}
}
1.5.2、Timeout(超时)
Timeout(超时),单位为秒,事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。
* 而长时间占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。
* 此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,结束事务,把资源让出来,让其他正常程序可以执行。此时可以设置超时回滚,释放资源
* 超时抛出异常:org.springframework.transaction.TransactionTimedOutException: Transaction timed out:
接口:
void tradeTimeout(Integer uid1,Integer uid2,Integer money);
实现类:
/**
* 测试事务的超时属性
*/
@Override
@Transactional(timeout = 3) //单位为秒
public void tradeTimeout(Integer uid1, Integer uid2, Integer money) {
//第一个人的钱减少
tabUserDao.subMoney(uid1,money);
//利用多线程让程序休眠5秒
try {
// 两种效果是一样
// Thread.sleep(5000);//单位是毫秒
TimeUnit.SECONDS.sleep(5);//单位是秒
} catch (InterruptedException e) {
e.printStackTrace();
}
测试类:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring.xml")
public class TestTx {
@Test
public void testTxTimeout(){
tabUserService.tradeTimeout(1,2,500);
}
}
1.5.3、Rollback(回滚策略)
Rollback(回滚策略):声明式事务默认只针对运行时异常回滚,编译时异常不回滚。
* rollbackFor属性:需要设置一个Class类型的对象(rollbackFor = ArithmeticException.class)
* rollbackForClassName属性:需要设置一个字符串类型的全类名(rollbackForClassName = "java.lang.ArithmeticException")
* noRollbackFor属性:需要设置一个Class类型的对象
* noRollbackForClassName属性:需要设置一个字符串类型的全类名
接口:
void tradeRollback(Integer uid1,Integer uid2,Integer money);
void tradeNoRollback(Integer uid1,Integer uid2,Integer money);
实现类:
/**
* 测试事务的回滚策略中的不回滚
* 两种效果是一样的,只是参数的形式不同
*/
@Override
@Transactional(noRollbackFor = ArithmeticException.class)
// @Transactional(noRollbackForClassName = "java.lang.ArithmeticException")
public void tradeNoRollback(Integer uid1, Integer uid2, Integer money) {
//第一个人的钱减少
tabUserDao.subMoney(uid1,money);
// 第二个人的钱增加
tabUserDao.addMoney(uid2,money);
// 假如事务完成后发生异常
System.out.println(1/0);
}
/**
* 测试事务的回滚策略中的回滚
* 两种效果是一样的,只是参数的形式不同
*/
@Override
//@Transactional(rollbackFor = ArithmeticException.class)
@Transactional(rollbackForClassName = "java.lang.ArithmeticException")
public void tradeRollback(Integer uid1, Integer uid2, Integer money) {
//第一个人的钱减少
tabUserDao.subMoney(uid1,money);
// 第二个人的钱增加
tabUserDao.addMoney(uid2,money);
// 假如事务完成后发生异常
System.out.println(1/0);
}
测试类:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring.xml")
public class TestTx {
@Test
public void testTxRollback(){
tabUserService.tradeRollback(1,2,500);
}
@Test
public void testTxNoRollback(){
tabUserService.tradeNoRollback(1,2,500);
}
}
1.5.4、propagation(传播行为)
传播行为是指当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。
假设有一个这样的事务:用户1向用户2,用户3分别转钱500;
初始用户1有900,用户2有500,用户3有300,
用户1向每个人转钱都是 收钱方金额先加,用户1金额再减
在接口1中定义一个向多人转账的方法tradeToMany,接口2中有一个向单人转账的方法tradeMany
在接口1对应的实现类中实现向多人转账事务时,是通过循环调用接口2中这个向单人转账事务
下面看代码:
propagation(传播行为):当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。
* @Transactional(propagation=Propagation.REQUIRED):默认的spring事务传播级别,使用该级别的特点是,如果上下文中已经存在事务,那么就挂起当前的事务加入到上下文的事务中执行,如果当前上下文中不存在事务,则新建事务执行,所以这个级别通常能满足处理大多数的业务场景。
*
* @Transactional(propagation=PROPAGATION.SUPPORTS):从字面意思就知道,supports(支持),该传播级别的特点是,如果上下文存在事务,则支持当前事务,加入到事务执行,如果没有事务,则使用非事务的方式执行。
* 所以说,并非所有的包在transactionTemplate.execute中的代码都会有事务支持。这个通常是用来处理那些并非原子性的非核心业务逻辑操作,应用场景较少。
*
* @Transactional(propagation=PROPAGATION.MANDATORY):该级别的事务要求上下文中必须要存在事务,否则就会抛出异常!配置该方式的传播级别是有效的控制上下文调用代码遗漏添加事务控制的保证手段。比如一段代码不能单独被调用执行,但是一旦被调用,就必须有事务包含的情况,就可以使用这个传播级别。
*
* @Transactional(propagation=PROPAGATION.REQUIRES_NEW):从字面即可知道,每次都要一个新的事务,该传播级别的特点是,每次都会新建一个事务,并且同时将上下文中的事务挂起,当新建事务执行完成以后,上下文事务再恢复执行。
*
*
* @Transactional(propagation=PROPAGATION.NOT_SUPPORTED) :这个也可以从字面得知,not supported(不支持),当前级别的特点是,如果上下文中存在事务,
* 则挂起事务,执行当前逻辑,结束后恢复上下文的事务。
* 这个级别有什么好处?可以帮助你将事务极可能的缩小。我们知道一个事务越大,它存在的风险也就越多。所以在处理事务的过程中,要保证尽可能的缩小范围。
* 比如一段代码,是每次逻辑操作都必须调用的,比如循环1000次的某个非核心业务逻辑操作。这样的代码如果包在事务中,势必造成事务太大,导致出现一些难以考虑周全的异常情况。所以这个事务这个级别的传播级别就派上用场了,用当前级别的事务模板包起来就可以了。
*
* @Transactional(propagation=PROPAGATION.NEVER):该事务更严格,上面一个事务传播级别只是不支持而已,有事务就挂起,而PROPAGATION_NEVER传播级别要求上下文中不能存在事务,一旦有事务,就抛出runtime异常,强制停止执行!
* @Transactional(propagation=PROPAGATION.NESTED):字面也可知道,nested,嵌套级别事务。该传播级别特征是,如果上下文中存在事务,则创建一个子事务执行(子事务不能单独提交,提交的时候父事务也会提交。)如果不存在事务,则新建事务。
接口方法:
接口1:
public interface TabUserServiceMany {
//第一个人的id,第二个人的id,转账的金额
void tradeToMany(Integer uid1, Integer[] uid2, Integer money);
}
接口2:
void tradeMany(Integer uid1,Integer uid2,Integer money);
实现类:
接口1实现类:
@Service
public class TabManyUserServiceImpl implements TabUserServiceMany {
@Resource
private TabUserService tabUserService;
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void tradeToMany(Integer uid1, Integer[] uid2, Integer money) {
for (Integer uid : uid2) {
tabUserService.tradeMany(uid1,uid,money);
}
}
}
接口2实现类:
在这个事务中,向多人转账这个事务算是向单人转账事务的上下文事务
/**
* 测试事务的传播行为(上下文的事务的传播行为都是REQUIRED)
* (事件:用户1向用户2,用户3分别转钱500;初始用户1有900,用户2有500,用户3有300,向每个人转钱都是 先加后减)
* REQUIRED:如果上下文中有事务,就是会挂起当前的事务,把事件添加到上下文的事务中(1:900---2:500---3:300)
* REQUIRES_NEW:无论上下文中有没有事务,都会挂起上下文的事务,去创建一个新的事务,(1:400---2:1000---3:300)
* SUPPORTS:如果上下文存在事务,则支持当前事务,加入到事务执行;如果上下文中没有事务,则使用非事务的方式执行。
* 假如上下文有事务,那么会挂起当前事务,加入到上下文的事务中执行.(1:900---2:500---3:300)
* 假如上下文没有事务,那这里也不会按照事务进行运行,每一句sql都是单独提交.(1:400---2:1000---3:800)
* MANDATORY:要求上下文中必须要存在事务,否则就会抛出异常!
* 上下文没有事务时:报出异常:org.springframework.transaction.IllegalTransactionStateException:
* 上下文有事务时:(1:900---2:500---3:300)
* NOT_SUPPORTED:不支持事务,如果上下文中存在事务, 则挂起事务,执行当前逻辑,结束后恢复上下文的事务。(1:400---2:1000---3:800)
* NEVER:要求上下文中不能存在事务,一旦有事务,就抛出runtime异常,强制停止执行!
* 上下文没有事务时:(1:400---2:1000---3:800)
* 上下文有事务时:报出异常:org.springframework.transaction.IllegalTransactionStateException:
* NESTED:嵌套级别事务。如果上下文中存在事务,则创建一个子事务执行(子事务不能单独提交,提交的时候父事务也会提交)(1:900---2:500---3:300)
* 如果上下文中不存在事务,则新建事务。(1:400---2:1000---3:300)
*/
@Override
@Transactional(propagation = Propagation.REQUIRED)
//@Transactional(propagation = Propagation.REQUIRES_NEW)
//@Transactional(propagation = Propagation.SUPPORTS)
//@Transactional(propagation = Propagation.MANDATORY)
//@Transactional(propagation = Propagation.NOT_SUPPORTED)
//@Transactional(propagation = Propagation.NEVER)
//@Transactional(propagation = Propagation.NESTED)
public void tradeMany(Integer uid1, Integer uid2, Integer money) {
// 第二个人的钱增加
tabUserDao.addMoney(uid2,money);
//第一个人的钱减少
tabUserDao.subMoney(uid1,money);
}
测试类:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring.xml")
public class TestTx {
@Resource
private TabUserServiceMany tabUserServiceMany;
@Test
public void testTxTradeMany(){
tabUserServiceMany.tradeToMany(1,new Integer[]{2,3},500);
}
}
1.5.5、isolation (隔离级别)
一个事务与其他事务隔离的程度称为隔离级别。
数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。
SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。
隔离级别一共有四种:
类别 | 描述 |
---|---|
读未提交:READ UNCOMMITTED | 允许Transaction01读取Transaction02未提交的修改。 |
读已提交:READ COMMITTED | 要求Transaction01只能读取Transaction02已提交的修改。(解决脏读问题) |
可重复读:REPEATABLE READ | 确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它事务对这个字段进行更新。(行锁)解决脏读和不可重复读 |
串行化:SERIALIZABLE | 确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。(表锁)解决脏读、不可重复读、幻读 |
隔离级别 | Dirty reads 脏读 | non-repeatable reads 不可重复读 | phantom reads 幻读 |
---|---|---|---|
SERIALIZABLE(串行化) | 不会 | 不会 | 不会 |
REPEATABLE_READ(可重复读) | 不会 | 不会 | 会 |
READ_COMMITTED(读已提交) | 不会 | 会 | 会 |
READ_UNCOMMITTED(读未提交) | 会 | 会 | 会 |
1、脏读:脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个修改但是未提交的数据。
例如:
张三的工资为5000,事务1中把他的工资改为8000,但事务1尚未提交。
与此同时,事务2正在读取张三的工资,读取到张三的工资为8000。
随后,事务1发生异常,而回滚了事务。张三的工资又回滚为5000。
最后,事务2读取到的张三工资为8000的数据即为脏数据,事务B做了一次脏读。
2、不可重复读:是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。
例如:
在事务1中,读取到张三的工资为5000,操作没有完成,事务还没提交。
与此同时,事务2把张三的工资改为8000,并提交了事务。
随后,在事务1中,再次读取张三的工资,此时工资变为8000。
在一个事务中前后两次读取的结果并不致,导致了不可重复读。
不可重复读的重点是修改:
同样的条件,你读取过的数据,再次读取出来发现值不一样了
3、幻读:是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。
例如:
目前工资为5000的员工有10人,事务1读取所有工资为5000的人数为10人。
此时,事务2插入一条工资也为5000的记录。
这时,事务1再次读取工资为5000的员工,记录为11人。事务1产生了幻读。
幻读的重点在于新增或者删除:
同样的条件,第 1 次和第 2 次读出来的记录数不一样
常用数据库默认事务隔离级别:
MYSQL:默认为REPEATABLE_READ(可重复读)
SQLSERVER:默认为READ_COMMITTED(读已提交)
ORACLE:默认为READ_COMMITTED(读已提交)
各种数据库的支持程度:
隔离级别 | Oracle | MySQL |
---|---|---|
READ UNCOMMITTED | × | √ |
READ COMMITTED | √(默认) | √ |
REPEATABLE READ | × | √(默认) |
SERIALIZABLE | √ | √ |
1.6 基于XML的声明式事务(了解)
基于xml实现的声明式事务,必须引入aspectJ的依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.1</version>
</dependency>
将Spring配置文件中去掉tx:annotation-driven 标签(关闭注解驱动),并添加配置:
<aop:config>
<!-- 配置事务通知和切入点表达式 -->
<aop:advisor advice-ref="txAdvice" pointcut="execution(*
com.aaa.service.impl.*.*(..))"></aop:advisor>
</aop:config>
<!-- tx:advice标签:配置事务通知 -->
<!-- id属性:给事务通知标签设置唯一标识,便于引用 -->
<!-- transaction-manager属性:关联事务管理器 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- tx:method标签:配置具体的事务方法 -->
<!-- name属性:指定方法名,前面的字符必须都一样,可以使用星号代表多个字符 -->
<tx:method name="get*" read-only="true"/>
<tx:method name="query*" read-only="true"/>
<tx:method name="find*" read-only="true"/>
<!-- read-only属性:设置只读属性 -->
<!-- rollback-for属性:设置回滚的异常 -->
<!-- no-rollback-for属性:设置不回滚的异常 -->
<!-- isolation属性:设置事务的隔离级别 -->
<!-- timeout属性:设置事务的超时属性 -->
<!-- propagation属性:设置事务的传播行为 -->
<tx:method name="save*" read-only="false" rollbackfor="
java.lang.Exception" propagation="REQUIRES_NEW"/>
<tx:method name="update*" read-only="false" rollbackfor="
java.lang.Exception" propagation="REQUIRES_NEW"/>
<tx:method name="delete*" read-only="false" rollbackfor="
java.lang.Exception" propagation="REQUIRES_NEW"/>
</tx:attributes>
</tx:advice>
分析事务的使用原理
声明式事务的实现是:拦截被@Transaction修饰的类或方法,然后通过创建增强的方式将事务的逻辑织入到业务代码中。
而解析@Transactional注解的织入事务逻辑在MethodInterceptor的实现类TransactionInterceptor中,所以被@Transactional注解修饰的方法,执行业务逻辑时都会执行TransactionInterceptor的invoke方法,可以参考源码观看,由注解EnableAspectJAutoProxy作为一个入口进行源码的分析
2 定时任务
spring task,可以将它比作一个轻量级的Quartz,而且使用起来很简单,除spring相关的包外不需要额外的包,而且支持注解和配置文件两种
定时任务的使用:
1、添加jar包:
第一步加jar
<!--定时任务所需要的包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.3.1</version>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.1</version>
</dependency>
2、在配置文件中配置task相关代码
<!--
开启定时任务的注解,开启这个配置,spring才能识别@Scheduled注解
-->
<task:annotation-driven/>
3.1 xml配置形式使用定时任务:(启用Tomcat,观察控制台)
编写测试类
@Service
public class TaskService {
// 每两秒输出一句 hello,world!
@Scheduled(cron = "0/2 * * * * ?")
public void testScheduled(){
System.out.println("hello,world!");
}
}
对应的配置文件(xml代码)
<!--
配置定时任务
ref参数指定的即任务类,method指定的是需要运行的方法,cron就是cronExpression表达式
-->
<task:scheduled-tasks>
<task:scheduled ref="taskService" method="testScheduled" cron="0/2 * * * * ?"/>
</task:scheduled-tasks>
3.2 注解形式使用定时任务:(启用Tomcat,观察控制台)
/**
* 1、cron表达式:
* cron表达式是由若干数字、空格、符号按一定的规则,组成一组字符串,从而表达时间的信息。与正则表达式类似,都是一个字符串表示一些信息。
* 2、cron表达式标准结构:
* cron表达式是一个字符串,字符串以5或6个空格隔开,分为6或7个域,每一个域代表一个含义。
* corn从左到右(用空格隔开):秒 分 小时 月份中的日期 月份 星期中的日期 年份
* 3、cron有如下两种语法格式:
* Seconds Minutes Hours DayofMonth Month DayofWeek Year
* Seconds Minutes Hours DayofMonth Month DayofWeek
* 4、字段含义
* 字段 允许值 允许的特殊字符
* 秒 0-59 , - * /
* 分 0-59 , - * /
* 小时 0-23 , - * /
* 日期 1-31 , - * ? / L W C
* 月份 1-12 或者 JAN-DEC , - * /
* 星期 1-7 或者 SUN-SAT , - * ? / L C #
* 年(可选) 留空, 1970-2099 , - * /
* 5、字符含义
* *:表示匹配该域的任意值,假如在Minutes域使用*, 即表示每分钟都会触发事件。
* ?:只能用在DayofMonth和DayofWeek两个域。它也匹配域的任意值,
* 但实际不会。因为DayofMonth和 DayofWeek会相互影响。
* 例如想在每月的20日触发调度,不管20日到底是星期几,
* 则只能使用如下写法: 13 13 15 20 ?, 其中最后一位只能用?,
* 而不能使用,如果使用*表示不管星期几都会触发,实际上并不是这样。
* -:表示范围,例如在Minutes域使用5-20,表示从5分到20分钟每分钟触发一次。
* /:表示起始时间开始触发,然后每隔固定时间触发一次,例如在Minutes域使用5/20,
* 则意味着从第5分钟开始每隔20分钟触发一次,而25,45等分别触发一次。
* ,:表示列出枚举值值。例如:在Minutes域使用5,20,则意味着在5和20分每分钟触发一次。
* L:表示最后,只能出现在DayofWeek和DayofMonth域,如果在DayofWeek域使用5L,意味着在最后的一个星期四触发。
* W:表示有效工作日(周一到周五),只能出现在DayofMonth域,系统将在离指定日期的最近的有效工作日触发事件。
* 例如:在 DayofMonth使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。
* 如果5日是星期天,则在6日(周一)触发;
* 如果5日是星期一到星期五中的某一天,则就在5日触发。
* 另外一点,W的最近寻找不会跨过月份。
* LW:这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。
* #:用于确定每个月第几个星期几,只能出现在DayofMonth域。
* 例如在4#2,表示某月的第二个星期三。
* 值得注意的一点是:在星期这个字段上,周日代表是1,周一则是2
* 6、例
* "0 0 12 * * ?" 每天中午十二点触发
* "0 15 10 ? * *" 每天早上10:15触发
* "0 15 10 * * ?" 每天早上10:15触发
* "0 15 10 * * ? *" 每天早上10:15触发
* "0 15 10 * * ? 2005" 2005年的每天早上10:15触发
* "0 * 14 * * ?" 每天从下午2点开始到2点59分每分钟一次触发
* "0 0/5 14 * * ?" 每天从下午2点开始到2:55分结束每5分钟一次触发
* "0 0/5 14,18 * * ?" 每天的下午2点至2:55和6点至6点55分两个时间段内每5分钟一次触发
* "0 0-5 14 * * ?" 每天14:00至14:05每分钟一次触发
* "0 10,44 14 ? 3 WED" 三月的每周三的14:10和14:44触发
* "0 15 10 ? * MON-FRI" 每个周一、周二、周三、周四、周五的10:15触发
*/
@Service
public class TaskService {
// 每两秒输出一句 hello,world!
@Scheduled(cron = "0/2 * * * * ?")
public void testScheduled(){
System.out.println("hello,world!");
}
}