目錄
一、爲什麼使用事務
如下代碼模擬用戶購買一定數量的圖書,支付時的場景:
當用戶選擇購買數量後,點擊立即購買,來到如下的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方法被調用時會開啓一個新的事務,也就是在上述事例中就算拋出異常提示餘額不足,數據庫中第一本書還是交易成功了: