代码地址:https://gitee.com/lymgoforIT/golang-trick/tree/master/19-sorted-json
一:需求与方案
要求实现对于某个数据,一个月内如果内容相同只能存入DB
一次。如有时候系统需要根据一些算法结果,决定给用户发送短信或者站内信,但是算法计算得出的内容可能相同,这样一个月内发送同样内容肯定是不太合适的 ,那么如何去重呢?
方案一:使用纯DB
通过查询DB
获取上次发送站内信的内容以及发送时间,与本次需要发送的内容以及时间做对比,如下:
上面的方案思路是没有问题的,但是如果用户量很大,且每个用户每天都会判断一次,如果还是集中在高峰期的话,对DB
的压力还是蛮大的,所以很容易想到还是redis
缓存比较好。
方案二:Redis + DB
将主要的读压力给到了Redis
,提高了性能,也保护了DB
,相对方案一主要是改动了标蓝色的地方,如下:
- 将从
DB
获取数据改为从Redis
获取,减轻DB
压力,也提高了性能。Redis
中查出无结果可认为是从未发过站内信,或者距离上次已经超过一个月,key
过期了。 - 比较时,只需要比较站内信内容了,距今是否一个月可从
Redis
中查询有结果判断出没有超过一个月。 - 发送结果存入
DB
后,需要更新Redis
中对应key
的value
和过期时间为一个月。
二:对比内容是否一致
Go
语言的JSON
是有序的,即JSON
的输出顺序是按照结构体定义中的字段顺序来排列的,也就是说,Go
中结构体序列化为JSON
后,其格式是有序的。这种特性在一些场景下非常实用,比如此处比较内容。此外,Go
中比较两个结构体,比较的是结构体的每个字段内容是否相等。
package main
import (
"encoding/json"
"fmt"
)
type Content struct {
Image string `json:"image"`
Text string `json:"text"`
Video string `json:"video"`
}
func main() {
c1 := Content{
Image: "https://i-blog.csdnimg.cn/blog_migrate/b1689a808984943fca16eb774fa52cc1.png",
Text: "尊敬的XXX:最近。。。。",
}
res1, _ := json.Marshal(c1)
// 注意:Text写到了Image前面,且School赋值了空串,模拟没有视频链接
c2 := Content{
Text: "尊敬的XXX:最近。。。。",
Image: "https://i-blog.csdnimg.cn/blog_migrate/b1689a808984943fca16eb774fa52cc1.png",
Video: "",
}
res2, _ := json.Marshal(c2)
fmt.Println("c1:", c1)
fmt.Println("c2:", c2)
fmt.Println("c1 == c2:", c1 == c2)
fmt.Println("res1:", string(res1))
fmt.Println("res2:", string(res2))
fmt.Println("string(res1) == string(res2):", string(res1) == string(res2))
// 验证反序列化后是否相等
var (
c3 = &Content{}
c4 = &Content{}
)
_ = json.Unmarshal(res1, c3)
_ = json.Unmarshal(res2, c4)
fmt.Println()
fmt.Println()
fmt.Println()
fmt.Println("c3:", c3)
fmt.Println("c4:", c4)
// 注意是*c3和*c4,这才是比较结构体中每个字段是否相等,如果是c3和c4比较的是地址
fmt.Println("*c3 == *c4:", *c3 == *c4)
fmt.Println("c3 == c4:", c3 == c4)
}
输出结果:
c1: {https://img-blog.csdnimg.cn/9912dbf5bbf944faa570f84f83ae71d2.png 尊敬的XXX:最近。。。。 }
c2: {https://img-blog.csdnimg.cn/9912dbf5bbf944faa570f84f83ae71d2.png 尊敬的XXX:最近。。。。 }
c1 == c2: true
res1: {"image":"https://i-blog.csdnimg.cn/blog_migrate/b1689a808984943fca16eb774fa52cc1.png","text":"尊敬的XXX:最近。。。。","video":""}
res2: {"image":"https://i-blog.csdnimg.cn/blog_migrate/b1689a808984943fca16eb774fa52cc1.png","text":"尊敬的XXX:最近。。。。","video":""}
string(res1) == string(res2): true
c3: &{https://img-blog.csdnimg.cn/9912dbf5bbf944faa570f84f83ae71d2.png 尊敬的XXX:最近。。。。 }
c4: &{https://img-blog.csdnimg.cn/9912dbf5bbf944faa570f84f83ae71d2.png 尊敬的XXX:最近。。。。 }
*c3 == *c4: true
c3 == c4: false
Process finished with the exit code 0
从输出结果中可以发现如下三点:
- 当两个结构体的每个字段赋值是一样时(即使赋值顺序不一样,或者没有赋值的字段使用了默认值),进行比较时是相等的。
- 将他们序列化字符串后,字符串
JSON
中的字段顺序就是结构体定义的顺序,且两个字符串相等。 - 反序列化后,两个结构体内容相等。但是结构体指针因为地址不同,所以不相等。
三:需求实现
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/go-redis/redis/v8"
"time"
)
var redisClient *redis.Client
var ctx = context.Background()
var SceneLimitKey = "SceneKey_%d" // 场景key_用户id 保证唯一不与其他场景冲突即可。 redis string类型 获取上次所发站内信内容
var MonthExpireTime = time.Duration(30*86400) * time.Second // 为了简化,一个月定为30天
func init() {
config := &redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0, // 使用默认DB
PoolSize: 15,
MinIdleConns: 10, //在启动阶段创建指定数量的Idle连接,并长期维持idle状态的连接数不少于指定数量;。
//超时
//DialTimeout: 5 * time.Second, //连接建立超时时间,默认5秒。
//ReadTimeout: 3 * time.Second, //读超时,默认3秒, -1表示取消读超时
//WriteTimeout: 3 * time.Second, //写超时,默认等于读超时
//PoolTimeout: 4 * time.Second, //当所有连接都处在繁忙状态时,客户端等待可用连接的最大等待时长,默认为读超时+1秒。
}
redisClient = redis.NewClient(config)
}
// 假设站内信内容包含了图片、文本、视频
type Content struct {
Image string `json:"image"`
Text string `json:"text"`
Video string `json:"video"`
}
func main() {
// 本次想要给123用户发送站内信
userId := 234
content := Content{
Image: "https://i-blog.csdnimg.cn/blog_migrate/b1689a808984943fca16eb774fa52cc1.png",
Text: "尊敬的XXX:最近。。。。",
}
contextStr, _ := json.Marshal(content)
sceneLimitKey := fmt.Sprintf(SceneLimitKey, userId)
fmt.Println("sceneLimitKey:", sceneLimitKey)
exists, _ := redisClient.Exists(ctx, sceneLimitKey).Result()
if exists != 1 { // 还没有发过站内信,或者距离上次发送已经超过一个月,key过期了
fmt.Printf("模拟发送站内信成功,content:%v\n", content)
fmt.Printf("模拟站内信内容存入DB成功,content:%v\n", content)
// 站内信发送成功后更新redis
redisClient.Set(ctx, sceneLimitKey, string(contextStr), MonthExpireTime)
return
}
// redis中有结果,则需要比较两次站内信内容是否一致了
res, _ := redisClient.Get(ctx, sceneLimitKey).Result()
if res == string(contextStr) {
fmt.Println("本月已经发过相同内容的站内信了,不要重复发送哦。")
return
}
}
四:测试
开启Redis
服务端,首次执行程序
Redis
客户端查看key
内容和过期时间
再次执行程序,会因为内容相同而被拦截