基于Redis实现分布式锁、限流操作(基于SpringBoot)的实现

基于Redis实现分布式锁、限流操作——基于SpringBoot实现

  • 本文总结了一种利用Redis实现分布式锁、限流的较优雅的实现方式
  • 本文原理介绍较为通俗,希望能帮到有需要的人
  • 本文的demo地址:https://gitee.com/rederxu/lock_distributed.git

一、本文基本实现

  • 利用redis的key是否存在判断锁是否存在
  • 利用redis的increment/decrement方法进行计数,从而实现限流
  • 利用注解,对需要锁定/限流的方法进行配置(用起来超简单!!!)
  • 利用SpringBoot的拦截器,在访问前判断并加锁,访问完成后释放锁
  • 利用@ControllerAdvice捕获全局异常和终止访问

二、为什么选redis

  • 由于redis的原子性(即其操作属于最基本的操作,要么执行要么不执行,要么全部成功,要么全部不成功),因此,用来计数、记录值是否存在具有较高优势。
  • 此外,redis作为效率极高的程序外应用,能有效地独立保存数据,即使是多个服务,也能保持数据一致性(进而实现分布式控制)
  • 本文总结的分布式锁、限流操作都基于redis实现
  • 分布式锁:利用redis的key-value存储结构,判断key是否存在,key在即锁在
  • 限流操作:利用redis的原子性,通过increment/decrement方法进行计数,超出则抛出异常,终止访问。

三、Spring Boot的实现方式

  • 这里的Spring Boot可以理解为微服务中的一个子服务
  • 以下实现是编码过程,运行效果见第四部分,核心实现见本节第3、5部分

1. Reids配置和服务实现

1.1 redis配置
  • properties中的配置
server.port=8080
#redis
spring.redis.host=localhost
spring.redis.database=0
spring.redis.port=6379
  • Java代码配置:
package tech.xujian.lock.distributed.lockdistributed.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        // 1.创建一个redis模板对象
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);// 设置连接器

        // 2.配置redis模板的普通键值对的序列化策略
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class));

        // 3.配置redis模板的Hash键值对的序列化策略
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new StringRedisSerializer());

        // 4.返回该redis模板对象,加入到spring容器中
        return redisTemplate;
    }
}

1.2 redis服务实现
  • 包含redis的常见基本操作
1.3 RedisService
package tech.xujian.lock.distributed.lockdistributed.service;

public interface RedisService {
    void set(String k,String value);
    void set(String k,String value,int expireMinute);
    String get(String k);

    long getLong(String k);

    long increment(String k);
    long decrement(String k);

    String getAndDelete(String k);
}

1.4 RedisServiceImpl
package tech.xujian.lock.distributed.lockdistributed.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import tech.xujian.lock.distributed.lockdistributed.service.RedisService;

import java.util.concurrent.TimeUnit;

@Service
public class RedisServiceImpl implements RedisService {

    @Autowired
    RedisTemplate<String,String> redisTemplate;

    @Override
    public void set(String k, String value) {
        redisTemplate.opsForValue().set(k,value);
    }

    @Override
    public void set(String k, String value, int expireMinute) {
        redisTemplate.opsForValue().set(k,value,expireMinute, TimeUnit.MINUTES);
    }

    @Override
    public String get(String k) {
        return redisTemplate.opsForValue().get(k);
    }

    @Override
    public long getLong(String k) {
        String str = get(k);
        return str == null ? 0 : Long.parseLong(str);
    }

    @Override
    public long increment(String k) {
        return redisTemplate.opsForValue().increment(k);
    }

    @Override
    public String getAndDelete(String k) {
        return redisTemplate.opsForValue().getAndDelete(k);
    }

    @Override
    public long decrement(String k) {
        return redisTemplate.opsForValue().decrement(k);
    }
}

2 注解实现

2.1 锁类型枚举
package tech.xujian.lock.distributed.lockdistributed.annotation;

public enum RedisLockType {
    IP(1),
    USERNAME(2),
    COUNT(3),
    ANY(4);

    private int value;

    RedisLockType(int value){
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}
2.2 注解声明
package tech.xujian.lock.distributed.lockdistributed.annotation;

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

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLock {
    RedisLockType type() default RedisLockType.IP;
}

3 拦截去实现

3.1拦截器配置
package tech.xujian.lock.distributed.lockdistributed.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import tech.xujian.lock.distributed.lockdistributed.interceptor.RedisLockInterceptor;

@Component
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    RedisLockInterceptor redisLockInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(redisLockInterceptor).addPathPatterns("/**");
    }
}
3.2拦截器实现
  • 实现过程不赘述,直接看代码
package tech.xujian.lock.distributed.lockdistributed.interceptor;

import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.ServletUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import tech.xujian.lock.distributed.lockdistributed.annotation.RedisLock;
import tech.xujian.lock.distributed.lockdistributed.annotation.RedisLockType;
import tech.xujian.lock.distributed.lockdistributed.service.RedisService;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
@Slf4j
public class RedisLockInterceptor implements HandlerInterceptor {

    @Autowired
    RedisService redisService;

    private static final String CACHE_KEY_REDIS_LOCK = "system:distributed:lock:";

