一些很古老的项目里使用了memcache作为缓存组件,这些组件基本都是来源于自研环境没有上云,存在很多难以解决的问题。导致无法管理,更没有跨AZ的特性
- 没有控制面进行管理
- 不支持扩容、跨AZ部署
- 连接错误、连接超时频发
有些项目后来改用了云上Redis作为缓存组件,但是memcached在并行运行,新逻辑使用Redis,旧的逻辑能不变就不变。这也给项目维护和迭代带来了很多麻烦。
本文以域名注册项目为例,详细阐述将memcache迁移到Redis,从而完全废弃memcache的过程。
- 总体迁移步骤
数据迁移应当平滑的进行,不能影响现网用户。如果只将原来的memcache的相关get、set等方法改成对应redis的方法,直接发布到现网是绝对不可以的,有可能会导致缓存频繁取不到。以发验证码为例
在灰度发布中,现网业务旧版本接入了memcached、新版本接入了redis,用户流量随机调度到这些后端服务,用户发验证码的请求如果走的是旧版本,校验验证码走到了新版本,必然从redis里取不到验证码,这就导致校验失败。
所以我们需要对缓存采取双写双读的策略,即写的时候往redis和memcached都写一份,读的时候优先从redis读,读不到再从memcached读
灰度发布完成后,需要在现网运行至少一周以上,确保没有任何异常,再将双写双读改成只redis读写重新发布一次,这次发布由于现网所有实例都已经写到redis了,可以将memcached完全下线掉
- 主要操作命令双写双读代码修改示例
双写双读示例:
def get(self, key, default=None):
"""
灰度发布过程中,避免后发布的节点写了memcached
先发布的节点从redis读不到数据
"""
value = self.redis.get(key)
if value:
return value
return self.cache.get(key, default)
def save(self, key, value, **options):
"""
设置缓存
Args:
key: str, 缓存key
value: str, 缓存value
expire: int, 过期时间
同步memcached,灰度发布过程中,避免先发布的节点写了redis,
其他节点从memcached读不到数据
"""
expire = options['expire'] if "expire" in options else 0
noreply = options['noreply'] if "noreply" in options else None
flags = options['flags'] if "flags" in options else None
self.cache.set(key, value, expire, noreply, flags)
return self.redis.set(key, value, expire)
def delete(self, key):
self.cache.delete(key)
return self.redis.delete(key)
def add(self, key, value, **options):
"""
Args:
key: str, 缓存key
value: str, 缓存value
expire: int, 过期时间
"""
expire = options['expire'] if "expire" in options else 0
noreply = options['noreply'] if "noreply" in options else None
flags = options['flags'] if "flags" in options else None
self.redis.set(key, value, ex=expire, nx=True)
return self.cache.add(key, value, expire, noreply, flags)
def incr(self, key, value, **options):
noreply = options['noreply'] if "noreply" in options else None
self.redis.incr(key, value)
return self.cache.incr(key, value, noreply)
完全删除Memcached切Redis:
def save(self, key, value, **options):
"""
设置缓存
Args:
key: str, 缓存key
value: str, 缓存value
expire: int, 过期时间
同步memcached,灰度发布过程中,避免先发布的节点写了redis,
其他节点从memcached读不到数据
"""
expire = options['expire'] if "expire" in options else 0
if expire == 0:
return self.redis.set(key, value)
return self.redis.set(key, value, expire)
def get(self, key, default=None):
return self.redis.get(key)
def delete(self, key):
return self.redis.delete(key)
def add(self, key, value, **options):
"""
Args:
key: str, 缓存key
value: str, 缓存value
expire: int, 过期时间
"""
expire = options['expire'] if "expire" in options else 0
return self.redis.set(key, value, ex=expire, nx=True)
def incr(self, key, value, **options):
return self.redis.incr(key, value)
- 非常容易忽视的坑
3.1 缓存key前缀不一样
MEMCACHE = {
'host': '9.2.10.2',
'port': 9101,
'connect_timeout': 5,
'timeout': 30,
'prefix': 'register_'
}
REDIS = {
'host': '9.1.10.1',
'port': 6379,
'password': 'xxxxx',
'connect_timeout': 5,
'prefix': 'register_:'
}
可以看到memcached加了一个统一的前缀 register_, 后来接入了redis,开发人员为了表示区分,设置了一个新的key前缀 register_:,这里在修改双写双读的时候必须要注意这个前缀的不同,在读和写的时候需要拼各自的前缀.
例如有一个key tld_maintain_notice,
在memcached里的key是register_tld_maintain_notice,
在redis里的key是register_:tld_maintain_notice
3.2 expire=0在memcached和redis的含义不同
在memcached设置expire=0表示永久存储
set key flags exptime bytes
而在redis中设置expire=0是数据立即删除
expire key 0
这里一定要确保修改后兼容
3.3 memcached的add命令和redis的add命令
add命令是将一个缓存中不存在的key加入缓存中,如果已经在缓存中,该命令返回失败,需要明确的是memcached有add命令,而redis没有。
localhost:11211> add hello 11
true
localhost:11211> add hello 11
false
为了减少修改的代码量,尽可能收敛变更的逻辑,就要封装一个redis的add命令
可以有两种方法:
1.set命令nx参数:
127.0.0.1:6379> SET hello 1 EX 10 NX
OK
127.0.0.1:6379> SET hello 1 EX 10 NX
(nil)
2.eval命令script脚本:
local key = KEYS[1]
local value = ARGV[1]
local expire = ARGV[2]
if redis.call('EXISTS', key) == 1 then
return 0
else
redis.call('SET', key, value)
redis.call('EXPIRE', key, expire)
return 1
end
通常我们觉得这两种方法没有什么区别,但是发布到现网,并发量比较大且过期时间很小的时候第一种方法会出错,有一定概率将ex的过期时间没有写进去(原因不明),所以在并发较高的时候不要使用第一种方法,之前域名注册老的限频逻辑使用了memcached的add方法,改造为redis为了兼容老逻辑,使用了set方法模拟add,就出现了莫名其妙的丢失过期时间,导致限频的计数越来越大,接口一直访问失败。
3.4 memcached和redis的incr命令的区别
二者都有自增命令,但是memcached是从0开始的,redis是从1开始,这导致有些场景两者是不能混用的,迁移的时候要仔细查看关联的代码
localhost:11211> increment mykey 1
[ true, 0 ]
localhost:11211> get mykey
0
localhost:11211> increment mykey 1
[ true, 1 ]
localhost:11211> get mykey
1
127.0.0.1:6379> incr mykey
(integer) 1
127.0.0.1:6379> get mykey
"1"
127.0.0.1:6379> incr mykey
(integer) 2
127.0.0.1:6379> get mykey
"2"
3.5 双写的时候先写redis和后写redis的区别
在add和incr命令里,双写的顺序必须要先写redis,后写memcached,然后return写memcached的值,这样能保证在灰度环境下,这两个命令获取的值不会受到redis干扰而错乱,两者维护了不相关的add和incr的结果
可以参看上面的双写双读示例 add和incr方法
3.6 限频逻辑两者不一样,不能复用
memcached限频逻辑使用add命令初始化赋值1,再对其incr计数,redis不要使用3.3中的add命令,expire=1时及易丢失expire
两者限频逻辑对比:
def freq(key,time,max_count):
cache=Memcached()
ret = cache.add(key, 1, expire=time)
if not ret:
count = cache.incr(key, 1)
if count > max_count:
return True
return False
def freq(key,time,max_count):
cache=Redis()
count = cache.incr(key)
if count == 1:
cache.expire(key, time)
if count > max_count:
return True
return False