因为BitMap,白白搭进去8台服务器...
最近,因为增加了一些风控措施,导致新人拼团订单接口的 QPS、TPS 下降了约 5%~10%,这还了得!
首先,快速解释一下【新人拼团】活动:
业务简介:顾名思义,新人拼团是由新用户发起的拼团,如果拼团成功,系统会自动奖励新用户一张满 15.1 元减 15 的平台优惠券。
这相当于是无门槛优惠了。每个用户仅有一次机会。新人拼团活动的最大目的主要是为了拉新。
新用户判断标准:是否有支付成功的订单 ? 不是新用户 : 是新用户。
当前问题:由于像这种优惠力度较大的活动很容易被羊毛党、黑产盯上。因此,我们完善了订单风控系统,让黑产无处遁形!
然而由于需要同步调用风控系统,导致整个下单接口的的 QPS、TPS 的指标皆有下降,从性能的角度来看,【新人拼团下单接口】无法满足性能指标要求。因此 CTO 指名点姓让我带头冲锋……冲啊!
问题分析
风控系统的判断一般分为两种:在线同步分析和离线异步分析。在实际业务中,这两者都是必要的。
在线同步分析可以在下单入口处就拦截掉风险,而离线异步分析可以提供更加全面的风险判断基础数据和风险监控能力。
最近我们对在线同步这块的风控规则进行了加强和优化,导致整个新人拼团下单接口的执行链路更长,从而导致 TPS 和 QPS 这两个关键指标下降。
解决思路
要提升性能,最简单粗暴的方法是加服务器!然而,无脑加服务器无法展示出一个出色的程序员的能力。CTO 说了,要加服务器可以,买服务器的钱从我工资里面扣……
在测试环境中,我们简单的通过使用 StopWatch 来简单分析,伪代码如下:
@Transactional(rollbackFor = Exception.class) public CollageOrderResponseVO colleageOrder(CollageOrderRequestVO request) { StopWatch stopWatch = new StopWatch(); stopWatch.start("调用风控系统接口"); // 调用风控系统接口, http调用方式 stopWatch.stop(); stopWatch.start("获取拼团活动信息"); // // 获取拼团活动基本信息. 查询缓存 stopWatch.stop(); stopWatch.start("获取用户基本信息"); // 获取用户基本信息。http调用用户服务 stopWatch.stop(); stopWatch.start("判断是否是新用户"); // 判断是否是新用户。 查询订单数据库 stopWatch.stop(); stopWatch.start("生成订单并入库"); // 生成订单并入库 stopWatch.stop(); // 打印task报告 stopWatch.prettyPrint(); // 发布订单创建成功事件并构建响应数据 return new CollageOrderResponseVO(); }
执行结果如下:
StopWatch '新人拼团订单StopWatch': running time = 1195896800 ns --------------------------------------------- ns % Task name --------------------------------------------- 014385000 021% 调用风控系统接口 010481800 010% 获取拼团活动信息 013989200 015% 获取用户基本信息 028314600 030% 判断是否是新用户 028726200 024% 生成订单并入库
在测试环境整个接口的执行时间在 1.2s 左右。其中最耗时的步骤是【判断是否是新用户】逻辑。
这是我们重点优化的地方(实际上,也只能针对这点进行优化,因为其他步骤逻辑基本上无优化空间了)。
确定方案
在这个接口中,【判断是否是新用户】的标准是是用户是否有支付成功的订单。因此开发人员想当然的根据用户 ID 去订单数据库中查询。
我们的订单主库的配置如下: 这配置还算豪华吧。然而随着业务的积累,订单主库的数据早就突破了千万级别了,虽然会定时迁移数据,然而订单量突破千万大关的周期越来越短……(分库分表方案是时候提上议程了,此次场景暂不讨论分库分表的内容)而用户 ID 虽然是索引,但毕竟不是唯一索引。因此查询效率相比于其他逻辑要更耗时。
通过简单分析可以知道,其实只需要知道这个用户是否有支付成功的订单,至于支付成功了几单我们并不关心。
因此此场景显然适合使用 Redis 的 BitMap 数据结构来解决。在支付成功方法的逻辑中,我们简单加一行代码来设置 BitMap:
// 说明:key表示用户是否存在支付成功的订单标记 // userId是long类型 String key = "order:f:paysucc"; redisTemplate.opsForValue().setBit(key, userId, true);
通过这一番改造,在下单时【判断是否是新用户】的核心代码就不需要查库了,而是改为:
Boolean paySuccFlag = redisTemplate.opsForValue().getBit(key, userId); if (paySuccFlag != null && paySuccFlag) { // 不是新用户,业务异常 }
修改之后,在测试环境的测试结果如下:
StopWatch '新人拼团订单StopWatch': running time = 82207200 ns --------------------------------------------- ns % Task name --------------------------------------------- 014113100 017% 调用风控系统接口 010193800 012% 获取拼团活动信息 013965900 017% 获取用户基本信息 014532800 018% 判断是否是新用户 029401600 036% 生成订单并入库
测试环境下单时间变成了 0.82s,主要性能损耗在生成订单入库步骤,这里涉及到事务和数据库插入数据,因此是合理的。接口响应时长缩短了 31%!相比生产环境的性能效果更明显……接着舞!
晴天霹雳
这次的优化效果十分明显,想着 CTO 该给我加点绩效了吧,不然我工资要被扣完了呀~
一边这样想着,一边准备生产环境灰度发布。发完版之后,准备来个葛优躺好好休息一下,等着测试妹子验证完就下班走人。
然而在我躺下不到 1 分钟的时间,测试妹子过来紧张的跟我说:“接口报错了,你快看看!”What?
当我打开日志一看,立马傻眼了。报错日志如下:
io.lettuce.core.RedisCommandExecutionException: ERR bit offset is not an integer or out of range at io.lettuce.core.ExceptionFactory.createExecutionException(ExceptionFactory.java:135) ~[lettuce-core-5.2.1.RELEASE.jar:5.2.1.RELEASE] at io.lettuce.core.ExceptionFactory.createExecutionException(ExceptionFactory.java:108) ~[lettuce-core-5.2.1.RELEASE.jar:5.2.1.RELEASE] at io.lettuce.core.protocol.AsyncCommand.completeResult(AsyncCommand.java:120) ~[lettuce-core-5.2.1.RELEASE.jar:5.2.1.RELEASE] at io.lettuce.core.protocol.AsyncCommand.complete(AsyncCommand.java:111) ~[lettuce-core-5.2.1.RELEASE.jar:5.2.1.RELEASE] at io.lettuce.core.protocol.CommandHandler.complete(CommandHandler.java:654) ~[lettuce-core-5.2.1.RELEASE.jar:5.2.1.RELEASE] at io.lettuce.core.protocol.CommandHandler.decode(CommandHandler.java:614) ~[lettuce-core-5.2.1.RELEASE.jar:5.2.1.RELEASE] …………
bit offset is not an integer or out of range。这个错误提示已经很明显:我们的 offset 参数 out of range。
为什么会这样呢?我不禁开始思索起来:Redis BitMap 的底层数据结构实际上是 String 类型,Redis 对于 String 类型有最大值限制不得超过 512M,即 2^32 次方 byte…………我靠!!!
恍然大悟
由于测试环境历史原因,userId 的长度都是 8 位的,最大值 99999999,假设 offset 就取这个最大值。
那么在 Bitmap 中,bitarray=999999999=2^29byte。因此 setbit 没有报错。
而生产环境的 userId,经过排查发现用户中心生成 ID 的规则变了,导致以前很老的用户的 id 长度是 8 位的,新注册的用户 id 都是 18 位的。
以测试妹子的账号 id 为例:652024209997893632=2^59byte,这显然超出了 Redis 的最大值要求。不报错才怪!
紧急回退版本,灰度发布失败~还好,CTO 念我不知道以前的这些业务规则,放了我一马~该死,还想着加绩效,没有扣绩效就是万幸的了!
本次事件暴露出几个非常值得注意的问题,值得反思:
①懂技术体系,还要懂业务体系
对于 BitMap 的使用,我们是非常熟悉的,对于多数高级开发人员而言,他们的技术水平也不差,但是因为不同业务体系的变迁而无法评估出精准的影响范围,导致无形的安全隐患。
本次事件就是因为没有了解到用户中心的 ID 规则变化以及为什么要变化从而导致问题发生。
②预生产环境的必要性和重要性
导致本次问题的另一个原因,就是因为没有预生产环境,导致无法真正模拟生产环境的真实场景,如果能有预生产环境,那么至少可以拥有生产环境的基础数据:用户数据、活动数据等。
很大程度上能够提前暴露问题并解决。从而提升正式环境发版的效率和质量。
③敬畏心
要知道,对于一个大型的项目而言,任何一行代码其背后都有其存在的价值:正所谓存在即合理。
别人不会无缘无故这样写。如果你觉得不合理,那么需要通过充分的调研和了解,确定每一个参数背后的意义和设计变更等。以尽可能降低犯错的几率。
后记
通过此次事件,本来想着优化能够提升接口效率,从而不需要加服务器。这下好了,不仅生产环境要加 1 台服务器以临时解决性能指标不达标的问题,还要另外加 7 台服务器用于预生产环境的搭建!
因为 BitMap,搭进去了 8 台服务器。痛并值得。接着奏乐,接着舞~~~
来源:r6a.cn/dNTk
欢迎关注我的微信公众号「码农突围」,分享Python、Java、大数据、机器学习、人工智能等技术,关注码农技术提升•职场突围•思维跃迁,20万+码农成长充电第一站,陪有梦想的你一起成长

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Docker:网络模式详解
袖珍指南 Docker作为目前最火的轻量级容器技术,牛逼的功能,如Docker的镜像管理,不足的地方网络方面。 Docker自身的4种网络工作方式,和一些自定义网络模式 安装Docker时,它会自动创建三个网络,bridge(创建容器默认连接到此网络)、 none 、host host:容器将不会虚拟出自己的网卡,配置自己的IP等,而是使用宿主机的IP和端口。 Container:创建的容器不会创建自己的网卡,配置自己的IP,而是和一个指定的容器共享IP、端口范围。 None:该模式关闭了容器的网络功能。 Bridge:此模式会为每一个容器分配、设置IP等,并将容器连接到一个docker0虚拟网桥,通过docker0网桥以及Iptables nat表配置与宿主机通信。 以上都是不用动手的,真正需要配置的是自定义网络。 一、前言 当你开始大规模使用Docker时,你会发现需要了解很多关于网络的知识。Docker作为目前最火的轻量级容器技术,有很多令人称道的功能,如Docker的镜像管理。然而,Docker同样有着很多不完善的地方,网络方面就是Docker比较薄弱的部分。因此,我们有必要...
- 下一篇
Redis基础——剖析基础数据结构及其用法
这是一个系列的文章,打算把Redis的基础数据结构、高级数据结构、持久化的方式以及高可用的方式都讲一遍,公众号会比其他的平台提前更新,感兴趣的可以提前关注,「SH的全栈笔记」,下面开始正文。 如果你是一个有经验的后端或者服务器开发,那么一定听说过Redis,其全称叫Remote Dictionary Server。是由C语言编写的基于Key-Value的存储系统。说直白点就是一个内存数据库,既然是内存数据库就会遇到如果服务器意外宕机造成的数据不一致的问题。 这跟很多游戏服务器也是一样的,感兴趣的可以参考我之前的文章游戏服务器和Web服务器的区别。其数据首先会流向内存,基于快速的内存读写来实现高性能,然后定期将内存的数据中的数据落地。Redis其实也是这么个流程,基于快速的内存读写操作,单机的Redis甚至能够扛住10万的QPS。 Redis除了高性能之外,还拥有丰富的数据结构,支持大多数的业务场景。这也是其为什么如此受欢迎的原因之一,下面我们就来看一看Redis有哪些基础数据类型,以及他们底层都是怎么实现的。 1. 数据类型 其基础数据类型有String、List、Hash、Set、S...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- CentOS8编译安装MySQL8.0.19
- CentOS8安装Docker,最新的服务器搭配容器使用
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- SpringBoot2整合Redis,开启缓存,提高访问速度
- Mario游戏-低调大师作品
- Docker安装Oracle12C,快速搭建Oracle学习环境
- CentOS7安装Docker,走上虚拟化容器引擎之路
- CentOS6,7,8上安装Nginx,支持https2.0的开启