java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot redis计数器限流

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

作者:颇有几分姿色

本文主要介绍了SpringBoot整合redis实现计数器限流的示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

使用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支持事务,但这样可能存在已下几种问题:

这里使用的是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的事务在某些方面是不如关系型数据库的:

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脚本有几点需要注意:

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

您可能感兴趣的文章:
阅读全文