    //访问前拦截枷锁,锁定时抛出异常,由全局异常捕获
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if(handler instanceof HandlerMethod){
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            RedisLock redisLock = handlerMethod.getMethodAnnotation(RedisLock.class);
            if(redisLock == null){
                return HandlerInterceptor.super.preHandle(request, response, handler);
            }
            //需要进行锁判断
            switch (redisLock.type()){
                case IP:
                    doIpLock(request);
                    break;
                case USERNAME:
                    //TODO: 从request中通过header等读取到用户信息,然后参考doIpLock实现
                    //略
                    break;
                case COUNT:
                    //某个方法同时访问的人数限制的实现
                    doCountLock(handlerMethod);
                    break;
                case ANY:
                    //TODO: 该方法同时只允许一个人访问,实现方法与doCountLock一致
                    //略
                    break;
            }
        }
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

    //访问结束后释放锁
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

        if(handler instanceof HandlerMethod){
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            RedisLock redisLock = handlerMethod.getMethodAnnotation(RedisLock.class);
            if(redisLock == null){
                HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
                return;
            }
            //需要进行锁判断
            switch (redisLock.type()){
                case IP:
                    releaseIpLock(request);
                    break;
                case USERNAME:
                    //TODO: 略,释放锁
                    break;
                case COUNT:
                    //某个方法同时访问的人数限制的实现
                    releaseCountLock(handlerMethod);
                    break;
                case ANY:
                    //TODO: 略,释放锁
                    break;
            }
        }
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }

    //访问数量限制lock
    private void doCountLock(HandlerMethod handlerMethod){
        //根据方法名进行锁定
        String redisKey = CACHE_KEY_REDIS_LOCK + "count:" + handlerMethod.getMethod().getDeclaringClass() + ":" + handlerMethod.getMethod().toGenericString();
        redisKey = redisKey.replaceAll(" ","");
        log.info(redisKey);
        long countNow = redisService.getLong(redisKey);
        log.info("当前方法访问人数:" + countNow);
        //设限制100人,也可以考虑在注解中设置
        Assert.isTrue(countNow < 10,"系统拥堵,请稍后重试!当前访问人数:" + countNow);

        redisService.increment(redisKey);
    }

    private void releaseCountLock(HandlerMethod handlerMethod) {
        String redisKey = CACHE_KEY_REDIS_LOCK + "count:" + handlerMethod.getMethod().getDeclaringClass() + ":" + handlerMethod.getMethod().toGenericString();
        redisKey = redisKey.replaceAll(" ","");
        long countNow = redisService.decrement(redisKey);
        log.info("当前方法访问人数:" + countNow);
    }

    //ip判断和lock
    private void doIpLock(HttpServletRequest request){
        //如果是IP锁,则到redis中读取是否已经存在key
        String ip = ServletUtil.getClientIP(request);
        String redisKey = CACHE_KEY_REDIS_LOCK + "ip:" + ip;
        String value = redisService.get(redisKey);
        if(StrUtil.isEmpty(value)){
            //redis自然过期时间为1分钟,即为在一分钟内完成的请求会自动解锁,也可以考虑设置得更短
            redisService.set(redisKey,ip,1);
            return;
        }else{
            throw new RuntimeException("操作太快,请稍后重试");
        }
    }


    //释放锁
    private void releaseIpLock(HttpServletRequest request) {
        String ip = ServletUtil.getClientIP(request);
        String redisKey = CACHE_KEY_REDIS_LOCK + "ip:" + ip;
        redisService.getAndDelete(redisKey);
    }

}

4 全局异常拦截

  • 这里是一个简单的实现
package tech.xujian.lock.distributed.lockdistributed.exception;

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

@ControllerAdvice
public class RedisLockExceptionHandler {
    @ExceptionHandler(value =Exception.class)
    @ResponseBody
    public String exceptionHandler(Exception e){
        e.printStackTrace();
        return e.getMessage();
    }
}

5 Controller上进行注解

  • 示例如下,一看就懂
  • 需要加锁/限流的方法,只需要添加一个注解就像了
@RestController
@RequestMapping("/lock")
public class LockController {

    @Autowired
    RedisService redisService;

    @RedisLock(type = RedisLockType.IP)
    @GetMapping("/test/ip")
    public String lockTest() throws InterruptedException {
        Thread.sleep(20000);
        return "succeed.";
    }

    @RedisLock(type = RedisLockType.COUNT)
    @GetMapping("/test/count")
    public String testCount() throws InterruptedException {
        Thread.sleep(20000);
        return "succeed.";
    }
}

四、运行效果

  • 上文controller中使用sleep使接口卡顿20秒,用来示意接口访问需要时间

1. 正常访问

  • 访问中
    在这里插入图片描述
  • 访问结束后
    在这里插入图片描述

2. 锁

  • 如果在访问的时候再次发起访问,则提示错误(前者访问继续执行)
    在这里插入图片描述

3. 限流

  • 超出流量限制时:
    在这里插入图片描述
  • 本文的demo地址:https://gitee.com/rederxu/lock_distributed.git
posted @   成旭元  阅读(83)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示