✅ 包版本问题
注意包版本的问题,因为rockscache需要使用go-redis v8版本的Client,但是如果项目使用的是v9版本的话会有问题(文档中有),我是把官网的项目下载
下来了,(改动说明文档中有),其实就是少了一个Context方法多了一个其他的方法~
# 改动说明
rockscache官方地址:https://github.com/dtm-labs/rockscache
rockscache官方源码使用go-redis v8版本,拉到本地使用v9版本的go-redis并对其中的几个方法(Fetch、FetchBatch与TagAsDeletedBatch)做了简单的修改:
需要在调用的时候加上业务中的ctx。
之前只有 FetchBatch与TagAsDeletedBatch 这2个方法需要用 rdb.Client.Context() 初始化一下ctx,
现在将这2个方法也改成了需要传业务的ctx ,不影响原包的基本功能。
go-redis v9版本与v8版本的 redis.UniversalClient 接口不一样:
v9版本没有 Context 方法,多了一个 SSubscribe 方法,两个interface不一样。
✅ 使用场景建议
建议在"读多写少或有热key"的场景用rockscache,写操作多的话虽然可以使用Write-Through 结合分布式锁的方案但是也会影响性能。
举例:
在用户的账户系统中,所有用户的充值、道具的操作都会修改主账户的数据,主账户的数据写操作比较多,所以如果把
主账户的数据也放到redis中,需要加额外的影响性能的代码,而且考虑到主账户的数据量不是特别大,倒不如只将主账户的数据存到数据库中,只修改数据库的数据;
其他的一些有热key访问并且该key对应的数据需要一定的后台计算时间的场景,建议使用rockscache。
✅ 简单的用法及存储的改变
1、初始化及用法:
- 项目中封装初始化,需要用到go-redis Client
- 项目中使用:简单的get接口,用Fetch2那个方法讲解
2、存储的改变~在里面都存成了hash —— 因为内部使用lua一次执行,执行的过程中其他脚本或命令无法执行,而且同时保存 key/value
与 key/lock
// rockscache将string数据存成了hash类型,比如说,我们之前在redis中存的数据是string,用get方法获取:
```
get countValue1
"{\"id\":\"1\",\"count\":100,\"createTime\":\"2023-02-13T11:42:42+08:00\",\"updateTime\":\"2023-02-13T11:42:42+08:00\"}"
```
// 使用rockescache的话变成了hash,用hgetall方法获取:
```
hgetall countCache1
1) "value"
2) "{\"id\":\"1\",\"count\":100,\"createTime\":\"2023-02-13T11:42:42+08:00\",\"updateTime\":\"2023-02-13T11:42:42+08:00\"}"
```
✅ 配置项说明
1-1、详细讲一下那些配置项
func NewRocksCache(c *conf.Data, logger log.Logger, rdb *redis.Client) *rockscache_local.Client {
var dc = rockscache_local.NewClient(rdb, rockscache_local.NewDefaultOptions())
// 常用参数设置
// 1、强一致性(默认关闭强一致性,如果开启的话会影响性能)
dc.Options.StrongConsistency = false
// 2、redis出现问题需要缓存降级时设置为true
dc.Options.DisableCacheRead = false // 关闭缓存读,默认false;如果打开,那么Fetch就不从缓存读取数据,而是直接调用fn获取数据
dc.Options.DisableCacheDelete = false // 关闭缓存删除,默认false;如果打开,那么TagAsDeleted就什么操作都不做,直接返回
// 3、其他设置
// 标记删除的延迟时间,默认10秒,设置为3秒表示:被删除的key在3秒后才从redis中彻底清除
dc.Options.Delay = time.Second * time.Duration(3)
// 防穿透: 若fn返回空字符串,空结果在缓存中的缓存时间,默认60秒
dc.Options.EmptyExpire = time.Second * time.Duration(120)
// 防雪崩: 默认0.1,当前设置为0.1的话,如果设定为600的过期时间,那么过期时间会被设定为540s - 600s中间的一个随机数,避免数据出现同时到期
dc.Options.RandomExpireAdjustment = 0.1 // 设置为默认或不设置就行
/*
锁相关参数,这里配置的默认值,没有特殊情况建议默认
rockscache使用lua脚本一次性执行,执行过程中其他脚本或命令无法执行,
并且使用hash存储,同时保存 key/value 与 key/lock
*/
// 更新缓存时分配的锁的过期时间。默认为 3s。注意设置为下级计算数据时间的最大值。
dc.Options.LockExpire = time.Second * time.Duration(3)
// 锁失败后的重试等待时间 100ms
dc.Options.LockSleep = time.Millisecond * time.Duration(100)
log.NewHelper(logger).Infow("kind", "rocksCache", "status", "enable")
return dc
}
1-2、rockscache原理解读
1、参考(这个文章用的rockscache的版本有点低,参考一下就行):
https://xboom.github.io/2022/07/31/Microservices/微服务-缓存一致性/
2、lua脚本
使用脚本进行redis操作,lua的好处是一次性执行,执行过程其他脚本或命令无法执行(注意不确定参数)。
这里使用hash
进行数据存储,同时保存 key/value
与 key/lock
✅ 配置项演示
1、简单的get请求
FindCountById这个接口,使用Fetch2方法,传ctx。
数据库中的数据:
id count create_time update_time
1 100 2023-02-13 11:42:42 2023-02-13 11:42:42
2 200 2023-02-13 11:42:46 2023-02-13 11:42:46
一开始缓存中没有数据。
查询id为1的数据库中有的数据:
// 127.0.0.1:19000
{
"id": 1
}
可以看到打印出来:查询数据库了~~
查看下缓存:
hgetall countCache1
1) "value"
2) "{\"id\":\"1\",\"count\":100,\"createTime\":\"2023-02-13T11:42:42+08:00\",\"updateTime\":\"2023-02-13T11:42:42+08:00\"}"
再查询一下,就不会打印查询数据库了~此时会从redis中查数据
看一下缓存的TTL:
ttl countCache1
(integer) 89
2、缓存穿透(查id不存在数据的写法)
1、演示一下:缓存穿透,db中没有数据的情况~ 会生成一个空值(key对应的是countValue3,value里面的值是空字符串) ttl是自己设置的,❗️特别注意一下,需要代码中做“无记录”特殊的处理——不要返回错误!返回空字符串!
dc.Options.EmptyExpire = time.Second * time.Duration(120)
业务代码的写法:
func (r *layoutRepo) FindCountById(ctx context.Context, id int64) (*biz.Count, error) {
db := r.data.DB(ctx).Table(biz.CountTableName)
cacheKey := fmt.Sprintf(countCacheFormatKey, id)
value, errValue := r.data.rocksCache.Fetch2(ctx, cacheKey, time.Second*100, func() (string, error) {
// 缓存中没有数据默认从数据库查
fmt.Println("查数据库了~~~~~~~~~~~~~~~~~~~~~~~~")
model := &biz.Count{}
errFirst := db.Where("id = ?", id).First(model).Error
// ❗️❗️ ❗️ 数据库中无记录,返回一个空字符串~~redis中会记录一个这个key对应空字符串数据的记录,防止缓存穿透!
if errFirst == gorm.ErrRecordNotFound {
return "", nil
}
if errFirst != nil {
return "", errFirst
}
// Marshal
ma, errMa := json.Marshal(model)
if errMa != nil {
return "", errMa
}
return convertor.ToString(ma), nil
})
if errValue != nil {
return nil, errValue
}
ret := &biz.Count{}
// value在数据库没有数据时返回空,这里得做空判断
if value != "" {
body, errBody := convertor.ToBytes(value)
if errBody != nil {
return nil, errBody
}
errUnmarshal := json.Unmarshal(body, ret)
if errUnmarshal != nil {
return nil, errUnmarshal
}
}
return ret, nil
}
演示:发送一个id不存在的请求:
// 127.0.0.1:19000
{
"id": 333
}
一开始查询了一下数据库,但是后面就不再从数据库中查数据了,可以看到缓存中有了相对应的key:
hgetall countCache333
1) "value"
2) ""
3、redis降级场景
需要设置参数:
dc.Options.DisableCacheRead = true // 关闭缓存读,默认false;如果打开,那么Fetch就不从缓存读取数据,而是直接调用fn获取数据
dc.Options.DisableCacheDelete = true // 关闭缓存删除,默认false;如果打开,那么TagAsDeleted就什么操作都不做,直接返回
然后把docker中的redis关了,试试:
// redis关了
hgetall countCache333
Could not connect to Redis at 127.0.0.1:6379: Connection refused
发送下请求,可以看到,所有的查询都走了数据库~