Memcached Meta Commands
涛叔Memcached 是老牌的内存缓存服务器。因为不支持像 Redis 那样丰富的数据结构,Memcached 用的越来越少。但我认为 Memcached 是 Unix 哲学的典范,一次只做一件事,并把它做好。而 Memcached 就是专注于内存缓存这一件事。其中,Memcached 在 1.6 版本引入的 Meta Commands 就是最好的佐证。今天就给大家介绍一下 Meta Commands(元指令)。
元指令的结构如下:
<cm> <key> <datalen*> <flag1> <flag2> <...>\r\n
这里<cm>
表示command
,使用两位字母表示不同的指令。<key>
表示要操作的键值。<datalen*>
表示本次请求数据部分的长度,有的元指令不需要传输额外数据,那就不需要这个字段。再后面就是 Meta 命令的精华:flag(标记)。每个元指令可以附加多个标记来影响 Memcached 的行为和返回值的内容。
元指令返回值结构如下:
<RC> <datalen*> <flag1> <flag2> <...>\r\n
这里<RC>
表示return code
,也就是返回码。<RC>
跟<cm>
一样,也是使用两位字母表示。同样有可选的<datalen*>
,后面附带多个标记。
下面给大家展示几个元指令的示例。
设置缓存:
ms foo 2 T90 F1\r\n
hi\r\n
HD\r\n
元指令以m
开头。ms
表示set
,用于设置缓存。在这个例子中,foo
是键名,2
表示数据的长度。再后面是两个标记参数:T90
表示设置TTL为90秒,F1
表示将缓存对应的业务数据设为1
。另起一行传输数据,长度为二。
查询缓存:
mg foo t f v\r\n
VA 2 s2 t78 f1\r\n
hi\r\n
mg
表示get
,用于查询缓存信息。本例中使用了三个标记参数:t
表示返回缓存的TTL,f
表示返回缓存的业务标记数据,v
表示返回缓存数据。是的,mg
默认不返回缓存数据。因为在很多场景下,缓存的内容会比较大,而我们又只需要查看TTL/CAS之类的信息,所以mg
默认不返回缓存的数据,需要加上v
标记才会返回。
删除缓存:
md foo I\r\n
HD\r\n
md
表示delete
,用于删除缓存。但在删除的时候,也可以通过标记来调整服务器的行为。比如这里的I
就是告诉服务端把缓存标记为过期(stale),并不实际删除。具体用法我们下面再说。
有了上面的基础,我们接下来就介绍元指令具体是怎么解决各类缓存问题的。总共有三类问题,分别是:
- 缓存穿透问题
- 热Key问题
- 数据传输与内存占用问题
缓存穿透问题
所谓缓存穿透就是在缓存失效或者删除之后,大量请求回源而导致的系统性能问题。应对穿透问题的方法也无外乎两类:一类是使用回源锁,只有拿到并发锁的请求才可以回源;另一类就是避免缓存失效。使用元指令,就可以轻松实现这两类方法。
先说回源并发锁。
我们在查询缓存的时候可以使用N
标记:
mg foo c v N30
VA 0 c2 W
这里表示查询foo
对应的缓存,如果不存在,则创建新的空缓存。如果同时有多个请求执行这条语句,只有一个请求的返回值中有W
标记,表示拿到了回源锁,该请求需要通过回源更新缓存。其他请求收到的是Z
标记。因为这次查询也通过c
标记返回了CAS
版本,所以更新缓存的时候应该加上c
标记,也就是:
ms foo 2 c2
hi
如果在回源过程中有其他进程更新了缓存,那缓存的CAS
版本也会更新,刚才的回源更新就会失败,避免写入脏数据。
以上是对于缓存已经失效的场景。缓存失效也就两种情况,一种是超过了过期时间,再一种是源数据发生变更,缓存被主动删除。元指令分别为这种两情况提供了回源控制机制。
第一种叫缓存预刷新(Early Recache)。所以谓预刷新,就是在缓存临近过期的时候获取更新并发锁,争取在过期之前更新缓存,从而避免穿透。具体方法如下:
mg foo v t R30
VA 2 t29 W
hi
这里的mg
添加了R30
标记,表示如果缓存的TTL不足30秒,则获取并发锁。多个请求同时执行的时候,只有一个请求会返回W
标记。该请求可以从容地完成回源和更新缓存。更新缓存的时候需要传入T
设置新的TTL,从而避免出现缓存过期问题。
另一种叫脏缓存(Stale Cache)。也就是说,我们在删除缓存的时候并不是真正的删除,而是次其标记为脏缓存:
md foo I T30
HD
这里的I
标记表示只标记,不删除。T30
表示脏缓存最大保持30秒。查询的时候一旦遇到脏缓存,就会额外返回X
标记:
mg foo t c v
VA 4 t29 c777 W X
data
有X
标记表示当前缓存已经过时了,但业务代码还可以决定是否继续使用。更重要的是,在所有并发请求中,只有一个请求会收到W
标记,求救该请求可以回源更新缓存。通过脏缓存可以实现更加精确的回源控制。
热Key问题
缓存的另一个突出问题就是热Key问题。如果某个Key访问量特别大,会对单个节点产生很大的压力。通常大家会通过分片的方式将压力分散到不同的实例。但这样做需要一个前提,就是你需要事先知道哪些Key是热Key。所以,这只能说是一种事后补救的办法。元指令为我们提供了自动发现热Key的能力。
其实也就是引入了两个标记而已:
mg foo v h l c
VA 4 h1 l5 c4
data
这里的标记分别是h
和l
。其中h1
表示当前key创建之后有被访问过,而l5
表示上次访问距离当前的时间,也就是5秒前有过访问。通过这两个标记,业务代码就可以决定是否需要设置本地缓存。比如,我们可以这样:
if (h == 1 && l < 5 && random(1000) == 0) {
(it)
add_to_local_cache}
这里的random(1000)
可以是其他数据指标。如果某个key最近有过访问,就可能是热key,可以考虑在本地缓存一小段时间。
一旦在本地缓存了数据,如何更新就成了麻烦事。因为是热key,又不能把缓存时间设置的太小。一种方案是使用消息队列广播数据更新,但这种太重了。我们可以利用mg
起一个本地的定时进程,周期性地查询热key的CAS
版本:
mg foo c
HD c500
这里的mg
只返回了版本号。如果版本有变更,再更新本地缓存。这样就比较简单地解决了热key的发现和更新问题。
数据传输与内存占用问题
在减少数据传输方面也有两种优化方式,一种是只传必要的数据,另一种是支持在一次请求中返回多种数据。
我们前面说过,mg
指令,默认不返回缓存的数据和key。这样,如果只想看某个 key 的 TTL,只需要执行:
mg foo t
HD t94
这里的HD
表示header
,后面的t94
表示当前TTL是94秒。我们还可以使用一条指令查询多种信息:
mg foo t c
HD t94 c8
这里的c
表示CAS
版本,缓存数据更新后这个版本都会变。
如果需要查多条缓存,传统的指令需要写成这样:
get bar foooooooooooooooooooooooooooooooo baz
VALUE foooooooooooooooooooooooooooooooo 0 2
hi
END
这里只有foooooooooooooooooooooooooooooooo
有值,END
表示结束。但是mg
不支持查询多个key。同样的效果需要写成:
mg bar v\r\nmg foooooooooooooooooooooooooooooooo v\r\ngm baz v\r\n
VA 2
hi
因为mg默认不返回key,所以分不清楚hi
对应的是哪个key。这个问题我们当然可以通过添加k
标记来要求服务器返回对应的key:
mg bar k v\r\nmg foooooooooooooooooooooooooooooooo k v\r\ngm baz k v\r\n
VA 2 kfoooooooooooooooooooooooooooooooo
hi
但这里的foo+
相比数据hi
来说,有点太浪费了。为了减少数据传输,mg
支持O
标记来传入业务数据:
mg bar O1 v\r\nmg foooooooooooooooooooooooooooooooo O2 v\r\ngm baz O3 v\r\n
VA 2 O1
hi
这样,业务代码就可以通过返回值中的O
标记完成映射。又因为mg
不像get
那样返回END
,为了明确标记数据传输完成,我们需要额外传一个mn
命令,服务端会直接返回MN
响应。完整的指令如下:
mg bar O1 v\r\nmg foooooooooooooooooooooooooooooooo O2 v\r\ngm baz O3 v\r\nmn\r\n
VA 2 O1
hi
MN
这样就以最小的数据传输实现了查询多个key的效果。
因为元指令可以附加多种标记信息,我们可以在一次交到过程中完成多次操作。举个例子。如果我们想使用缓存控制秒杀库存,最大库存数量为100,则可以执行如下操作:
ma foo M- N120 J99 v
VA 2
99
这里的ma
表示arithmetic
,也就是算术操作。M-
表示数字减一;N120
表示没有缓存的话就新建一个,TTL为120秒;J99
表示新添加的缓存值为99
;v
表示返回结果。因为 Memcached 创建缓存时并不会执行减一操作,所以将初始值定为100
,保证每一次都会减一。但减到0之后就没再继续减小了,也就是说,不管当前值是1
还是0
,减一之后的结果都是0
。所以我们需要把返回值1
当作库存用完的标志。如果没有ma
指令,我们需要用decr
和add
两种命令才能实现上面的效果。
以上就是 Memcached Meta Commands 的主要功能和设计。元指令的内容还有很多,可以完全替换原有的指令,如果有兴趣可以继续阅读官方协议,也可以阅读官方的元指令介绍。如果把 Memcached 当成内存数据库,那自然没办法跟 Redis 竞争。但如果只把 Memcached 当成内存缓存,那它基本上已经做到了极致。虽然现有 Memcached 用的比之前少了,但 AWS 的 Elasticache 居然也支持 1.6 版的 Memcached,也就是说在 AWS 上的服务可以用元指令了。由此可见,Memcached 正是老骥伏枥,志在前里,应该不会那么容易被 Redis 取代。