Memcached平滑迁移Redis方案--有很多坑

 

一些很古老的项目里使用了memcache作为缓存组件,这些组件基本都是来源于自研环境没有上云,存在很多难以解决的问题。导致无法管理,更没有跨AZ的特性

  • 没有控制面进行管理

  • 不支持扩容、跨AZ部署

  • 连接错误、连接超时频发
    有些项目后来改用了云上Redis作为缓存组件,但是memcached在并行运行,新逻辑使用Redis,旧的逻辑能不变就不变。这也给项目维护和迭代带来了很多麻烦。

本文以域名注册项目为例,详细阐述将memcache迁移到Redis,从而完全废弃memcache的过程。

  1. 总体迁移步骤

数据迁移应当平滑的进行,不能影响现网用户。如果只将原来的memcache的相关get、set等方法改成对应redis的方法,直接发布到现网是绝对不可以的,有可能会导致缓存频繁取不到。以发验证码为例

 

 

在灰度发布中,现网业务旧版本接入了memcached、新版本接入了redis,用户流量随机调度到这些后端服务,用户发验证码的请求如果走的是旧版本,校验验证码走到了新版本,必然从redis里取不到验证码,这就导致校验失败。
所以我们需要对缓存采取双写双读的策略,即写的时候往redis和memcached都写一份,读的时候优先从redis读,读不到再从memcached读

 

灰度发布完成后,需要在现网运行至少一周以上,确保没有任何异常,再将双写双读改成只redis读写重新发布一次,这次发布由于现网所有实例都已经写到redis了,可以将memcached完全下线掉

  1. 主要操作命令双写双读代码修改示例

双写双读示例:

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)
  1. 非常容易忽视的坑

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

 

posted @ 2023-08-31 16:07  流火行者  阅读(172)  评论(0编辑  收藏  举报