Spring事務(wù)失效場景
一、Spring事務(wù)失效場景
1.1 前言
身為Java開發(fā)工程師,相信大家對Spring種事務(wù)的使用并不陌生。但是你可能只停留在基礎(chǔ)的使用層面上,在遇到一些比較特殊的場景,事務(wù)可能沒有生效,直接在生產(chǎn)上暴露了,這可能就會導(dǎo)致比較嚴(yán)重的生產(chǎn)事故。今天,我們就簡單來說一下Spring事務(wù)的原理,然后總結(jié)出對應(yīng)的解決方案。
- 聲明式事務(wù)是Spring 功能中最爽之一,可是有些時(shí)候,我們在使用聲明式事務(wù)并為生效,這是為什么呢?
- 再次就聊聊聲明式事務(wù)的幾種失效場景。本文將會從以下兩個(gè)方面來說一下事務(wù)為什么會失效?
1.2 Spring 事務(wù)原理
大家還記得在JDBC中是如何操作事務(wù)的嗎?偽代碼可能如下:
//Get database connection
Connection connection = DriverManager.getConnection();
//Set autoCommit is false
connection.setAutoCommit(false);
//use sql to operate database
.........
//Commit or rollback
connection.commit()/connection.rollback
connection.close();
需要再各個(gè)業(yè)務(wù)中編寫代碼如 commit() 、close() 來控制事務(wù)。
但是 Spring 不樂意這么干了,這樣對業(yè)務(wù)代碼侵入性太大了,所有就用一個(gè)事務(wù)注解 @Transaction 來控制事務(wù),底層實(shí)現(xiàn)是基于切面編程 AOP 實(shí)現(xiàn)的,而Spring 中實(shí)現(xiàn) AOP 機(jī)制采用的動態(tài)代理,具體分為 JDK 動態(tài)代理和 CGLib 動態(tài)代理兩種模式。

