一谅畅、數(shù)據(jù)庫的事務(wù)的基本特性
事務(wù)是區(qū)分文件存儲系統(tǒng)與Nosql數(shù)據(jù)庫重要特性之一胚迫,其存在的意義是為了保證即使在并發(fā)情況下也能正確的執(zhí)行crud操作贾铝。怎樣才算是正確的呢薯定?這時提出了事務(wù)需要保證的四個特性即ACID:
-
A: 原子性(atomicity)
事務(wù)中各項操作始绍,要么全做要么全不做,任何一項操作的失敗都會導(dǎo)致整個事務(wù)的失敾爸丁亏推; -
C: 一致性(consistency)
事務(wù)結(jié)束后系統(tǒng)狀態(tài)是一致的; -
I: 隔離性(isolation)
并發(fā)執(zhí)行的事務(wù)彼此無法看到對方的中間狀態(tài)年堆; -
D: 持久性(durability)
事務(wù)完成后所做的改動都會被持久化吞杭,即使發(fā)生災(zāi)難性的失敗。
在高并發(fā)的情況下变丧,要完全保證其ACID特性是非常困難的芽狗,除非把所有的事務(wù)串行化執(zhí)行,但帶來的負面的影響將是性能大打折扣痒蓬。很多時候我們有些業(yè)務(wù)對事務(wù)的要求是不一樣的译蒂,所以數(shù)據(jù)庫中設(shè)計了四種隔離級別,供用戶基于業(yè)務(wù)進行選擇谊却。
隔離級別 | 臟讀(Dirty Read) | 不可重復(fù)讀(NonRepeatable Read) | 幻讀(Phantom Read) |
---|---|---|---|
讀未提交(Read uncommitted) | 可能 | 可能 | 可能 |
讀已提交(Read committed) | 不可能 | 可能 | 可能 |
可重復(fù)讀(Repeatable read) | 不可能 | 不可能 | 可能 |
可串行化(SERIALIZABLE) | 不可能 | 不可能 | 不可能 |
臟讀 :
一個事務(wù)讀取到另一事務(wù)未提交的更新數(shù)據(jù)
不可重復(fù)讀 :
在同一事務(wù)中,多次讀取同一數(shù)據(jù)返回的結(jié)果有所不同, 換句話說, 后續(xù)讀取可以讀到另一事務(wù)已提交的更新數(shù)據(jù). 相反, “可重復(fù)讀”在同一事務(wù)中多次讀取數(shù)據(jù)時, 能夠保證所讀數(shù)據(jù)一樣, 也就是后續(xù)讀取不能讀到另一事務(wù)已提交的更新數(shù)據(jù)柔昼。
幻讀 :
查詢表中一條數(shù)據(jù)如果不存在就插入一條,并發(fā)的時候卻發(fā)現(xiàn)炎辨,里面居然有兩條相同的數(shù)據(jù)捕透。這就幻讀的問題。
二、Sring 對事務(wù)的支持與使用
知識點:
1.spring 事務(wù)相關(guān)API說明
2.聲明式事務(wù)的使用
3.事務(wù)傳播機制
- spring 事務(wù)相關(guān)API說明
spring 事務(wù)是在數(shù)據(jù)庫事務(wù)的基礎(chǔ)上進行封裝擴展 其主要特性如下:
a.支持原有的數(shù)據(jù)事務(wù)的隔離級別
b.加入了事務(wù)傳播的概念 提供多個事務(wù)的和并或隔離的功能
c.提供聲明式事務(wù)乙嘀,讓業(yè)務(wù)代碼與事務(wù)分離末购,事務(wù)變得更易用。
怎么樣去使用Spring事務(wù)呢虎谢?spring 提供了三個接口供使用事務(wù)盟榴。分別是:
-
TransactionDefinition
事務(wù)定義
image.png -
PlatformTransactionManager
事務(wù)管理
image.png -
TransactionStatus
事務(wù)運行時狀態(tài)
image.png
基于API實現(xiàn)事務(wù)
public class SpringTransactionExample {
private static String url = "jdbc:mysql:///localhost:3306/test";
private static String user = "root";
private static String password = "123456";
public static Connection openConnection() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");
return conn;
}
public static void main(String[] args) {
final DriverManagerDataSource ds = new DriverManagerDataSource(url, user, password);
final TransactionTemplate template = new TransactionTemplate();
template.setTransactionManager(new DataSourceTransactionManager(ds));
template.execute(new TransactionCallback<Object>() {
@Override
public Object doInTransaction(TransactionStatus status) {
Connection conn = DataSourceUtils.getConnection(ds);
Object savePoint = null;
try {
{
// 插入
PreparedStatement prepare = conn.
prepareStatement("insert INTO account (accountName,user,money) VALUES (?,?,?)");
prepare.setString(1, "111");
prepare.setString(2, "aaaa");
prepare.setInt(3, 10000);
prepare.executeUpdate();
}
// 設(shè)置保存點
savePoint = status.createSavepoint();
{
// 插入
PreparedStatement prepare = conn.
prepareStatement("insert INTO account (accountName,user,money) VALUES (?,?,?)");
prepare.setString(1, "222");
prepare.setString(2, "bbb");
prepare.setInt(3, 10000);
prepare.executeUpdate();
}
{
// 更新
PreparedStatement prepare = conn.
prepareStatement("UPDATE account SET money= money+1 where user=?");
prepare.setString(1, "asdflkjaf");
Assert.isTrue(prepare.executeUpdate() > 0, "");
}
} catch (SQLException e) {
e.printStackTrace();
} catch (Exception e) {
System.out.println("更新失敗");
if (savePoint != null) {
status.rollbackToSavepoint(savePoint);
} else {
status.setRollbackOnly();
}
}
return null;
}
});
}
}
輸出
更新失敗
2、聲明示事務(wù)
我們前面是通過調(diào)用API來實現(xiàn)對事務(wù)的控制婴噩,這非常的繁瑣擎场,與直接操作JDBC事務(wù)并沒有太多的改善,所以Spring提出了聲明示事務(wù)几莽,使我們對事務(wù)的操作變得非常簡單迅办,甚至不需要關(guān)心它。
編寫服務(wù)類
@Transactional
public void addAccount(String name, int initMenoy) {
String accountid = new SimpleDateFormat("yyyyMMddhhmmss").format(new Date());
jdbcTemplate.update("insert INTO account (accountName,user,money) VALUES (?,?,?)", accountid, name, initMenoy);
// 人為報錯
int i = 1 / 0;
}
l 演示添加 @Transactional 注解和不添加注解的情況章蚣。
3站欺、事務(wù)傳播機制
類別 | 事務(wù)傳播類型 | 說明 |
---|---|---|
支持當(dāng)前事務(wù) | PROPAGATION_REQUIRED (必須的) |
如果當(dāng)前沒有事務(wù),就新建一個事務(wù)纤垂,如果已經(jīng)存在一個事務(wù)中矾策,加入到這個事務(wù)中。這是最常見的選擇峭沦。 |
支持當(dāng)前事務(wù) | PROPAGATION_SUPPORTS (支持) |
支持當(dāng)前事務(wù)贾虽,如果當(dāng)前沒有事務(wù),就以非事務(wù)方式執(zhí)行熙侍。 |
支持當(dāng)前事務(wù) | PROPAGATION_MANDATORY (強制) |
使用當(dāng)前的事務(wù)榄鉴,如果當(dāng)前沒有事務(wù),就拋出異常蛉抓。 |
不支持當(dāng)前事務(wù) | PROPAGATION_REQUIRES_NEW (隔離) |
新建事務(wù)庆尘,如果當(dāng)前存在事務(wù),把當(dāng)前事務(wù)掛起巷送。 |
不支持當(dāng)前事務(wù) | PROPAGATION_NOT_SUPPORTED (不支持) |
以非事務(wù)方式執(zhí)行操作驶忌,如果當(dāng)前存在事務(wù),就把當(dāng)前事務(wù)掛起笑跛。 |
不支持當(dāng)前事務(wù) | PROPAGATION_NEVER (強制非事務(wù)) |
以非事務(wù)方式執(zhí)行付魔,如果當(dāng)前存在事務(wù),則拋出異常飞蹂。 |
套事務(wù) | PROPAGATION_NESTED (嵌套事務(wù)) |
如果當(dāng)前存在事務(wù)几苍,則在嵌套事務(wù)內(nèi)執(zhí)行。如果當(dāng)前沒有事務(wù)陈哑,則執(zhí)行與PROPAGATION_REQUIRED類似的操作妻坝。 |
常用事務(wù)傳播機制:
- PROPAGATION_REQUIRED伸眶,
這個也是默認(rèn)的傳播機制; - PROPAGATION_NOT_SUPPORTED
可以用于發(fā)送提示消息刽宪,站內(nèi)信厘贼、短信、郵件提示等圣拄。不屬于并且不應(yīng)當(dāng)影響主體業(yè)務(wù)邏輯嘴秸,即使發(fā)送失敗也不應(yīng)該對主體業(yè)務(wù)邏輯回滾。 - PROPAGATION_REQUIRES_NEW
總是新啟一個事務(wù)庇谆,這個傳播機制適用于不受父方法事務(wù)影響的操作岳掐,比如某些業(yè)務(wù)場景下需要記錄業(yè)務(wù)日志,用于異步反查族铆,那么不管主體業(yè)務(wù)邏輯是否完成岩四,日志都需要記錄下來哭尝,不能因為主體業(yè)務(wù)邏輯報錯而丟失日志哥攘;
l 演示常用事務(wù)的傳播機制
用例1:
創(chuàng)建用戶時初始化一個帳戶,表結(jié)構(gòu)和服務(wù)類如下材鹦。
表結(jié)構(gòu) | 服務(wù)類 | 功能描述 |
---|---|---|
user | UserSerivce | 創(chuàng)建用戶逝淹,并添加帳戶 |
account | AccountService | 添加帳戶 |
UserSerivce.createUser(name) 實現(xiàn)代碼
@Transactional
public void createUser(String name) {
// 新增用戶基本信息
jdbcTemplate.update("INSERT INTO `user` (name) VALUES(?)", name);
//調(diào)用accountService添加帳戶
accountService.addAccount(name, 10000);
}
AccountService.addAccount(name,initMoney) 實現(xiàn)代碼(方法的最后有一個異常)
@Transactional(propagation = Propagation.REQUIRED)
public void addAccount(String name, int initMoney) {
String accountid = new SimpleDateFormat("yyyyMMddhhmmss").format(new Date());
jdbcTemplate.update("insert INTO account (accountName,user,money) VALUES (?,?,?)", accountid, name, initMenoy);
// 出現(xiàn)分母為零的異常
int i = 1 / 0;
}
實驗預(yù)測一:
createUser | addAccount(異常) | 預(yù)測結(jié)果 | |
---|---|---|---|
場景一 | 無事務(wù) | required | createUser (成功) addAccount(不成功) |
場景二 | required | 無事務(wù) | createUser (不成功) addAccount(不成功) |
場景三 | required | not_supported | createUser (不成功) addAccount(成功) |
場景四 | required | required_new | createUser (不成功) addAccount(不成功) |
場景五 | required(異常移至createUser方法未尾) | required_new | createUser(不成功) addAccount(成功) |
場景六 | required(異常移至createUser方法未尾)(addAccount 方法移至createUser方法的同一個類里) | required_new | createUser (不成功) addAccount(不成功) |
三、aop 事務(wù)底層實現(xiàn)原理
講事務(wù)原理之前我們先來做一個實驗桶唐,當(dāng)場景五的環(huán)境改變栅葡,把addAccount 方法移至UserService 類下,其它配置和代碼不變:
@Override
@Transactional
public void createUser(String name) {
jdbcTemplate.update("INSERT INTO `user` (name) VALUES(?)", name);
addAccount(name, 10000);
// 人為報錯
int i = 1 / 0;
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addAccount(String name, int initMoney) {
String accountid = new SimpleDateFormat("yyyyMMddhhmmss").format(new Date());
jdbcTemplate.update("insert INTO account (accountName,user,money) VALUES (?,?,?)", accountid, name, initMoney);
}
演示新場景
經(jīng)過演示我們發(fā)現(xiàn)得出的結(jié)果與場景五并不 一至尤泽,required_new 沒有起到其對應(yīng)的作用欣簇。原因在于spring 聲明示事務(wù)使用動態(tài)代理實現(xiàn),而當(dāng)調(diào)用同一個類的方法時坯约,是會不會走代理邏輯的熊咽,自然事務(wù)的配置也會失效。
通過一個動態(tài)代理的實現(xiàn)來模擬這種場景
UserSerivce proxyUserSerivce = (UserSerivce) Proxy.newProxyInstance(LubanTransaction.class.getClassLoader(),
new Class[]{UserSerivce.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
System.out.println("開啟事務(wù):"+method.getName());
return method.invoke(userSerivce, args);
} finally {
System.out.println("關(guān)閉事務(wù):"+method.getName());
}
}
});
proxyUserSerivce.createUser("kpioneer");
當(dāng)我們調(diào)用createUser 方法時 僅打印了 createUser 的事務(wù)開啟闹丐、關(guān)閉横殴,并沒有打印addAccount 方法的事務(wù)開啟、關(guān)閉卿拴,由此可見addAccount 的事務(wù)配置是失效的衫仑。
如果業(yè)務(wù)當(dāng)中上真有這種場景該如何實現(xiàn)呢?
在spring xml中配置 暴露proxy 對象堕花,然后在代碼中用AopContext.currentProxy() 就可以獲當(dāng)前代理對象
<!-- 配置暴露proxy -->
<aop:aspectj-autoproxy expose-proxy="true"/>
// 基于代理對象調(diào)用創(chuàng)建帳戶文狱,事務(wù)的配置又生效了
@Transactional
public void createUser(String name) {
// 新增用戶基本信息
jdbcTemplate.update("INSERT INTO `user` (name) VALUES(?)", name);
// 暴露proxy 對象 調(diào)用accountService添加帳戶
((UserSerivce) AopContext.currentProxy()).addAccount(name, 10000);
// 人為報錯
int i = 1 / 0;
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addAccount(String name, int initMoney) {
String accountid = new SimpleDateFormat("yyyyMMddhhmmss").format(new Date());
jdbcTemplate.update("insert INTO account (accountName,user,money) VALUES (?,?,?)", accountid, name, initMoney);
}