Springboot-Starter造輪子之自動(dòng)鎖組件lock-starter實(shí)現(xiàn)
前言
可能有人會(huì)有疑問(wèn),為什么外面已經(jīng)有更好的組件,為什么還要重復(fù)的造輪子,只能說(shuō),別人的永遠(yuǎn)是別人的,自己不去造一下,就只能知其然,而不知其所以然。(其實(shí)就為了卷)
在日常業(yè)務(wù)開(kāi)發(fā)的過(guò)程中,我們經(jīng)常會(huì)遇到存在高并發(fā)的場(chǎng)景,這個(gè)時(shí)候都會(huì)選擇使用redis來(lái)實(shí)現(xiàn)一個(gè)鎖,來(lái)防止并發(fā)。
但是很多時(shí)候,我們可能業(yè)務(wù)完成后,就需要把鎖釋放掉,給下一個(gè)線程用,但是如果我們忘記了釋放鎖,可能就會(huì)存在死鎖的問(wèn)題。(對(duì)于使用鎖不太熟練的話,這種情況時(shí)常發(fā)生,雖然很多時(shí)候,我們的鎖是有過(guò)期時(shí)間的,但是如果忘記了釋放,那么在這個(gè)過(guò)期時(shí)間內(nèi),還是會(huì)存在大的損失)。
還有一點(diǎn)就是,在我們使用redis實(shí)現(xiàn)一個(gè)鎖的時(shí)候,我們需要導(dǎo)入redisClient,設(shè)置key,設(shè)置過(guò)期時(shí)間,設(shè)置是否鎖等等一些重復(fù)的操作。前面的哪些步驟,很多都是重復(fù)的,所以我們可以想一個(gè)方法,來(lái)把重復(fù)的東西都抽象出來(lái),做成統(tǒng)一的處理,同時(shí)哪些變化的值,提供一個(gè)設(shè)置的入口。
抽出來(lái)的東西,我們還可以封裝成一個(gè)spring-boot-stater,這樣我們只需要寫一份,就可以在不同的項(xiàng)目中使用了。 說(shuō)干就干,下面我們使用redisson,完成一個(gè)自動(dòng)鎖的starter。
實(shí)現(xiàn)
首先,我們分析一下哪些東西是我們需要進(jìn)行合并,哪些又是需要提供給使用方的。得到下面的一些問(wèn)題
- 加鎖、釋放鎖過(guò)程 我們需要合并起來(lái)
- 鎖key,加鎖時(shí)間......這些需要給使用方注入
- 鎖的key該怎么去生成(很多時(shí)候,我們需要根據(jù)業(yè)務(wù)字段去構(gòu)造一個(gè)key,比如 user:{userId}),那么這個(gè)userId該怎么獲???
我們從上面需要解決的問(wèn)題,去思考需要怎么去實(shí)現(xiàn)。我們需要封裝一些公共的邏輯,又需要提供一些配置的入庫(kù),這樣的話,我們可以嘗試一種方法,使用 注解+AOP,通過(guò)注解的方式完成加鎖、解鎖。(很多時(shí)候,如果需要抽出一些公共的方法,會(huì)用到注解+AOP去實(shí)現(xiàn))
定義注解
AutoLock 注解
一個(gè)鎖需要有的信息有,key,加鎖的時(shí)間,時(shí)間單位,是否嘗試加鎖,加鎖等待時(shí)間 等等。(如果還有其他的業(yè)務(wù)需要,可以添加一個(gè)擴(kuò)展內(nèi)容,自己去解析處理) 那么這個(gè)注解的屬性就可以知道有哪些了
/**
* 鎖的基本信息
*/
@Target({ElementType.METHOD})
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoLock {
/**
* 鎖前綴
*/
String prefix() default "anoxia:lock";
/**
* 加鎖時(shí)間
*/
long lockTime() default 30;
/**
* 是否嘗試加鎖
*/
boolean tryLock() default true;
/**
* 等待時(shí)間,-1 不等待
*/
long waitTime() default -1;
/**
* 鎖時(shí)間類型
*/
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}LockField 注解
這個(gè)注解添加到參數(shù)屬性上面,用來(lái)解決上面提到獲取不同的業(yè)務(wù)參數(shù)內(nèi)容構(gòu)造key的問(wèn)題。所以我們需要提供一個(gè)獲取哪些字段來(lái)構(gòu)造這個(gè)key配置,這里需要考慮兩個(gè)問(wèn)題:
- 1、參數(shù)是基本類型
- 2、參數(shù)是引用類型 - 這種類型需要從對(duì)象中拿到對(duì)象的屬性值
/**
* 構(gòu)建鎖的業(yè)務(wù)數(shù)據(jù)
* @author huangle
* @date 2023/5/5 15:01
*/
@Target({ElementType.PARAMETER})
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface LockField {
String[] fieldNames() default {};
}
定義切面
重點(diǎn)就在這個(gè)切面里面,我們需要在這里完成key的合成,鎖的獲取與釋放。整個(gè)過(guò)程可以分為以下幾步
- 獲取鎖的基本信息,構(gòu)建key
- 加鎖,執(zhí)行業(yè)務(wù)
- 業(yè)務(wù)完成,釋放鎖
/**
* 自動(dòng)鎖切面
* 處理加鎖解鎖邏輯
*
* @author huangle
* @date 2023/5/5 14:50
*/
@Aspect
@Component
public class AutoLockAspect {
private final static Logger LOGGER = LoggerFactory.getLogger(AutoLockAspect.class);
@Resource
private RedissonClient redissonClient;
private static final String REDIS_LOCK_PREFIX = "anoxiaLock";
private static final String SEPARATOR = ":";
/**
* 定義切點(diǎn)
*/
@Pointcut("@annotation(cn.anoxia.lock.annotation.AutoLock)")
public void lockPoincut() {
}
/**
* 定義攔截處理方式
*
* @return
*/
@Around("lockPoincut()")
public Object doLock(ProceedingJoinPoint joinPoint) throws Throwable {
// 獲取需要加鎖的方法
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
// 獲取鎖注解
AutoLock autoLock = method.getAnnotation(AutoLock.class);
// 獲取鎖前綴
String prefix = autoLock.prefix();
// 獲取方法參數(shù)
Parameter[] parameters = method.getParameters();
StringBuilder lockKeyStr = new StringBuilder(prefix);
Object[] args = joinPoint.getArgs();
// 遍歷參數(shù)
int index = -1;
LockField lockField;
// 構(gòu)建key
for (Parameter parameter : parameters) {
Object arg = args[++index];
lockField = parameter.getAnnotation(LockField.class);
if (lockField == null) {
continue;
}
String[] fieldNames = lockField.fieldNames();
if (fieldNames == null || fieldNames.length == 0) {
lockKeyStr.append(SEPARATOR).append(arg);
} else {
List<Object> filedValues = ReflectionUtil.getFiledValues(parameter.getType(), arg, fieldNames);
for (Object value : filedValues) {
lockKeyStr.append(SEPARATOR).append(value);
}
}
}
String lockKey = REDIS_LOCK_PREFIX + SEPARATOR + lockKeyStr;
RLock lock = redissonClient.getLock(lockKey);
// 加鎖標(biāo)志位
boolean lockFlag = false;
try {
long lockTime = autoLock.lockTime();
long waitTime = autoLock.waitTime();
TimeUnit timeUnit = autoLock.timeUnit();
boolean tryLock = autoLock.tryLock();
try {
if (tryLock) {
lockFlag = lock.tryLock(waitTime, lockTime, timeUnit);
} else {
lock.lock(lockTime, timeUnit);
lockFlag = true;
}
}catch (Exception e){
LOGGER.error("加鎖失敗!,錯(cuò)誤信息", e);
throw new RuntimeException("加鎖失敗!");
}
if (!lockFlag) {
throw new RuntimeException("加鎖失??!");
}
// 執(zhí)行業(yè)務(wù)
return joinPoint.proceed();
} finally {
// 釋放鎖
if (lockFlag) {
lock.unlock();
LOGGER.info("釋放鎖完成,key:{}",lockKey);
}
}
}
}獲取業(yè)務(wù)屬性
這個(gè)是一個(gè)獲取對(duì)象中字段的工具類,在一些常用的工具類里面也有實(shí)現(xiàn),可以直接使用也可以自己實(shí)現(xiàn)一個(gè)
/**
* @author huangle
* @date 2023/5/5 15:17
*/
public class ReflectionUtil {
public static List<Object> getFiledValues(Class<?> type, Object target, String[] fieldNames) throws IllegalAccessException {
List<Field> fields = getFields(type, fieldNames);
List<Object> valueList = new ArrayList();
Iterator fieldIterator = fields.iterator();
while(fieldIterator.hasNext()) {
Field field = (Field)fieldIterator.next();
if (!field.isAccessible()) {
field.setAccessible(true);
}
Object value = field.get(target);
valueList.add(value);
}
return valueList;
}
public static List<Field> getFields(Class<?> claszz, String[] fieldNames) {
if (fieldNames != null && fieldNames.length != 0) {
List<String> needFieldList = Arrays.asList(fieldNames);
List<Field> matchFieldList = new ArrayList();
List<Field> fields = getAllField(claszz);
Iterator fieldIterator = fields.iterator();
while(fieldIterator.hasNext()) {
Field field = (Field)fieldIterator.next();
if (needFieldList.contains(field.getName())) {
matchFieldList.add(field);
}
}
return matchFieldList;
} else {
return Collections.EMPTY_LIST;
}
}
public static List<Field> getAllField(Class<?> claszz) {
if (claszz == null) {
return Collections.EMPTY_LIST;
} else {
List<Field> list = new ArrayList();
do {
Field[] array = claszz.getDeclaredFields();
list.addAll(Arrays.asList(array));
claszz = claszz.getSuperclass();
} while(claszz != null && claszz != Object.class);
return list;
}
}
}
配置自動(dòng)注入
在我們使用 starter 的時(shí)候,都是通過(guò)這種方式,來(lái)告訴spring在加載的時(shí)候,完成這個(gè)bean的初始化。這個(gè)過(guò)程基本是定死的。 就是編寫配置類,如果通過(guò)springBoot的EnableAutoConfiguration來(lái)完成注入。注入后,我們就可以直接去使用這個(gè)封裝好的鎖了。
/**
* @author huangle
* @date 2023/5/5 14:50
*/
@Configuration
public class LockAutoConfig {
@Bean
public AutoLockAspect autoLockAspect(){
return new AutoLockAspect();
}
}
// spring.factories 中內(nèi)容
org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.anoxia.lock.config.LockAutoConfig
測(cè)試
我們先打包這個(gè)sarter,然后導(dǎo)入到一個(gè)項(xiàng)目里面(打包導(dǎo)入的過(guò)程就不說(shuō)了,自己去看一下就可以) 直接上測(cè)試類,下面執(zhí)行后可以看到鎖已經(jīng)完成了釋放。如果業(yè)務(wù)拋出異常導(dǎo)致中斷也不用擔(dān)心鎖不會(huì)釋放的問(wèn)題,因?yàn)槲覀兪窃?finally 中釋放鎖的
/**
* @author huangle
* @date 2023/5/5 14:28
*/
@RestController
@RequestMapping("/v1/user")
public class UserController {
@AutoLock(lockTime = 3, timeUnit = TimeUnit.MINUTES)
@GetMapping("/getUser")
public String getUser(@RequestParam @LockField String name) {
return "hello:"+name;
}
@PostMapping("/userInfo")
@AutoLock(lockTime = 1, timeUnit = TimeUnit.MINUTES)
public String userInfo(@RequestBody @LockField(fieldNames = {"id", "name"}) UserDto userDto){
return userDto.getId()+":"+userDto.getName();
}
}


