在小程序登陸的時候,在`MiniAppAuthenticationProvider`中我們看到這樣一行代碼
```java
yamiUserDetailsService.insertUserIfNecessary(appConnect);
```
這便是商城用戶創建的代碼,在`YamiUserServiceImpl#insertUserIfNecessary()`方法中,有一個這樣的注解
```java
@RedisLock(lockName = "insertUser", key = "#appConnect.appId + ':' + #appConnect.bizUserId")
```
這里便用了分布式鎖,為什么我們要在這里使用鎖?分布式鎖又是什么?
- 由于用戶是通過登錄直接注冊的,如果一個用戶在不刻意之間,又或者前端寫的東西有點問題,這就會導致整個系統創建了兩個相同的用戶,這是非常危險的事情,所以創建用戶這里必須加鎖。
- 至于為什么使用分布式鎖,是因為我們雖然沒有用上spring cloud、dubbo之類的東西,實際上我們也是希望我們的商城可以多實例部署的,也就是可以搞分布式的。因此用了分布式鎖
分布式鎖,簡單來說就是鎖,而且還是適合分布式環境的。分布式說起來也很奇怪,要是有什么不能共享的東西,那就抽出來共享。比如本地數據緩存不能共享,那么就抽出一個如redis之類的東西,進行共享。session不能共享,那么就將session抽出來,丟到redis之類的東西,又能共享了。
鎖不能共享,同樣可以丟一個標記到redis,由于redis是單線程的,所以也不用擔心redis的線程安全的問題。這個標記就是一個鎖的標記,那樣你就實現了分布式鎖...
我們看回`@RedisLock` 該類,里面有個`expire()`方法
```java
/**
* 過期毫秒數,默認為5000毫秒
*
* @return 鎖的時間
*/
int expire() default 5000;
```
由于網絡穩定、宕機等各種原因,分布式鎖,必須要有過期時間,否則鎖無法釋放的話,會阻塞一片的實例。
## 實現一個簡單的分布式鎖注解
由于自己去實現redis的分布式鎖,是比較困難的問題,還要考慮redis復制,宕機之類的問題,所以我們使用一個比較優秀的開源項目 **redisson**來實現我們的分布式鎖
被`@RedisLock`所注解的方法,會被 `RedisLockAspect` 進行切面管理,代碼如下:
```java
@Around("@annotation(redisLock)")
public Object around(ProceedingJoinPoint joinPoint, RedisLock redisLock) throws Throwable {
String spel = redisLock.key();
String lockName = redisLock.lockName();
// redissonClient 也就是通過redisson 進行對鎖管理
RLock rLock = redissonClient.getLock(getRedisKey(joinPoint,lockName,spel));
rLock.lock(redisLock.expire(),redisLock.timeUnit());
Object result = null;
try {
//執行方法
result = joinPoint.proceed();
} finally {
rLock.unlock();
}
return result;
}
```
## 識別spel表達式
在`@RedisLock(lockName = "insertUser", key = "#appConnect.appId + ':' + #appConnect.bizUserId")`中 `#appConnect.appId` 也僅僅是表示一串字符串而已,而能將其變成表達式,需要一定的轉換`SpelUtil.parse`
```java
/**
* 支持 #p0 參數索引的表達式解析
* @param rootObject 根對象,method 所在的對象
* @param spel 表達式
* @param method ,目標方法
* @param args 方法入參
* @return 解析后的字符串
*/
public static String parse(Object rootObject,String spel, Method method, Object[] args) {
if (StrUtil.isBlank(spel)) {
return StrUtil.EMPTY;
}
//獲取被攔截方法參數名列表(使用Spring支持類庫)
LocalVariableTableParameterNameDiscoverer u =
new LocalVariableTableParameterNameDiscoverer();
String[] paraNameArr = u.getParameterNames(method);
if (ArrayUtil.isEmpty(paraNameArr)) {
return spel;
}
//使用SPEL進行key的解析
ExpressionParser parser = new SpelExpressionParser();
//SPEL上下文
StandardEvaluationContext context = new MethodBasedEvaluationContext(rootObject,method,args,u);
//把方法參數放入SPEL上下文中
for (int i = 0; i < paraNameArr.length; i++) {
context.setVariable(paraNameArr[i], args[i]);
}
return parser.parseExpression(spel).getValue(context, String.class);
}
```
同時我們也害怕redis的key發生沖突,所以會對key加上一些統一的前綴:
redis 鎖的key能夠識別`spel` 表達式,并且不和其他方法的鎖名稱或緩存名稱重復
```java
/**
* 將spel表達式轉換為字符串
* @param joinPoint 切點
* @return redisKey
*/
private String getRedisKey(ProceedingJoinPoint joinPoint,String lockName,String spel) {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method targetMethod = methodSignature.getMethod();
Object target = joinPoint.getTarget();
Object[] arguments = joinPoint.getArgs();
return REDISSON_LOCK_PREFIX + lockName + StrUtil.COLON + SpelUtil.parse(target,spel, targetMethod, arguments);
}
```
- 開發環境準備
- 基本開發手冊
- 項目目錄結構
- 權限管理
- 通用分頁表格
- Swagger文檔
- undertow容器
- 對xss攻擊的防御
- 分布式鎖
- 統一的系統日志
- 統一驗證
- 統一異常處理
- 文件上傳下載
- 一對多、多對多分頁
- 認證與授權
- 從授權開始看源碼
- 自己寫個授權的方法-開源版
- 商城表設計
- 商品信息
- 商品分組
- 購物車
- 訂單
- 地區管理
- 運費模板
- 接口設計
- 必讀
- 購物車的設計
- 訂單設計-確認訂單
- 訂單設計-提交訂單
- 訂單設計-支付
- 生產環境
- nginx安裝與跨域配置
- 安裝mysql
- 安裝redis
- 傳統方式部署項目
- docker
- 使用docker部署商城
- centos jdk安裝
- docker centos 安裝
- Docker Compose 安裝與卸載
- docker 鏡像的基本操作
- docker 容器的基本操作
- 通過yum安裝maven
- 常見問題