- Spring 的 bean 的初始化過程中,發(fā)現(xiàn)方法有 @Transaction 注解,就需要對相應(yīng)的 Bean 進(jìn)行代理,生成代理對象。
- 然后再方法調(diào)用的時(shí)候,會執(zhí)行切面的邏輯,而這里切賣你的邏輯中就包含了開啟事務(wù),提交事務(wù)或者回滾事務(wù)等邏輯。
另外注意一點(diǎn)的是,Spring 本身不實(shí)現(xiàn)事務(wù),底層還是依賴于數(shù)據(jù)庫的事務(wù)。沒有數(shù)據(jù)庫事務(wù)的支持,Spring 事務(wù)是不會生效的。
1.3 Spring 事務(wù)失效場景
1.3.1 拋出檢查異常
比如你的事務(wù)控制代碼如下:
@Transactional
public void transactionTest() throws IOException{
User user = new User();
UserService.insert(user);
throw new IOException();
}
如果 @Transactional 沒有特別指定,Spring只會在遇到運(yùn)行時(shí)異常RuntimeException或者error時(shí)進(jìn)行回滾,而 IOException 等檢查異常不會影響回滾。
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}
解決方案:
知道原因后,解決方法也很簡單。配置rollbackFor 屬性,例如:
@Transactional(rollbackFor = Exception.class)
1.3.2 業(yè)務(wù)方法本身捕獲了異常
@Transactional(rollbackFor = Exception.class)
public void transactionTest() {
try {
User user = new User();
UserService.insert(user);
int i = 1 / 0;
}catch (Exception e) {
e.printStackTrace();
}
}
這種場景下,事務(wù)失敗的原因也很簡單,Spring 是否進(jìn)行回滾時(shí)根據(jù)你是否拋出異常決定的,所以如果你自己捕獲了異常,Spring也無能為力。
看了上面的代碼,你可能認(rèn)為這么簡單的問題你不可能犯這么愚蠢的錯(cuò)誤,但是我想告訴你的是,我身邊幾乎一半的人都被這一幕困擾過。
寫業(yè)務(wù)代碼的時(shí)候,代碼可能比較復(fù)雜,嵌套的方法很多。如果你不小心,很可能會觸發(fā)此問題。舉一個(gè)非常簡單的例子,假設(shè)你有一個(gè)審計(jì)功能。每個(gè)方法執(zhí)行后,審計(jì)結(jié)果保存在數(shù)據(jù)庫中,那么代碼可能會這樣寫。
@Service
public class TransactionService {
@Transactional(rollbackFor = Exception.class)
public void transactionTest() throws IOException {
User user = new User();
UserService.insert(user);
throw new IOException();
}
}
@Component
public class AuditAspect {
@Autowired
private auditService auditService;
@Around(value = "execution (* com.alvin.*.*(..))")
public Object around(ProceedingJoinPoint pjp) {
try {
Audit audit = new Audit();
Signature signature = pjp.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
String[] strings = methodSignature.getParameterNames();
audit.setMethod(signature.getName());
audit.setParameters(strings);
Object proceed = pjp.proceed();
audit.success(true);
return proceed;
} catch (Throwable e) {
log.error("{}", e);
audit.success(false);
}
auditService.save(audit);
return null;
}
}
上面的示例中,事務(wù)將失敗。原因是Spring 的事務(wù)切面 優(yōu)先級最低,所以如果異常被切面捕獲,Spring自然不能正常處理事務(wù),因?yàn)槭聞?wù)管理器無法捕獲異常。
解決方案:
雖然我們知道在處理使唔使業(yè)務(wù)代碼不能自己捕獲異常,但是只要代碼變得復(fù)雜,我們就很可能再次出錯(cuò),所以我們在處理事務(wù)的時(shí)候要小心,還是不要使用聲明式事務(wù),并使用編程式事務(wù):
transactionTemplate.execute()
1.3.3 同一類的方法調(diào)用
@Service
public class DefaultTransactionService implement Service {
public void saveUser() throws Exception {
//do something
doInsert();
}
@Transactional(rollbackFor = Exception.class)
public void doInsert() throws IOException {
User user = new User();
UserService.insert(user);
throw new IOException();
}
}
這也是一個(gè)容易出錯(cuò)的場景。事務(wù)失敗的原因也很簡單,因?yàn)镾pring的事務(wù)管理功能是通過動態(tài)代理實(shí)現(xiàn)的,而Spring默認(rèn)使用 JDK 動態(tài)代理,而 JDK 動態(tài)代理采用接口實(shí)現(xiàn)的方式,通過反射調(diào)用目標(biāo)類。簡單理解,就是 saveUser() 方法中調(diào)用 this.doInsert() ,這里的this 是被真實(shí)對象,所以會直接走 doInsert 的業(yè)務(wù)邏輯,而不走切面邏輯,所以事務(wù)失敗。
解決方案:
方案一:解決方法可以是直接在啟動類中添加 @Transcational 注解 saveUser()
方案二:@EnableAspectJAutoProxy(exposeProxy = true)在啟動類中添加,會由Cglib代理實(shí)現(xiàn)。
1.3.4 方法使用 final 或 static 關(guān)鍵字
如果Spring 使用了 Cglib 代理實(shí)現(xiàn) (比如你的代理類沒有實(shí)現(xiàn)接口),而你的業(yè)務(wù)方法敲好使用了 final 或者 static 關(guān)鍵字,那么事務(wù)也會失敗。更具體的說,它應(yīng)該拋出異常,因?yàn)?Cglib 使用字節(jié)碼增強(qiáng)技術(shù)生成被代理類的子類并重寫代理類的方法來實(shí)現(xiàn)代理。如果被代理的方法使用 final 或 static 關(guān)鍵字,則子類不能重寫被代理的方法。
如果 Spring 使用 JDK 動態(tài)代理實(shí)現(xiàn),JDK動態(tài)代理是基于接口實(shí)現(xiàn)的,那么 final 和 static 修飾的方法也就無法被代理。
總而言之,方法連代理都沒有,那么肯定無法實(shí)現(xiàn)事務(wù)回滾了。
解決方案:
想辦法去掉 final 或者 static 關(guān)鍵字
1.3.5 方法不是 public
如果方法不是 public,Spring 事務(wù)也會失敗,因?yàn)?spring 的事務(wù)管理源碼 AbstractFallbackTransactionAttributeSource中有判斷computeTransactionAttribute()。如果目標(biāo)方法不是公共的,則TransactionAttribute返回null。
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
解決方案:
將當(dāng)前方法訪問級別更改為 public
1.3.6 錯(cuò)誤使用傳播機(jī)制
Spring 事務(wù)的傳播機(jī)制是指在多個(gè)事務(wù)方法互相調(diào)用時(shí),確定事務(wù)應(yīng)該如何傳播的策略。Spring 提供了 7 種事務(wù)傳播機(jī)制:
- REQUIRED
- SUPPORT
- MANDATORY
- REQUIRES_NEW
- NOT_SUPPORTED
- NEVER
- NESTED
如果不知道這些傳播策略的原理,很可能會導(dǎo)致交易失敗。
@Service
public class TransactionService {
@Autowired
private UserMapper userMapper;
@Autowired
private AddressMapper addressMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
public void doInsert(User user,Address address) throws Exception {
//do something
userMapper.insert(user);
saveAddress(address);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAddress(Address address) {
//do something
addressMapper.insert(address);
}
}
在上面的例子中,如果用戶插入失敗,不會導(dǎo)致 saveAddress() 回滾,因?yàn)檫@里使用的傳播是 REQUIRES_NEW,傳播機(jī)制 REQUIRES_NEW 的原理是如果當(dāng)前方法中沒有事務(wù),就會創(chuàng)建一個(gè)新的事物。如果一個(gè)事物已經(jīng)存在,則當(dāng)前事務(wù)將被掛起,并創(chuàng)建一個(gè)新事務(wù)。在當(dāng)前事務(wù)完成之前,不會提交父事務(wù)。如果父事務(wù)發(fā)生異常,則不影響子事務(wù)的提交。
事務(wù)的傳播機(jī)制說明如下:
- REQUIRED:如果當(dāng)前上下文中存在事務(wù),那么加入該事務(wù),如果不存在事務(wù),創(chuàng)建一個(gè)事務(wù),這是默認(rèn)的傳播屬性值。
- SUPPORT:如果當(dāng)前上下文存在事務(wù),則支持事務(wù)加入事務(wù),如果不存在事務(wù),則使用非事務(wù)的方式執(zhí)行。
- MANDATORY:如果當(dāng)前上下文中存在事務(wù),并且同時(shí)將上下文中的事務(wù)掛起,執(zhí)行當(dāng)前新建事務(wù)完成以后,上下文事務(wù)回復(fù)在執(zhí)行。
- NOT_SUPPORTED 如果當(dāng)前上下文存在事務(wù),則掛起當(dāng)前事務(wù),然后新的方法在沒有事務(wù)的環(huán)境中執(zhí)行。
- NEVER 如果當(dāng)前上下文中存在書屋,則拋出異常,否則在無事務(wù)環(huán)境上執(zhí)行代碼。
- NESTED 如果當(dāng)前上下文中存在是我,則嵌套是我執(zhí)行,如果不存在事務(wù),則新建事務(wù)。
解決方案:
將事務(wù)傳播策略更改為默認(rèn)值 REQUIRED, REQUIRED 原理是如果當(dāng)前有一個(gè)事務(wù)被添加到一個(gè)事務(wù)中,如果沒有,則創(chuàng)建一個(gè)新事物,父事務(wù)和被調(diào)用的事務(wù)在同一個(gè)事務(wù)中。即時(shí)被調(diào)用的異常被捕獲,整個(gè)事務(wù)仍然會被回滾。
1.3.7 沒有被Spring管理
// @Service
public class OrderServiceImpl implements OrderService {
@Transactional
public void updateOrder(Order order) {
// update order
}
}
如果此時(shí)把 @Server 注解注釋掉,這個(gè)類就不會被加載成一個(gè) Bean ,那這個(gè)類就不會被 Spring 管理了,事務(wù)自然就失效了。
解決方案:
需要保證每個(gè)事物注解的每個(gè)Bean被Spring管理。
1.3.8 多線程
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
new Thread(() -> {
try {
test();
} catch (Exception e) {
roleService.doOtherThing();
}
}).start();
}
}
@Service
public class RoleService {
@Transactional
public void doOtherThing() {
try {
int i = 1/0;
System.out.println("保存role表數(shù)據(jù)");
}catch (Exception e) {
throw new RuntimeException();
}
}
}
我們可以看到實(shí)物方法add中,調(diào)用了實(shí)物方法 doOtherThing ,但是事務(wù)方法 doOtherThing 是在另外一個(gè)線程中被調(diào)用的。
這樣會導(dǎo)致兩個(gè)方法不再同一個(gè)線程中,獲取到的數(shù)據(jù)庫連接不一樣,從而使兩個(gè)不同的事務(wù)。如果想 doOtherThing 方法中拋出異常, add 方法也回滾時(shí)不可能的。
我們說的同一個(gè)事物,其實(shí)是指同一個(gè)數(shù)據(jù)庫連接,只有擁有同一個(gè)數(shù)據(jù)庫連接才能同時(shí)提交和回滾。如果在不同的線程,拿到的數(shù)據(jù)庫連接肯定時(shí)不一樣的,所以是不同的事務(wù)。
解決方案:
這里就有點(diǎn)分布式事務(wù)的感覺了,盡量還是保證在同一個(gè)事物中處理。
1.3.9 總結(jié)
以上總結(jié)了 Spring 中事務(wù)實(shí)現(xiàn)的原理,同時(shí)列舉了 8 重 Spring 事務(wù)失敗的場景,相信很多人都遇到過,失敗的原理也有詳細(xì)說明。希望大家對 Spring 事務(wù)有一個(gè)新的認(rèn)識。
二、JDK動態(tài)代理和CGLIB動態(tài)代理
2.1 什么是代理模式
代理模式(Proxy Pattern)給某一個(gè)對象提供一個(gè)代理,并用代理對象控制原對象的引用。代理對象再客戶端和目標(biāo)對象之間起到中介作用。
代理模式是常用的結(jié)構(gòu)型設(shè)計(jì)模式之一,當(dāng)直接訪問某些對象存在問題時(shí)可以通過一個(gè)代理對象來間接訪問,為了保證客戶端使用的透明性,所訪問的真實(shí)對象與代理對象需要實(shí)現(xiàn)相同的接口。代理模式屬于結(jié)構(gòu)型設(shè)計(jì)模式,屬于GOF23設(shè)計(jì)模式
代理模式可以分為靜態(tài)代理和動態(tài)代理兩種類型,而動態(tài)代理中又分為 JDK 動態(tài)代理和 CGLIB 代理兩種。

代理模式包含如下角色:
- Subject(抽象主體角色)抽象主體角色聲明了真實(shí)主體和代理主體的共同接口,這樣依賴在任何使用真實(shí)主體的地方都可以使用代理主體。客戶端需要針對抽象主體角色進(jìn)行編程。
- Proxy(代理主體角色)代理主體角色內(nèi)部包含對真實(shí)主體的引用,從而可以在任何時(shí)候操作真是主體對象。在代理主體角色中提供一個(gè)與真實(shí)主體角色相同的接口,以便在任何時(shí)候都可以代替真實(shí)主體。代理主體角色還可以控制對真實(shí)主體的使用,負(fù)責(zé)在需要的時(shí)候創(chuàng)建和刪除真是主體對象,并對真實(shí)主體對象的使用加以約束。代理角色通常在客戶端調(diào)用所引用的真實(shí)主體操作之前或之后還需要執(zhí)行其他操作,而不僅僅是單純的調(diào)用真是主體對象中的操作。
- RealSubject(真是主體角色)真是主體角色定義了代理角色所代表的真實(shí)對象,在真實(shí)主體角色中實(shí)現(xiàn)了真實(shí)的業(yè)務(wù)操作,客戶端可以通過代理主體角色間接調(diào)用真是主體角色重定義的方法。
2.2 代理模式的優(yōu)點(diǎn)
- 代理模式能將代理對象與真實(shí)被調(diào)用的目標(biāo)對象分離。
- 一定程度上降低了系統(tǒng)柜的耦合度,擴(kuò)展性好。
- 可以起到保護(hù)目標(biāo)對象的作用。
- 可以對目標(biāo)對象的功能增強(qiáng)
2.3 代理模式的缺點(diǎn)
- 代理模式會造成系統(tǒng)設(shè)計(jì)中類的數(shù)量增加。
- 在客戶端和目標(biāo)對象增加一個(gè)代理對象,會造成請求處理速度變慢。
2.4 JDK動態(tài)代理
在java的動態(tài)代理機(jī)制中,有兩個(gè)重要的類或接口,一個(gè)是 InvocationHandler(Interface)、另外一個(gè)則是Prox(Class),這個(gè)類和接口是實(shí)現(xiàn)我們動態(tài)代理所必須用到的。
2.4.1 InvocationHandler
每一個(gè)動態(tài)代理類都必須要實(shí)現(xiàn) InvocationHandler 這個(gè)接口,并且每個(gè)代理類的實(shí)例都關(guān)聯(lián)了一個(gè)handler,當(dāng)我們通過代理對象調(diào)用一個(gè)方法的時(shí)候,這個(gè)方法的調(diào)用就會被轉(zhuǎn)發(fā)為由 InvocationHandler 這個(gè)接口的 invoke 方法來進(jìn)行調(diào)用。
InvocationHandler 這個(gè)接口的唯一一個(gè)方法 invoke 方法:
Object invoke(Object proxy, Method method, Object[] args) throws Throwable
這個(gè)方法一共接受三個(gè)參數(shù),那么這三個(gè)參數(shù)分別代表如下:
- proxy :指代 JDK 動態(tài)生成的最終代理對象。
- method :指代的是我們所要調(diào)用真是對象的某個(gè)方法的 Method 對象。
- args : 指代的是調(diào)用真實(shí)對象某個(gè)方法時(shí)接受的參數(shù)。
2.4.2 Proxy
Proxy 這個(gè)類的作用就是來動態(tài)創(chuàng)建一個(gè)代理對象的類,它提供了許多的方法,但是我們用的最多的就是 newProxyInstance 這個(gè)方法:
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler handler) throws IllegalArgumentException
這個(gè)方法的作用就是得到一個(gè)動態(tài)的代理對象,其接收三個(gè)參數(shù),我們來看看這三個(gè)參數(shù)所代表的含義:
- loader :ClassLoader對象,定義了由那些 ClassLoader 來對生成的代理對象進(jìn)行加載,即代理類的類加載器。
- interfaces : Interface 對象的數(shù)組,表示的是我將要給我需要代理的對象提供一組什么接口,如果我提供了一組接口給它,那么這個(gè)代理對象就宣稱實(shí)現(xiàn)了該接口(多態(tài)),這樣我就能調(diào)用這組接口中的方法了。
- Handler :InvocationHandler 對象,表示的是當(dāng)我這個(gè)動態(tài)代理對象在調(diào)用方法的時(shí)候,會關(guān)聯(lián)到哪一個(gè) InvocationHandler 對象上。
所以我們所說的 DynamicProxy (動態(tài)代理類)是這樣一種class : 它是在運(yùn)行時(shí)生成的 class ,在生成它時(shí)你必須提供一組 interface 給它,然后改 class 就宣稱它實(shí)現(xiàn)了這些 interface。這個(gè) DynamicProxy 其實(shí)就是一個(gè) Proxy,它不會做實(shí)質(zhì)性的工作,在生成它的實(shí)例時(shí)你必須提供一個(gè) handler,由它接管實(shí)際的工作。
2.4.3 JDK 動態(tài)代理實(shí)例
- 創(chuàng)建接口類
public interface HelloInterface {
void sayHello();
}
- 創(chuàng)建被代理類,實(shí)現(xiàn)接口
/**
* 被代理類
*/
public class HelloImpl implements HelloInterface{
@Override
public void sayHello() {
System.out.println("hello");
}
}
- 創(chuàng)建InvocationHandler實(shí)現(xiàn)類
/**
* 每次生成動態(tài)代理類對象時(shí)都需要指定一個(gè)實(shí)現(xiàn)了InvocationHandler接口的調(diào)用處理器對象
*/
public class ProxyHandler implements InvocationHandler{
private Object subject; // 這個(gè)就是我們要代理的真實(shí)對象,也就是真正執(zhí)行業(yè)務(wù)邏輯的類
public ProxyHandler(Object subject) {// 通過構(gòu)造方法傳入這個(gè)被代理對象
this.subject = subject;
}
/**
*當(dāng)代理對象調(diào)用真實(shí)對象的方法時(shí),其會自動的跳轉(zhuǎn)到代理對象關(guān)聯(lián)的handler對象的invoke方法來進(jìn)行調(diào)用
*/
@Override
public Object invoke(Object obj, Method method, Object[] objs)
throws Throwable {
Object result = null;
System.out.println("可以在調(diào)用實(shí)際方法前做一些事情");
System.out.println("當(dāng)前調(diào)用的方法是" + method.getName());
result = method.invoke(subject, objs);// 需要指定被代理對象和傳入?yún)?shù)
System.out.println(method.getName() + "方法的返回值是" + result);
System.out.println("可以在調(diào)用實(shí)際方法后做一些事情");
System.out.println("------------------------");
return result;// 返回method方法執(zhí)行后的返回值
}
}
- 測試
public class Mytest {
public static void main(String[] args) {
//第一步:創(chuàng)建被代理對象
HelloImpl hello = new HelloImpl();
//第二步:創(chuàng)建handler,傳入真實(shí)對象
ProxyHandler handler = new ProxyHandler(hello);
//第三步:創(chuàng)建代理對象,傳入類加載器、接口、handler
HelloInterface helloProxy = (HelloInterface) Proxy.newProxyInstance(
HelloInterface.class.getClassLoader(),
new Class[]{HelloInterface.class}, handler);
//第四步:調(diào)用方法
helloProxy.sayHello();
}
}
- 結(jié)果
可以在調(diào)用實(shí)際方法前做一些事情
當(dāng)前調(diào)用的方法是sayHello
hello
sayHello方法的返回值是null
可以在調(diào)用實(shí)際方法后做一些事情
------------------------
2.4.4 JDK 動態(tài)代理步驟
JDK 動態(tài)代理分為以下幾步:
- 拿到被代理對象的引用,并且通過反射獲取到它的所有的接口。
- 通過 JDK Proxy 類重新生成一個(gè)新的類,同時(shí)新的類要實(shí)現(xiàn)被代理類所實(shí)現(xiàn)的所有的接口。
- 動態(tài)生成 Java 代碼,把新加的業(yè)務(wù)邏輯方法由一定的邏輯代碼去調(diào)用。
- 編譯新生成的 Java 代碼 .class。
- 將新生成的Class文件重新加載到JVM中運(yùn)行。
所以說 JDK 動態(tài)代理的核心是通過重寫被代理對象所實(shí)現(xiàn)的接口中的方法重新生成代理類來實(shí)現(xiàn)的,那么加入被代理對象沒有實(shí)現(xiàn)接口呢?那么這時(shí)候就需要 CGLIB 動態(tài)代理了。
2.5 CGLIB 動態(tài)代理
JDK 動態(tài)代理是通過重寫被代理對象實(shí)現(xiàn)的接口中的方法來實(shí)現(xiàn),而CGLIB是通過集成部誒代理對象來實(shí)現(xiàn)和 JDK 動態(tài)代理需要實(shí)現(xiàn)指定接口一樣,CGLIB 也要求代理對象必須要實(shí)現(xiàn) MethodInterceptor 接口,并重寫其唯一的方法 intercept 。
CGLib 采用了非常底層的字節(jié)碼技術(shù),其原理是通過字節(jié)碼技術(shù)為一個(gè)類創(chuàng)建子類,并在子類中采用方法攔截的技術(shù)攔截所有父類方法的調(diào)用,順勢植入橫切邏輯。(利用ASM開源包,對代理對象類的 class 文件加載進(jìn)來,通過修改其字節(jié)碼生成子類來處理)。
注意 :因?yàn)镃GLIB 是童工集成目標(biāo)類來重寫其方法來實(shí)現(xiàn)的,故而如果是 final 和 private 方法則無法被重寫,也就無法被代理。
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>2.2</version>
</dependency>
2.5.1 CGLIB 核心類
-
net.sf.cglib.proxy.Enhancer :主要增強(qiáng)類,通過字節(jié)碼技術(shù)動態(tài)創(chuàng)建委托類的子類實(shí)例;
Enhancer 可能是 CGLIB 中最常用的一個(gè)類,和Java1.3 動態(tài)代理中引入的Proxy類差不多。和Proxy不同的是,Enhancer 技能夠代理普通的 class,也能夠代理接口。Enhancer 創(chuàng)建一個(gè)被代理對象的子類并且攔截所有的方法調(diào)用(包括從 Object 中繼承的 toString 和 hashCode 方法)。Enhancer 不能夠攔截 final 方法,例如 Object.getClass() 方法,這是由于 Java final 方法語義決定的。基于同樣的道理,Enhancer 也不能對 final 類進(jìn)行 代理操作。這也是 Hibernate 為什么不能持久化 final class 的原因。
-
net.sf.cglib.proxy.MethodInterceptor :常用的方法攔截器接口,需要實(shí)現(xiàn) intercept 方法,實(shí)現(xiàn)具體攔截處理:
public java.lang.Object intercept(java.lang.Object obj,
java.lang.reflect.Method method,
java.lang.Object[] args,
MethodProxy proxy) throws java.lang.Throwable{}
-
obj : 動態(tài)生成的代理對象。
-
method : 實(shí)際調(diào)用的方法。
-
args : 調(diào)用方法入?yún)ⅰ?/p>
-
net.sf.cglib.proxy.MethodProxy :java Method類的代理類,可以實(shí)現(xiàn)委托類對象的方法的調(diào)用;常用方法:methodProxy.invokeSuper(proxy, args);在攔截方法內(nèi)可以調(diào)用多次。
2.5.2 CGLIB 代理實(shí)例
- 創(chuàng)建被代理類
public class SayHello {
public void say() {
System.out.println("hello")
}
}
- 創(chuàng)建代理類
/**
*代理類
*/
public class ProxyCglib implements MethodInterceptor{
private Enhancer enhancer = new Enhancer();
public Object getProxy(Class clazz){
//設(shè)置需要?jiǎng)?chuàng)建子類的類
enhancer.setSuperclass(clazz);
enhancer.setCallback(this);
//通過字節(jié)碼技術(shù)動態(tài)創(chuàng)建子類實(shí)例
return enhancer.create();
}
//實(shí)現(xiàn)MethodInterceptor接口方法
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("可以在調(diào)用實(shí)際方法前做一些事情");
//通過代理類調(diào)用父類中的方法
Object result = proxy.invokeSuper(obj, args);
System.out.println("可以在調(diào)用實(shí)際方法后做一些事情");
return result;
}
}
- 測試
public class Mytest {
public static void main(String[] args) {
ProxyCglib proxy = new ProxyCglib();
//通過生成子類的方式創(chuàng)建代理類
SayHello proxyImp = (SayHello)proxy.getProxy(SayHello.class);
proxyImp.say();
}
}
- 結(jié)果
可以在調(diào)用實(shí)際方法前做一些事情
hello
可以在調(diào)用實(shí)際方法后做一些事情
2.5.3 CGLIB 動態(tài)代理實(shí)現(xiàn)分析
CGLIB 動態(tài)代理采用了 FastClass 機(jī)制,其分別為代理類和被代理類各生成一個(gè)FastClass,這個(gè) FastClass 類會為代理類或被代理類的方法分配一個(gè) index(int類型)。這個(gè)index 當(dāng)作一個(gè)入?yún)ⅲ現(xiàn)astClass 就可以直接定位要調(diào)用的方法直接進(jìn)行調(diào)用,這樣省去了反射調(diào)用,所以調(diào)用效率比 JDK 動態(tài)代理通過反射調(diào)用更高。
但是我們看上面的源碼也可以明顯看到,JDK 動態(tài)代理只生成一個(gè)文件,而 CGLIB 生成了三個(gè)文件,所以生成代理對象的過程會更復(fù)雜。
2.6 JDK 和 CGLIB 動態(tài)代理對比
JDK 動態(tài)代理是實(shí)現(xiàn)了唄代理對象啊所實(shí)現(xiàn)的接口,CGLIB 是繼承了被代理對象。JDK 和 CGLIB 都是在運(yùn)行期生成字節(jié)碼,JDK 是直接寫 Class 字節(jié)碼,CGLib 使用 ASM 框架寫 Class 字節(jié)碼,Cglib 代理實(shí)現(xiàn)更為復(fù)雜,生成代理類的效率比 JDK 代理低。
JDK 調(diào)用代理方法,是通過反射機(jī)制調(diào)用,CGLIB 是通過 FastClass 機(jī)制直接調(diào)用方法,CGLIB 執(zhí)行效率更高。
2.6.1 原理區(qū)別:
java 動態(tài)代理是利用反射機(jī)制生成一個(gè)實(shí)現(xiàn)代理接口的匿名類,在調(diào)用具體方法前調(diào)用 InvocationHandler 來處理。核心是實(shí)現(xiàn) InvocationHandler 接口,使用 invoke() 方法進(jìn)行面向切面的處理,調(diào)用相應(yīng)的通知。
- 如果目標(biāo)對象實(shí)現(xiàn)了接口,默認(rèn)情況下會采用 JDK 的動態(tài)代理實(shí)現(xiàn) AOP。
- 如果目標(biāo)對象實(shí)現(xiàn)了接口,可以強(qiáng)制使用 CGLIB 實(shí)現(xiàn) AOP。
- 如果目標(biāo)對象沒有實(shí)現(xiàn)了接口,必須采用 CGLIB 庫,spring 會自動在 JDK 動態(tài)代理和 CGLIB 之間轉(zhuǎn)換
2.6.2 性能區(qū)別:
-
CGLib底層采用ASM字節(jié)碼生成框架,使用字節(jié)碼技術(shù)生成代理類,在jdk6之前比使用Java反射效率要高。唯一需要注意的是,CGLib不能對聲明為final的方法進(jìn)行代理,因?yàn)镃GLib原理是動態(tài)生成被代理類的子類。
-
在jdk6、jdk7、jdk8逐步對JDK動態(tài)代理優(yōu)化之后,在調(diào)用次數(shù)較少的情況下,JDK代理效率高于CGLIB代理效率,只有當(dāng)進(jìn)行大量調(diào)用的時(shí)候,jdk6和jdk7比CGLIB代理效率低一點(diǎn),但是到j(luò)dk8的時(shí)候,jdk代理效率高于CGLIB代理。
2.6.3 各自局限
-
JDK的動態(tài)代理機(jī)制只能代理實(shí)現(xiàn)了接口的類,而不能實(shí)現(xiàn)接口的類就不能實(shí)現(xiàn)JDK的動態(tài)代理。
-
cglib是針對類來實(shí)現(xiàn)代理的,他的原理是對指定的目標(biāo)類生成一個(gè)子類,并覆蓋其中方法實(shí)現(xiàn)增強(qiáng),但因?yàn)椴捎玫氖抢^承,所以不能對final修飾的類進(jìn)行代理。
| 類型 | 機(jī)制 | 回調(diào)方式 | 適用場景 | 效率 |
|---|---|---|---|---|
| JDK動態(tài)代理 | 委托機(jī)制,代理類和目標(biāo)類都實(shí)現(xiàn)了同樣的接口,InvocationHandler持有目標(biāo)類,代理類委托InvocationHandler去調(diào)用目標(biāo)類的原始方法 | 反射 | 目標(biāo)類是接口類 | 效率瓶頸在反射調(diào)用稍慢 |
| CGLIB動態(tài)代理 | 繼承機(jī)制,代理類繼承了目標(biāo)類并重寫了目標(biāo)方法,通過回調(diào)函數(shù)MethodInterceptor調(diào)用父類方法執(zhí)行原始邏輯 | 通過FastClass方法索引調(diào)用 | 非接口類、非final類,非final方法 | 第一次調(diào)用因?yàn)橐啥鄠€(gè)Class對象,比JDK方式慢。多次調(diào)用因?yàn)橛蟹椒ㄋ饕确瓷淇欤绻椒ㄟ^多,switch case過多其效率還需測試 |
2.7 靜態(tài)代理和動態(tài)代理的區(qū)別
靜態(tài)代理只能通過手動完成代理操作,如果被代理類增加新的方法,代理類需要同步新增,違背開閉原則。
動態(tài)代理采用在運(yùn)行時(shí)動態(tài)生成代碼的方式,取消了對被代理類的擴(kuò)展限制,遵循開閉原則。
若動態(tài)代理要對目標(biāo)類的增強(qiáng)邏輯擴(kuò)展,結(jié)合策略模式,只需要新增策略類便可完成,無需修改代理類的代碼。
三、Redlock :Redis 集群分布式鎖
3.1 前言
分布式鎖是一種非常有用的技術(shù)手段。實(shí)現(xiàn)高效的分布式鎖有三個(gè)屬性需要考慮。
- 安全屬性 : 互斥,不管什么時(shí)候,只有一個(gè)客戶端持有鎖。
- 效率屬性A :不會死鎖。
- 效率屬性B :容錯(cuò),只要大多數(shù) redis 節(jié)點(diǎn)能夠正常工作,客戶端都能獲取和釋放鎖。
3.2 普通版:單機(jī) redis 分布式鎖
Redis 分布式鎖大部分人都會想到 :setnx + lua 或者 set + lua ,加上過期時(shí)間,大多數(shù)都是使用的下面的 keyset 方法,具體實(shí)現(xiàn)過程這里就不再贅述。
- 實(shí)現(xiàn)比較輕,大多數(shù)能夠滿足需求;因?yàn)槭菃螜C(jī)單實(shí)例部署,如果 redis 服務(wù)宕機(jī),那么所有需要獲取分布式鎖的地方均無法獲取鎖,將全部阻塞,需要做好降級處理。
- 當(dāng)鎖過期后,執(zhí)行任務(wù)的進(jìn)程還沒有執(zhí)行完,但是鎖因?yàn)樽詣舆^期已經(jīng)解鎖,可能被其他進(jìn)程重新加鎖,這就造成多個(gè)金策會給你同時(shí)獲取到了鎖,這需要額外的方案來解決這種問題。
- 在集群模式時(shí)有復(fù)制延遲,以及主節(jié)點(diǎn)宕機(jī),會造成鎖丟失或者鎖延遲現(xiàn)象。
事實(shí)上這類鎖最大的缺點(diǎn)就是它加鎖時(shí)之作用在一個(gè) Redis 節(jié)點(diǎn)上,即時(shí) Redis 通過 sentinel 保證高可用,如果這個(gè) master 節(jié)點(diǎn)由于某些原因發(fā)生了主從切換,那么就會出現(xiàn) 鎖丟失的情況:
- 在 Redis 的 master 節(jié)點(diǎn)上拿到了鎖;
- 但是這個(gè)加鎖的 key 還沒有同步到 slave 節(jié)點(diǎn);
- master 故障,發(fā)生故障轉(zhuǎn)移, slave 節(jié)點(diǎn)升級為 master 節(jié)點(diǎn);
- 導(dǎo)致鎖丟失。
正因?yàn)槿绱耍琑edis 作者 基于分布式環(huán)境下提出一種更為高級的分布式鎖的實(shí)現(xiàn)方式 : Redlock ;Redlock 也是 Redis 所有分布式鎖實(shí)現(xiàn)分布式鎖實(shí)現(xiàn)方式中唯一能讓面試官高潮的方式。
基于單 Redis 節(jié)點(diǎn)的分布式鎖的算法就描述完了。這里面有好幾個(gè)問題需要重點(diǎn)分析一下。
- 首先第一個(gè)問題,這個(gè)鎖必須要設(shè)置一個(gè)過期時(shí)間。否則的話,當(dāng)一個(gè)客戶端獲取鎖成功之后,加入它崩潰了,或者由于發(fā)生了網(wǎng)路分割(network partition)導(dǎo)致它在無法和 Redis 節(jié)點(diǎn)通信了,那么它就會一直持有這個(gè) 鎖,而其它客戶端永遠(yuǎn)無法獲得鎖了。為了解決這個(gè)問題,redis作者把這個(gè)過期時(shí)間成為所的有效時(shí)間(lock validity time)。獲的鎖的客戶端必須在這個(gè)時(shí)間之內(nèi)完成對共享資源的訪問。
- 第二個(gè)問題,第一步獲取鎖的操作,不少文章把它實(shí)現(xiàn)成了兩個(gè) Redis 命令:
SETNX resource_name my_random_value
EXPIRE resource_name 30
雖然這兩個(gè)命令和前面算法描述中的一個(gè) set 命令執(zhí)行效果相同,但卻不是原子的。如果客戶端在執(zhí)行完 SETNX 后崩潰了,那么就沒有機(jī)會執(zhí)行 EXPIRE了,導(dǎo)致它一直持有這個(gè)鎖。
-
第三個(gè)問題:也是作者指出的,設(shè)置一個(gè)隨機(jī)字符串 my_random_value 是很有必要的,它保證了一個(gè)客戶端釋放的鎖必須是自己持有的那個(gè)鎖。加入獲取鎖時(shí) SET 的不是一個(gè)隨機(jī)字符串,而是一個(gè)固定值,那么可能會發(fā)生下面的執(zhí)行序列:
- 客戶端A 獲取鎖成功。
- 客戶端A在某個(gè)操作上阻塞了很長時(shí)間。
- 過期時(shí)間到了,鎖自動釋放了。
- 客戶端B獲取到了對應(yīng)同一個(gè)資源的鎖。
- 客戶端A從阻塞中回復(fù)過來,釋放掉了客戶端B 持有的鎖。
- 之后,客戶端B 在訪問共享資源的時(shí)候,它沒有鎖為它提供保護(hù)了。
-
第四個(gè)問題:釋放鎖的操作必須是使用Lua腳本實(shí)現(xiàn),釋放鎖其實(shí)包含三步操作 : "GET" ,判斷和 "DEL" ,用Lua腳本來實(shí)現(xiàn)能保證各三步的原子性。否則,如果把這三步操作方法哦客戶端邏輯中執(zhí)行的話,就有可能發(fā)生與前面第三個(gè)問題類似的執(zhí)行序列:
- 客戶端1獲取鎖成功。
- 客戶端1訪問共享資源。
- 客戶端1為了釋放鎖,先執(zhí)行’GET’操作獲取隨機(jī)字符串的值。
- 客戶端1判斷隨機(jī)字符串的值,與預(yù)期的值相等。
- 客戶端1由于某個(gè)原因阻塞住了很長時(shí)間。
- 過期時(shí)間到了,鎖自動釋放了。
- 客戶端2獲取到了對應(yīng)同一個(gè)資源的鎖。
- 客戶端1從阻塞中恢復(fù)過來,執(zhí)行DEL操縱,釋放掉了客戶端2持有的鎖。
- 實(shí)際上,在上述第三個(gè)問題和第四個(gè)問題的分析中,如果不是客戶端阻塞住了,而是出現(xiàn)了大的網(wǎng)絡(luò)延遲,也有可能導(dǎo)致類似的執(zhí)行序列發(fā)生。
前面的問題,只要實(shí)現(xiàn)分布式鎖的時(shí)候加以注意,就都能夠被正確的處理。但除此之外,還有一個(gè)問題,就是由 failover【故障切換】 引起的卻是基于單Redis界定的分布式鎖無法解決的。正是這個(gè)問題催生了 Redlock 的出現(xiàn)。
3.3 為什么基于failover【故障切換】的方案不夠好
為了理解我們想要提高的到底是什么,我們先看下當(dāng)前大多數(shù)基于Redis的分布式鎖三方庫的現(xiàn)狀。 用Redis來實(shí)現(xiàn)分布式鎖最簡單的方式就是在實(shí)例里創(chuàng)建一個(gè)鍵值,創(chuàng)建出來的鍵值一般都是有一個(gè)超時(shí)時(shí)間的(這個(gè)是Redis自帶的超時(shí)特性),所以每個(gè)鎖最終都會釋放。
而當(dāng)一個(gè)客戶端想要釋放鎖時(shí),它只需要?jiǎng)h除這個(gè)鍵值即可。 表面來看,這個(gè)方法似乎很管用,但是這里存在一個(gè)問題:在我們的系統(tǒng)架構(gòu)里存在一個(gè)單點(diǎn)故障,如果Redis的master節(jié)點(diǎn)宕機(jī)了怎么辦呢?有人可能會說:加一個(gè)slave節(jié)點(diǎn)!在master宕機(jī)時(shí)用slave就行了!但是其實(shí)這個(gè)方案明顯是不可行的,因?yàn)檫@種方案無法保證第1個(gè)安全互斥屬性,因?yàn)镽edis的復(fù)制是異步的。 總的來說,這個(gè)方案里有一個(gè)明顯的競爭條件(race condition),舉例來說:
1、客戶端A在master節(jié)點(diǎn)拿到了鎖。
2、master節(jié)點(diǎn)在把A創(chuàng)建的key寫入slave之前宕機(jī)了。
3、slave變成了master節(jié)點(diǎn)
4、B也得到了和A還持有的相同的鎖(因?yàn)樵瓉淼膕lave里還沒有A持有鎖的信息)
當(dāng)然,在某些特殊場景下,前面提到的這個(gè)方案則完全沒有問題,比如在宕機(jī)期間,多個(gè)客戶端允許同時(shí)都持有鎖,如果你可以容忍這個(gè)問題的話,那用這個(gè)基于復(fù)制的方案就完全沒有問題,否則的話我們還是建議你采用這篇文章里接下來要描述的方案。
3.4 集群分布式鎖
在redis集群模式下創(chuàng)建鎖和解鎖的方案,用到的 redis 命令依然和普通模式一樣,唯一不同的在于集群模式下的數(shù)據(jù)清理方式,基本命令如下:
SET key value [EX seconds] [PX milliseconds] [NX|XX]
Set the string value of a key
SET 指令可以將字符串的value和key綁定在一起。
EX seconds:設(shè)置key的過時(shí)時(shí)間,單位為秒。
PX milliseconds:設(shè)置key的過期時(shí)間,單位為毫秒。
NX:(if Not eXist)只有鍵key不存在的時(shí)候才會設(shè)置key的值
XX:只有鍵key存在的時(shí)候才會設(shè)置key的值
NX通常用于實(shí)現(xiàn)鎖機(jī)制,X
lua腳本
// 獲取鎖(unique_value可以是UUID等)
SET key_name unique_value NX PX 30000
// 釋放鎖(lua腳本中,一定要比較value,防止誤解鎖)
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
3.5 Redlock 實(shí)現(xiàn)
Redlock 算法大概是這樣的:
在 Redis 的分布式環(huán)境中,我們假設(shè)最孤立現(xiàn)象(最苛刻環(huán)境下):有 N 個(gè) Redis Master 。這些節(jié)點(diǎn)完全互相獨(dú)立(正常集群不會做成這么孤立),不存在主從復(fù)制或者其他集群卸掉機(jī)制。我們確保將在 N 個(gè)實(shí)例上使用與 Redis 單實(shí)例下相同方法獲取和釋放鎖。現(xiàn)在我們假設(shè)有 5 個(gè) Redis Master 節(jié)點(diǎn),同時(shí)我們需要在 5 臺服務(wù)器上賣弄運(yùn)行這些 Redis 實(shí)例,這樣保證他們不會同時(shí)宕機(jī)。

