文章目录
- [1. Pipeline](#1. Pipeline)
- [2. 事务](#2. 事务)
- [3. 结构体存储](#3. 结构体存储)
- [4. Pub/Sub 发布订阅](#4. Pub/Sub 发布订阅)
- [5. Lua 脚本](#5. Lua 脚本)
- [6. 滑动窗口](#6. 滑动窗口)
- [7. 小结](#7. 小结)
本系列文章:
- Java 转 go 学习 - 项目管理
- Java 转 go 学习 - 基本语法
- Java 转 go 学习 - 类型转换
- Java 转 go 学习 - 流程控制结构
- Java 转 go 学习 - 数组和切片
- Java 转 go 学习 - map
- Java 转 go 学习 - 函数(1)
- Java 转 go 学习 - 函数(2)
- Java 转 go 学习 - 结构体
- Java 转 go 学习 - 接口
- Java 转 go 学习 - 并发编程(1)
- Java 转 go 学习 - 并发编程(2)
- Java 转 go 学习 - 并发编程(3)
- Java 转 go 学习 - web 编程
- Java 转 go 学习 - Hertz 学习(1)
- Java 转 go 学习 - Hertz 学习(2)
- Java 转 go 学习 - Redis(1)
1. Pipeline
正常情况下命令执行的流程就是:发一条命令 → 等待 Redis 返回 → 再发下一条。
但是如果需要执行多条命令就需要进行多次命令发送,也就是有多次网络往返的开销,Pipeline 就是解决这个问题,会把 多条命令打包一次发给 Redis → 一次性拿回所有结果,下面是 pipeline 的两种写法。
- 通过
Pipeline()先获取管道,再通过Set方法添加要设置的k-v。 - 通过
Pipelined方法直接在匿名函数里面设置要添加的k-v。
go
func (s *RedisService) PipelineSet(ctx context.Context, kv map[string]string, expire time.Duration) error {
pipe := s.rdb.Pipeline()
defer pipe.Discard()
for k, v := range kv {
pipe.Set(ctx, k, v, expire)
}
// 统一执行命令
_, err := pipe.Exec(ctx)
return err
}
func (s *RedisService) PipelinedSet(ctx context.Context, kv map[string]string, expire time.Duration) error {
// 更加间接的写法
_, err := s.rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {
for k, v := range kv {
pipe.Set(ctx, k, v, expire)
}
return nil
})
return err
}
执行的例子和结果如下。
go
func (s *RedisService) RunDemo(ctx context.Context) error {
if err := s.PipelineSet(ctx, map[string]string{
"demo:pipe:k1": "v1",
"demo:pipe:k2": "v2",
}, 60*time.Second); err != nil {
return err
}
fmt.Println("PipelineSet: ok")
if err := s.PipelinedSet(ctx, map[string]string{
"demo:pipelined:k1": "v1",
"demo:pipelined:k2": "v2",
}, 60*time.Second); err != nil {
return err
}
fmt.Println("PipelinedSet: ok")
return nil
}
2. 事务
Redis 的事务能确保原子性,也就是所有命令要么全部执行,要么全部不执行,但是出错了没办法回滚,下面是 Go 使用事务的几个命令。
TxPipeline开启事务。Exec执行事务。Discard取消事务。
下面来看下事务的简单用法。
go
func (s *RedisService) TxPipelineSet(ctx context.Context, kv map[string]string, expire time.Duration) error {
// 开启事务
pipe := s.rdb.TxPipeline()
// 记得取消事务
defer pipe.Discard()
// 设置执行命令
for k, v := range kv {
pipe.Set(ctx, k, v, expire)
}
// 执行事务
_, err := pipe.Exec(ctx)
return err
}
那如果有多个事务同时执行,在当前事务执行的时候其他事务已经把当前事务读到的 key 修改了,这种情况下就会造成 value 的覆盖,这种情况下我们可以用 Watch + 事务 来监听某个 key,如果在事务执行前发现 key 已经修改过,事务就会直接失败,我们可以重试确保安全。
go
func (s *RedisService) WatchIncr(ctx context.Context, key string, delta int64) error {
return s.rdb.Watch(ctx, func(tx *redis.Tx) error {
// 先获取当前值
current, err := tx.Get(ctx, key).Int64()
if err != nil && err != redis.Nil {
return err
}
// 要更新的值
next := current + delta
// CAS 更新, 通过匿名函数, 简单写法
_, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
pipe.Set(ctx, key, strconv.FormatInt(next, 10), 0)
return nil
})
return err
// 监听 key
}, key)
}
这里通过 Watch 对 key 监听,所以外层调用需要检测是否失败,如果失败需要有重试机制。
go
// 简单重试
for i := 0; i < 10; i++ {
err := WatchIncr(ctx, key, delta)
if err == nil {
break
}
}
下面是两种用法的简单测试示例。
go
func (s *RedisService) RunDemo(ctx context.Context) error {
if err := s.TxPipelineSet(ctx, map[string]string{
"demo:txpipe:k1": "tv1",
"demo:txpipe:k2": "tv2",
}, 60*time.Second); err != nil {
return err
}
fmt.Println("TxPipelineSet: ok")
if err := s.SetString(ctx, "demo:tx:counter", "0"); err != nil {
return err
}
// 这里就不重试了
if err := s.WatchIncr(ctx, "demo:tx:counter", 5); err != nil {
return err
}
counter, err := s.GetString(ctx, "demo:tx:counter")
if err != nil {
return err
}
fmt.Println("WatchIncr counter:", counter)
// TxPipelineSet: ok
// WatchIncr counter: 5
return nil
}
3. 结构体存储
实际项目里面我们都会将结构体转成 JSON 存到 Redis,然后读取的时候再把 JSON 从 Redis 中读出来,然后还原成结构体。
go
func (s *RedisService) SetJSON(ctx context.Context, key string, value any, expire ...time.Duration) error {
data, err := json.Marshal(value)
if err != nil {
return fmt.Errorf("json marshal failed: %w", err)
}
ttl := time.Duration(0)
if len(expire) > 0 && expire[0] > 0 {
ttl = expire[0]
}
return s.rdb.Set(ctx, key, data, ttl).Err()
}
func (s *RedisService) GetJSON(ctx context.Context, key string, dst any) error {
raw, err := s.rdb.Get(ctx, key).Result()
if err != nil {
return err
}
if err := json.Unmarshal([]byte(raw), dst); err != nil {
return fmt.Errorf("json unmarshal failed: %w", err)
}
return nil
}
这两个方法就是通用的 get 和 set 方法,结构体类型都用 any 来表示,也就是 interface{}。下面是调用的示例。
go
type UserProfile struct {
Name string `json:"name"`
Age int `json:"age"`
Phone string `json:"phone"`
}
func (s *RedisService) RunDemo(ctx context.Context) error {
user := UserProfile{Name: "Alice", Age: 20, Phone: "18800000000"}
if err := s.SetJSON(ctx, "demo:json:user:1", user, 60*time.Second); err != nil {
return err
}
fmt.Println("SetJSON: ok")
var got UserProfile
if err := s.GetJSON(ctx, "demo:json:user:1", &got); err != nil {
return err
}
fmt.Println("GetJSON:", got)
return nil
// SetJSON: ok
// GetJSON: {Alice 20 18800000000}
}
4. Pub/Sub 发布订阅
一般来说都用 RocketMQ/Kafka 等消息队列,不过既然学了 Redis 也可以看下 Go 语言怎么使用发布订阅模式,下面是订阅的代码。
go
func (s *RedisService) SubscribeSingle(ctx context.Context, channel string) (*redis.PubSub, error) {
// 订阅单个通道
pubSub := s.rdb.Subscribe(ctx, channel)
// 等待订阅确认
if _, err := pubSub.Receive(ctx); err != nil {
_ = pubSub.Close()
return nil, err
}
return pubSub, nil
}
func (s *RedisService) SubscribeMulti(ctx context.Context, channels ...string) (*redis.PubSub, error) {
// 订阅多个通道
pubSub := s.rdb.Subscribe(ctx, channels...)
// 等待订阅确认
if _, err := pubSub.Receive(ctx); err != nil {
_ = pubSub.Close()
return nil, err
}
return pubSub, nil
}
func (s *RedisService) PSubscribe(ctx context.Context, patterns ...string) (*redis.PubSub, error) {
// 订阅通道, 模糊匹配
pubSub := s.rdb.PSubscribe(ctx, patterns...)
// 等待订阅确认
if _, err := pubSub.Receive(ctx); err != nil {
_ = pubSub.Close()
return nil, err
}
return pubSub, nil
}
然后就是发布消息的方法。
go
// 发布消息
func (s *RedisService) Publish(ctx context.Context, channel, payload string) error {
return s.rdb.Publish(ctx, channel, payload).Err()
}
我们可以通过 Channel() 方法来获取到各个 Sub 方法返回的通道,然后通过 Select 来监听。
go
func receiveOneMessageAsync(msgCh <-chan *redis.Message, timeout time.Duration) <-chan messageResult {
resultCh := make(chan messageResult, 1)
go func() {
msg, err := receiveOneMessage(msgCh, timeout)
resultCh <- messageResult{msg: msg, err: err}
}()
return resultCh
}
最后再来看下主程序的示例代码和输出。
go
func (s *RedisService) RunDemo(ctx context.Context) error {
// 订阅 demo:pubsub:single 通道
singleSub, err := s.SubscribeSingle(ctx, "demo:pubsub:single")
if err != nil {
return err
}
defer singleSub.Close()
// 发布消息到 demo:pubsub:single
if err := s.Publish(ctx, "demo:pubsub:single", "hello-single"); err != nil {
return err
}
// goroutine 获取消息输出到 singleResult 这个只读通道, 然后在这里获取结果
singleResult := <-receiveOneMessageAsync(singleSub.Channel(), 2*time.Second)
if singleResult.err != nil {
return singleResult.err
}
fmt.Printf("Single channel => channel=%s payload=%s\n", singleResult.msg.Channel, singleResult.msg.Payload)
// 订阅 demo:pubsub:c1, demo:pubsub:c2 通道
multiSub, err := s.SubscribeMulti(ctx, "demo:pubsub:c1", "demo:pubsub:c2")
if err != nil {
return err
}
defer multiSub.Close()
// goroutine 获取消息输出到 multiSub 这个只读通道, 然后在这里获取结果
if err := s.Publish(ctx, "demo:pubsub:c2", "hello-multi"); err != nil {
return err
}
multiResult := <-receiveOneMessageAsync(multiSub.Channel(), 2*time.Second)
if multiResult.err != nil {
return multiResult.err
}
fmt.Printf("Multi channel => channel=%s payload=%s\n", multiResult.msg.Channel, multiResult.msg.Payload)
// 订阅 demo:pubsub:user: 开头的通道
patternSub, err := s.PSubscribe(ctx, "demo:pubsub:user:*")
if err != nil {
return err
}
defer patternSub.Close()
// goroutine 获取消息输出到 patternSub 这个只读通道, 然后在这里获取结果
if err := s.Publish(ctx, "demo:pubsub:user:1001", "hello-pattern"); err != nil {
return err
}
patternResult := <-receiveOneMessageAsync(patternSub.Channel(), 2*time.Second)
if patternResult.err != nil {
return patternResult.err
}
fmt.Printf("Pattern channel => pattern=%s channel=%s payload=%s\n", patternResult.msg.Pattern, patternResult.msg.Channel, patternResult.msg.Payload)
return nil
// Single channel => channel=demo:pubsub:single payload=hello-single
// Multi channel => channel=demo:pubsub:c2 payload=hello-multi
// Pattern channel => pattern=demo:pubsub:user:* channel=demo:pubsub:user:1001 payload=hello-pattern
}
5. Lua 脚本
Lua 的作用就不多说了,把多条命令打包成一个脚本来执行,下面主要来看下如何执行 lua 脚本。
go
func (s *RedisService) LuaIncrOrInit(ctx context.Context, key string, init, delta int64, expireSec int64) (int64, error) {
// 计数, 如果不存在 key 就设置 key, 存在就 +1, 返回 inc 的结果
const script = `
local exists = redis.call("EXISTS", KEYS[1])
if exists == 0 then
redis.call("SET", KEYS[1], ARGV[1], "EX", ARGV[3])
return tonumber(ARGV[1])
end
return redis.call("INCRBY", KEYS[1], ARGV[2])
`
// 执行 lua 脚本
res, err := s.EvalLua(ctx, script, []string{key}, init, delta, expireSec)
if err != nil {
return 0, err
}
switch v := res.(type) {
case int64:
return v, nil
case string:
return strconv.ParseInt(v, 10, 64)
default:
return 0, fmt.Errorf("unexpected lua result type: %T", res)
}
}
- KEYS1:传入的第一个键(推荐用 KEYS 传所有键)
- 后面的 init、delta、expireSec 作为 ARGV 传进去
下面是调用示例。
go
func (s *RedisService) RunDemo(ctx context.Context) error {
luaKey := "demo:lua:counter"
if _, err := s.Del(ctx, luaKey); err != nil {
return err
}
v1, err := s.LuaIncrOrInit(ctx, luaKey, 100, 5, 60)
if err != nil {
return err
}
v2, err := s.LuaIncrOrInit(ctx, luaKey, 100, 5, 60)
if err != nil {
return err
}
fmt.Printf("LuaIncrOrInit => first=%d second=%d\n", v1, v2)
// LuaIncrOrInit => first=100 second=105
return nil
}
6. 滑动窗口
下面我们看下用 Redis 实现一个滑动窗口,主要用的就是 zset,score 就是时间,下面使用 pipeline 来统计计数,实际上用 lua 表达式会比较好。
go
func (l *SlidingWindowLimiter) Allow(ctx context.Context, userKey string) (bool, int64, error) {
redisKey := l.prefix + userKey
nowMs := time.Now().UnixMilli()
windowStartMs := nowMs - l.window.Milliseconds()
member := fmt.Sprintf("%d-%d", nowMs, time.Now().UnixNano())
// 开启事务
pipe := l.rdb.TxPipeline()
// 删除 [0, nowMs - l.window.Milliseconds()) 区间的计数
pipe.ZRemRangeByScore(ctx, redisKey, "0", strconv.FormatInt(windowStartMs, 10))
// 计数 + 1
pipe.ZAdd(ctx, redisKey, redis.Z{Score: float64(nowMs), Member: member})
// 然后统计计数
countCmd := pipe.ZCard(ctx, redisKey)
// 设置过期时间, time.Second 是留一点缓冲
pipe.Expire(ctx, redisKey, l.window+time.Second)
if _, err := pipe.Exec(ctx); err != nil {
return false, 0, err
}
// 满了
current := countCmd.Val()
if current > l.limit {
_, _ = l.rdb.ZRem(ctx, redisKey, member).Result()
return false, 0, nil
}
remaining := l.limit - current
if remaining < 0 {
remaining = 0
}
return true, remaining, nil
}
接下来看下调用示例。
go
func RunSlidingWindowDemo(ctx context.Context, rdb *redis.Client) error {
limiter := NewSlidingWindowLimiter(rdb, "sliding:demo:", 5, 10*time.Second)
user := "user:1001"
for i := 1; i <= 8; i++ {
allowed, remain, err := limiter.Allow(ctx, user)
if err != nil {
return err
}
fmt.Printf("request=%d allowed=%v remaining=%d\n", i, allowed, remain)
time.Sleep(500 * time.Millisecond)
}
// request=1 allowed=true remaining=4
// request=2 allowed=true remaining=3
// request=3 allowed=true remaining=2
// request=4 allowed=true remaining=1
// request=5 allowed=true remaining=0
// request=6 allowed=false remaining=0
// request=7 allowed=false remaining=0
// request=8 allowed=false remaining=0
return nil
}
7. 小结
redis 就先学到这里,主要演示了下一些常用的方法,后面继续学习 mysql 的相关内容。
如有错误,欢迎指出!!!