目录
一、为什么使用事务
如下代码模拟用户购买一定数量的图书,支付时的场景:
当用户选择购买数量后,点击立即购买,来到如下的coupon模块中生成订单的insert方法
首先调用book模块中的enough方法判断库存中该书数量是否足够,如果足够则库存中该图书减少规定数量;
继而调用money模块中enough方法判断用户的钱包中余额是否足够,如果足够则开始生成订单;
这时问题便出现了,如果用户所选择的图书,库存中数量足够,并已经减少完库存后,发现用户钱包中的余额不够,这时订单没有生成,交易失败,但是book表中的库存却减少了,这时就需要回滚操作来取消刚才对book表中的操作,即添加事务。
package com.jd.coupon.service;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.jd.book.dao.IBookDao;
import com.jd.coupon.dao.ICouponDao;
import com.jd.money.dao.IMoneyDao;
import com.jd.vo.Coupon;
@Service
public class CouponService implements ICouponService {
@Autowired
private IBookDao bookDao;
@Autowired
private IMoneyDao moneyDao;
@Autowired
private ICouponDao couponDao;
//立即购买
@Override
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;
}
}
二、如何使用事务
1.添加事务
首先,在上面的代码中,insert方法前加上@Transactional注解
然后在application.xml文件中进行如下配置:
第25行:配置数据源事务驱动器,并用p标签获取数据库连接的id,这里要注意,该标签的id必须叫transactionManager
第27行:启动@Transaction注解,使insert方法前的@Transaction注解生效,该标签还有proxy-target-class属性可选择使用JDK代理类还是CGlib代理类,二者区别见博客: AOP中JDK代理与CGLib代理的区别
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">
<context:component-scan base-package="com.jd"></context:component-scan>
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf8"></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"></property>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" p:dataSource-ref="dataSource"></bean>
<tx:annotation-driven proxy-target-class="true"/>
</beans>
2.测试
在book和money表中设置如下数据:
在测试类中购买一本活着,库存是足够的,但是钱包中的余额不够,所以执行后book表中的数据并没有变化:
package com.jd.test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.jd.coupon.service.CouponService;
import com.jd.coupon.service.ICouponService;
public class Test {
public static void main(String[] args){
ClassPathXmlApplicationContext application = new ClassPathXmlApplicationContext("application.xml");
//立即购买
ICouponService couponService = application.getBean(CouponService.class);
System.out.println(couponService.getClass().getName());
String userId = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";
String bookId = "a2f39533-659f-42ca-af91-c688a83f6e49";
int count=1;
couponService.insert(userId, bookId, count);
}
}
三、@Transactional常用属性
1.timeout
该属性用于设置该事务存在的最长时间,单位为秒,如下示例中设置该值为3秒,并在方法中开启一个持续4秒的线程,则这时再调用测试类会抛出异常:
package com.jd.coupon.service;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.jd.book.dao.IBookDao;
import com.jd.coupon.dao.ICouponDao;
import com.jd.money.dao.IMoneyDao;
import com.jd.vo.Coupon;
@Service
public class CouponService implements ICouponService {
@Autowired
private IBookDao bookDao;
@Autowired
private IMoneyDao moneyDao;
@Autowired
private ICouponDao couponDao;
//立即购买
@Override
@Transactional(timeout=3)
public boolean insert(String userId,String bookId, int count){
if(bookDao.enough(bookId, count)) {//书籍足够
//书籍表库存递减
bookDao.update(bookId, count);
}
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
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;
}
}
2.readOnly
该属性用于限制该事务是否只读,如果设置值为true,则不能在事务内对数据库进行修改操作:
package com.jd.coupon.service;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.jd.book.dao.IBookDao;
import com.jd.coupon.dao.ICouponDao;
import com.jd.money.dao.IMoneyDao;
import com.jd.vo.Coupon;
@Service
public class CouponService implements ICouponService {
@Autowired
private IBookDao bookDao;
@Autowired
private IMoneyDao moneyDao;
@Autowired
private ICouponDao couponDao;
//立即购买
@Override
@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;
}
}
3.rollbackFor
Transaction事务有一个特点就是,只能对运行时异常有回滚功能,对于检查时异常,只能使用rollbackFor属性。
假如把上面的自定义的moneyException异常从运行时异常改为检查时异常,则需要将rollbackFor属性设置为该异常类的class类,才回对事务内部的操作起到回滚的作用。
@Transactional(rollbackFor= {MoneyException.class})
4.propagation
指定事务传播行为,一个事务方法被另一个事务方法调用时,必须指定事务应该如何传播,也就是套在外面的事务回滚时是否能被调用的事务方法一并回滚。
如下示例:当一次购买两本书时,调用batch方法批量处理订单,在batch方法中每次循环调用上述的insert方法。
package com.jd.car.service;
import java.util.*;
import java.util.Map.Entry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.jd.coupon.service.ICouponService;
@Service
public class CarService implements ICarService {
@Autowired
private ICouponService couponService;
//购物车购买
@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;
}
}
但是如果用户的钱包中的余额只有5元,只够支付一本书,也就是batch方法中第一次循环时调用insert方法是订单生成成功,而第二次循环时余额不足所以订单生成失败,这时batch中的事务回滚后,第一次调用的insert中的事务默认也是会一并回滚的。
但如果在insert方法处将propagation改为:
@Transactional(propagation=Propagation.REQUIRES_NEW//开启一个新事务)
则insert方法被调用时会开启一个新的事务,也就是在上述事例中就算抛出异常提示余额不足,数据库中第一本书还是交易成功了: