一.为什么要使用 @Transactional注解
举例:创建三个表 book money coupon
create table book(
id char(36) primary key comment '主键',
name varchar(12) comment '书名',
quantity int(5) comment '数量',
price float(5,2) comment '单价'
);
create table money(
id char(36) primary key comment '主键',
user_id char(36) comment '外键,用户id',
balance float(5,2) comment '余额'
);
create table coupon(
id char(36) primary key comment '主键',
user_id char(36) comment '外键,用户id',
book_id char(36) comment '外键,书籍id',
total float(5,2) comment '总额'
);
创建BookDao类
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import com.jd.exception.BookException;
@Component
public class BookDao implements IBookDao{
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public boolean enough(String id, int count) {
String sql="select quantity from book where id=?";
int quantity = jdbcTemplate.queryForObject(sql, Integer.class,id);
if(quantity<count) {//库存不足
throw new BookException("库存不足,购买失败......");
}
return true;
}
@Override
public double getPrice(String id) {
String sql="select price from book where id=?";
return jdbcTemplate.queryForObject(sql, Double.class,id);
}
@Override
public boolean update(String id, int count) {
String sql = "update book set quantity = quantity- "+count+" where id=?";
return jdbcTemplate.update(sql, id)>0;
}
}
创建MoneyDao类
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import com.jd.exception.MoneyException;
@Component
public class MoneyDao implements IMoneyDao{
@Autowired
private JdbcTemplate jdbcTemplate;
@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;
}
@Override
public boolean update(String userId, double total) {
String sql = "update money set balance = balance- "+total+" where user_id=?";
return jdbcTemplate.update(sql, userId)>0;
}
}
创建Coupon类
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import com.jd.vo.Coupon;
@Component
public class CouponDao implements ICouponDao{
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public boolean insert(Coupon coupon) {
String sql = "insert into coupon (id,user_id,book_id,total) values (?,?,?,?)";
return jdbcTemplate.update(sql, coupon.getId(),coupon.getUserId(),coupon.getBookId(),coupon.getTotal())>0;
}
}
创建CouponService类
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
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//添加该注解不仅为其创建代理对象,而且在该方法中引入事务
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;
}
}
创建Test类
import org.springframework.context.support.ClassPathXmlApplicationContext;
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(ICouponService.class);
System.out.println(couponService.getClass().getName());
String userId = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";
String bookId = "a2f39533-659f-42ca-af91-c688a83f6e49";
int count=5;
couponService.insert(userId, bookId, count);
}
}
在运行的时候我们会发现,书本余量不足或者钱不够时,如果仍进行了对书本或者钱数的修改,另一者仍然也会继续修改,这就导致了逻辑性的错误,使得书本数目或者钱数会出现问题。这个问题能让我们联系到事务四大特性中的一致性 ,因此,为了解决这个问题,我们需要引入事务的概念,而在Spring框架中引入事务的办法就是使用@Transactional注解
二.@Transactional注解的常用属性
1.timeout
设置一个事务所允许执行的最长时长(单位:秒),如果超过该时长且事务还没有完成,则自动回滚事务且出现org.springframework.transaction.TransactionTimedOutException异常
修改ConponService类,在其中加入timeout属性
@Service
public class CouponService implements ICouponService {
@Autowired
private IBookDao bookDao;
@Autowired
private IMoneyDao moneyDao;
@Autowired
private ICouponDao couponDao;
@Transactional(timeout = 3 /*如果超过3s时间则回滚*/)
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;
try {
Thread.sleep(4000);//等待4s
} catch (InterruptedException e) {
e.printStackTrace();
}
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;
}
}
运行Test后,会发现由于线程阻塞的时间大于了timeout所设定的时间,导致事务回滚,数据并不会被改变。
2.readOnly
事务只读,指对事务性资源进行只读操作,不能修改。
修改CouponService类
mport java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
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);
}
try {
Thread.sleep(4000);//令线程阻塞4秒
} 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;
}
}
由于设置了只读属性,所以运行Test时会报错,抛出如下异常
3.rollbackFor
指定对哪些异常回滚事务。默认情况下,如果没有抛出任何异常,或者抛出了检查时异常,依然提交事务。而rollbackFor可以控制事务在抛出某些检查时异常时回滚事务。
修改CouponService类
mport java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.jd.book.dao.IBookDao;
import com.jd.coupon.dao.ICouponDao;
import com.jd.exception.BookException;
import com.jd.exception.MoneyException;
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(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;
}
}
由于设置了rollbackFor属性,在抛出MoneyException异常时,事务将会回滚。
4.propagation
指定事务传播行为,一个事务方法被另一个事务方法调用时,必须指定事务应该如何传播。
为了验证在两个不同事务同时发生,而一个出错一个正常时,程序会如何运行,我们需要使用另一种购买逻辑
修改Test类
import java.util.Map;
import java.util.HashMap;
import com.jd.car.service.CarService;
import com.jd.car.service.ICarService;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args){
ClassPathXmlApplicationContext application = new ClassPathXmlApplicationContext("application.xml");
//购物车购买
ICarService carService = application.getBean(CarService.class);
String userId = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";
Map<String,Integer> commodities = new HashMap<String,Integer>();
commodities.put("a2f39533-659f-42ca-af91-c688a83f6e49",11);
commodities.put("4c37672a-653c-4cc8-9ab5-ee0c614c7425",10);
carService.batch(userId, commodities);
application.close();
}
添加CarService类
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
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;
}
}
运行Test类后,我们发现默认情况下,数据并没有发生改变,实际上是由于默认情况下,两个事务会被合并,因此其中一个出错时,这个合并后的事务将不会执行。
为了能够提高用户体验,我们应该只是停止执行出错的事务,因此需要用到propagation属性,来设置事务合并规则
修改CouponService类
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.jd.book.dao.IBookDao;
import com.jd.coupon.dao.ICouponDao;
import com.jd.exception.BookException;
import com.jd.exception.MoneyException;
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(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;
}
}
在这里我们使用了 propagation属性来阻止事务合并,因此两个事务相互独立,在运行Test后我们发现有且只有没有出错的事务会正常执行。