序言
偶然间登录掘金账号,想起了周末去咖啡店写文章的我,现在改为了健身(健康为主,力争满头秀发),还是觉得惭愧,既然路过就写一段吧。深度不深,只是通过一个小案例引发的一些思考,不喜勿喷~
背景
在日常工作中,我们经常会遇到需要使用缓存的场景,例如存一些临时数据,减少下游存储介质访问等。
首先,我们要抉择remote cache还是local cache, remote cache通常以中间件的形式在其mem中存储,服务集群中的各个节点都可以通过相同的key访问remote cache拿到数据;local cache则采用该服务的内存资源,服务集群的各个节点存储自己所需要的数据,各个节点的key分布各不相同(随着业务而定)。
其次,该以什么形式存储缓存数据,remote cache比较简单,像redis会规定存序列化数据;而local cache属于自由发挥,我们可以直接set一个对象进去,get时也不用反序列化。
那local cache存对象不是一劳永逸吗?既节省耗时又省序列化开销。
Local Cache存对象
简单用golang
来释义(省略了error等其他逻辑),实际工作当中,看到了如下代码:
scss
// lru cache实现
func (l *lruCache) Set(key string, val *Data) {
l.lock.Lock()
defer l.lock.Unlock()
l.Set(key, val)
}
func (l *lruCache) Get(key string) *Data {
l.lock.Lock()
defer l.lock.Unlock()
return l.Get(key)
}
// 业务
data := l.Get(key)
// 拿到data,继续写逻辑
...
可以看到,local cache存了对象,看起来没啥问题,因为从cache里拿到对象继续写业务逻辑,也对并发场景用mutex进行了处理。
注意点:怎么保证从cache里拿到的对象是immutable?
如果仔细看就会发现,我虽然在cache实现里用锁做了控制,但我拿到的data指针是不可控的,其他人很可能就改了里面的数据。
kotlin
// 业务
data := l.Get(key)
// 拿到data,我要改里面的value
...
data.value = b
...
这样的情况下,cache里的数据也被更改,在并发环境中甚至会热key会被频繁更改,导致线上问题。 当然,像C++/Java为对象和成员变量提供了些immutable语义,但这种设计是否把"锅"甩到了cache方法之外呢? 如果必须要改数据(很有可能过了一阵子有一个新需求),后续业务逻辑根据场景处理浅拷贝/深拷贝,保证cache里的数据不被变更。
Local Cache存序列化数据
存序列化数据就和remote cache实现方式差不多了(error处理等逻辑略过)。
scss
// lru cache实现
func (l *lruCache) Set(key string, val *Data) {
bytes, _ := marshal(&val)
l.lock.Lock()
defer l.lock.Unlock()
l.Set(key, bytes)
}
func (l *lruCache) Get(key string) *Data {
l.lock.Lock()
bytes, _ := l.Get(key)
l.lock.Unlock()
d, _:= unmarshal(bytes)
return d
}
// 业务
data := l.Get(key)
// 拿到data,继续写逻辑
...
这样,后续业务逻辑就不会担心对对象做任何操作,因为相当于进行了一次深拷贝,拿到的就是新的对象。
总结
从上述小的例子可以看出,使用local cache也要注意场景,特别是关注cache中取出来的数据有无变更,导致影响业务逻辑。