SpringBoot 集成 redis 分佈式鎖

繼上一篇 SpringBoot 整合 redis 踩坑日誌以後,又學習了 redis 分佈式鎖,那爲何須要分佈式鎖?java

redis 分佈式鎖原理

在傳統單體應用單機部署的狀況下,可使用 Java 併發相關的鎖,如 ReentrantLcok 或 synchronized 進行互斥控制。可是,隨着業務發展的須要,原單體單機部署的系統,漸漸的被部署在多機器多JVM上同時提供服務,這使得原單機部署狀況下的併發控制鎖策略失效了,爲了解決這個問題就須要一種跨JVM的互斥機制來控制共享資源的訪問,這就是分佈式鎖要解決的問題。mysql

分佈式鎖的實現條件

  • 互斥性,和單體應用同樣,要保證任意時刻,只能有一個客戶端持有鎖
  • 可靠性,要保證系統的穩定性,不能產生死鎖
  • 一致性,要保證鎖只能由加鎖人解鎖,不能產生A的加鎖被B用戶解鎖的狀況

分佈式鎖的實現

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

原理圖以下:緩存

分佈式鎖

redis 分佈式鎖實戰

項目代碼結構圖安全

1570783101690

導入依賴

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 註解,屬性配置以下

  • 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 ":";
    
    }
    
複製代碼

二、 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生成策略

一、接口

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();
    }


}
複製代碼

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 裏面了,此時服務器異常關閉或是重啓,將不會執行咱們的業務邏輯,也就不會設置鎖的有效期,這樣的話鎖就不會釋放了,就會產生死鎖。 因此還須要對鎖進行優化,好好學習學習,嘎嘎嘎嘎。

相關文章
相關標籤/搜索