在限量抢购或者限时秒杀类场景中,除了要有效应对秒杀前后的流量高峰,还需要防止发生接受的下单量超过商品限购数量的问题,云原生内存数据库Tair的TairString数据结构支持简洁高效的限流器,可以很好地解决订单超量问题。本文介绍的方案也适用于其它需要限速或者限流的场景。

抢购限流器

TairString是Tair新增的数据结构,比原生Redis String功能更加强大,除了比特位(bit)操作外能够覆盖原生Redis String的所有功能。

TairString的EXINCRBY/EXINCRBYFLOAT命令与原生Redis String的INCRBY/INCRBYFLOAT命令功能类似,都可对value进行递增或递减运算,但EXINCRBY/EXINCRBYFLOAT支持更多选项,例如EXNXVERMINMAX等,详细说明请参见TairString。下文介绍的方案涉及MINMAX两个选项:

选项说明
MIN设置TairString value的最小值。
MAX设置TairString value的最大值。

使用原生Redis String实现抢购,代码逻辑复杂,一旦管理不当,容易出现漏网订单,即明明商品已经抢完,却还有用户收到抢购成功的提示,造成不良影响,而使用TairString,只需要非常简单的代码即可实现严谨的订单数量限制,伪代码如下:

if(EXINCRBY(key_iphone, -1, MIN:0) == "would overflow")
    run_out();

限流计数器

抢购限流器类似,使用EXINCRBY命令的MAX选项可以实现限流计数器,伪代码如下:

if(EXINCRBY(rate_limitor, 1, MAX:1000) == "would overflow")
    traffic_control();

限流计数器的应用场景很多,例如并发限流、访问频率限制、密码修改次数限制等等。以并发限流为例,在请求的并发量突然超过系统的性能限制时,为了防止服务彻底崩溃引发更大的问题,采用限速器限制并发量,保证系统处理能力内的请求得到及时回应,是一种较合理的临时解决方案。例如配置QPS(Query Per Second)限制,您可以使用TairStringEXINCRBY命令,通过简单的代码设置一个并发限流器:

/**
 * tryAcquire is thread-safe and will increment the key from 0 to the upper bound within an interval of time,
 * and return failure once it exceeds
 * @param key the key
 * @param upperBound the max value
 * @param interval the time interval
 * @return acquire success: true; fail: false
 */
public static boolean tryAcquire(String key, int upperBound, int interval) {
    try (Jedis jedis = jedisPool.getResource()) {
        jedis.eval("if redis.call('exists', KEYS[1]) == 1 "
                + "then return redis.call('EXINCRBY', KEYS[1], '1', 'MAX', ARGV[1], 'KEEPTTL') "
                + "else return redis.call('EXSET', KEYS[1], 0, 'EX', ARGV[2]) end",
            Arrays.asList(key), Arrays.asList(String.valueOf(upperBound), String.valueOf(interval)));
        return true;
    } catch (Exception e) {
        if (e.getMessage().contains("increment or decrement would overflow")) {
            return false;
        }
        e.printStackTrace();
    }
    return false;
}