總結(jié)
很多時(shí)候,一些公共的業(yè)務(wù)邏輯都可以被抽象出來(lái)成為一個(gè)獨(dú)立的組件而存在,我們可以在日常開(kāi)發(fā)過(guò)程中,不斷的去思考和尋找看哪些可以被抽象出來(lái),哪些可以更加簡(jiǎn)化一些。然后嘗試去抽象出一個(gè)組件出來(lái),這樣的話不但可以鍛煉自己的能力,還可以得到一些很好用的工具,當(dāng)然自己抽出的組件可以存在問(wèn)題,但是慢慢的鍛煉下來(lái),總會(huì)變的越來(lái)越好。 怎么說(shuō)呢,嘗試去做,能不能做好再說(shuō),做不好就一次又一次的去做。
以上就是Springboot-Starter造輪子之自動(dòng)鎖組件(lock-starter)的詳細(xì)內(nèi)容,更多關(guān)于Springboot-Starter自動(dòng)鎖組件(lock-starter)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Springboot通過(guò)run啟動(dòng)web應(yīng)用的方法
這篇文章主要介紹了Springboot通過(guò)run啟動(dòng)web應(yīng)用的方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-03-03
Java入門絆腳石之Override和Overload的區(qū)別詳解
重寫是子類對(duì)父類的允許訪問(wèn)的方法的實(shí)現(xiàn)過(guò)程進(jìn)行重新編寫, 返回值和形參都不能改變。即外殼不變,核心重寫!重寫的好處在于子類可以根據(jù)需要,定義特定于自己的行為。重載是在一個(gè)類里面,方法名字相同,而參數(shù)不同。返回類型可以相同也可以不同2021-10-10
Mybatis-Plus使用ID_WORKER生成主鍵id重復(fù)的解決方法
本文主要介紹了Mybatis-Plus使用ID_WORKER生成主鍵id重復(fù)的解決方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07
Java編程中隨機(jī)數(shù)的生成方式總結(jié)
在Java中利用自帶的類庫(kù)可以有三種途徑可以產(chǎn)生隨機(jī)數(shù),這里我們舉了一些簡(jiǎn)單的例子來(lái)進(jìn)行Java編程中隨機(jī)數(shù)的生成方式總結(jié),需要的朋友可以參考下2016-05-05

