目前go語言有一些比較可靠的異步隊列的開源組件比如asynq、Machinery等,但是組件一個有學習成本,二是出現問題的時候比較頭疼,排查起來比較費時間還要分析源碼,所以自己比較傾向於寫一個輕量級。
此過程使用的是redis的list,左進右出,一般生產者使用lpush壓入數據,消費者調用rpop取出數據。這個具體看自己的需求,如果任務是可以做到冪等操作的,可以使用lrange+ltrim替代lpop,在處理成功後調用ltrim,這樣可做到至少處理一次。
首先在應用啓動的時候初始話加載redis連接
package initialization import ( "context" "fmt" "log" "time" "github.com/go-redis/redis/v8" ) var RedisClient *redis.Client type RedisOptoins struct { Host string `yaml:"host" json:"host"` Port int `yaml:"port" json:"port"` DB int `yaml:"db" json:"db"` PoolSize int `yaml:"pool_size" json:"pool_size"` MinIdle int `yaml:"min_idle" json:"min_idle"` } func NewRedisClient() *redis.Client { redis_config := Config.Redis RedisClient = redis.NewClient(&redis.Options{ Addr: fmt.Sprintf("%s:%d", redis_config.Host, redis_config.Port), PoolSize: redis_config.PoolSize, MinIdleConns: redis_config.MinIdle, DB: redis_config.DB, PoolTimeout: time.Second * 5, WriteTimeout: time.Second * 5, IdleCheckFrequency: 60 * time.Second, //閒置連接檢查的週期,默認爲1分鐘,-1表示不做週期性檢查,只在客戶端獲取連接時對閒置連接進行處理。 IdleTimeout: 5 * time.Minute, //閒置超時,默認5分鐘,-1表示取消閒置超時檢查 MaxConnAge: 600 * time.Second, //連接存活時長,從創建開始計時,超過指定時長則關閉連接,默認爲0,即不關閉存活時長較長的連接 //命令執行失敗時的重試策略 MaxRetries: 1, // 命令執行失敗時,最多重試多少次,默認爲0即不重試 MinRetryBackoff: 8 * time.Millisecond, //每次計算重試間隔時間的下限,默認8毫秒,-1表示取消間隔 MaxRetryBackoff: 512 * time.Millisecond, //每次計算重試間隔時間的上限,默認512毫秒,-1表示取消間隔 }) var ctx = context.Background() pong, err := RedisClient.Ping(ctx).Result() if err != nil { fmt.Println("redis 連接失敗:", pong, err) log.Panicf("redis connect failed: %v", err) } return RedisClient }
然後將任務寫入redis
if err := initialization.RedisClient.LPush(ctx, string(QueueName), string(queueValue)).Err(); err != nil { return err }
然後啓動一個異步監聽
func ListenQueue(queueName string) { // 確保即使某個代碼執行異常,整個程序不會panic中斷 defer func() { err := recover() if err != nil { log.Println("程序遭遇嚴重異常!!!!") ListenQueue(queueName) } }() for { ctx := initialization.NewTraceIDContext() // 每個業務啓動創建一個trace_id values, err := initialization.RedisClient.BRPop(ctx, 5*time.Second, queueName).Result() // 設置一個5秒的超時時間 if err == redis.Nil { // 查詢不到數據 time.Sleep(1 * time.Second) continue } if err != nil { // 查詢出錯,則每隔10秒重新查詢 time.Sleep(10 * time.Second) continue } initialization.WithContext(ctx).Info("消費到數據:", values) if len(values) != 2 { initialization.WithContext(ctx).Errorf("數據格式不正確,需要長度爲2,但實際長度%d", len(values)) continue } // TODO 消費操作,values是數組,但理論上values的默認長度是1 QueueExecute(ctx, queueName, values[1]) // 實際執行任務處理的方法 } }
用這種方式,需要注意,超時時間要稍微長一些,因爲超時時間短了,會觸發重試,一旦重試可能會造成隊列裏面的數據丟失