SpringBoot 整合 redis 分散式鎖
繼上一篇 SpringBoot 整合 redis 踩坑日誌之後,又學習了 redis 分散式鎖,那為什麼需要分散式鎖?
redis 分散式鎖原理
在傳統單體應用單機部署的情況下,可以使用 Java 併發相關的鎖,如 ReentrantLcok 或 synchronized 進行互斥控制。但是,隨著業務發展的需要,原單體單機部署的系統,漸漸的被部署在多機器多JVM上同時提供服務,這使得原單機部署情況下的併發控制鎖策略失效了,為瞭解決這個問題就需要一種跨JVM的互斥機制來控制共享資源的訪問,這就是分散式鎖要解決的問題。
分散式鎖的實現條件
- 互斥性,和單體應用一樣,要保證任意時刻,只能有一個客戶端持有鎖
- 可靠性,要保證系統的穩定性,不能產生死鎖
- 一致性,要保證鎖只能由加鎖人解鎖,不能產生A的加鎖被B使用者解鎖的情況
分散式鎖的實現
Redis 實現分散式鎖不同的人可能有不同的實現邏輯,但是核心就是下面三個方法。
1.SETNXSETNX key val 當且僅當 key 不存在時,set 一個 key 為 val 的字串,返回1;若 key存在,則什麼都不做,返回0。
2.Expireexpire key timeout 為 key 設定一個超時時間,單位為second,超過這個時間鎖會自動釋放,避免死鎖。
3.Deletedelete key 刪除 key 。
原理圖如下:
redis 分散式鎖實戰
專案程式碼結構圖
匯入依賴
在 pom.xml 中新增 starter-web、starter-aop、starter-data-redis 的依賴
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
複製程式碼
屬性配置
在 application.properites 資原始檔中新增 redis 相關的配置項
server:
port: 1999
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mybatis-plus-test?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
driverClassName: com.mysql.cj.jdbc.Driver
username: root
password: root
redis:
host: 127.0.0.1
port: 6379
timeout: 5000ms
password:
database: 0
jedis:
pool:
max-active: 50
max-wait: 3000ms
max-idle: 20
min-idle: 2
複製程式碼
註解
1、建立一個 CacheLock 註解,屬性配置如下
- prefix: 快取中 key 的字首
- expire: 過期時間,此處預設為 5 秒
- timeUnit: 超時單位,此處預設為秒
- delimiter: key 的分隔符,將不同引數值分割開來
package com.tuhu.twosample.chen.distributed.annotation;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* 鎖的註解
* @author chendesheng
* @create 2019/10/11 16:06
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface CacheLock {
/**
* redis 鎖key的字首
*
* @return redis 鎖key的字首
*/
String prefix() default "";
/**
* 過期秒數,預設為5秒
*
* @return 輪詢鎖的時間
*/
int expire() default 5;
/**
* 超時時間單位
*
* @return 秒
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* <p>Key的分隔符(預設 :)</p>
* <p>生成的Key:N:SO1008:500</p>
*
* @return String
*/
String delimiter() default ":";
}
複製程式碼
2、 key 的生成規則是自己定義的,如果通過表示式語法自己得去寫解析規則還是比較麻煩的,所以依舊是用註解的方式
package com.tuhu.twosample.chen.distributed.annotation;
import java.lang.annotation.*;
/**
* 鎖的引數
* @author chendesheng
* @create 2019/10/11 16:08
*/
@Target({ElementType.PARAMETER,ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface CacheParam {
/**
* 欄位名稱
*
* @return String
*/
String name() default "";
}
複製程式碼
key生成策略
1、介面
package com.tuhu.twosample.chen.distributed.common;
import org.aspectj.lang.ProceedingJoinPoint;
/**
* key生成器
* @author chendesheng
* @create 2019/10/11 16:09
*/
public interface CacheKeyGenerator {
/**
* 獲取AOP引數,生成指定快取Key
*
* @param pjp PJP
* @return 快取KEY
*/
String getLockKey(ProceedingJoinPoint pjp);
}
複製程式碼
2、介面實現
package com.tuhu.twosample.chen.distributed.common;
import com.tuhu.twosample.chen.distributed.annotation.CacheLock;
import com.tuhu.twosample.chen.distributed.annotation.CacheParam;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
/**
* 通過介面注入的方式去寫不同的生成規則
* @author chendesheng
* @create 2019/10/11 16:09
*/
public class LockKeyGenerator implements CacheKeyGenerator {
@Override
public String getLockKey(ProceedingJoinPoint pjp) {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
CacheLock lockAnnotation = method.getAnnotation(CacheLock.class);
final Object[] args = pjp.getArgs();
final Parameter[] parameters = method.getParameters();
StringBuilder builder = new StringBuilder();
//預設解析方法裡面帶 CacheParam 註解的屬性,如果沒有嘗試著解析實體物件中的
for (int i = 0; i < parameters.length; i++) {
final CacheParam annotation = parameters[i].getAnnotation(CacheParam.class);
if (annotation == null) {
continue;
}
builder.append(lockAnnotation.delimiter()).append(args[i]);
}
if (StringUtils.isEmpty(builder.toString())) {
final Annotation[][] parameterAnnotations = method.getParameterAnnotations();
for (int i = 0; i < parameterAnnotations.length; i++) {
final Object object = args[i];
final Field[] fields = object.getClass().getDeclaredFields();
for (Field field : fields) {
final CacheParam annotation = field.getAnnotation(CacheParam.class);
if (annotation == null) {
continue;
}
field.setAccessible(true);
builder.append(lockAnnotation.delimiter()).append(ReflectionUtils.getField(field,object));
}
}
}
return lockAnnotation.prefix() + builder.toString();
}
}
複製程式碼
Lock攔截器(AOP)
熟悉 Redis 的朋友都知道它是執行緒安全的,我們利用它的特性可以很輕鬆的實現一個分散式鎖,如opsForValue().setIfAbsent(key,value)它的作用就是如果快取中沒有當前 Key 則進行快取同時返回 true 反之亦然;當快取後給 key 在設定個過期時間,防止因為系統崩潰而導致鎖遲遲不釋放形成死鎖; 那麼我們是不是可以這樣認為當返回 true 我們認為它獲取到鎖了,在鎖未釋放的時候我們進行異常的丟擲….
package com.tuhu.twosample.chen.distributed.interceptor;
import com.tuhu.twosample.chen.distributed.annotation.CacheLock;
import com.tuhu.twosample.chen.distributed.common.CacheKeyGenerator;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.StringUtils;
import java.lang.reflect.Method;
/**
* @author chendesheng
* @create 2019/10/11 16:11
*/
@Aspect
@Configuration
public class LockMethodInterceptor {
@Autowired
public LockMethodInterceptor(StringRedisTemplate lockRedisTemplate,CacheKeyGenerator cacheKeyGenerator) {
this.lockRedisTemplate = lockRedisTemplate;
this.cacheKeyGenerator = cacheKeyGenerator;
}
private final StringRedisTemplate lockRedisTemplate;
private final CacheKeyGenerator cacheKeyGenerator;
@Around("execution(public * *(..)) && @annotation(com.tuhu.twosample.chen.distributed.annotation.CacheLock)")
public Object interceptor(ProceedingJoinPoint pjp) {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
CacheLock lock = method.getAnnotation(CacheLock.class);
if (StringUtils.isEmpty(lock.prefix())) {
throw new RuntimeException("lock key can't be null...");
}
final String lockKey = cacheKeyGenerator.getLockKey(pjp);
try {
//key不存在才能設定成功
final Boolean success = lockRedisTemplate.opsForValue().setIfAbsent(lockKey,"");
if (success) {
lockRedisTemplate.expire(lockKey,lock.expire(),lock.timeUnit());
} else {
//按理來說 我們應該丟擲一個自定義的 CacheLockException 異常;
throw new RuntimeException("請勿重複請求");
}
try {
return pjp.proceed();
} catch (Throwable throwable) {
throw new RuntimeException("系統異常");
}
} finally {
//如果演示的話需要註釋該程式碼;實際應該放開
// lockRedisTemplate.delete(lockKey);
}
}
}
複製程式碼
控制層
在介面方法上新增 @CacheLock(prefix = "test"),然後動態的值可以加上@CacheParam;生成後的新 key 將被快取起來;(如:該介面 token = 1,那麼最終的 key 值為 test:1,如果多個條件則依次類推)
package com.tuhu.twosample.chen.controller;
import com.tuhu.twosample.chen.distributed.annotation.CacheLock;
import com.tuhu.twosample.chen.distributed.annotation.CacheParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @author chendesheng
* @create 2019/10/11 16:13
*/
@RestController
@RequestMapping("/chen/lock")
@Slf4j
public class LockController {
@CacheLock(prefix = "test")
@GetMapping("/test")
public String query(@CacheParam(name = "token") @RequestParam String token) {
return "success - " + token;
}
}
複製程式碼
主函式
需要注入前面定義好的 CacheKeyGenerator 介面具體實現 ….
package com.tuhu.twosample;
import com.tuhu.twosample.chen.distributed.common.CacheKeyGenerator;
import com.tuhu.twosample.chen.distributed.common.LockKeyGenerator;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
/**
* @author chendesheng
* @since 2019-08-06
*/
@SpringBootApplication
@MapperScan("com.baomidou.mybatisplus.samples.quickstart.mapper")
@MapperScan("com.tuhu.twosample.chen.mapper")
public class TwoSampleApplication {
public static void main(String[] args) {
SpringApplication.run(TwoSampleApplication.class,args);
}
@Bean
public CacheKeyGenerator cacheKeyGenerator() {
return new LockKeyGenerator();
}
}
複製程式碼
測試
啟動專案,在postman中輸入url:<http://localhost:1999/chen/lock/test?token=1 >
第一次請求結果:
第二次請求結果:
等key過期了請求又恢復正常。
最後
但是這種分散式鎖也存在著缺陷,如果A在setnx成功後,A成功獲取鎖了,也就是鎖已經存到 Redis 裡面了,此時伺服器異常關閉或是重啟,將不會執行我們的業務邏輯,也就不會設定鎖的有效期,這樣的話鎖就不會釋放了,就會產生死鎖。 所以還需要對鎖進行優化,好好學習學習,嘎嘎嘎嘎。