1. 程式人生 > >SpringBoot、Redis、Jedis(JedisPool) 分散式鎖、分散式限流 詳解

SpringBoot、Redis、Jedis(JedisPool) 分散式鎖、分散式限流 詳解

前言:網上針對基於Redis的分散式鎖,分散式限流的教程很多,也很全面,但是大多重點著墨於分散式鎖和限流的實現細節,筆者閱讀完之後,可以很好的梳理出 相應的邏輯,但是具體操作時,卻發現缺少了Jedis連線池的部分細節,導致仍然要花點時間去研究下,所以 筆者想寫一篇Blog從頭至尾介紹 Jedis配置、分散式鎖、分散式限流的實現細節,目的在於 讓讀者僅靠一篇Blog就可以實操基於Redis的分散式鎖和分散式限流(部分)。

一、Jedis配置

maven:  Jedis和Lombk

<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.2</version>
            <scope>provided</scope>
        </dependency>
<!--ExceptionUtils.getFullStackTrace(e)-->
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
            <scope>compile</scope>
        </dependency>

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
            <version>2.0.6.RELEASE</version>
        </dependency>

yml配置:

jedis:
  pool:
    host: 127.0.0.1
    port: 6379
    config:
      maxTotal: 50  #最大連線數
      maxIdle: 5    #最大空閒連線數
      maxWaitMillis: 3000  #獲取連線時的最大等待時間
      password:

配置讀取類:

package com.example.redislock;

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

/**
 * @author fandong
 * @create 2018/10/24
 */
@Configuration
@Data   
public class JedisPoolConfigProperties {

    @Value("${jedis.pool.host}")
    private String host;
    @Value("${jedis.pool.port}")
    private Integer port;
    @Value("${jedis.pool.config.maxTotal}")
    private Integer maxTotal;
    @Value("${jedis.pool.config.maxIdle}")
    private Integer maxIdle;
    @Value("${jedis.pool.config.maxWaitMillis}")
    private Long maxWaitMillis;
    @Value("${jedis.pool.config.password}")
    private String password;

    public JedisPoolConfigProperties() {
    }
}

JedisPool配置

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Protocol;

/**
 * @author fandong
 * @create 2018/10/24
 */
@Configuration
public class RedisConfig {

    @Autowired
    private JedisPoolConfigProperties jedisPoolConfigProperties;

    @Bean
    public JedisPool getJedisPool(){
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxIdle(jedisPoolConfigProperties.getMaxIdle());
        jedisPoolConfig.setMaxTotal(jedisPoolConfigProperties.getMaxTotal());
        jedisPoolConfig.setMaxWaitMillis(jedisPoolConfigProperties.getMaxWaitMillis());
        return new JedisPool(jedisPoolConfig, jedisPoolConfigProperties.getHost(), jedisPoolConfigProperties.getPort(), Protocol.DEFAULT_TIMEOUT);
    }
}

二、基於Redis的分散式鎖


import redis.clients.jedis.Jedis;

import java.util.Collections;

/**
 * @author fandong
 * @create 2018/10/24
 * 基於redis的分散式鎖
 */
public class RedisLockTool {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    /**
     * ms
     */
    private static final String PX_EXPIRE_TIME = "PX";
    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 嘗試獲取分散式鎖  利用redis set值的 NX引數
     * @param jedis reids客戶端
     * @param lockKey 鎖
     * @param requestId 鎖擁有者標識
     * @param expireTime 超期時間 ms為單位
     * @return
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime){
        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, PX_EXPIRE_TIME, expireTime);
        return LOCK_SUCCESS.equals(result);
    }

    /**
     * 釋放分散式鎖  利用 LUA指令碼保證操作的原子性(Redis單程序單執行緒並保證執行LUA指令碼時不執行其它命令)
     * @param jedis redis客戶端
     * @param lockKey 鎖
     * @param requestId 鎖擁有者標識
     * @return
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId){
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object res = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
        return RELEASE_SUCCESS.equals(res);
    }



}

實際使用程式碼 帶資源的try語句

@Autowired
private JedisPool jedisPool;

try (Jedis jedis = jedisPool.getResource()) {
            res = RedisLockTool.tryGetDistributedLock(jedis, lockKey, requestId, expireTime);
            Thread.sleep(2000);
        } catch (Exception e) {
            logger.error(ExceptionUtils.getFullStackTrace(e));
        }

三、基於Redis的分散式限流


import redis.clients.jedis.Jedis;

import java.util.Collections;

/**
 * 同樣利用 Redis+LUA的組合  可以對 任意方法限制一秒內 任意次數的訪問
 * 以 指定的字首和當前時間的秒數組合為 KEY 
 * @author fandong
 * @create 2018/10/25
 */
public class RedisLimitTool {

    private static final String LUA_LIMIT_SCRIPT = "local key = KEYS[1]\n" +
            "local limit = tonumber(ARGV[1])\n" +
            "local current = tonumber(redis.call('get', key) or \"0\")\n" +
            "if current + 1 > limit then\n" +
            "   return 0\n" +
            "else\n" +
            "   redis.call(\"INCRBY\", key,\"1\")\n" +
            "   redis.call(\"expire\", key,\"2\")\n" +
            "   return 1\n" +
            "end";

    private static final Long SUCCESS_CODE = 1L;

    public static Boolean limit(Jedis jedis, String keyPrefix, String limit){
        String key = keyPrefix + ":" + System.currentTimeMillis() / 1000;
        Long res =(Long) jedis.eval(LUA_LIMIT_SCRIPT, Collections.singletonList(key), Collections.singletonList(limit));
        return SUCCESS_CODE.equals(res);
    }
}

實際使用時 使用 AOP剝離出 限流邏輯:

自定義註解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author fandong
 * @create 2018/10/25
 */

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RedisLimiter {

    String keyPrefix() default "";
    String limit() default  "";
}

Aspect:

import org.apache.commons.lang.exception.ExceptionUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

/**
 * @author fandong
 * @create 2018/10/25
 */
@Component
@Aspect
public class RedisLimitAspect {

    @Autowired
    private JedisPool jedisPool;

    private final Logger logger = LoggerFactory.getLogger(this.getClass().getName());

    @Around("execution(* com.example.redislock..*(..)) && @annotation(redisLimiter)")
    public Object redisLimiter(ProceedingJoinPoint proceedingJoinPoint, RedisLimiter redisLimiter){
        try(Jedis jedis = jedisPool.getResource()) {
            if (RedisLimitTool.limit(jedis, redisLimiter.keyPrefix(), redisLimiter.limit())){
                return proceedingJoinPoint.proceed();
            }else {
                logger.error("限流:" + redisLimiter.keyPrefix());
                return null;
            }
        } catch (Throwable throwable) {
            logger.error(ExceptionUtils.getFullStackTrace(throwable));
        }
        return null;
    }
}

實際使用:

@RedisLimiter(keyPrefix = "testLimit", limit = "10")
    public Object testLimit(){
        return null;
    }

四、測試

使用 Jmeter進行壓測,實際結果符合預期。

歡迎指正。