今天在用Redis时遇到一个奇怪的问题,虽然很快就定位到了问题,但是在开发时确实忽略了,还是意识不到位,这里记录一下问题的来龙去脉让自己铭记。

背景

首先介绍一下背景,在一个类似抢票的项目中,其中有一步是需要把奖品数据存放在Redis的一个集合(Set)中,中奖时通过SPOP命名从集合中取出一个奖品。这个奖品集合的数据在开始抢票之前生成,放在了一个脚本中去做,问题就出现在了这个脚本。

奖品数据生成脚本

上面我们说的脚本要做的工作无非就是:

1. 读出全部的奖品

2. 分批把奖品存入Redis的一个Set中(考虑奖品过多,没有一次把全部存入,做了分批处理)

这么看脚本很简单,没有什么复杂的逻辑,但是需要确保全部奖品都写入Redis成功,不能只写入部分奖品。如果有失败,需要进行重试或者报警。

下面直接看下代码,代码用世界上最好的PHP语言,Redis客户端用的phpredis。

function setPrize($arrPrizes) {    try {        // 真实的奖品是由上面的参数$arrPrizes传入,数据格式如下:        // $arrPrizes = array(        //     array('code' => 'A0001', 'name' => 'iPhone'),        //     array('code' => 'A0002', 'name' => 'iPhone'),        // );        foreach ($arrPrizes as $intIndex => $arrPrize) {            $arrPrizes[$intIndex] = json_encode($arrPrize);        }

        // 获取Redis实例        $objRedis       = RedisUtil::getInstance();        $strRedisKey    = 'www.daemoncoder.com';        $arrPrizePages  = array_chunk($arrPrizes, 100);        foreach ($arrPrizePages as $arrPrizePage) {            // 分批写入redis中            $intAddResult = $objRedis->sAddArray($strRedisKey, $arrPrizePage);            echo sprintf("AddResult: %s\n", var_export($intAddResult, true));        }

        // 判断写入的集合最终大小是否和传入的数组大小一致        // 不一致则中间有数据没有成功写入,需要返回给上层重试或者报警        $intScardResult = $objRedis->sCard($strRedisKey);        echo sprintf("ScardResult: %s\n", var_export($intScardResult, true));        if ($intScardResult == count($arrPrizes)) {            return true;        }    } catch (Exception $e) {        var_dump($e);    }    return false;}

代码中 RedisUtil::getInstance() 是一个封装过的Redis工具类,里面封装了获取redis连接、自动选择主从库等操作。$objRedis->sAddArray() 把奖品数据分批写入,最后通过$objRedis->sCard()来判断是否全部成功写入,有失败的话需要返回给上层调用的地方,进行重试或者报警处理。

问题的表现

上面的代码实在太简单,一顿操作就把代码写完了,然后开心地去测试,没问题。不过为了稳妥,还是设置了重试一定次数,依然执行失败就邮件报警,万事具备,不可能再有问题了,上线!然后,就翻车了!喜获报警邮件一份!

经过多次在测试发现不稳定,大多数情况可能成功执行,但是有很小的概率会失败。问题是失败的情况下,并不是因为写Redis失败。从输出的数据看,失败的时候也成功执行了写入操作,而且sCard()操作也成功执行,就是得到的数据不对。

问题的定位

手动清除原有数据,重新执行脚本,问题复现之后,查看输出,一切正常,只有sCard()返回的数据不对,还手动查了下当时集合中写入的数据,确实成功写入了,所以问题就锁定在了sCard()方法。

查了下Redis SCARD命令的官方文档:https://redis.io/commands/scard,先确认自己的没有用错。命令功能很简单,没有什么特别的地方,就是返回集合中的元素数量,如果key不存在,返回0。

难不成redis的scard命令有bug不成?兴奋之际,再单独执行了一下sCard(),这次结果竟然对了,这么看,问题不在sCard(),大概可以猜到问题的原因是出在了数据延时的问题。就是说之前通过sAddArray()写入集合的数据,有部分还没有生效。Redis本身用单线程处理请求,理论不应该存在出现这种延时,但是线上环境的Redis往往都是主从结构的,主库到从库同步数据是会有延时的,这也是出现这个问题的真实的原因。

上述代码中用RedisUtil::getInstance()来获取redis示例,前面也有介绍,这个是我们自己封装的Redis工具类,会根据不同的redis命令做读写分离。sAddArray()是一个写请求,会自动选择主库连接执行,而sCard()是一个读请求,默认会选择从库去执行。所以会出现用sCard()读取不到集合真实的大小,因为从库此时可能还没有同步到最新的数据。

解决方案

调整代码,强制让sCard()方法选择主库(每个人连接的Redis工具类不同,这里不再贴代码,大概的方式就是连接时指定主库的IP)。这样经过多次反复测试,没有再出现这个问题。

一些反思

为什么上线前测试的时候没有发现这个问题?

部分原因是问题出现的概率比较小,还有更重要的一个原因,是我们线下测试环境的Redis就只有一个库!没有那么多资源去给测试环境做个主从,最根本的原因可能还是因为穷(囧)。我想应该有不少公司和我们一样的,所以希望这个问题对你也有帮助。

不仅仅是主从延时的问题不易发现,如果线上Redis有多台机器,选择机器连接出错的问题也不易发现。

用一个比较常见的场景为例,存储用户的数据时,往往根据用户的ID做哈希,分布存储在多台机器上,如果代码有bug计算哈希值时用错了值,就有可能选择错误的机器。如果恰好你和我们一样,测试环境只有可怜的一台机器,那么测试阶段可能发现不了这个问题,细思极恐有木有。

为什么开发时没有考虑到会有主从延时的问题?

这个确实要从自己找原因了,还要把提高自己的主从意识。不仅仅是这种场景要考虑主从,从Redis中读任何数据时,都要第一时间想到读到可能不是最新数据。也不仅仅是Redis,MySQL等其他主从结构数据库,也都要第一时间想到主从延时。

转载请注明出处,本文原始链接:https://www.daemoncoder.com/a/%E8%AE%B0%E4%B8%80%E6%AC%A1Redis%20scard%E8%AF%BB%E5%8F%96%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%9C%E4%B8%8D%E5%AF%B9%E7%9A%84%E9%97%AE%E9%A2%98/4d54673d

点击下方阅读原文,或访问 daemoncoder.com 发现更多优质内容。

扫码关注公共号,第一时间收到优质内容

记一次Redis scard读取数据结果不对的问题【DaemonCoder】相关推荐

  1. python如何读取数据时出现错误_在python3中,关于redis读取数据带有‘b’的问题...

    在python3中,关于redis读取数据带有'b'的问题 #encoding=utf-8 from redis import * #读取数据 d1=input("您输入的数据是:" ...

  2. php 不识别redis,redis,_redis卡死无法读取数据如何解决?,redis - phpStudy

    redis卡死无法读取数据如何解决? 之前是redis dump.rdb 的时候会卡死,然后我关掉了save功能,但是过了一段时间之后又卡死了,这次不知道什么原因,怎样解决了. 以下是info的信息: ...

  3. [转载] python怎么获取redis中的数据_python操作redis数据库

    参考链接: 使用Python在Selenium中进行非阻塞等待 3.redis基本命令 String set(name, value, ex=None, px=None, nx=False, xx=F ...

  4. 使用Spring操作Redis的key-value数据

    前言 最近工作一直忙的不可开交,小Alan已经很久没有和大家分享知识了,在深圳待了两年多,依然感觉自己还是个小菜鸟,工作中还是会遇到很多自己在短期内无法搞定的事情,每当这个时候总是会感觉到很沮丧,就会 ...

  5. python读取数据文件-python多种读写excel等数据文件的方式(收藏篇)

    前言: python处理数据文件的途径有很多种,可以操作的文件类型主要包括文本文件(csv.txt.json等).excel文件.数据库文件.api等其他数据文件.下面小编整理下python到底有哪些 ...

  6. Memcache,Redis,MongoDB(数据缓存系统)方案对比与分析

    一.问题:     数据库表数据量极大(千万条),要求让服务器更加快速地响应用户的需求. 二.解决方案:      1.通过高速服务器Cache缓存数据库数据      2.内存数据库   (这里仅从 ...

  7. SpringBoot中实现连接多个Redis分别读写数据

    场景 在SpringBoot项目中需要连接两个Redis实例,并实现从A中获取数据并存取到B中. 另外A中redis的地址和密码不能外漏,则将A的地址和密码写在jar包中,B中redis参数可以在外置 ...

  8. Redis和mysql数据怎么保持数据一致的?

    需求起因 在高并发的业务场景下,数据库大多数情况都是用户并发访问最薄弱的环节.所以,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问MySQL等数据库. 这个业务场景,主要 ...

  9. Redis进阶实践之十六 Redis大批量增加数据

    原文:Redis进阶实践之十六 Redis大批量增加数据 一.介绍 有时候,Redis实例需要在很短的时间内加载大量先前存在或用户生成的数据,以便尽可能快地创建数百万个键.这就是所谓的批量插入,本文档 ...

最新文章

  1. P1781 宇宙总统
  2. 一年数十万次实验背后的架构与数据科学
  3. 记了老是忘记那就写下来吧宏任务微任务
  4. 启动mysq服务_mysql安装、启动
  5. linux mdel 命令详解
  6. 物料编码是计算机识别和检索物料的( ),物料编码是计算机识别和检索物料的。...
  7. Anroid app版本更新
  8. 团队作业1--团队展示选题
  9. 梆梆加固函数抽取执行流程
  10. 使用table标签制作常用的html表格
  11. ubuntu 18.04 卸载firebox
  12. r语言中如何进行两组独立样本秩和检验
  13. 酷盘 文件服务器,酷盘网页登陆
  14. 工作感受月记 201907月
  15. linux环境oracle环境变量,Linux下设置oracle环境变量
  16. EndNote x8/ x9基础使用教程+中文毕业论文格式GB/T 7714-2005
  17. Siemens-PLM-TeamCenter虚拟机安装与配置
  18. 文件共享-ftp、pure-ftpd、sftp共享
  19. 负数的补码计算,简洁明了
  20. 2020年全国大学生数学建模赛后回忆

热门文章

  1. 《程序员的成长课》:增加收入的 3 大方向
  2. 使用showdown.js打造自己的markdown编辑器,支持table
  3. 一个测试工程师走进一家酒吧……
  4. 惯用过程模型_惯用代码
  5. 文献阅读-GNC----IEEE Robotics and Automation Letters (RA-L), 2020.
  6. 浏览器提示代理服务器没有响应
  7. 前度控制器源代码分析
  8. 中e管家如何投资理财收益最大化
  9. balanced-match 源码解析
  10. 使用 GitHub Actions 来构建应用程序