乐趣区

关于java:因为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

退出移动版