Redis固定長キュー(Redisクラスターをサポート)

序文

一部のビジネスシナリオでは、アトミックで信頼性が高く、分散された固定長キューを実装する必要があります。次の機能が必要です。

  • キューには有効期限があります。
  • キューの最大長は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

おすすめ

転載: blog.csdn.net/fwhezfwhez/article/details/113741980