上一篇中使用的Guava Cache,若是在集羣中就不能夠用了,須要藉助Redis、Zookeeper之類的中間件實現分佈式鎖。java
導入依賴web
在pom.xml中須要添加的依賴包:stater-web、starter-aop、starter-data-redisredis
<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>
屬性配置spring
spring: redis: host: 10.211.55.5 #redis服務器地址 timeout: 10000 #超時時間 database: 0 #0-15 16個庫 默認0 lettuce: pool: max-active: 8 #最大鏈接數 max-wait: -1 #默認-1 最大鏈接阻塞等待時間 max-idle: 8 #最大空閒鏈接 默認8 min-idle: 0 #最小空閒鏈接
CacheLock註解緩存
package com.spring.boot.annotation; import java.lang.annotation.*; import java.util.concurrent.TimeUnit; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface CacheLock { /** * redis 鎖的前綴 * @return */ String prefix() default ""; /** * 過時時間 * @return */ int expire() default 5; /** * 超時時間單位 * @return */ TimeUnit timeUnit() default TimeUnit.SECONDS; /** * 能夠的分隔符(默認:) * @return */ String delimiter() default ":"; }
CacheParam註解服務器
package com.spring.boot.annotation; import java.lang.annotation.*; @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface CacheParam { /** * 字段名稱 * * @return String */ String name() default ""; }
Key生成策略(接口)app
package com.spring.boot.annotation; import org.aspectj.lang.ProceedingJoinPoint; public interface CacheKeyGenerator { String getLockKey(ProceedingJoinPoint pjp); }
Key生成策略(實現)分佈式
主要是解析帶CacheLock註解的屬性,獲取對應的屬性值,生成一個全新的緩存Keyide
package com.spring.boot.annotation; import io.lettuce.core.dynamic.support.ReflectionUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.reflect.MethodSignature; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Parameter; 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(); // TODO 默認解析方法裏面帶 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 (builder == null || 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)spring-boot
opsForValue().setIfAbsent(key,value)若是緩存中沒有當前key則進行緩存,同時返回true,不然 返回false。當緩存後給key在設置個過時時間,防止由於系統崩潰而致使鎖遲遲不釋放造成死鎖。 咱們就能夠這樣人物當返回true它獲取到鎖了,在所未釋放的時候咱們進行異常的拋出
package com.spring.boot.annotation; import org.aspectj.lang.annotation.Aspect; import org.springframework.context.annotation.Configuration; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.connection.RedisStringCommands; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.types.Expiration; import org.springframework.util.StringUtils; import java.lang.reflect.Method; @Aspect @Configuration public class LockMethodInterceptor { private final StringRedisTemplate lockRedisTemplate; private final CacheKeyGenerator cacheKeyGenerator; @Autowired public LockMethodInterceptor(StringRedisTemplate lockRedisTemplate, CacheKeyGenerator cacheKeyGenerator) { this.lockRedisTemplate = lockRedisTemplate; this.cacheKeyGenerator = cacheKeyGenerator; } @Around("execution(public * *(..)) && @annotation(com.spring.boot.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 don't null..."); } final String lockKey = cacheKeyGenerator.getLockKey(pjp); try { // 採用原生 API 來實現分佈式鎖 final Boolean success = lockRedisTemplate.execute((RedisCallback<Boolean>) connection -> connection.set(lockKey.getBytes(), new byte[0], Expiration.from(lock.expire(), lock.timeUnit()), RedisStringCommands.SetOption.SET_IF_ABSENT)); if (!success) { // TODO 按理來講 咱們應該拋出一個自定義的 CacheLockException 異常; throw new RuntimeException("請勿重複請求"); } try { return pjp.proceed(); } catch (Throwable throwable) { throw new RuntimeException("系統異常"); } } finally { // TODO 若是演示的話須要註釋該代碼;實際應該放開 // lockRedisTemplate.delete(lockKey); } } }
控制器
package com.spring.boot.controller; import com.spring.boot.annotation.CacheLock; import com.spring.boot.annotation.CacheParam; 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; @RestController @RequestMapping("/books") public class BookController { @CacheLock(prefix = "books") @GetMapping public String query(@CacheParam(name = "token") @RequestParam String token){ return "success - " + token; } }
還要在啓動類中注入CacheKeyGenerator接口具體實現
@SpringBootApplication public class BootApplication{ public static void main(String[] args) { SpringApplication.run(BootApplication.class,args); } @Bean public CacheKeyGenerator cacheKeyGenerator() { return new LockKeyGenerator(); }
測試:
http://localhost:8088/books?token=1
5秒內再次請求