基于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
在Java中实现Redis可以使用Redisson框架。Redisson是一个基于Redis分布式Java对象和服务框架,它提供了一系列的分布式锁实现。 下面是一个使用Redisson实现Redis的示例代码: ```java import org.redisson.Redisson; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.redisson.config.Config; public class RedisLockExample { public static void main(String[] args) { // 创建Redisson配置 Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); // 创建Redisson客户端 RedissonClient redisson = Redisson.create(config); // 获取对象 RLock lock = redisson.getLock("myLock"); try { // 尝试加,最多等待10秒,自动释放时间为30秒 boolean isLocked = lock.tryLock(10, 30, TimeUnit.SECONDS); if (isLocked) { // 成功获取到,执行业务逻辑 System.out.println("获取到,执行业务逻辑"); } else { // 获取失败 System.out.println("获取失败"); } } catch (InterruptedException e) { e.printStackTrace(); } finally { // 释放 lock.unlock(); } // 关闭Redisson客户端 redisson.shutdown(); } } ``` 上述代码中,首先创建了一个Redisson配置对象,并指定了连接的Redis地址。然后通过Redisson.create方法创建了Redisson客户端。接着使用getLock方法获取了一个对象,的名称为"myLock"。在try块中使用tryLock方法尝试加,如果成功获取到,则执行业务逻辑;如果获取失败,则输出获取失败的信息。最后在finally块中释放,并关闭Redisson客户端。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值