search
HomeDatabaseRedisHow to use Go+Redis to implement common current limiting algorithms

    Fixed window

    Using Redis to implement a fixed window is relatively simple, mainly because there will only be one fixed window at the same time, so we can When entering the window, use the pexpire command to set the expiration time to the window time size, so that the window will expire with the expiration time. At the same time, we use the incr command to increase the window count.

    Because we need to set the expiration time of the window when counter==1, in order to ensure atomicity, we use a simple Lua script implementation.

    const fixedWindowLimiterTryAcquireRedisScript = `
    -- ARGV[1]: 窗口时间大小
    -- ARGV[2]: 窗口请求上限
    
    local window = tonumber(ARGV[1])
    local limit = tonumber(ARGV[2])
    
    -- 获取原始值
    local counter = tonumber(redis.call("get", KEYS[1]))
    if counter == nil then 
       counter = 0
    end
    -- 若到达窗口请求上限,请求失败
    if counter >= limit then
       return 0
    end
    -- 窗口值+1
    redis.call("incr", KEYS[1])
    if counter == 0 then
        redis.call("pexpire", KEYS[1], window)
    end
    return 1
    `
    rrree

    Sliding window

    hash implementation

    We use Redis’s hash to store the count of each small window, and each request will store all The count of valid windows is accumulated to count, use hdel to delete the invalid windows, and finally determine whether the total count of windows is greater than the upper limit.

    We basically put all the logic into the Lua script, where the big head is the traversal of hash, the time complexity is O(N), N is the number of small windows, so It is best not to have too many small windows.

    package redis
    
    import (
       "context"
       "errors"
       "github.com/go-redis/redis/v8"
       "time"
    )
    
    // FixedWindowLimiter 固定窗口限流器
    type FixedWindowLimiter struct {
       limit  int           // 窗口请求上限
       window int           // 窗口时间大小
       client *redis.Client // Redis客户端
       script *redis.Script // TryAcquire脚本
    }
    
    func NewFixedWindowLimiter(client *redis.Client, limit int, window time.Duration) (*FixedWindowLimiter, error) {
       // redis过期时间精度最大到毫秒,因此窗口必须能被毫秒整除
       if window%time.Millisecond != 0 {
          return nil, errors.New("the window uint must not be less than millisecond")
       }
    
       return &FixedWindowLimiter{
          limit:  limit,
          window: int(window / time.Millisecond),
          client: client,
          script: redis.NewScript(fixedWindowLimiterTryAcquireRedisScript),
       }, nil
    }
    
    func (l *FixedWindowLimiter) TryAcquire(ctx context.Context, resource string) error {
       success, err := l.script.Run(ctx, l.client, []string{resource}, l.window, l.limit).Bool()
       if err != nil {
          return err
       }
       // 若到达窗口请求上限,请求失败
       if !success {
          return ErrAcquireFailed
       }
       return nil
    }
    const slidingWindowLimiterTryAcquireRedisScriptHashImpl = `
    -- ARGV[1]: 窗口时间大小
    -- ARGV[2]: 窗口请求上限
    -- ARGV[3]: 当前小窗口值
    -- ARGV[4]: 起始小窗口值
    
    local window = tonumber(ARGV[1])
    local limit = tonumber(ARGV[2])
    local currentSmallWindow = tonumber(ARGV[3])
    local startSmallWindow = tonumber(ARGV[4])
    
    -- 计算当前窗口的请求总数
    local counters = redis.call("hgetall", KEYS[1])
    local count = 0
    for i = 1, #(counters) / 2 do 
       local smallWindow = tonumber(counters[i * 2 - 1])
       local counter = tonumber(counters[i * 2])
       if smallWindow < startSmallWindow then
          redis.call("hdel", KEYS[1], smallWindow)
       else 
          count = count + counter
       end
    end
    
    -- 若到达窗口请求上限,请求失败
    if count >= limit then
       return 0
    end
    
    -- 若没到窗口请求上限,当前小窗口计数器+1,请求成功
    redis.call("hincrby", KEYS[1], currentSmallWindow, 1)
    redis.call("pexpire", KEYS[1], window)
    return 1
    `

    list implementation

    If the number of small windows is particularly large, you can use list to optimize the time complexity. The structure of the list is:

    [counter, smallWindow1, count1, smallWindow2, count2, smallWindow3, count3...]

    That is, we use the first element of the list to store the counter, and each window is represented by two elements. One element represents the small window value, and the second element represents the count of this small window. Since the Redis Lua script does not support the string splitting function, the value and count of the small window cannot be placed in the same element.

    Specific operation process:

    1. Get the length of the list

    2. If the length is 0, set counter, the length is 1

    3. If the length is greater than 1. Get the second and third elements

    If the value is less than the starting small window value, counter-the value of the third element, delete the second and third elements, length-2

    4. If counter is greater than or equal to limit, the request fails

    5. If the length is greater than 1, get the second to last element

    • If the second to last element The small window value is greater than or equal to the current small window value, which means that due to network delay, the window has expired when the current request reaches the server. The penultimate element is regarded as the current small window (because it is updated), and the penultimate element is regarded as the current small window (because it is updated). Value 1

    • Otherwise, add new window value, add new count (1), update expiration time

    6. Otherwise, add New window value, add new count (1), update expiration time

    7.counter 1

    8.Return successful

    package redis
    
    import (
       "context"
       "errors"
       "github.com/go-redis/redis/v8"
       "time"
    )
    
    // SlidingWindowLimiter 滑动窗口限流器
    type SlidingWindowLimiter struct {
       limit        int           // 窗口请求上限
       window       int64         // 窗口时间大小
       smallWindow  int64         // 小窗口时间大小
       smallWindows int64         // 小窗口数量
       client       *redis.Client // Redis客户端
       script       *redis.Script // TryAcquire脚本
    }
    
    func NewSlidingWindowLimiter(client *redis.Client, limit int, window, smallWindow time.Duration) (
       *SlidingWindowLimiter, error) {
       // redis过期时间精度最大到毫秒,因此窗口必须能被毫秒整除
       if window%time.Millisecond != 0 || smallWindow%time.Millisecond != 0 {
          return nil, errors.New("the window uint must not be less than millisecond")
       }
    
       // 窗口时间必须能够被小窗口时间整除
       if window%smallWindow != 0 {
          return nil, errors.New("window cannot be split by integers")
       }
    
       return &SlidingWindowLimiter{
          limit:        limit,
          window:       int64(window / time.Millisecond),
          smallWindow:  int64(smallWindow / time.Millisecond),
          smallWindows: int64(window / smallWindow),
          client:       client,
          script:       redis.NewScript(slidingWindowLimiterTryAcquireRedisScriptHashImpl),
       }, nil
    }
    
    func (l *SlidingWindowLimiter) TryAcquire(ctx context.Context, resource string) error {
       // 获取当前小窗口值
       currentSmallWindow := time.Now().UnixMilli() / l.smallWindow * l.smallWindow
       // 获取起始小窗口值
       startSmallWindow := currentSmallWindow - l.smallWindow*(l.smallWindows-1)
    
       success, err := l.script.Run(
          ctx, l.client, []string{resource}, l.window, l.limit, currentSmallWindow, startSmallWindow).Bool()
       if err != nil {
          return err
       }
       // 若到达窗口请求上限,请求失败
       if !success {
          return ErrAcquireFailed
       }
       return nil
    }

    Algorithms are all operations listHead or tail, so the time complexity is close to O(1)

    Leaky bucket algorithm

    The leaky bucket needs to save the current water level and the last water release time, so we use hash to save these two values.

    const slidingWindowLimiterTryAcquireRedisScriptListImpl = `
    -- ARGV[1]: 窗口时间大小
    -- ARGV[2]: 窗口请求上限
    -- ARGV[3]: 当前小窗口值
    -- ARGV[4]: 起始小窗口值
    
    local window = tonumber(ARGV[1])
    local limit = tonumber(ARGV[2])
    local currentSmallWindow = tonumber(ARGV[3])
    local startSmallWindow = tonumber(ARGV[4])
    
    -- 获取list长度
    local len = redis.call("llen", KEYS[1])
    -- 如果长度是0,设置counter,长度+1
    local counter = 0
    if len == 0 then 
       redis.call("rpush", KEYS[1], 0)
       redis.call("pexpire", KEYS[1], window)
       len = len + 1
    else
       -- 如果长度大于1,获取第二第个元素
       local smallWindow1 = tonumber(redis.call("lindex", KEYS[1], 1))
       counter = tonumber(redis.call("lindex", KEYS[1], 0))
       -- 如果该值小于起始小窗口值
       if smallWindow1 < startSmallWindow then 
          local count1 = redis.call("lindex", KEYS[1], 2)
          -- counter-第三个元素的值
          counter = counter - count1
          -- 长度-2
          len = len - 2
          -- 删除第二第三个元素
          redis.call("lrem", KEYS[1], 1, smallWindow1)
          redis.call("lrem", KEYS[1], 1, count1)
       end
    end
    
    -- 若到达窗口请求上限,请求失败
    if counter >= limit then 
       return 0
    end 
    
    -- 如果长度大于1,获取倒数第二第一个元素
    if len > 1 then
       local smallWindown = tonumber(redis.call("lindex", KEYS[1], -2))
       -- 如果倒数第二个元素小窗口值大于等于当前小窗口值
       if smallWindown >= currentSmallWindow then
          -- 把倒数第二个元素当成当前小窗口(因为它更新),倒数第一个元素值+1
          local countn = redis.call("lindex", KEYS[1], -1)
          redis.call("lset", KEYS[1], -1, countn + 1)
       else 
          -- 否则,添加新的窗口值,添加新的计数(1),更新过期时间
          redis.call("rpush", KEYS[1], currentSmallWindow, 1)
          redis.call("pexpire", KEYS[1], window)
       end
    else 
       -- 否则,添加新的窗口值,添加新的计数(1),更新过期时间
       redis.call("rpush", KEYS[1], currentSmallWindow, 1)
       redis.call("pexpire", KEYS[1], window)
    end 
    
    -- counter + 1并更新
    redis.call("lset", KEYS[1], 0, counter + 1)
    return 1
    `
    const leakyBucketLimiterTryAcquireRedisScript = `
    -- ARGV[1]: 最高水位
    -- ARGV[2]: 水流速度/秒
    -- ARGV[3]: 当前时间(秒)
    
    local peakLevel = tonumber(ARGV[1])
    local currentVelocity = tonumber(ARGV[2])
    local now = tonumber(ARGV[3])
    
    local lastTime = tonumber(redis.call("hget", KEYS[1], "lastTime"))
    local currentLevel = tonumber(redis.call("hget", KEYS[1], "currentLevel"))
    -- 初始化
    if lastTime == nil then 
       lastTime = now
       currentLevel = 0
       redis.call("hmset", KEYS[1], "currentLevel", currentLevel, "lastTime", lastTime)
    end 
    
    -- 尝试放水
    -- 距离上次放水的时间
    local interval = now - lastTime
    if interval > 0 then
       -- 当前水位-距离上次放水的时间(秒)*水流速度
       local newLevel = currentLevel - interval * currentVelocity
       if newLevel < 0 then 
          newLevel = 0
       end 
       currentLevel = newLevel
       redis.call("hmset", KEYS[1], "currentLevel", newLevel, "lastTime", now)
    end
    
    -- 若到达最高水位,请求失败
    if currentLevel >= peakLevel then
       return 0
    end
    -- 若没有到达最高水位,当前水位+1,请求成功
    redis.call("hincrby", KEYS[1], "currentLevel", 1)
    redis.call("expire", KEYS[1], peakLevel / currentVelocity)
    return 1
    `

    Token Bucket

    Token bucket can be regarded as the opposite algorithm of leaky bucket. One of them is to pour water into the bucket, and the other is to obtain tokens from the bucket.

    package redis
    
    import (
       "context"
       "github.com/go-redis/redis/v8"
       "time"
    )
    
    // LeakyBucketLimiter 漏桶限流器
    type LeakyBucketLimiter struct {
       peakLevel       int           // 最高水位
       currentVelocity int           // 水流速度/秒
       client          *redis.Client // Redis客户端
       script          *redis.Script // TryAcquire脚本
    }
    
    func NewLeakyBucketLimiter(client *redis.Client, peakLevel, currentVelocity int) *LeakyBucketLimiter {
       return &LeakyBucketLimiter{
          peakLevel:       peakLevel,
          currentVelocity: currentVelocity,
          client:          client,
          script:          redis.NewScript(leakyBucketLimiterTryAcquireRedisScript),
       }
    }
    
    func (l *LeakyBucketLimiter) TryAcquire(ctx context.Context, resource string) error {
       // 当前时间
       now := time.Now().Unix()
       success, err := l.script.Run(ctx, l.client, []string{resource}, l.peakLevel, l.currentVelocity, now).Bool()
       if err != nil {
          return err
       }
       // 若到达窗口请求上限,请求失败
       if !success {
          return ErrAcquireFailed
       }
       return nil
    }
    const tokenBucketLimiterTryAcquireRedisScript = `
    -- ARGV[1]: 容量
    -- ARGV[2]: 发放令牌速率/秒
    -- ARGV[3]: 当前时间(秒)
    
    local capacity = tonumber(ARGV[1])
    local rate = tonumber(ARGV[2])
    local now = tonumber(ARGV[3])
    
    local lastTime = tonumber(redis.call("hget", KEYS[1], "lastTime"))
    local currentTokens = tonumber(redis.call("hget", KEYS[1], "currentTokens"))
    -- 初始化
    if lastTime == nil then 
       lastTime = now
       currentTokens = capacity
       redis.call("hmset", KEYS[1], "currentTokens", currentTokens, "lastTime", lastTime)
    end 
    
    -- 尝试发放令牌
    -- 距离上次发放令牌的时间
    local interval = now - lastTime
    if interval > 0 then
       -- 当前令牌数量+距离上次发放令牌的时间(秒)*发放令牌速率
       local newTokens = currentTokens + interval * rate
       if newTokens > capacity then 
          newTokens = capacity
       end 
       currentTokens = newTokens
       redis.call("hmset", KEYS[1], "currentTokens", newTokens, "lastTime", now)
    end
    
    -- 如果没有令牌,请求失败
    if currentTokens == 0 then
       return 0
    end
    -- 果有令牌,当前令牌-1,请求成功
    redis.call("hincrby", KEYS[1], "currentTokens", -1)
    redis.call("expire", KEYS[1], capacity / rate)
    return 1
    `

    Sliding Log

    The algorithm process is the same as the sliding window, except that it can specify multiple strategies. At the same time, when the request fails, the caller needs to be notified which strategy was intercepted.

    package redis
    
    import (
       "context"
       "github.com/go-redis/redis/v8"
       "time"
    )
    
    // TokenBucketLimiter 令牌桶限流器
    type TokenBucketLimiter struct {
       capacity int           // 容量
       rate     int           // 发放令牌速率/秒
       client   *redis.Client // Redis客户端
       script   *redis.Script // TryAcquire脚本
    }
    
    func NewTokenBucketLimiter(client *redis.Client, capacity, rate int) *TokenBucketLimiter {
       return &TokenBucketLimiter{
          capacity: capacity,
          rate:     rate,
          client:   client,
          script:   redis.NewScript(tokenBucketLimiterTryAcquireRedisScript),
       }
    }
    
    func (l *TokenBucketLimiter) TryAcquire(ctx context.Context, resource string) error {
       // 当前时间
       now := time.Now().Unix()
       success, err := l.script.Run(ctx, l.client, []string{resource}, l.capacity, l.rate, now).Bool()
       if err != nil {
          return err
       }
       // 若到达窗口请求上限,请求失败
       if !success {
          return ErrAcquireFailed
       }
       return nil
    }
    rrree

    The above is the detailed content of How to use Go+Redis to implement common current limiting algorithms. For more information, please follow other related articles on the PHP Chinese website!

    Statement
    This article is reproduced at:亿速云. If there is any infringement, please contact admin@php.cn delete
    Redis: Exploring Its Data Model and StructureRedis: Exploring Its Data Model and StructureApr 16, 2025 am 12:09 AM

    Redis's data model and structure include five main types: 1. String: used to store text or binary data, and supports atomic operations. 2. List: Ordered elements collection, suitable for queues and stacks. 3. Set: Unordered unique elements set, supporting set operation. 4. Ordered Set (SortedSet): A unique set of elements with scores, suitable for rankings. 5. Hash table (Hash): a collection of key-value pairs, suitable for storing objects.

    Redis: Classifying Its Database ApproachRedis: Classifying Its Database ApproachApr 15, 2025 am 12:06 AM

    Redis's database methods include in-memory databases and key-value storage. 1) Redis stores data in memory, and reads and writes fast. 2) It uses key-value pairs to store data, supports complex data structures such as lists, collections, hash tables and ordered collections, suitable for caches and NoSQL databases.

    Why Use Redis? Benefits and AdvantagesWhy Use Redis? Benefits and AdvantagesApr 14, 2025 am 12:07 AM

    Redis is a powerful database solution because it provides fast performance, rich data structures, high availability and scalability, persistence capabilities, and a wide range of ecosystem support. 1) Extremely fast performance: Redis's data is stored in memory and has extremely fast read and write speeds, suitable for high concurrency and low latency applications. 2) Rich data structure: supports multiple data types, such as lists, collections, etc., which are suitable for a variety of scenarios. 3) High availability and scalability: supports master-slave replication and cluster mode to achieve high availability and horizontal scalability. 4) Persistence and data security: Data persistence is achieved through RDB and AOF to ensure data integrity and reliability. 5) Wide ecosystem and community support: with a huge ecosystem and active community,

    Understanding NoSQL: Key Features of RedisUnderstanding NoSQL: Key Features of RedisApr 13, 2025 am 12:17 AM

    Key features of Redis include speed, flexibility and rich data structure support. 1) Speed: Redis is an in-memory database, and read and write operations are almost instantaneous, suitable for cache and session management. 2) Flexibility: Supports multiple data structures, such as strings, lists, collections, etc., which are suitable for complex data processing. 3) Data structure support: provides strings, lists, collections, hash tables, etc., which are suitable for different business needs.

    Redis: Identifying Its Primary FunctionRedis: Identifying Its Primary FunctionApr 12, 2025 am 12:01 AM

    The core function of Redis is a high-performance in-memory data storage and processing system. 1) High-speed data access: Redis stores data in memory and provides microsecond-level read and write speed. 2) Rich data structure: supports strings, lists, collections, etc., and adapts to a variety of application scenarios. 3) Persistence: Persist data to disk through RDB and AOF. 4) Publish subscription: Can be used in message queues or real-time communication systems.

    Redis: A Guide to Popular Data StructuresRedis: A Guide to Popular Data StructuresApr 11, 2025 am 12:04 AM

    Redis supports a variety of data structures, including: 1. String, suitable for storing single-value data; 2. List, suitable for queues and stacks; 3. Set, used for storing non-duplicate data; 4. Ordered Set, suitable for ranking lists and priority queues; 5. Hash table, suitable for storing object or structured data.

    How to implement redis counterHow to implement redis counterApr 10, 2025 pm 10:21 PM

    Redis counter is a mechanism that uses Redis key-value pair storage to implement counting operations, including the following steps: creating counter keys, increasing counts, decreasing counts, resetting counts, and obtaining counts. The advantages of Redis counters include fast speed, high concurrency, durability and simplicity and ease of use. It can be used in scenarios such as user access counting, real-time metric tracking, game scores and rankings, and order processing counting.

    How to use the redis command lineHow to use the redis command lineApr 10, 2025 pm 10:18 PM

    Use the Redis command line tool (redis-cli) to manage and operate Redis through the following steps: Connect to the server, specify the address and port. Send commands to the server using the command name and parameters. Use the HELP command to view help information for a specific command. Use the QUIT command to exit the command line tool.

    See all articles

    Hot AI Tools

    Undresser.AI Undress

    Undresser.AI Undress

    AI-powered app for creating realistic nude photos

    AI Clothes Remover

    AI Clothes Remover

    Online AI tool for removing clothes from photos.

    Undress AI Tool

    Undress AI Tool

    Undress images for free

    Clothoff.io

    Clothoff.io

    AI clothes remover

    AI Hentai Generator

    AI Hentai Generator

    Generate AI Hentai for free.

    Hot Article

    R.E.P.O. Energy Crystals Explained and What They Do (Yellow Crystal)
    4 weeks agoBy尊渡假赌尊渡假赌尊渡假赌
    R.E.P.O. Best Graphic Settings
    4 weeks agoBy尊渡假赌尊渡假赌尊渡假赌
    R.E.P.O. How to Fix Audio if You Can't Hear Anyone
    4 weeks agoBy尊渡假赌尊渡假赌尊渡假赌
    R.E.P.O. Chat Commands and How to Use Them
    4 weeks agoBy尊渡假赌尊渡假赌尊渡假赌

    Hot Tools

    Zend Studio 13.0.1

    Zend Studio 13.0.1

    Powerful PHP integrated development environment

    SublimeText3 Linux new version

    SublimeText3 Linux new version

    SublimeText3 Linux latest version

    DVWA

    DVWA

    Damn Vulnerable Web App (DVWA) is a PHP/MySQL web application that is very vulnerable. Its main goals are to be an aid for security professionals to test their skills and tools in a legal environment, to help web developers better understand the process of securing web applications, and to help teachers/students teach/learn in a classroom environment Web application security. The goal of DVWA is to practice some of the most common web vulnerabilities through a simple and straightforward interface, with varying degrees of difficulty. Please note that this software

    VSCode Windows 64-bit Download

    VSCode Windows 64-bit Download

    A free and powerful IDE editor launched by Microsoft

    MinGW - Minimalist GNU for Windows

    MinGW - Minimalist GNU for Windows

    This project is in the process of being migrated to osdn.net/projects/mingw, you can continue to follow us there. MinGW: A native Windows port of the GNU Compiler Collection (GCC), freely distributable import libraries and header files for building native Windows applications; includes extensions to the MSVC runtime to support C99 functionality. All MinGW software can run on 64-bit Windows platforms.