序文
一部のビジネスシナリオでは、アトミックで信頼性が高く、分散された固定長キューを実装する必要があります。次の機能が必要です。
- キューには有効期限があります。
- キューの最大長はnであり、要素がいっぱいになると、新しいデータの追加を拒否します。
- キューの最大長はnです。要素がいっぱいになると、最も古いデータがポップされ、新しいデータが受け入れられます。
- 分散APIは、低コストでアクセスして使用できます。
- redisクラスターをサポートする
実装分析
- redisを使用して達成する
- 有効期限メカニズムを使用して有効期限を達成する
- luaスクリプトを使用してCASアトミック性を実現する
- 単一のキーを使用して、redisクラスターがevalコマンドをサポートしていることを確認します
ソースコード
package redistool
import (
"github.com/fwhezfwhez/errorx"
"github.com/garyburd/redigo/redis"
)
// 定长list
type RedisLimitList struct {
expireSeconds int
maxLen int
scheme int // 1时,list打满时,将拒绝新的push。 2时,list打满时,将pop掉一个最早的,再将新的压进来。
}
func NewLimitList(expireSeconds int, maxLen int, scheme int) RedisLimitList {
return RedisLimitList{
expireSeconds: expireSeconds,
maxLen: maxLen,
scheme: scheme,
}
}
var BeyondErr = errorx.NewServiceError("超出list最大长度限制", 1)
// key list的key
// value 压入的值
// maxLength 最大长度
func (rll RedisLimitList) LPush(conn redis.Conn, key string, value []byte) (int, error) {
return rll.LPushScheme1(conn, key, value)
}
// key list的key
// value 压入的值
// maxLength 最大长度
// list打满后,将不再接受新的数据,除非随后pop腾出了位置。
func (rll RedisLimitList) LPushScheme1(conn redis.Conn, key string, value []byte) (int, error) {
var keyNum = 1 // eval的key的数量,为1
var arg1 = value
var arg2 = rll.maxLen // 队列最大长度
var arg3 = rll.expireSeconds // 队列key失效时间
var scriptf = `
local num = redis.call('llen',KEYS[1]);
if tonumber(ARGV[2])>0 and tonumber(num) >= tonumber(ARGV[2]) then
return -3
end
redis.call('lpush',KEYS[1],ARGV[1])
if tonumber(ARGV[3]) > 0 then
redis.call('expire', KEYS[1], ARGV[3])
end
local result = redis.call('llen',KEYS[1])
return result
`
vint, e := redis.Int(conn.Do("eval", scriptf, keyNum, key, arg1, arg2, arg3))
if e != nil {
return 0, errorx.Wrap(e)
}
if vint == -3 {
return rll.maxLen, BeyondErr
}
return vint, nil
}
// key list的key
// value 压入的值
// maxLength 最大长度
// list打满后,将Pop出最老的,再push进新数据
func (rll RedisLimitList) LPushScheme2(conn redis.Conn, key string, value []byte) (int, error) {
var keyNum = 1 // eval的key的数量,为1
var arg1 = value
var arg2 = rll.maxLen // 队列最大长度
var arg3 = rll.expireSeconds // 队列key失效时间
var scriptf = `
local num = redis.call('llen',KEYS[1]);
if tonumber(ARGV[2])>0 and tonumber(num) >= tonumber(ARGV[2]) then
redis.call('rpop', KEYS[1])
end
redis.call('lpush',KEYS[1],ARGV[1])
if tonumber(ARGV[3]) > 0 then
redis.call('expire', KEYS[1], ARGV[3])
end
local result = redis.call('llen',KEYS[1])
return result
`
vint, e := redis.Int(conn.Do("eval", scriptf, keyNum, key, arg1, arg2, arg3))
if e != nil {
return 0, errorx.Wrap(e)
}
return vint, nil
}
func (rll RedisLimitList) LLen(conn redis.Conn, key string) int {
l, e := redis.Int(conn.Do("llen", key))
if e != nil {
return 0
}
return l
}
func (rll RedisLimitList) LRANGE(conn redis.Conn, key string, start int, stop int) ([] []byte, error) {
rsI, e := conn.Do("lrange", key, start, stop)
//fmt.Printf("%s\n", reflect.TypeOf(rsI).Name())
//fmt.Printf("%v\n", rsI)
return redis.ByteSlices(rsI, e)
}
func (rll RedisLimitList) RPOP(conn redis.Conn, key string) ([]byte, error) {
return redis.Bytes(conn.Do("rpop", key))
}
テスト、ユースケース
package redistool
import (
"fmt"
"testing"
)
func TestLimitList(t *testing.T) {
var ll =NewLimitList(20, 3,1)
conn := RedisPool.Get()
defer conn.Close()
// lenth, e := ll.LPushScheme2(conn, "test_limit_list", []byte(fmt.Sprintf("msg%d", i)))
lenth, e := ll.LPushScheme1(conn, "test_limit_list", []byte(fmt.Sprintf("msg%d", i)))
if e != nil && e != BeyondErr {
if e == BeyondErr {
fmt.Println(lenth, e.Error())
return
}
panic(e)
}
fmt.Println(lenth)
rs, e := ll.LRANGE(conn, "test_limit_list", 0, -1)
if e != nil {
panic(e)
}
for _, v := range rs {
fmt.Println(string(v))
}
}
出力
- 20秒間に何回挿入しても、これら3つの値は同じです
msg2
msg1
msg0
スキーム2を使用する場合(最も古いものをポップして最新のものを挿入する)、出力
- 5回挿入され、最後の2回は長すぎたため、msg0とmsg1が排出されました
msg4
msg3
msg2