Go+Redis實(shí)現(xiàn)常見(jiàn)限流算法的示例代碼
限流是項(xiàng)目中經(jīng)常需要使用到的一種工具,一般用于限制用戶的請(qǐng)求的頻率,也可以避免瞬間流量過(guò)大導(dǎo)致系統(tǒng)崩潰,或者穩(wěn)定消息處理速率。并且有時(shí)候我們還需要使用到分布式限流,常見(jiàn)的實(shí)現(xiàn)方式是使用Redis作為中心存儲(chǔ)。
這個(gè)文章主要是使用Go+Redis實(shí)現(xiàn)常見(jiàn)的限流算法,如果需要了解每種限流算法的原理可以閱讀文章 Go實(shí)現(xiàn)常見(jiàn)的限流算法
下面的代碼使用到了go-redis客戶端
固定窗口
使用Redis實(shí)現(xiàn)固定窗口比較簡(jiǎn)單,主要是由于固定窗口同時(shí)只會(huì)存在一個(gè)窗口,所以我們可以在第一次進(jìn)入窗口時(shí)使用pexpire命令設(shè)置過(guò)期時(shí)間為窗口時(shí)間大小,這樣窗口會(huì)隨過(guò)期時(shí)間而失效,同時(shí)我們使用incr命令增加窗口計(jì)數(shù)。
因?yàn)槲覀冃枰?code>counter==1的時(shí)候設(shè)置窗口的過(guò)期時(shí)間,為了保證原子性,我們使用簡(jiǎn)單的Lua腳本實(shí)現(xiàn)。
const fixedWindowLimiterTryAcquireRedisScript = `
-- ARGV[1]: 窗口時(shí)間大小
-- ARGV[2]: 窗口請(qǐng)求上限
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
-- 若到達(dá)窗口請(qǐng)求上限,請(qǐng)求失敗
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
`package redis
import (
"context"
"errors"
"github.com/go-redis/redis/v8"
"time"
)
// FixedWindowLimiter 固定窗口限流器
type FixedWindowLimiter struct {
limit int // 窗口請(qǐng)求上限
window int // 窗口時(shí)間大小
client *redis.Client // Redis客戶端
script *redis.Script // TryAcquire腳本
}
func NewFixedWindowLimiter(client *redis.Client, limit int, window time.Duration) (*FixedWindowLimiter, error) {
// redis過(guò)期時(shí)間精度最大到毫秒,因此窗口必須能被毫秒整除
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
}
// 若到達(dá)窗口請(qǐng)求上限,請(qǐng)求失敗
if !success {
return ErrAcquireFailed
}
return nil
}滑動(dòng)窗口
hash實(shí)現(xiàn)
我們使用Redis的hash存儲(chǔ)每個(gè)小窗口的計(jì)數(shù),每次請(qǐng)求會(huì)把所有有效窗口的計(jì)數(shù)累加到count,使用hdel刪除失效窗口,最后判斷窗口的總計(jì)數(shù)是否大于上限。
我們基本上把所有的邏輯都放到Lua腳本里面,其中大頭是對(duì)hash的遍歷,時(shí)間復(fù)雜度是O(N),N是小窗口數(shù)量,所以小窗口數(shù)量最好不要太多。
const slidingWindowLimiterTryAcquireRedisScriptHashImpl = `
-- ARGV[1]: 窗口時(shí)間大小
-- ARGV[2]: 窗口請(qǐng)求上限
-- ARGV[3]: 當(dāng)前小窗口值
-- ARGV[4]: 起始小窗口值
local window = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local currentSmallWindow = tonumber(ARGV[3])
local startSmallWindow = tonumber(ARGV[4])
-- 計(jì)算當(dāng)前窗口的請(qǐng)求總數(shù)
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
-- 若到達(dá)窗口請(qǐng)求上限,請(qǐng)求失敗
if count >= limit then
return 0
end
-- 若沒(méi)到窗口請(qǐng)求上限,當(dāng)前小窗口計(jì)數(shù)器+1,請(qǐng)求成功
redis.call("hincrby", KEYS[1], currentSmallWindow, 1)
redis.call("pexpire", KEYS[1], window)
return 1
`package redis
import (
"context"
"errors"
"github.com/go-redis/redis/v8"
"time"
)
// SlidingWindowLimiter 滑動(dòng)窗口限流器
type SlidingWindowLimiter struct {
limit int // 窗口請(qǐng)求上限
window int64 // 窗口時(shí)間大小
smallWindow int64 // 小窗口時(shí)間大小
smallWindows int64 // 小窗口數(shù)量
client *redis.Client // Redis客戶端
script *redis.Script // TryAcquire腳本
}
func NewSlidingWindowLimiter(client *redis.Client, limit int, window, smallWindow time.Duration) (
*SlidingWindowLimiter, error) {
// redis過(guò)期時(shí)間精度最大到毫秒,因此窗口必須能被毫秒整除
if window%time.Millisecond != 0 || smallWindow%time.Millisecond != 0 {
return nil, errors.New("the window uint must not be less than millisecond")
}
// 窗口時(shí)間必須能夠被小窗口時(shí)間整除
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 {
// 獲取當(dāng)前小窗口值
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
}
// 若到達(dá)窗口請(qǐng)求上限,請(qǐng)求失敗
if !success {
return ErrAcquireFailed
}
return nil
}list實(shí)現(xiàn)
如果小窗口數(shù)量特別多,可以使用list優(yōu)化時(shí)間復(fù)雜度,list的結(jié)構(gòu)是:
[counter, smallWindow1, count1, smallWindow2, count2, smallWindow3, count3...]
也就是我們使用list的第一個(gè)元素存儲(chǔ)計(jì)數(shù)器,每個(gè)窗口用兩個(gè)元素表示,第一個(gè)元素表示小窗口值,第二個(gè)元素表示這個(gè)小窗口的計(jì)數(shù)。不直接把小窗口值和計(jì)數(shù)放到一個(gè)元素里是因?yàn)镽edis Lua腳本里沒(méi)有分割字符串的函數(shù)。
具體操作流程:
1.獲取list長(zhǎng)度
2.如果長(zhǎng)度是0,設(shè)置counter,長(zhǎng)度+1
3.如果長(zhǎng)度大于1,獲取第二第三個(gè)元素
如果該值小于起始小窗口值,counter-第三個(gè)元素的值,刪除第二第三個(gè)元素,長(zhǎng)度-2
4.如果counter大于等于limit,請(qǐng)求失敗
5.如果長(zhǎng)度大于1,獲取倒數(shù)第二第一個(gè)元素
- 如果倒數(shù)第二個(gè)元素小窗口值大于等于當(dāng)前小窗口值,表示當(dāng)前請(qǐng)求因?yàn)榫W(wǎng)絡(luò)延遲的問(wèn)題,到達(dá)服務(wù)器的時(shí)候,窗口已經(jīng)過(guò)時(shí)了,把倒數(shù)第二個(gè)元素當(dāng)成當(dāng)前小窗口(因?yàn)樗拢?,倒?shù)第一個(gè)元素值+1
- 否則,添加新的窗口值,添加新的計(jì)數(shù)(1),更新過(guò)期時(shí)間
6.否則,添加新的窗口值,添加新的計(jì)數(shù)(1),更新過(guò)期時(shí)間
7.counter + 1
8.返回成功
const slidingWindowLimiterTryAcquireRedisScriptListImpl = `
-- ARGV[1]: 窗口時(shí)間大小
-- ARGV[2]: 窗口請(qǐng)求上限
-- ARGV[3]: 當(dāng)前小窗口值
-- ARGV[4]: 起始小窗口值
local window = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local currentSmallWindow = tonumber(ARGV[3])
local startSmallWindow = tonumber(ARGV[4])
-- 獲取list長(zhǎng)度
local len = redis.call("llen", KEYS[1])
-- 如果長(zhǎng)度是0,設(shè)置counter,長(zhǎng)度+1
local counter = 0
if len == 0 then
redis.call("rpush", KEYS[1], 0)
redis.call("pexpire", KEYS[1], window)
len = len + 1
else
-- 如果長(zhǎng)度大于1,獲取第二第個(gè)元素
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-第三個(gè)元素的值
counter = counter - count1
-- 長(zhǎng)度-2
len = len - 2
-- 刪除第二第三個(gè)元素
redis.call("lrem", KEYS[1], 1, smallWindow1)
redis.call("lrem", KEYS[1], 1, count1)
end
end
-- 若到達(dá)窗口請(qǐng)求上限,請(qǐng)求失敗
if counter >= limit then
return 0
end
-- 如果長(zhǎng)度大于1,獲取倒數(shù)第二第一個(gè)元素
if len > 1 then
local smallWindown = tonumber(redis.call("lindex", KEYS[1], -2))
-- 如果倒數(shù)第二個(gè)元素小窗口值大于等于當(dāng)前小窗口值
if smallWindown >= currentSmallWindow then
-- 把倒數(shù)第二個(gè)元素當(dāng)成當(dāng)前小窗口(因?yàn)樗拢?,倒?shù)第一個(gè)元素值+1
local countn = redis.call("lindex", KEYS[1], -1)
redis.call("lset", KEYS[1], -1, countn + 1)
else
-- 否則,添加新的窗口值,添加新的計(jì)數(shù)(1),更新過(guò)期時(shí)間
redis.call("rpush", KEYS[1], currentSmallWindow, 1)
redis.call("pexpire", KEYS[1], window)
end
else
-- 否則,添加新的窗口值,添加新的計(jì)數(shù)(1),更新過(guò)期時(shí)間
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
`算法都是操作list頭部或者尾部,所以時(shí)間復(fù)雜度接近O(1)
漏桶算法
漏桶需要保存當(dāng)前水位和上次放水時(shí)間,因此我們使用hash來(lái)保存這兩個(gè)值。
const leakyBucketLimiterTryAcquireRedisScript = `
-- ARGV[1]: 最高水位
-- ARGV[2]: 水流速度/秒
-- ARGV[3]: 當(dāng)前時(shí)間(秒)
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
-- 嘗試放水
-- 距離上次放水的時(shí)間
local interval = now - lastTime
if interval > 0 then
-- 當(dāng)前水位-距離上次放水的時(shí)間(秒)*水流速度
local newLevel = currentLevel - interval * currentVelocity
if newLevel < 0 then
newLevel = 0
end
currentLevel = newLevel
redis.call("hmset", KEYS[1], "currentLevel", newLevel, "lastTime", now)
end
-- 若到達(dá)最高水位,請(qǐng)求失敗
if currentLevel >= peakLevel then
return 0
end
-- 若沒(méi)有到達(dá)最高水位,當(dāng)前水位+1,請(qǐng)求成功
redis.call("hincrby", KEYS[1], "currentLevel", 1)
redis.call("expire", KEYS[1], peakLevel / currentVelocity)
return 1
`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 {
// 當(dāng)前時(shí)間
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
}
// 若到達(dá)窗口請(qǐng)求上限,請(qǐng)求失敗
if !success {
return ErrAcquireFailed
}
return nil
}令牌桶
令牌桶可以看作是漏桶的相反算法,它們一個(gè)是把水倒進(jìn)桶里,一個(gè)是從桶里獲取令牌。
const tokenBucketLimiterTryAcquireRedisScript = `
-- ARGV[1]: 容量
-- ARGV[2]: 發(fā)放令牌速率/秒
-- ARGV[3]: 當(dāng)前時(shí)間(秒)
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
-- 嘗試發(fā)放令牌
-- 距離上次發(fā)放令牌的時(shí)間
local interval = now - lastTime
if interval > 0 then
-- 當(dāng)前令牌數(shù)量+距離上次發(fā)放令牌的時(shí)間(秒)*發(fā)放令牌速率
local newTokens = currentTokens + interval * rate
if newTokens > capacity then
newTokens = capacity
end
currentTokens = newTokens
redis.call("hmset", KEYS[1], "currentTokens", newTokens, "lastTime", now)
end
-- 如果沒(méi)有令牌,請(qǐng)求失敗
if currentTokens == 0 then
return 0
end
-- 果有令牌,當(dāng)前令牌-1,請(qǐng)求成功
redis.call("hincrby", KEYS[1], "currentTokens", -1)
redis.call("expire", KEYS[1], capacity / rate)
return 1
`package redis
import (
"context"
"github.com/go-redis/redis/v8"
"time"
)
// TokenBucketLimiter 令牌桶限流器
type TokenBucketLimiter struct {
capacity int // 容量
rate int // 發(fā)放令牌速率/秒
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 {
// 當(dāng)前時(shí)間
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
}
// 若到達(dá)窗口請(qǐng)求上限,請(qǐng)求失敗
if !success {
return ErrAcquireFailed
}
return nil
}滑動(dòng)日志
算法流程與滑動(dòng)窗口相同,只是它可以指定多個(gè)策略,同時(shí)在請(qǐng)求失敗的時(shí)候,需要通知調(diào)用方是被哪個(gè)策略所攔截。
const slidingLogLimiterTryAcquireRedisScriptHashImpl = `
-- ARGV[1]: 當(dāng)前小窗口值
-- ARGV[2]: 第一個(gè)策略的窗口時(shí)間大小
-- ARGV[i * 2 + 1]: 每個(gè)策略的起始小窗口值
-- ARGV[i * 2 + 2]: 每個(gè)策略的窗口請(qǐng)求上限
local currentSmallWindow = tonumber(ARGV[1])
-- 第一個(gè)策略的窗口時(shí)間大小
local window = tonumber(ARGV[2])
-- 第一個(gè)策略的起始小窗口值
local startSmallWindow = tonumber(ARGV[3])
local strategiesLen = #(ARGV) / 2 - 1
-- 計(jì)算每個(gè)策略當(dāng)前窗口的請(qǐng)求總數(shù)
local counters = redis.call("hgetall", KEYS[1])
local counts = {}
-- 初始化counts
for j = 1, strategiesLen do
counts[j] = 0
end
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
for j = 1, strategiesLen do
if smallWindow >= tonumber(ARGV[j * 2 + 1]) then
counts[j] = counts[j] + counter
end
end
end
end
-- 若到達(dá)對(duì)應(yīng)策略窗口請(qǐng)求上限,請(qǐng)求失敗,返回違背的策略下標(biāo)
for i = 1, strategiesLen do
if counts[i] >= tonumber(ARGV[i * 2 + 2]) then
return i - 1
end
end
-- 若沒(méi)到窗口請(qǐng)求上限,當(dāng)前小窗口計(jì)數(shù)器+1,請(qǐng)求成功
redis.call("hincrby", KEYS[1], currentSmallWindow, 1)
redis.call("pexpire", KEYS[1], window)
return -1
`package redis
import (
"context"
"errors"
"fmt"
"github.com/go-redis/redis/v8"
"sort"
"time"
)
// ViolationStrategyError 違背策略錯(cuò)誤
type ViolationStrategyError struct {
Limit int // 窗口請(qǐng)求上限
Window time.Duration // 窗口時(shí)間大小
}
func (e *ViolationStrategyError) Error() string {
return fmt.Sprintf("violation strategy that limit = %d and window = %d", e.Limit, e.Window)
}
// SlidingLogLimiterStrategy 滑動(dòng)日志限流器的策略
type SlidingLogLimiterStrategy struct {
limit int // 窗口請(qǐng)求上限
window int64 // 窗口時(shí)間大小
smallWindows int64 // 小窗口數(shù)量
}
func NewSlidingLogLimiterStrategy(limit int, window time.Duration) *SlidingLogLimiterStrategy {
return &SlidingLogLimiterStrategy{
limit: limit,
window: int64(window),
}
}
// SlidingLogLimiter 滑動(dòng)日志限流器
type SlidingLogLimiter struct {
strategies []*SlidingLogLimiterStrategy // 滑動(dòng)日志限流器策略列表
smallWindow int64 // 小窗口時(shí)間大小
client *redis.Client // Redis客戶端
script *redis.Script // TryAcquire腳本
}
func NewSlidingLogLimiter(client *redis.Client, smallWindow time.Duration, strategies ...*SlidingLogLimiterStrategy) (
*SlidingLogLimiter, error) {
// 復(fù)制策略避免被修改
strategies = append(make([]*SlidingLogLimiterStrategy, 0, len(strategies)), strategies...)
// 不能不設(shè)置策略
if len(strategies) == 0 {
return nil, errors.New("must be set strategies")
}
// redis過(guò)期時(shí)間精度最大到毫秒,因此窗口必須能被毫秒整除
if smallWindow%time.Millisecond != 0 {
return nil, errors.New("the window uint must not be less than millisecond")
}
smallWindow = smallWindow / time.Millisecond
for _, strategy := range strategies {
if strategy.window%int64(time.Millisecond) != 0 {
return nil, errors.New("the window uint must not be less than millisecond")
}
strategy.window = strategy.window / int64(time.Millisecond)
}
// 排序策略,窗口時(shí)間大的排前面,相同窗口上限大的排前面
sort.Slice(strategies, func(i, j int) bool {
a, b := strategies[i], strategies[j]
if a.window == b.window {
return a.limit > b.limit
}
return a.window > b.window
})
for i, strategy := range strategies {
// 隨著窗口時(shí)間變小,窗口上限也應(yīng)該變小
if i > 0 {
if strategy.limit >= strategies[i-1].limit {
return nil, errors.New("the smaller window should be the smaller limit")
}
}
// 窗口時(shí)間必須能夠被小窗口時(shí)間整除
if strategy.window%int64(smallWindow) != 0 {
return nil, errors.New("window cannot be split by integers")
}
strategy.smallWindows = strategy.window / int64(smallWindow)
}
return &SlidingLogLimiter{
strategies: strategies,
smallWindow: int64(smallWindow),
client: client,
script: redis.NewScript(slidingLogLimiterTryAcquireRedisScriptHashImpl),
}, nil
}
func (l *SlidingLogLimiter) TryAcquire(ctx context.Context, resource string) error {
// 獲取當(dāng)前小窗口值
currentSmallWindow := time.Now().UnixMilli() / l.smallWindow * l.smallWindow
args := make([]interface{}, len(l.strategies)*2+2)
args[0] = currentSmallWindow
args[1] = l.strategies[0].window
// 獲取每個(gè)策略的起始小窗口值
for i, strategy := range l.strategies {
args[i*2+2] = currentSmallWindow - l.smallWindow*(strategy.smallWindows-1)
args[i*2+3] = strategy.limit
}
index, err := l.script.Run(
ctx, l.client, []string{resource}, args...).Int()
if err != nil {
return err
}
// 若到達(dá)窗口請(qǐng)求上限,請(qǐng)求失敗
if index != -1 {
return &ViolationStrategyError{
Limit: l.strategies[index].limit,
Window: time.Duration(l.strategies[index].window),
}
}
return nil
}總結(jié)
由于Redis擁有豐富而且高性能的數(shù)據(jù)類型,因此使用Redis實(shí)現(xiàn)限流算法并不困難,但是每個(gè)算法都需要編寫Lua腳本,所以如果不熟悉Lua可能會(huì)踩一些坑。
需要完整代碼和測(cè)試代碼可以查看:github.com/jiaxwu/limiter/tree/main/redis
以上就是Go+Redis實(shí)現(xiàn)常見(jiàn)限流算法的示例代碼的詳細(xì)內(nèi)容,更多關(guān)于Go Redis限流算法的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
go語(yǔ)言區(qū)塊鏈學(xué)習(xí)調(diào)用智能合約
這篇文章主要為大家介紹了go語(yǔ)言區(qū)塊鏈學(xué)習(xí)中如何調(diào)用智能合約的實(shí)現(xiàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2021-10-10
Golang 實(shí)現(xiàn)簡(jiǎn)單隨機(jī)負(fù)載均衡
均衡算法又分為 隨機(jī),輪詢,加權(quán)輪詢,哈希,而隨機(jī)負(fù)載均衡算法就是本文的重點(diǎn),需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-06-06
GO語(yǔ)言中Chan實(shí)現(xiàn)原理的示例詳解
這篇文章主要為大家詳細(xì)介紹了Go語(yǔ)言中Chan實(shí)現(xiàn)原理的相關(guān)資料,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Go語(yǔ)言有一定的幫助,需要的可以參考一下2023-02-02

