使用redis实现一个高可用还高性能的方案

开始琢磨这个方案是偶然看到redis的①wait指令,能够在指定数量的从节点同步了最新的写操作后再返回。结合②redis的同步刷盘配置和我很早之前对③mysql crashsafe方案的模仿,这个wait指令让我感觉找到了实现标题最后一块拼图,就开始了琢磨,因为是没落地的纯脑测,有问题可以指点一下哇。

目录

高可靠高性能方案详细

自我梳理一些问题


高可靠高性能方案详细

思想就是下面的4个步骤(必须用lua脚本,得保证下面四个步骤操作的原子性)。

一、先创建一个key(下面称它为事务key)缓存接下来要操作的值,修改一个key就缓存旧的value,到时回滚把它恢复为旧的值;新增一个key就缓存这个key,到时回滚就把它删了。

//根据自己业务规定事务key保存什么,比如我这里缓存了要新增的订单id、订单对应的账号id、对应的商品编号、这商品原来的库存

redis.call('HMSET', '_transactional', did,account,kucunproductid,kucun);

二、中间就是寻常操作,该干嘛干嘛 。

三、最后在所有操作完毕后,删除事务key,执行一下wait指令让从节点同步自己最新的写操作,返回结果。

redis.call('DEL', '_transactional')
local result = redis.call('WAIT', waitNum, 50);
return result..'|'..did..'|'..account";

有人想骂了,什么鬼东西,这哪里保证了高可用?这事务key起了什么作用?中间一堆写操作随便其中一步宕机都会让数据报废。

步骤四、这个事务key的作用来了,在lua脚本开头,插入些代码,判断事务key是否存在,不存在就直接执行上面说的三个步骤。所以步骤四实际是最先执行的步骤。

如果事务key存在呢。证明上一个lua操作出现了redis服务宕机,只创建了事务key,没能执行删除事务key,那我们要对上一次的操作进行回滚,怎么回滚呢?我们不是缓存了所有操作在事务key里面嘛,修改操作就更新旧值回去,新增key就把它删了,插进了list里照样pop出来扔了。执行完删除事务key。

//结合步骤一,我这里往事务key缓存了1、订单id,value为新增的订单信息,2、订单对应的账号id,value订单号3、对应的商品编号,4、这商品原来的库存。5、对哦,还有一步是往一个list里放进了我新增的订单信息

//往队列塞了个订单信息,拿我就拿出来看看是不是跟我这个事务的订单信息一模一样(不一样有可能是没来得及到这一步就宕机了,或者有操作已经把它给lpop扔掉但是扔掉后立马又宕机了),一样就lpop出来扔了。

//新增了订单号和订单信息的映射,删除,redis.call('DEL',transactional[1]);

//新增了账号和订单号的映射(提一嘴,我不该直接映射的,因为账号和订单是一对多的关系,该用list缓存而不是直接映射),删除,redis.call('DEL',transactional[2]);

//修改了商品的库存,把它还原回去,redis.call('SET',transactional[3],transactional[4]);

if redis.call('EXISTS', '_transactional') == 1 then
   local transactional = redis.call('HGETALL', '_transactional');
   local length = redis.call('LLEN', '_dQueue')
   if length ~= 0 then
       local last_value = redis.call('LINDEX', '_dQueue', 0);
       if redis.call('GET', transactional[1])==last_value then
           redis.call('LPOP', '_dQueue' ,1);
       end;
   end;
   redis.call('DEL',transactional[1]);
   redis.call('DEL',transactional[2]);
   redis.call('SET',transactional[3],transactional[4]);
   redis.call('DEL', '_transactional');
end;

直接贴个相对完整的lua脚本

local waitNum = tonumber(redis.call('GET', 'waitnum')) or 0;
if redis.call('EXISTS', '_transactional') == 1 then
   local transactional = redis.call('HGETALL', '_transactional');
   local length = redis.call('LLEN', '_dQueue')
   if length ~= 0 then
       local last_value = redis.call('LINDEX', '_dQueue', 0);
       if redis.call('GET', transactional[1])==last_value then
           redis.call('LPOP', '_dQueue' ,1);
       end;
   end;
   redis.call('DEL',transactional[1]);
   redis.call('DEL',transactional[2]);
   redis.call('SET',transactional[3],transactional[4]);
   redis.call('DEL', '_transactional');
