Spring Boot (33) 分布式鎖
阿新 • • 發佈:2018-06-15
ted book red 釋放 gre prefix color catch pan
上一篇中使用的Guava Cache,如果在集群中就不可以用了,需要借助Redis、Zookeeper之類的中間件實現分布式鎖。
導入依賴
在pom.xml中需要添加的依賴包:stater-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>
屬性配置
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生成策略(接口)
package com.spring.boot.annotation; import org.aspectj.lang.ProceedingJoinPoint; public interface CacheKeyGenerator { String getLockKey(ProceedingJoinPoint pjp); }
Key生成策略(實現)
主要是解析帶CacheLock註解的屬性,獲取對應的屬性值,生成一個全新的緩存Key
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)
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秒內再次請求
Spring Boot (33) 分布式鎖