前言

什么是increment?

Redis 的INCR 命令将key中存储的数字值递增。如果key不存在,那么key的值会先被初始化为0,然后在执行INhttps和http的区别CR操作。如https协议果值包含错误的类型,或字符串类型的值不能表测试智商示为数字,那么返回一个错误。本操作的值限制在 64 位(bit)有符号数字表接口测试面试题示之内。 [1]

使用场景

  1. id自增生成,且满足并发要求
  2. 使用计数的特性来防止重复提交的请求
  3. 记录用户点击量的
  4. 并发请求场景进行限流

实战

开发环境

  • sprspringing-boot 版本2.1.6.RELEASE
  • spring-boot-starter-data-redis 版本2.1.6.RELEASE

源码部分

//接口 ValueOperations 代码
/**
 * 将 {@code key} 下存储为字符串值的整数值加一。
 *
 * @param key must not be {@literal null}.
 * @return {@literal null} when used in pipeline / transaction.
 * @since 2.1
 * @see <a href="http://redis.io/commands/incr">Redis Documentation: INCR</a>
 */
@Nullable
Long increment(K key);
/**
 *  {@code key} 下的字符串值按照 {@code delta} 的整数值来进行递增。
 *
 * @param key must not be {@literal null}.
 * @param delta 递增值
 * @return {@literal null} when used in pipeline / transaction.
 * @see <a href="http://redis.io/commands/incrby">Redis Documentation: INCRBY</a>
 */
@Nullable
Long increment(K key, long delta);
/**
 * {@code key} 下的字符串值按照 {@code delta} 的浮点值来进行递增。
 *
 * @param key must not be {@literal null}.
 * @param delta
 * @return {@literal null} when used in pipeline / transaction.
 * @see <a href="http://redis.io/commands/incrbyfloat">Redis Documentation: INCRBYFLOAT</a>
 */
@Nullable
Double increment(K key, double delta);
//class DefaultValueOperations实现
@Override
public Long increment(K key) {
    byte[] rawKey = rawKey(key);
    return execute(connection -> connection.incr(rawKey), true);
}
@Override
public Long increment(K key, long delta) {
    byte[] rawKey = rawKey(key);
    return execute(connection -> connection.incrBy(rawKey, delta), true);
}
@Override
public Double increment(K key, double delta) {
    byte[] rawKey = rawKey(key);
    return execute(connection -> connection.incrBy(rawKey, delta), true);
}
//感兴趣的同学可以翻阅一下源码

业务需求

假设需要对用户生成二维码的接口调用次数进行限制,在某一时刻内只能调用多少次测试你的自卑程度,超过次数后将不再走生成二维码逻辑而是直接提示用户“访问频繁” 或 “次数已达上接口限”。

代码实现

TestLimitContjava培训机构roller.java只是简测试工程师单的实现java环境变量配置了如何通过计数器进行限流,没有实现具体的生成二spring维码逻辑

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
@Slf4j
@RestController
public class TestLimitController {
    @Autowired
    private RedisTemplate<String, String> stringRedisTemplate;
    @GetMapping(path = "/getqrcode")
    public String getQRCode(String username, HttpServletResponse response) {
        //需求:假设需要对用户生成二维码的接口调用次数进行限制,在某一时刻内只能调用多少次,超过次数后将不再走生成二维码逻辑而是直接提示用户“访问频繁” 或 “次数已达上限”。
        //当前线程名称
        String currentThreadName = Thread.currentThread().getName();
        //根据用户名来标记用户访问接口次数
        String key = username + "_getQRCode";
        //阈值:用户调用接口次数上限
        long threshold = 2;
        //30秒
        final long timeout = 30;
        final TimeUnit unit = TimeUnit.SECONDS;
        Long increment = stringRedisTemplate.opsForValue().increment(key);
        log.info("{} 计数器的值:{}", currentThreadName, increment);
        if (null != increment) {
            if (increment > threshold) {
                log.info("{} 次数已达上限", currentThreadName);
                //方便jmeter测试时 查看结果树的状态
                try {
                    response.sendError(HttpStatus.LOCKED.value(), "次数已达上限");
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return "次数已达上限";
            } else {
                if (1L == increment) {
                    //设置key过期时间,当key失效后 相当于次数限制归零
                    Boolean expire = stringRedisTemplate.expire(key, timeout, unit);
                }
                log.info("{} 二维码生成成功", currentThreadName);
                return "二维码生成成功";
            }
        }
        log.info("{} increment is null 二维码生成失败", currentThreadName);
        //方便jmeter测试时 查看结果树的状态
        try {
            response.sendError(HttpStatus.LOCKED.value(), "increment is null 二维码生成失败");
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "二维码生成失败";
    }
}

代码实测

来折腾一下自己写的代码是否能经受住考验,我在这里使用的测试工具是jmeter[2]来模拟用户并发场景调用生成二维码接口,假设:我们设定https和http的区别一个用户30秒内只能生成2次二维码,那么接口反馈的结果应该是达到2次之后应该提示用户“次数已达上限”;验证结果如下:

测试场景:jmeter模拟一个用户同一时刻开了10个线程来调用接口

当Redis的increment遇上了高并发,结果让人...

当Redis的increment遇上了高并发,结果让人...

jmeter测试结果树:

当Redis的increment遇上了高并发,结果让人...

当Redis的increment遇上了高并发,结果让人...

查看后台日志结测试抑郁程度的问卷果:只有2个线程生成成功

当Redis的increment遇上了高并发,结果让人...

存储在Redis中用户访问getQRC接口卡ode接口的标记

当Redis的increment遇上了高并发,结果让人...

当然这里测试只是用了10个线程来模拟,spring测试结果也达到预期效果。但不能代表真实生产环境的效果,建议看java语言完这篇文章的同学,如javascript果是要上生产环境还是要加大测试力度,避免测试不充足导致上线之后出现问题。

总结

综上所述,在一些对高并发请求有限制的系统中,我们可以使用Redis的 increment 和 expire 来实现了对接口进行限流,当用户频繁请求时通过限制次数对后台系统进行保护,防止过大的流量冲击导致系统崩溃。

问题探讨

在使用Redis的 increment 和 expire实现高并发限流时会不会出现问题?

会出现哪些问题?为什么会出现这些问题?

出现的问题该如果解决或者怎样避免问题的发生?

Java技术栈中还有哪些是可以实现限流的?

哈哈,看到这里的同学一定是对技术精益求精的,不妨我们在评论区探讨一下