欢迎访问悦橙教程(wld5.com),关注java教程。悦橙教程  java问答|  每日更新
页面导航 : > > 文章正文

SpringBoot整合redis实现计数器限流的示例,

来源: javaer 分享于  点击 9947 次 点评:117

SpringBoot整合redis实现计数器限流的示例,


目录
  • 1.引入依赖
  • 2.代码示例
    • 2.1 基本代码
    • 2.2 使用redis事务
      • 2.2.1 SessionCallback(不推荐)
      • 2.2.2 分布式锁(推荐)
    • 2.3 使用Lua脚本(推荐)

    使用redis的自增对接口进行限流

    1.引入依赖

    <!-- springboot已集成,不需要再引入版本 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    

    2.代码示例

    2.1 基本代码

    我这里使用使用了手机号和一些其他的字符串组成了redis的key,你可以自定义自己的key.

    private void validRateBasic (String phone) {
            String key = "LIMIT:RATE:" + phone;
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            redisTemplate.setValueSerializer(new StringRedisSerializer());
            try {
                String num = (String) redisTemplate.opsForValue().get(key);
                if (ObjectUtil.isNull(num)) {
                    redisTemplate.opsForValue().set(key, "1", 60, TimeUnit.SECONDS);
                } else if (Integer.parseInt(num) >= 20) {
                    Long expire = redisTemplate.getExpire(key);
                    throw new CheckedException("操作频繁,请" + expire + "s后再试");
                } else {
                    redisTemplate.opsForValue().increment(key);
                }
            } catch (Exception e) {
                if (e instanceof CheckedException) {
                    throw new CheckedException(e.getMessage());
                } else {
                    log.info("校验上传速率失败,error:{}",e);
                    throw new CheckedException("操作失败,请稍后再试");
                }
            }
    
        }
    

    这段代码实现了同一个接口中,同一个手机号在60s内只能访问20次,虽然redis是单线程的,但在高并发情况下,这段代码仍有并发问题。 在获取访问次数和增加访问次数之间,访问次数可能已经被其他线程修改 。如果你对多出来的一两次请求要求不高,那这个限制基本符合需求。
    在redis中,我们可以使用lua脚本和redis事务来保证操作的原子性。

    2.2 使用redis事务

    2.2.1 SessionCallback(不推荐)

    有人使用redisTemplate.setEnableTransactionSupport(true),使用redisTemplate支持事务,但这样可能存在已下几种问题:

    • 如果你在分布式环境中使用Redis,事务支持可能会有问题,因为Redis的事务模型是乐观锁,如果在事务中的操作被其他实例修改,那么事务就会失败。在高并发场景中,这可能会导致大量的事务失败。
    • 使RedisTemplate支持事务会导致所有的Redis操作都在事务中执行,这可能会降低性能,特别是在需要执行大量Redis操作的情况下。
    • 这个设置将影响所有使用这个RedisTemplate实例的代码,所以需要确保所有相关的代码都能正确地处理在事务中的Redis操作。

    这里使用的是Spring Data Redis提供的会话回调(SessionCallback)接口。它可以让我们在一个Redis连接中执行多个操作,并保持原子性。

    private void validRate(String phone) {
            String key = "LIMIT:RATE:" + phone;SpringBoot整合redis实现计数器限流
            int retryTimes = 0;
            // 失败重试五次
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            redisTemplate.setValueSerializer(new StringRedisSerializer());
            while(retryTimes < 6) {
                retryTimes++;
                try {
                    // 在事务之外获取这个键的值
                    String num = (String) redisTemplate.opsForValue().get(key);
    
                    // 使用SessionCallback进行原子性操作
                    SessionCallback<Object> sessionCallback = new SessionCallback<Object>() {
                        @Override
                        public Object execute(RedisOperations operations) throws DataAccessException {
                            operations.watch(key);
                            operations.multi();
                            // 在事务内部再次检查这个键的值
                            String currentNum = (String) operations.opsForValue().get(key);
                            if (num == null ? currentNum != null : !num.equals(currentNum)) {
                                // 这个键的值被修改了,所以取消这个事务
                                operations.discard();
                                return null;
                            }
                            if (ObjectUtil.isNull(num)) {
                                operations.opsForValue().set(key, "1", 60, TimeUnit.SECONDS);
                            } else if (Integer.parseInt(num) >= 5) {
                                Long expire = operations.getExpire(key);
                                throw new CheckedException("操作频繁,请" + expire + "s后再试");
                            } else {
                                operations.opsForValue().increment(key);
                            }
                            // 提交事务并返回结果
                            return operations.exec();
                        }
                    };
    
                    // 执行SessionCallback
                    List<Object> results = (List<Object>) redisTemplate.execute(sessionCallback);
                    if (CollectionUtils.isEmpty(results)) {
                        // 如果事务执行失败,重新尝试事务
                        log.info("重试");
                        continue;
                    }
                    return;
                } catch (Exception e) {
                    // 在重试的情况下捕获任何异常
                    if (retryTimes >= 5) {
                        throw new CheckedException("操作频繁,请稍后再试");
                    }
                }
            }
        }
    

    这一段代码看起来没啥毛病,一运行你会发现 String num = (String) operations.opsForValue().get(key);一直是null。这是因为在redis事务中,事务中的所有命令都会被放在队列中,等到exec命令被调用时才会一次性执行。redis的事务在某些方面是不如关系型数据库的:

    • 无隔离性:redis的事务没有隔离性,在事务开始(multi命令执行)之后,其他的客户端仍然可以对事务中的键进行读写操作,这可能会影响到事务的结果。
    • 无原子读:无法读取到自己事务未提交的数据,也无法读取到其他事务写入的数据。如上面代码,事务开始后的get命令返回的是null,而不是最新数据。
    • 无回滚:一旦一个事务被提交(exec命令执行),事务中的所有操作都会被执行,即使其中某些操作失败了,其他的操作也不会被回滚。
    • 无锁:redis事务并不提供锁,或者说redis并没有锁的概念,和无隔离性造的结果是一样的。

    2.2.2 分布式锁(推荐)

    分布式锁已经有很多成熟的框架了和很多优秀的博客了,这里就不赘述了,有空会补充一篇。

    2.3 使用Lua脚本(推荐)

    Lua脚本在执行时是原子性的:当脚本正在运行的时候,不会有其他的脚本或Redis命令被执行。

    private void validRateLua(String phone) {
            String key = "LIMIT:RATE:" + phone;
            int retryTimes = 0;
            // 创建Lua脚本,返回新的计数值
            String luaScript =
                    "local num = redis.call('GET', KEYS[1]);" +
                            "if num == false then " +
                            "   redis.call('SET', KEYS[1], ARGV[1], 'EX', ARGV[2]);" +
                            "   return ARGV[1];" +
                            "elseif tonumber(num) <= tonumber(ARGV[3]) then " +
                            "   local newNum = redis.call('INCR', KEYS[1]);" +
                            "   return newNum;" +
                            "else " +
                            "   return num;" +
                            "end;";
            RedisScript<String> redisScript = new DefaultRedisScript<>(luaScript, String.class);
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            redisTemplate.setValueSerializer(new StringRedisSerializer());
            while(retryTimes < 5) {
                retryTimes++;
                try {
                    // 执行Lua脚本
                    String num = (String) redisTemplate.execute(redisScript, Collections.singletonList(key), "1", "60","5");
                    if (num != null && Integer.parseInt(num) > 5) {
                        Long expire = redisTemplate.getExpire(key);
                        throw new CheckedException("操作频繁,请" + expire + "s后再试");
                    }
    
                    return;
                } catch (Exception e) {
                    if (e instanceof CheckedException) {
                        throw new CheckedException(e.getMessage());
                    } else {
                        // 在重试的情况下捕获任何异常
                        // 有需要的可以加入指数退避、最大重试时间等
                        if (retryTimes >= 5) {
                            log.error("上传失败,error:{}",e);
                            throw new CheckedException("操作频繁,请稍后再试");
                        }
                    }
                }
            }
        }
    

    执行Lua脚本有几点需要注意:

    • lua脚本会阻塞Redis的所有操作,需要尽量保证Lua脚本的执行时间短,以免影响redis的性能.
    • lua脚本一旦被执行,它就会被加载到内存中,即使没被执行也会持续保存在内存中,这样设计的目的是方便快速执行,避免每次执行脚本都要重新加载
    • lua脚本一般都很小,但是如果你有大量的lua脚本长时间保存在内存中,被频繁的加载和执行,就会占用大量的内存。这个问题可以通过script命令和LUA-EVAL-NOLOAD配置选项来解决。

    到此这篇关于SpringBoot整合redis实现计数器限流的示例的文章就介绍到这了,更多相关SpringBoot redis计数器限流内容请搜索3672js教程以前的文章或继续浏览下面的相关文章希望大家以后多多支持3672js教程! 

    您可能感兴趣的文章:
    • Springboot+Redis实现API接口防刷限流的项目实践
    • SpringBoot整合Redis并且用Redis实现限流的方法 附Redis解压包
    • 基于SpringBoot+Redis实现一个简单的限流器
    • SpringBoot使用Redis对用户IP进行接口限流的项目实践
    • SpringBoot使用Redis对用户IP进行接口限流的示例详解
    • SpringBoot Redis用注释实现接口限流详解
    • 使用SpringBoot + Redis 实现接口限流的方式
    • SpringBoot中使用Redis对接口进行限流的实现
    • springboot+redis 实现分布式限流令牌桶的示例代码
    相关栏目:

    用户点评