Spring-声明式事务
一、概念的引出
? ? ? ? 事务这个在哪里会使用到?当我们处理一个比较复杂的业务需求时,比如银行转账,比如淘宝网上支付,涉及到多个表的问题。甚至还会涉及到跨系统.
●编程式事务
在编码中直接控制,一般是硬编码.看一段示意代码.
Connection con = JdbcUtils.getConnectionO;
con.setAutocommit(false);
try{
//开始我们一系列的操作…
//..…
}catch(Exception e){? //只要报错,就将所有数据库操作回滚
? ? con.rollabck0;
}
con.
con.commitO;
特点:
1.比较直接,也比较好理解
2.灵活度不够,而且功能不够强大。(如果事务涉及多个Dao,那么就不好控制)
●声明式事务
即我们可以通过注解的方式来说明哪些方法是事务控制的,并且可以指定事务的隔离级别
二、事务的简单示例
首先,使用事务需要先添加依赖,并配置appliction.xml
1、@TRANSACTIONANL事务是基于AOP面向切面的,所以aop的包需要
2、maven依赖
<!-- 事务@TRANSACTIONAL-->
<!-- 代码生成库-->
<dependency>
<groupId>net.sourceforge.cglib</groupId>
<artifactId>com.springsource.net.sf.cglib</artifactId>
<version>2.2.0</version>
</dependency>
<!-- 事务@TRANSACTIONAL-->
<!-- aop面向切面编程相关-->
<dependency>
<groupId>org.aopalliance</groupId>
<artifactId>com.springsource.org.aopalliance</artifactId>
<version>1.0.0</version>
</dependency>
<!-- AOP切面编程框架 aspect-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.9</version>
</dependency>
3、配置xml
<!-- 配置事务管理器-->
<bean id="DataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 开启基于注解的声明式事务功能-->
<tx:annotation-driven transaction-manager="DataSourceTransactionManager"/>
create table `user_account` (
user_id int UNSIGNED PRIMARY KEY auto_increment,
user_name varchar(32) not null DEFAULT '',
money DOUBLE not null default 0.0
)CHARSET=utf8 engine=innodb;
insert into `user_account` VALUES(null,'张三',1000),(null,'李四',2000);
create table `goods` (
goods_id int unsigned primary key auto_increment,
goods_name varchar(32) not null default '',
price DOUBLE not null DEFAULT 0.0
)charset = utf8 engine=innodb;
insert into `goods` VALUES (null,'小风扇',10.00),
(null,'小台灯',12.00),(null,'可口可乐',3.00);
create table `goods_amount` (
goods_id int UNSIGNED PRIMARY key auto_increment,
goods_num int UNSIGNED DEFAULT 0
)charset = utf8 engine= innodb;
insert into `goods_amount` VALUES(1,200),
(2,20),(3,15);
@Repository
public class GoodsDao {
@Autowired
private JdbcTemplate jdbcTemplate;
//根据商品获得价格
public float queryById(int goodsId) {
String sql = "SELECT price FROM goods WHERE goods_id = ?";
Float price = jdbcTemplate.queryForObject(sql, new Object[]{goodsId}, Float.class);
return price;
}
//修改某个用户的余额
public void updateBalance(int userId,float money) {
String sql = "UPDATE user_account SET money = money - ? WHERE user_id = ?";
jdbcTemplate.update(sql,money,userId);
}
//修改库存量
public void updateAmount(int goodsId,int amount) {
String sql = "UPDATE goods_amount SET goods_num = goods_num-? WHERE goods_id = ?";
jdbcTemplate.update(sql,amount,goodsId);
}
}
使用@TRANSACTIONAL注解?,?明显地?
@Service
public class GoodsService {
@Autowired
private GoodsDao goodsDao;
@Transactional //事务注解!!!!
public void buyGoods(int goodsId,int userId,int nums){
//第一步查询价格
float price = goodsDao.queryById(goodsId);
float price2 = price*nums;
//判断用户余额是否足够
//更新余额
goodsDao.updateBalance(goodsId,price2);
//更新库存
goodsDao.updateAmount(goodsId,nums);
}
}
说明:
明显地,在buyGoods买东西的操作中,
如果不加入事务控制,?假如更新余额成功,而更新库存失败 ,?那么就会发生超卖风险。
添加了@TRANSACTIONAL后,在GoodsService中,更新余额?操作,?与?更新库存操作?只要有一个抛异常,?那么两个操作在数据库都不会成功。?即是一个整体,?要么都成功,?要么都不成功。数据一致性。
请注意 @TRANSACTIONAL?一般修饰?service?层
三、声明式事务:事务传播机制
基本说明:
当有多个事务处理并存时,如何控制?比如用户去购买两次商品(使用不同的方法),每个方法都是一个事务,那么如何控制呢?=>这个就是事务的传播机制,看一个具体的案例:
当事务嵌套时,应该如何管理事务呢?事务传播机制
spring一共提供了7中事务传播机制,常用的是REQUIRED和REQUIRED_NEW事务传播机制.(描述看不懂,请继续往下看,我也看不懂哈哈哈)
使用说明
@Service
public class GoodsService {
@Autowired
private GoodsDao goodsDao;
@Transactional(propagation = Propagation.REQUIRED)
public void buyGoods(int goodsId,int userId,int nums){
//第一步查询价格
float price = goodsDao.queryById(goodsId);
float price2 = price*nums;
//判断用户余额是否足够
//更新余额
goodsDao.updateBalance(goodsId,price2);
//更新库存
goodsDao.updateAmount(goodsId,nums);
}
}
常用的两个传播机制
请看截图解释,常用的两种传播机制
REQUIRED :? 事务的默认传播机制就是REQUIRED?
?当事务嵌套时,如果全部事务(包括Tx1事务,嵌套事务方法1,方法2)的传播机制指定的都是REQUIRED, 那么可以看作Tx1方法中任意报错,那么整体回滚(即方法1,方法2也要回滚)。
?
?详细说明:
1、在buyGoods这个事务中,我们执行“更新余额”, “更新库存”这两个操作
@Service
public class GoodsService {
@Autowired
private GoodsDao goodsDao;
public void buyGoods(int goodsId,int userId,int nums){
//第一步查询价格
float price = goodsDao.queryById(goodsId);
float price2 = price*nums;
//判断用户余额是否足够
//更新余额
goodsDao.updateBalance(goodsId,price2);
//更新库存
goodsDao.updateAmount(goodsId,nums);
}
2、故意将“修改库存量”的sql写错。那么在没有事务的情况下,调用buyGoods(),明显地更新余额会成功而更新库存不会成功。?
3、现在我们在buyGoods上加了@TRANSACTIONAL,测试一下看看会有什么不同
@Service
public class GoodsService {
@Autowired
private GoodsDao goodsDao;
@TRANSACTIONAL
public void buyGoods(int goodsId,int userId,int nums){
//第一步查询价格
float price = goodsDao.queryById(goodsId);
float price2 = price*nums;
//判断用户余额是否足够
//更新余额
goodsDao.updateBalance(goodsId,price2);
//更新库存
goodsDao.updateAmount(goodsId,nums);
}
?4、在测试之前,查询mysql,
5、测试下,发现商品1的数量并没有改变,说明事务发生了作用,进行了回滚。且默认传播机制就是REQUIRED?
@Test
public void test04() throws SQLException {
GoodsService goodsService = (GoodsService) applicationContext.getBean("goodsService");
goodsService.buyGoods(1, 1, 1);
}
REQUIRED_NEW:
TX1 ?@Transactional(propagation = Propagation.REQUIRED)
TX2??@Transactional(propagation = Propagation.REQUIRED_NEW)
TX3? @Transactional(propagation = Propagation.REQUIRED_NEW)
当TX2开始时,TX1会挂起,直到TX2结束,TX1继续。以此类推
简单理解,即TX1,TX2,TX3这三个事务之间互不影响,? 即使?方法1报错,也不影响?方法2的操作,也不影响tx1
详细说明:
1、重新再写一个dao,
@Repository
public class GoodsDao {
@Autowired
private JdbcTemplate jdbcTemplate;
//根据商品获得价格
public float queryById(int goodsId) {
String sql = "SELECT price FROM goods WHERE goods_id = ?";
Float price = jdbcTemplate.queryForObject(sql, new Object[]{goodsId}, Float.class);
return price;
}
//修改某个用户的余额
public void updateBalance(int userId,float money) {
String sql = "UPDATE user_account SET money = money - ? WHERE user_id = ?";
jdbcTemplate.update(sql,money,userId);
}
//修改库存量
public void updateAmount(int goodsId,int amount) {
String sql = "UPDATE goods_amount SET goods_num = goods_num-? WHERE goods_id = ?";
jdbcTemplate.update(sql,amount,goodsId);
}
public float queryById2(int goodsId) {
String sql = "SELECT price FROM goods WHERE goods_id = ?";
Float price = jdbcTemplate.queryForObject(sql, new Object[]{goodsId}, Float.class);
return price;
}
//修改某个用户的余额
public void updateBalance2(int userId,float money) {
String sql = "UPDATE user_account SET money = money - ? WHERE user_id = ?";
jdbcTemplate.update(sql,money,userId);
}
//修改库存量
public void updateAmount2(int goodsId,int amount) {
String sql = "UPDATE goods_amount SET1 goods_num = goods_num-? WHERE goods_id = ?";
jdbcTemplate.update(sql,amount,goodsId);
}
}
2、重新再写一个Service,buyGoods2全部用方法2,并设置好REQUIERS_NEW
@Service
public class GoodsService {
@Autowired
private GoodsDao goodsDao;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void buyGoods(int goodsId,int userId,int nums){
//第一步查询价格
float price = goodsDao.queryById(goodsId);
float price2 = price*nums;
//判断用户余额是否足够
//更新余额
goodsDao.updateBalance(goodsId,price2);
//更新库存
goodsDao.updateAmount(goodsId,nums);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void buyGoods2(int goodsId,int userId,int nums){
//第一步查询价格
float price = goodsDao.queryById2(goodsId);
float price2 = price*nums;
//判断用户余额是否足够
//更新余额
goodsDao.updateBalance2(goodsId,price2);
//更新库存
goodsDao.updateAmount2(goodsId,nums);
}
}
3、?写一个multiService
@Service
public class MultiService {
@Autowired
private GoodsService goodsService;
@Transactional
public void multiBuyGoods () {
goodsService.buyGoods(1,1,1);
goodsService.buyGoods2(2,2,1);
}
}
4、故意将buyGoods2的sql写错。 测试,看看会发生什么变化?会回滚吗?
先查mysql,?注意看张三的钱930
执行
@Test
public void test05() throws SQLException {
MultiService multiService = (MultiService)applicationContext.getBean("multiService");
multiService.multiBuyGoods();
}
?测试后,查mysql ,发现张三的钱变了,李四没有变,
由此说明?REQUIRED_NEW修饰的话,就是你是你,我是我,事务间互不影响
四、声明式事务-隔离级别
mysql中的四种隔离级别(详见mysql篇。哦,还没补笔记,呵呵~)
细节:
1.如要看到隔离级别效果,设置隔离级别时,这里我们需要将其传播方式设置为REQUIRES_NEW
2.因为如果是REQUIRES 是不会开新事务,这样这个隔离级别就是hibernate默认的隔离级别(一般是repeatable read )
细节的解释:
当老师说需要将事务的传播机制设置成REQUIRES_NEW
才能看到隔离级别的效果时,可能有以下原因:
- 确保新的事务总是开始一个新的事务。如果一个事务启动了另一个事务,那么新的事务将具有自己的隔离级别,而不会受到原始事务隔离级别的影响。这样可以更清晰地观察不同隔离级别之间的差异。
- 在一些数据库系统中,如MySQL,当使用InnoDB存储引擎时,默认的事务隔离级别是可重复读(REPEATABLE READ)。在这个隔离级别下,如果一个事务在执行过程中有其他事务尝试修改或删除其中的数据,将会看到不可重复读(Non-repeatable Read)现象,即同一行数据在不同的事务中可能表现出不同的状态。而将事务传播机制设置为
REQUIRES_NEW
可以确保每个事务都以自己的隔离级别执行,不受其他事务的影响。
测试事务隔离级别?
@Service
public class GoodsService {
@Autowired
private GoodsDao goodsDao;
@Transactional(
propagation = Propagation.REQUIRES_NEW
// ,isolation = Isolation.READ_COMMITTED
)
public void buyGoods(int goodsId,int userId,int nums){
//第一步查询价格
float price = goodsDao.queryById(goodsId);
float price2 = price*nums;
//判断用户余额是否足够
//更新余额
goodsDao.updateBalance(goodsId,price2);
//更新库存
goodsDao.updateAmount(goodsId,nums);
//第二次查询价格,测试事务隔离级别
price = goodsDao.queryById(goodsId);
System.out.println("goods_price"+price);
}
??????
?debug以下测试方法,程序在断点停滞,
@Test
public void test06() throws SQLException {
GoodsService goodsService = (GoodsService) applicationContext.getBean("goodsService");
goodsService.buyGoods(1, 1, 1);
}
此时先查mysql,? id =1?的商品的价格是10
开启一个新事务并?修改price = 20, where goods_id = 1 ,?然后commit
stepOver程序,发现控制台输出的仍然是?goods_price = 10
说明默认的@TRANSACTIONAL的事务隔离级别是 REPEATABLE READ?可重复读
那么现在来修改一下隔离级别
@Transactional(
propagation = Propagation.REQUIRES_NEW
,isolation = Isolation.READ_COMMITTED
)
public void buyGoods(int goodsId,int userId,int nums){
//第一步查询价格
重复刚才得步骤
到断点时,查mysql,?价格是20
登录另一个mysql,开启事务,并修改price=30 where goods_id = 1,?然后commit提交;
继续debug,会看到什么呢?首先我们定义的是,isolation = Isolation.READ_COMMITTED读已提交
那么预计的控制台输出会发生不可重复读的错误(读已提交会发生幻读,不可重复读),即price= 30
结束
mysql默认的隔离级别是可重复读,那么什么时候需要改变事务的隔离级别呢?(以下是文心一言的回答,仅供参考,不保证对)
MySQL默认的隔离级别是“可重复读”(REPEATABLE READ),这个级别在大多数情况下都能够满足需求。然而,在某些特定场景下,你可能需要改变事务的隔离级别,以满足特定的需求或解决某些问题。
以下是一些可能需要改变事务隔离级别的情况:
- 读已提交(READ COMMITTED)隔离级别:如果你需要读取其他事务已经提交的更改,而不是等待当前事务提交后再读取,你可以将隔离级别设置为“读已提交”。这可以减少读取操作的延迟,但可能会增加脏读(读取到其他未提交事务的更改)的风险。
- 读未提交(READ UNCOMMITTED)隔离级别:在某些情况下,你可能需要读取其他事务尚未提交的更改。例如,你可能需要实时监控系统中的某些指标,而这些指标是由其他事务频繁更新的。在这种情况下,你可以将隔离级别设置为“读未提交”,以便读取最新的未提交数据。但是请注意,这种级别的脏读风险非常高。
- 序列化(SERIALIZABLE)隔离级别:如果你需要执行一系列严格按照顺序执行的操作,并且要求这些操作是原子的(即不可中断的),你可以将隔离级别设置为“序列化”。这将强制所有读写操作按照严格的顺序执行,避免了并发操作可能引起的冲突。但是,这种级别的性能开销较大,可能会降低系统的并发性能。
需要注意的是,改变事务的隔离级别可能会带来一些副作用。较低的隔离级别可能导致数据一致性问题(如脏读、不可重复读和幻读),而较高的隔离级别可能导致性能下降。因此,在决定改变事务隔离级别之前,你应该仔细评估系统的需求和性能要求,并进行充分的测试。
?声明式事务-事务的超时回滚
?基本介绍:
? ? ? ? 如果一个事务执行的时间超过某个时间限制,就让该事务回滚。可以通过设置事务超时回顾来实现
测试执行,发现控制台报超时错误。?
声明式事务:事务的只读模式
基本介绍
? ? ? ? 如果一个事务执行的操作都是读的操作,我们可以明确的指定该事务是readOnly,这样便于数据库底层对其操作进行优化处理。
@Transactional(
propagation = Propagation.REQUIRES_NEW
,isolation = Isolation.READ_COMMITTED, timeout = 2,
readOnly = true
)
public void buyGoods(int goodsId,int userId,int nums){
//timeout = 2 表示这个事务两秒钟没有执行完,就会自动回滚
?基于XML的声明式事务
基本介绍
除了通过注解来配置声明式事务,还可以通过xml的方式来配置事务。
?
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!