全部產品
Search
文件中心

ApsaraDB for Redis:基於TairString實現高效限流器

更新時間:Jul 30, 2024

在限量搶購或者限時秒殺類情境中,除了要有效應對秒殺前後的流量高峰,還需要防止發生接受的下單量超過商品限購數量的問題,雲原生記憶體資料庫Tair的TairString資料結構支援簡潔高效的限流器,可以很好地解決訂單超量問題。本文介紹的方案也適用於其它需要限速或者限流的情境。

搶購限流器

exString是Tair新增的資料結構,比原生Redis String功能更加強大,除了位元位(bit)操作外能夠覆蓋原生Redis String的所有功能。

TairString的EXINCRBY/EXINCRBYFLOAT命令與原生Redis String的INCRBY/INCRBYFLOAT命令功能類似,都可對value進行遞增或遞減運算,但EXINCRBY/EXINCRBYFLOAT支援更多選項,例如EXNXVERMINMAX等,詳細說明請參見exString。下文介紹的方案涉及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)限制,您可以使用TairString的EXINCRBY命令,通過簡單的代碼設定一個並發限流器:

/**
 * 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;
}