1. 程式人生 > >Spring Boot (33) 分布式鎖

Spring Boot (33) 分布式鎖

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) 分布式鎖