這里把上圖中的各個(gè)redis主從連線去掉,就變成各個(gè)獨(dú)立的集群了(實(shí)現(xiàn)孤立場景:集群之間掉線不通等極端情況)
為了取到鎖,客戶端應(yīng)該執(zhí)行以下操作:
-
獲取當(dāng)前Unix時(shí)間,以毫秒為單位。
-
依次嘗試從5個(gè)實(shí)例,使用相同的key和具有唯一性的value(例如UUID)獲取鎖。當(dāng)向Redis請求獲取鎖時(shí),客戶端應(yīng)該設(shè)置一個(gè)網(wǎng)絡(luò)連接和響應(yīng)超時(shí)時(shí)間,這個(gè)超時(shí)時(shí)間應(yīng)該小于鎖的失效時(shí)間。例如你的鎖自動失效時(shí)間為10秒,則超時(shí)時(shí)間應(yīng)該在5-50毫秒之間。這樣可以避免服務(wù)器端Redis已經(jīng)掛掉的情況下,客戶端還在死死地等待響應(yīng)結(jié)果。如果服務(wù)器端沒有在規(guī)定時(shí)間內(nèi)響應(yīng),客戶端應(yīng)該盡快嘗試去另外一個(gè)Redis實(shí)例請求獲取鎖。
-
客戶端使用當(dāng)前時(shí)間減去開始獲取鎖時(shí)間(步驟1記錄的時(shí)間)就得到獲取鎖使用的時(shí)間。當(dāng)且僅當(dāng)從大多數(shù)(N/2+1,這里是3個(gè)節(jié)點(diǎn))的Redis節(jié)點(diǎn)都取到鎖,并且使用的時(shí)間小于鎖失效時(shí)間時(shí),鎖才算獲取成功。
// 計(jì)算方式如下 1. 當(dāng)前時(shí)間 - 獲取鎖的時(shí)間 = 鎖使用時(shí)間 2. N/2 + 1 三個(gè)Redis節(jié)點(diǎn)都獲取到鎖 3. 鎖使用時(shí)間 < 鎖失效時(shí)間 (才認(rèn)為獲取到鎖) -
如果取到了鎖,key的真正有效時(shí)間等于有效時(shí)間減去獲取鎖所使用的時(shí)間(步驟3計(jì)算的結(jié)果)。
-
如果因?yàn)槟承┰颍?strong>獲取鎖失敗(沒有在至少N/2+1個(gè)Redis實(shí)例取到鎖或者取鎖時(shí)間已經(jīng)超過了有效時(shí)間),客戶端應(yīng)該在所有的Redis實(shí)例上進(jìn)行解鎖(即便某些Redis實(shí)例根本就沒有加鎖成功,防止某些節(jié)點(diǎn)獲取到鎖但是客戶端沒有得到響應(yīng)而導(dǎo)致接下來的一段時(shí)間不能被重新獲取鎖)。
3.5.1實(shí)現(xiàn)代碼
- POM依賴
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.3.2</version>
</dependency>
Config config = new Config();
config.useSentinelServers().addSentinelAddress("127.0.0.1:6369","127.0.0.1:6379", "127.0.0.1:6389")
.setMasterName("masterName")
.setPassword("password").setDatabase(0);
RedissonClient redissonClient = Redisson.create(config);
// 還可以getFairLock(), getReadWriteLock()
RLock redLock = redissonClient.getLock("REDLOCK_KEY");
boolean isLock;
try {
isLock = redLock.tryLock();//使用默認(rèn)方式獲取:默認(rèn)租約時(shí)間(leaseTime)是LOCKEXPIRATIONINTERVAL_SECONDS,即30s
// 因?yàn)樯弦恍蝎@取到了,這里獲取不到:如果獲取到了就500ms, 就認(rèn)為獲取鎖失敗。10000ms即10s是鎖失效時(shí)間。
isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
if (isLock) { //如果獲取成功
//TODO if get lock success, do something;
}
} catch (Exception e) {
} finally {
// 無論如何, 最后都要解鎖
redLock.unlock();
}
- key對應(yīng)唯一值
防止誤刪除鎖:實(shí)現(xiàn)分布式鎖的一個(gè)非常重要的點(diǎn)就是set的value要具有唯一性,redisson的value是怎樣保證value的唯一性呢? 答案是UUID+threadId。入口在redissonClient.getLock(“REDLOCK_KEY”),源碼在Redisson.java和RedissonLock.java中:
- 獲取鎖
設(shè)置過期時(shí)間:代碼為redLock.tryLock()或者redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS),兩者的最終核心源碼都是下面這段代碼,只不過前者獲取鎖的默認(rèn)租約時(shí)間(leaseTime)是LOCKEXPIRATIONINTERVAL_SECONDS,即30s:

浙公網(wǎng)安備 33010602011771號