1. 程式人生 > 程式設計 >SpringBoot 整合 redis 分散式鎖

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 分散式鎖實戰

專案程式碼結構圖

1570783101690

匯入依賴

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 >

第一次請求結果:

1570783805599

第二次請求結果:

1570783834354

等key過期了請求又恢復正常。

最後

但是這種分散式鎖也存在著缺陷,如果A在setnx成功後,A成功獲取鎖了,也就是鎖已經存到 Redis 裡面了,此時伺服器異常關閉或是重啟,將不會執行我們的業務邏輯,也就不會設定鎖的有效期,這樣的話鎖就不會釋放了,就會產生死鎖。 所以還需要對鎖進行優化,好好學習學習,嘎嘎嘎嘎。