繼上一篇 SpringBoot 整合 redis 踩坑日誌以後,又學習了 redis 分佈式鎖,那爲何須要分佈式鎖?java
在傳統單體應用單機部署的狀況下,可使用 Java 併發相關的鎖,如 ReentrantLcok 或 synchronized 進行互斥控制。可是,隨着業務發展的須要,原單體單機部署的系統,漸漸的被部署在多機器多JVM上同時提供服務,這使得原單機部署狀況下的併發控制鎖策略失效了,爲了解決這個問題就須要一種跨JVM的互斥機制來控制共享資源的訪問,這就是分佈式鎖要解決的問題。mysql
Redis 實現分佈式鎖不一樣的人可能有不一樣的實現邏輯,可是核心就是下面三個方法。web
1.SETNXSETNX key val 當且僅當 key 不存在時,set 一個 key 爲 val 的字符串,返回1;若 key存在,則什麼都不作,返回0。redis
2.Expireexpire key timeout 爲 key 設置一個超時時間,單位爲second,超過這個時間鎖會自動釋放,避免死鎖。spring
3.Deletedelete key 刪除 key 。sql
原理圖以下:緩存
項目代碼結構圖安全
在 pom.xml 中添加 starter-web、starter-aop、starter-data-redis 的依賴bash
<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
複製代碼
一、建立一個 CacheLock 註解,屬性配置以下
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 ":";
}
複製代碼
二、 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 "";
}
複製代碼
一、接口
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);
}
複製代碼
二、接口實現
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();
}
}
複製代碼
熟悉 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 裏面了,此時服務器異常關閉或是重啓,將不會執行咱們的業務邏輯,也就不會設置鎖的有效期,這樣的話鎖就不會釋放了,就會產生死鎖。 因此還須要對鎖進行優化,好好學習學習,嘎嘎嘎嘎。