事务声明方式
如何实现声明式事务:
- 添加spring-aspects-4.3.10.RELEASE.jar包
- 在Spring配置文件中添加如下配置:
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 启用事务注解 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
- 在Service层public方法上添加事务注解——@Transactional
注意:不能在protected、默认或者private的方法上使用@Transactional注解,否则无效。
事务的属性:事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上。下面介绍@Transactional注解的五种属性:
场景:涉及三个数据库表book——书号,库存,单价;money——账户号,余额;购买情况表coupon——书号,账户号,应付金额
是否只读
事务只读,指对事务性资源进行只读操作。所谓事务性资源就是指那些被事务管理的资源,比如数据源、 JMS 资源,以及自定义的事务性资源等等。如果确定只对事务性资源进行只读操作,那么可以将事务标志为只读的(readOnly=true),以提高事务处理的性能。若readOnly=false则表示事务可读可写。
代码背景:A类书库存有3本,单价10元,余额30元,现需要购买2
//立即购买
@Transactional(readOnly=true)
public boolean insert(String userId,String bookId, int count){
if(bookDao.enough(bookId, count)) {//书籍足够
//书籍表库存递减
bookDao.update(bookId, count);
}
double price = bookDao.getPrice(bookId);
double total = price*count;
if(moneyDao.enough(userId, total)) {//余额足够
//订单表添加数据
Coupon coupon = new Coupon();
coupon.setId(UUID.randomUUID().toString());
coupon.setUserId(userId);
coupon.setBookId(bookId);
coupon.setTotal(total);
couponDao.insert(coupon);
//钱包表递减
moneyDao.update(userId, total);
}
return true;
}
分析:库存、余额均满足购买条件,可进行购买,进而修改相应表,但@Transactional注解中添加了readOnly=true,但@Transactional注解修饰的方法涉及数据的修改,因此抛出如下异常:Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
事务超时
设置一个事务所允许执行的最长时长,形式为timeout=x(单位:秒),如果超过该时长且事务还没有完成,则自动回滚事务且出现org.springframework.transaction.TransactionTimedOutException异常,如下代码:
代码背景:A类书库存有3本,单价10元,余额30元,现需要购买2
//立即购买
@Transactional(timeout=3)
public boolean insert(String userId,String bookId, int count){
if(bookDao.enough(bookId, count)) {//书籍足够
//书籍表库存递减
bookDao.update(bookId, count);
}
try {
Thread.sleep(5000);//休眠5秒
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
double price = bookDao.getPrice(bookId);
double total = price*count;
if(moneyDao.enough(userId, total)) {//余额足够
//订单表添加数据
Coupon coupon = new Coupon();
coupon.setId(UUID.randomUUID().toString());
coupon.setUserId(userId);
coupon.setBookId(bookId);
coupon.setTotal(total);
couponDao.insert(coupon);
//钱包表递减
moneyDao.update(userId, total);
}
return true;
}
分析:Thread.sleep(5000)会使得当前事务5秒之后结束,该时长超出了所允许的最长时长3秒,因此事务自动回滚,书籍表库存递减操作无效,程序出现
org.springframework.transaction.TransactionTimedOutException异常!
回滚规则
rollbackFor和rollbackForClassName:指定对哪些异常回滚事务。默认情况下,如果在事务中抛出了运行时异常(继承自RuntimeException异常类),则回滚事务;如果没有抛出任何异常,或者抛出了检查时异常,则依然提交事务。语句rollbackFor=异常类名.class可使人们根据需要控制事务在抛出某些运行时异常时仍然提交事务,或者在抛出某些检查时异常时回滚事务。
代码背景:A类书库存有3本,单价10元,余额10元,现需要购买2本
//立即购买
@Transactional
public boolean insert(String userId,String bookId, int count){
if(bookDao.enough(bookId, count)) {//书籍足够
//书籍表库存递减
bookDao.update(bookId, count);
}
double price = bookDao.getPrice(bookId);
double total = price*count;
if(moneyDao.enough(userId, total)) {//余额足够
//订单表添加数据
Coupon coupon = new Coupon();
coupon.setId(UUID.randomUUID().toString());
coupon.setUserId(userId);
coupon.setBookId(bookId);
coupon.setTotal(total);
couponDao.insert(coupon);
//钱包表递减
moneyDao.update(userId, total);
}
return true;
}
//MoneyDao中方法enough
@Override
public boolean enough(String id, double total) {
String sql="select balance from money where user_id=?";
Double balance = jdbcTemplate.queryForObject(sql, Double.class,id);
if(balance<total) {//余额不足
throw new MoneyException("余额不足,购买失败......");
}
return true;
}
//异常
public class MoneyException extends RuntimeException {
private static final long serialVersionUID = 669695292705313042L;
public MoneyException(String message) {
super(message);
}
}
分析:书籍足够但余额不足,则代码抛出MoneyException异常,但由于该异常为运行时异常,所以回滚事务,不对表进行修改!
代码背景:A类书库存有3本,单价10元,余额10元,现需要购买2本
//立即购买
@Transactional
public boolean insert(String userId,String bookId, int count) throws MoneyException{
if(bookDao.enough(bookId, count)) {//书籍足够
//书籍表库存递减
bookDao.update(bookId, count);
}
double price = bookDao.getPrice(bookId);
double total = price*count;
if(moneyDao.enough(userId, total)) {//余额足够
//订单表添加数据
Coupon coupon = new Coupon();
coupon.setId(UUID.randomUUID().toString());
coupon.setUserId(userId);
coupon.setBookId(bookId);
coupon.setTotal(total);
couponDao.insert(coupon);
//钱包表递减
moneyDao.update(userId, total);
}
return true;
}
//MoneyDao中方法enough
@Override
public boolean enough(String id, double total) {
String sql="select balance from money where user_id=?";
Double balance = jdbcTemplate.queryForObject(sql, Double.class,id);
if(balance<total) {//余额不足
throw new MoneyException("余额不足,购买失败......");
}
return true;
}
//异常
public class MoneyException extends Exception {
private static final long serialVersionUID = 669695292705313042L;
public MoneyException(String message) {
super(message);
}
}
代码结果分析:书籍足够但余额不足,则代码抛出MoneyException异常,但由于该异常为检查时异常,
所以依然提交事务,即book表中库存减少,余额不变,不一致。
代码背景:A类书库存有3本,单价10元,余额10元,现需要购买2本
//立即购买
@Transactional(rollbackFor=MoneyException.class)
public boolean insert(String userId,String bookId, int count) throws MoneyException{
if(bookDao.enough(bookId, count)) {//书籍足够
//书籍表库存递减
bookDao.update(bookId, count);
}
double price = bookDao.getPrice(bookId);
double total = price*count;
if(moneyDao.enough(userId, total)) {//余额足够
//订单表添加数据
Coupon coupon = new Coupon();
coupon.setId(UUID.randomUUID().toString());
coupon.setUserId(userId);
coupon.setBookId(bookId);
coupon.setTotal(total);
couponDao.insert(coupon);
//钱包表递减
moneyDao.update(userId, total);
}
return true;
}
//MoneyDao中方法enough
@Override
public boolean enough(String id, double total) {
String sql="select balance from money where user_id=?";
Double balance = jdbcTemplate.queryForObject(sql, Double.class,id);
if(balance<total) {//余额不足
throw new MoneyException("余额不足,购买失败......");
}
return true;
}
//异常
public class MoneyException extends Exception {
private static final long serialVersionUID = 669695292705313042L;
public MoneyException(String message) {
super(message);
}
}
分析:书籍足够但余额不足,则代码抛出MoneyException异常,尽管该异常为检查时异常,但由于@Transactional注解中添加了rollbackFor=MoneyException.class,所以回滚事务,即所有表都没有修改,一致。
注意:若不是通过 throws MoneyException抛出异常而是用try-catch处理异常,则即便@Transactional注解中添加了rollbackFor=MoneyException.class,事务也不会回滚。
传播行为
propagation:指定事务传播行为,一个事务方法被另一个事务方法调用时,必须指定事务应该如何传播,例如:方法可能继承在现有事务中运行,也可能开启一个新事物,并在自己的事务中运行。Spring定义了如下7种事务传播行为,下面着重介绍其中较常用的两种:
- REQUIRED:默认值,如果有事务在运行,当前的方法就在这个事务内运行 ; 否则,就启动一个新的事务,并在自己的事务内运行
代码背景:有A类书库存1本,单价10元;B类书库存1本,单价10元。余额共10元,欲买两类书各一本
public class Test {
public static void main(String[] args){
ClassPathXmlApplicationContext application = new ClassPathXmlApplicationContext("application.xml");
//购物车购买
ICarService carService = application.getBean(ICarService.class);
String userId = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";
Map<String,Integer> commodities = new HashMap<String,Integer>();
commodities.put("a2f39533-659f-42ca-af91-c688a83f6e49",1);
commodities.put("4c37672a-653c-4cc8-9ab5-ee0c614c7425",1);
carService.batch(userId, commodities);
application.close();
}
}
//购物车购买,CarService类中的batch方法
@Override
@Transactional
public boolean batch(String userId,Map<String,Integer> commodities) {
Set<Entry<String, Integer>> set = commodities.entrySet();
for (Entry<String, Integer> commodity : set) {
String bookId = commodity.getKey();
int count = commodity.getValue();
System.out.println(bookId+","+count);
couponService.insert(userId,bookId, count);
}
return true;
}
//立即购买,CouponService类中的insert方法
@Override
@Transactional【(propagation=Propagation.REQUIRED)】
public boolean insert(String userId,String bookId, int count){
if(bookDao.enough(bookId, count)) {//书籍足够
//书籍表库存递减
bookDao.update(bookId, count);
}
double price = bookDao.getPrice(bookId);
double total = price*count;
if(moneyDao.enough(userId, total)) {//余额足够
//订单表添加数据
Coupon coupon = new Coupon();
coupon.setId(UUID.randomUUID().toString());
coupon.setUserId(userId);
coupon.setBookId(bookId);
coupon.setTotal(total);
couponDao.insert(coupon);
//钱包表递减
moneyDao.update(userId, total);
}
return true;
}
分析:事务方法insert被另一个事务方法batch调用时,事务方法insert默认在batch方法的事务内运行,即insert方法和batch方法在同一个事务中(相当于修饰insert方法的@Transactional注解中添加了“propagation=Propagation.REQUIRED”属性),因此结算第二类书籍时钱包钱不够,insert方法抛出了异常以至于insert方法和batch方法所在的事务进行了回滚,最终导致第一类书籍的结算也时效。
- REQUIRES_NEW:当前方法必须启动新事务,并在它自己的事务内运行,如果有事务在运行,则把当前事务挂起,直到新的事务提交或者回滚才恢复执行,即每调用一次该方法就意味着启动一次新事务,如果此事务成功则提交,否则回滚
代码背景:有A类书库存1本,单价10元;B类书库存1本,单价10元。余额共10元,欲买两类书各一本
public class Test {
public static void main(String[] args){
ClassPathXmlApplicationContext application = new ClassPathXmlApplicationContext("application.xml");
//购物车购买
ICarService carService = application.getBean(ICarService.class);
String userId = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";
Map<String,Integer> commodities = new HashMap<String,Integer>();
commodities.put("a2f39533-659f-42ca-af91-c688a83f6e49",1);
commodities.put("4c37672a-653c-4cc8-9ab5-ee0c614c7425",1);
carService.batch(userId, commodities);
application.close();
}
}
//购物车购买,CarService类中的batch方法
@Override
@Transactional
public boolean batch(String userId,Map<String,Integer> commodities) {
Set<Entry<String, Integer>> set = commodities.entrySet();
for (Entry<String, Integer> commodity : set) {
String bookId = commodity.getKey();
int count = commodity.getValue();
System.out.println(bookId+","+count);
couponService.insert(userId,bookId, count);
}
return true;
}
//立即购买,CouponService类中的insert方法
@Override
@Transactional(propagation=Propagation.REQUIRES_NEW)
public boolean insert(String userId,String bookId, int count){
if(bookDao.enough(bookId, count)) {//书籍足够
//书籍表库存递减
bookDao.update(bookId, count);
}
double price = bookDao.getPrice(bookId);
double total = price*count;
if(moneyDao.enough(userId, total)) {//余额足够
//订单表添加数据
Coupon coupon = new Coupon();
coupon.setId(UUID.randomUUID().toString());
coupon.setUserId(userId);
coupon.setBookId(bookId);
coupon.setTotal(total);
couponDao.insert(coupon);
//钱包表递减
moneyDao.update(userId, total);
}
return true;
}
分析:余额10元只能买A、B中任一本。修饰insert方法的@Transactional注解中添加了“propagation=Propagation.REQUIRES_NEW”属性,则每次执行该方法时都会启动新事务,所以结算第一类书籍时钱够库存也够则购买成功并提交当次事务,但结算第二类书籍时由于钱包钱不够导致支付失败,致使此次事务回滚。
隔离规则
同一个应用程序中的多个事务或不同应用程序中的多个事务在同一个数据集上并发执行时, 可能会出现许多意外的问题,这些问题可分为如下三种类型:
- 脏读(Drity Read): 已知有两个事务A和B, A读取了已经被B更新但还没有被提交的数据,之后,B回滚事务,A读取的数据就是脏数据。
- 不可重复读(Non-repeatable read): 已知有两个事务A和B,A 多次读取同一数据,B 在A多次读取的过程中对数据作了修改并提交,导致A多次读取同一数据时,结果不一致
- 幻读(Phantom Read): 已知有两个事务A和B,A从一个表中读取了数据,然后B在该表中插入了一些新数据,导致A再次读取同一个表, 就会多出几行,简单地说,一个事务中先后读取一个范围的记录,但每次读取的纪录数不同,称之为幻象读
注意:应区分开不可重复读的和幻读,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。
根据实际需求,通过isolation设置数据库的事务隔离级别可以解决多个事务并发情况下出现的脏读、不可重复读和幻读问题,数据库事务隔离级别由低到高依次为Read uncommitted、Read committed、Repeatable read和Serializable等四种:
- Read uncommitted(读未提交):可能出现脏读、不可重复读和幻读。
- Read committed(读提交):可以避免脏读,但可能出现不可重复读和幻读。大多数数据库默认级别就是Read committed,比如Sql Server数据库和Oracle数据库。注意:该隔离级别在写数据时只会锁住相应的行。
- Repeatable read(重复读):可以避免脏读和不可重复读,但可能出现幻读。注意:事务隔离级别为可重复读时,如果检索条件有索引(包括主键索引)的时候,默认加锁方式是next-key 锁;如果检索条件没有索引,更新数据时会锁住整张表。一个间隙被事务加了锁,其他事务是不能在这个间隙插入记录的,这样可以防止幻读。
- Serializable(序列化):可以避免脏读、不可重复读和幻读,但是并发性极低,一般很少使用。注意:该隔离级别在读写数据时会锁住整张表。
注意:隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。