end;
local did = tostring(KEYS[1]);
if redis.call('EXISTS', did) == 1 then
    return '-1';
end;
local orderDate = tostring(ARGV[1])
local account = tostring(ARGV[2])
local productId = tostring(ARGV[3])
local koukucun = tonumber(ARGV[4])
local kucunproductid = tostring('_kucun_'..productId)
local kucun= tonumber(redis.call('GET', kucunproductid));
redis.call('HMSET', '_transactional', did,account,kucunproductid,kucun);
if koukucun > kucun then
    return '-2';
end;
kucun = kucun - koukucun;
redis.call('SET', kucunproductid, kucun);
local jsonString = '{\"orderId\":\"' .. did .. '\", \"orderDate\":\"' .. orderDate .. '\", 
\"account\":\"' .. account .. '\", \"productId\":\"' .. productId .. '\"}';
redis.call('SET',did , jsonString)
redis.call('SET',account , did)
redis.call('LPUSH', '_dQueue', jsonString)
redis.call('DEL', '_transactional')
local result = redis.call('WAIT', waitNum, 50);
return result..'|'..did..'|'..account";

自我梳理一些问题

Q:你这样做就不怕宕机了吗?

我觉得是可以不需要担心了,lua的原子性可以让人放心地利用事务key缓存的值来修正数据。

其中,可能有疑惑的是最后删除事务key的时候,如果刚删除事务key,redis就宕机了,客户端接收到的只有超时异常,那客户端怎么知道你到底有没有操作完成。

我这就得提mysql了,我是抄的mysql crashsafe操作,mysql也照样有相同的问题,mysql宕机重启后对于所有未提交的事务,得根据binlog和redolog的数据状态来判断该回滚还是设为已提交,也就是说客户端也照样不知道刚刚的事务到底是提交完毕了还是被mysql回滚了。

这时候我们跟mysql一样,需要用到适当的幂等性校验,我lua脚本这里就缓存了订单id为key,能做一定的幂等性校验。如果你说我重新起了个新的订单,id不一样了,这个场景mysql也救不了,等人工干预吧。

Q:你这么长一个lua脚本,性能不会烂到家的吧?

直接上图,lua脚本的执行效率是非常惊人的(这个主要看主从节点的状态,其实我之前测的lua脚本耗时一直是2600毫秒左右)。哪怕是2600毫秒,这个方案带来的效果只损失这点性能已经很值了。

 Q :说一下这个方案有什么雷吧

确实有一些问题。

一、就是需要把本来在代码里mysql的事务里所有操作都搬到redis上来,比如你本来事务里要做1、扣减库存。2、插入一张订单。那么就需要把这两个操作全拿到redis上来做才能有可靠性,单纯搬个扣减库存操作进来没用。

二、查询效果及其垃圾。根本没有什么按条件、范围查询能力,需要定期从redis的list中读取key来插入到数据库,查询还是交给mysql或者查询效果更强的es吧。亲测每次从list取500个值出来操作不会有什么big key问题。

三、直接使用这个lua脚本,会抛异常的,因为redis不允许lua脚本里执行wait指令这种可能会阻塞进程的危险指令,需要修改源码把这块校验给屏蔽掉。

四、wait指令里有个等待同步完毕的从节点数量参数,这个值我是缓存一个waitnum key在redis节点中的。比如一主一从集群,主节点肯定传参为1,但是从节点不能配置为1,如果主节点宕机发生主从切换,从节点每次都等待1个从节点同步完毕,但是这时候从节点称为新的主节点后不存在从节点,每次都要阻塞一个超时时间,会降低redis的性能。需要每次启动或者维护redis集群由运维手动设置主节点数量和从节点需要等待同步的从节点数量。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值