文章

learn - 分布式内存缓存的妙用

learn - 分布式内存缓存的妙用

分布式内存缓存

背景:系统中为了降低数据存储的压力,业务与数据层之间会增加一层或两层缓存来用。然后数据层里面便包含了:缓存层+数据持久化层。

缓存分类:

  1. 在业务机器之外申请一个缓存,例如Redis/Memcache。优点是降低耦合,提高并发;缺点是增加资源成本。
  2. 在当前业务机器上,同一进程内或外,增加便利内存式缓存。优点是降低资源成本,能显著提高并发;缺点是增加了部分开发维护工作来保证集群情况下的数据一致性。

内存缓存LRU

优良的设计:可以是一个循环连表,容量大小由开发人控制。过期和超出容量时失效策略也是由程序员来定(最近最少使用:Least Recently Used )。

项目使用方案

抽象出一个lru要干什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// LRUCache is the interface for simple LRU cache.
type LRUCache interface {
        // Adds a value to the cache, returns true if an eviction occurred and
        // updates the "recently used"-ness of the key.
        // 目的:添加一条本地缓存记录
        Set(key, value interface{}, expiresIn int64) bool

        // Returns key's value from the cache and
        // updates the "recently used"-ness of the key. #value, isFound
        // 目的:拿到一条本地缓存(内部校验是否过期,并且设置最近使用)
        Get(key interface{}) (value interface{}, ok bool)

        // Checks if a key exists in cache without updating the recent-ness.
        // 目的:仅仅判断是否有该缓存被设置(不影响最近试用使用)
        Contains(key interface{}) (ok bool)

        // Returns key's value without updating the "recently used"-ness of the key.
        // 目的:拿到一条本地缓存(不影响最近试用使用,用的时候频率不高)
        Peek(key interface{}) (value interface{}, ok bool)

        // Removes a key from the cache.
        // 目的:删除缓存
        Remove(key interface{}) bool

        // Removes the oldest entry from cache.
        // 目的:删除一条最不常用的旧记录,返回该记录的key, value,  isFound
        RemoveOldest() (interface{}, interface{}, bool)

        // Returns the oldest entry from the cache. #key, value, isFound
        // 目的:返回一条最不常用的旧记录的key, value,  isFound。并不影响最近使用情况
        GetOldest() (interface{}, interface{}, bool)

        // Returns a slice of the keys in the cache, from oldest to newest.
        // 目的:返回所有的key列表,从最旧未使用到最近使用过的
        Keys() []interface{}

        // Returns the number of items in the cache.
        // 目的:返回当前的缓存使用长度
        Len() int

        // Clears all cache entries.
        // 目的:清空缓存
        Purge()

        // Resizes cache, returning number evicted
        // 目的:重新设置缓存容量
        Resize(int) int
}

具体实现上,可以自己写一个循环列表,或者用包 “container/list” 列表数据结构,进行包装。

软件设计架构上的运用

我不建议直接在model里面把 实体(entry)、映射关系(mapping)、数据访问(dal)、数据处理(dml)放在同一个文件内。有条件的话可以单独放在不同作用的package中。

例如包的目录结构建议:

1
2
3
4
5
6
7
8
9
10
./service/user/user_dto/   # 数据传输实体对象,有接收request也有resp,也有合并多个持久层数据后的中间结构 例如UserFullDTO  包含user基础信息/开关信息/资产信息等
./service/user/internal/dal # 对持久层的访问,包括增删改查,不做非访问的事情,dml可直接使用。让使用者无需关心持久层是怎么存储的(例如mysql/mssql/es/redis)。
./service/user/internal/dml  # 这里说仅给user服务提供的 数据处理和数据转换(数据持久层与业务数据操作。包含是否底层要增加缓存),若要与底持久层数据打交道,统统转交给dal,二次数据处理,例如redis/lru等
./service/user/usersrv.go   #  这里说提供user共同服务的封装,可以输出成resp数据,或者dto给其他logic用
./logic/user/user.go.  #  这里 user相关的逻辑处理,可以给handle用
./handle/web/user.go    # web控制器
./handle/admin/user.go  # admin控制器
./handle/pc/user.go    # pc控制器
./router/   ## 路由

引发lru变化

最后关心的是,分布式微服务下,如何去保证我们的本地内存缓存一致性。

参考如下图:

jpg

要求有一处负责更改数据,其他端负责监听数据的变动心跳即可。

本文由作者按照 CC BY 4.0 进行授权