func GenerateKey(keys ...string) string {
	return strings.Join(keys, KeySplitChar)
}
GenerateKey generate key for redis
package redis_client  
import (
	"context"
	"encoding/json"
	"fmt"
	"strings"
	"sync"
	"time"
	"github.com/go-redis/redis/v8"
	"github.com/pkg/errors"
	"go.uber.org/zap"
	"github.com/scryinfo/dot/dot"
)
const RedisClientTypeID = "0ae35550-7e37-4afe-866e-b129099759b7"
type configRedis struct {
	Addr                string `json:"addr"`
	KeepMaxVersionCount int64  `json:"keepMaxVersionCount"`  
	VersionFromRedis    bool   `json:"versionFromRedis"`     
}
type RedisClient struct {
	conf configRedis
	subscribe *redis.PubSub
	versions  sync.Map
	clientV8  *redis.Client
	ctx       context.Context     
	cancelFun context.CancelFunc  
}
type Operation uint8
const (
	Set Operation = iota
	Clean
)
type VersionControl struct {
	Op      Operation `json:"op"`
	Key     string    `json:"key"`
	Version string    `json:"version"`
}
func (c *VersionControl) Marshal() string {
	bs, err := json.Marshal(c)
	if err == nil {
		return string(bs)
	}
	return ""
}
func (c *VersionControl) Unmarshal(jsonStr string) (*VersionControl, error) {
	vc := c
	err := json.Unmarshal([]byte(jsonStr), vc)
	if err != nil {
		vc = nil
	}
	return vc, err
}
func (c *RedisClient) ClientV8() *redis.Client {
	return c.clientV8
}
func (c *RedisClient) GetVersion(category string) (string, error) {
	categoryKey := CategoryKey(category)
	if c.conf.VersionFromRedis {
		return c.clientV8.Get(c.ctx, categoryKey).Result()
	}
	versionCache, ok := c.versions.Load(categoryKey)
	if !ok {
		return "", errors.New(fmt.Sprintf("category: %s is not exist", category))
	}
	version, ok := versionCache.(string)
	if !ok {
		return "", errors.New(fmt.Sprintf("category: %s is not string type", category))
	}
	return version, nil
}
func (c *RedisClient) SetVersion(category, version string) error {
	categoryKey := CategoryKey(category)
	categoriesKey := CategoriesKey(category)
	pipe := c.clientV8.TxPipeline()
	pipe.Set(c.ctx, categoryKey, version, 0)
	pipe.ZAdd(c.ctx, categoriesKey, &redis.Z{
		Score:  0,
		Member: version,
	})  
	vcJSON := (&VersionControl{
		Op: Set, Key: categoryKey, Version: version,
	}).Marshal()
	pipe.Publish(c.ctx, VersionControlChannelName, vcJSON)
	_, err := pipe.Exec(c.ctx)
	if err != nil {
		dot.Logger().Errorln("redis set version failed", zap.NamedError("error", err))
		return err
	}
	if !c.conf.VersionFromRedis {
		c.versions.Store(categoryKey, version)
	}
	length, err := c.clientV8.ZCard(c.ctx, categoriesKey).Result()
	if err != nil {
		dot.Logger().Errorln("redis get list.length failed",
			zap.NamedError("error", err),
			zap.String("list name", category))
		return nil
	}
	if length > c.conf.KeepMaxVersionCount {
		if err = c.clientV8.ZRemRangeByRank(c.ctx, categoriesKey, length-c.conf.KeepMaxVersionCount, -1).Err(); err != nil {
			dot.Logger().Errorln("redis.rPop failed", zap.NamedError("error", err))
		}
	}
	return nil
}
func (c *RedisClient) Get(category, version, itemKey string) (string, error) {
	key := VersionItemKey(category, version, itemKey)
	return c.clientV8.Get(c.ctx, key).Result()
}
func (c *RedisClient) GetJsonSerialize(category, version, itemKey string, value interface{}) error {
	key := VersionItemKey(category, version, itemKey)
	str, err := c.clientV8.Get(c.ctx, key).Result()
	if err == nil {
		err = json.Unmarshal([]byte(str), value)
	}
	return err
}
func (c *RedisClient) Set(category, version, itemKey, value string, expiration time.Duration) (string, error) {
	key := VersionItemKey(category, version, itemKey)
	return c.clientV8.Set(c.ctx, key, value, expiration).Result()
}
func (c *RedisClient) SetJsonSerialize(category, version, itemKey string, value interface{}, expiration time.Duration) (string, error) {
	bs, err := json.Marshal(value)
	if err == nil {
		key := VersionItemKey(category, version, itemKey)
		jsonValue := string(bs)
		return c.clientV8.Set(c.ctx, key, jsonValue, expiration).Result()
	} else {
		return "", err
	}
}
func (c *RedisClient) Del(category, version, itemKey string) (int64, error) {
	key := VersionItemKey(category, version, itemKey)
	return c.clientV8.Del(c.ctx, key).Result()
}
func (c *RedisClient) CleanVersion(category string, version string) error {
	categoryKey := CategoryKey(category)
	categoriesKey := CategoriesKey(category)
	versionKey := VersionKey(category, version)
	var keys []string
	var cursor uint64
	var err error
	for {
		keys, cursor, err = c.clientV8.Scan(c.ctx, cursor, versionKey+KeySplitChar+"*", 100).Result()
		if err != nil {
			dot.Logger().Errorln("RedisClient CleanVersion", zap.NamedError("error", err))
			return err
		}
		if len(keys) > 0 {
			pipe := c.clientV8.TxPipeline()
			pipe.Del(c.ctx, keys...)
			_, err = pipe.Exec(c.ctx)
			if err != nil {
				dot.Logger().Errorln("RedisClient CleanVersion", zap.NamedError("error", err))
				return err
			}
		}
		if cursor < 1 {  
			break
		}
	}
	pipe := c.clientV8.TxPipeline()
	pipe.Del(c.ctx, categoryKey)
	pipe.ZRem(c.ctx, categoriesKey, version)
	vsJSON := (&VersionControl{
		Op: Clean, Key: categoryKey, Version: "",
	}).Marshal()
	pipe.Publish(c.ctx, VersionControlChannelName, vsJSON)
	_, err = pipe.Exec(c.ctx)
	if err != nil {
		dot.Logger().Errorln("RedisClient CleanVersion", zap.NamedError("error", err))
	}
	return err
}
func (c *RedisClient) GetVersions(category string) ([]string, error) {
	categoryKey := CategoriesKey(category)
	versions, err := c.clientV8.ZRange(c.ctx, categoryKey, 0, -1).Result()
	if err != nil {
		dot.Logger().Errorln("redis get all versions failed", zap.NamedError("error", err))
	}
	return versions, err
}
func (c *RedisClient) Create(dot.Line) error {
	c.clientV8 = redis.NewClient(&redis.Options{
		Addr: c.conf.Addr,
	})
	if c.conf.VersionFromRedis {
		return nil
	}
	go func() {
		c.subscribe = c.clientV8.Subscribe(c.ctx, VersionControlChannelName)
		for {
			select {
			case <-c.ctx.Done():
				return
			case msg, ok := <-c.subscribe.Channel():  
				if !ok || msg == nil {
					continue
				}
				vc := &VersionControl{}
				vc, err := vc.Unmarshal(msg.Payload)
				if err != nil {
					dot.Logger().Errorln("unmarshal key with version failed",
						zap.NamedError("error", err),
						zap.String("key with version", msg.Payload))
				} else {
					if vc.Op == Set {
						c.versions.Store(vc.Key, vc.Version)
					} else if vc.Op == Clean {
						c.versions.Delete(vc.Key)
					}
				}
			}
		}
	}()
	return nil
}
func (c *RedisClient) AfterAllDestroy(_ dot.Line) {
	if c.cancelFun != nil {
		c.cancelFun()  
	}
	if c.subscribe != nil {
		_ = c.subscribe.Close()
		c.subscribe = nil
	}
	if c.clientV8 != nil {
		_ = c.clientV8.Close()
		c.clientV8 = nil
	}
}
func newRedisClient(conf []byte) (dot.Dot, error) {
	dconf := &configRedis{}
	err := dot.UnMarshalConfig(conf, dconf)
	if err != nil {
		return nil, err
	}
	d := &RedisClient{conf: *dconf}
	d.ctx, d.cancelFun = context.WithCancel(context.Background())
	return d, nil
}
func RedisClientTypeLives() []*dot.TypeLives {
	return []*dot.TypeLives{{
		Meta: dot.Metadata{TypeID: RedisClientTypeID, NewDoter: newRedisClient},
	}}
}
func RedisClientTest(jsonConfig string) *RedisClient {
	d, err := newRedisClient([]byte(jsonConfig))
	if err != nil || d == nil {
		return nil
	}
	redisClient := d.(*RedisClient)
	_ = redisClient.Create(nil)
	time.Sleep(2 * time.Second)
	return redisClient
}
func RedisClientConfigTypeLive() *dot.ConfigTypeLive {
	return &dot.ConfigTypeLive{
		TypeIDConfig: RedisClientTypeID,
		ConfigInfo:   &configRedis{},
	}
}