拉巴力的纸皮箱

技术博客 | 记录学习笔记和思考


  • 首页

  • 标签

  • 归档

  • 关于

  • 搜索

财务数据报表的跨天之苦

发表于 2020-07-09

数据报表,尤其是金钱相关的财务报表,对数据的准确性犹为敏感。而服务系统间的处理时间点的存在差异的客观事实(网络时延,失败重试等原因),导致在以天(或月/年)为维度的数据报表中,或对账中,常常出现数据不平的问题。

业务描述

以用户充值加金币的业务为例

充值请求---------------->  充值服务A
                 (支付成功的时间x:2020-07-08 23:59:59)
                 ----------------> 金币服务B
                           (加币成功的时间y:2020-07-09 00:00:01)

现在,有以下数据报表的需求:输出每一天支付金额以及对应的加金币数目。

  • 从上图可以看出,时间x和时间y不在同一天,在出数据报表的时候,这条充值记录应该归为7月8日还是7月9日呢?
    1. 从用户的角度看,充值成功的行为是一个原子操作,用户不关心服务方底层有区分支付时间和加币时间;
    2. 从公司财务的角度看,他们同样不关心1描述的问题,只关心报表是否平帐(支付金币=加币数目)

解决方案

要解决以上问题:需要统一充值请求的业务时间

充值请求---------------->  充值服务A
(生成当前时间addTime并传入)
                 (支付成功的时间x:2020-07-08 23:59:59)
                 (接收addTime参数)
                 ----------------> 金币服务B
                           (加币成功的时间y:2020-07-09 00:00:01)
                           (接收addTime参数)
  • 充值请求在发起时就会生成一个当前时间addTime,并一直透传到底层服务,那么统一以addTime作为充值的时间点,就不会出现跨天差异导致的数据不平问题。

注意事项和监控重跑

  • 上述的方案有一个问题需要解决:addTime 时间的合法性
    1. 传入的addTime大于当前时间
    2. 传入的addTime远远小于当前时间

问题分析

  • 问题1显然是错误的,是不合法的请求,但由于系统间可能存在微小差异,可以在逻辑上拒绝addTime大于当前时间超1分钟的请求
  • 问题2是客观存在的。
    1. addTime是一开始生成的时间,当传到每个服务之后,理论上服务的当前时间必然大于addTime
    2. 由于网络超时,重试等原因,相差的时间可能达到分钟级;而由于链路上下层服务因为故障等原因暂时无法处理,那么相差的时间可能到小时级以上。
    3. 一般情况下,一个充值请求正常情况下,一分钟之内就能完成。所以有一个定时任务会在凌晨生成昨天的报表,以供财务人员第二天查看。
      如果报表已经生成之后,addTime是昨天的充值记录才重试成功,那么将导致该条数据没被统计到!

解决方案

针对上述问题2导致的数据报表错误

  1. (程序阻断)当数据报表已经生成之后,拒绝addTime<=昨天的请求,避免充值数据和数据报表不一致。同时增加告警,支持解除限制和重跑报表。
  2. (告警重跑)当数据报表已经生成之后,接收到addTime<=昨天的请求,且执行成功时告警,通知相关人员重跑报表。

扩展

有人可能会问,上述的解决方案始终依赖告警和人工补偿。有没有更加自动化的手段?

  1. 二八法则:避免将资金、精力和时间花在琐碎的多数问题上。上述的方案其实已经解决了大多数场景的问题,如果要完美的解决,必然需要花费更大的精力(可能花费时间比实现原功能更多)。
    当然可以想办法解决,如果确实有必要的话。
  2. 针对一些不常发生的异常问题:增加限制、增加告警、增加人工补偿,是安全快捷、高效成本低的手段。

《一只特里独行的猪》摘要

发表于 2020-07-09

  • 我的看法也许不值得别人重视,但对自己却很重要。这说明我有自己的好恶、爱憎等等。假如没有这些,做人也没什么味道。

  • 我已经四十岁了,除了这只猪,还没见过谁敢于如此无视对生活的设置。相反,我倒见过很多想要设置别人生活的人,还有对被设置的生活安之若素的人。因为这个缘故,我一直怀念这只特立独行的猪。

  • 真正有出息的人是对名人感兴趣的东西感兴趣,并且在那上面做出成就,而不是仅仅对名人感兴趣。

  • 奇怪的是:错得越厉害就越有人信——这都是因为它让人振奋。

  • 整个人类是一个物种,科学是全人类的事业,它的成就不能为民族所专有,所以它是全人类的光荣;这样就能有一些平常心。有了平常心,也就不容易被人骗。

  • 如罗素所言,科学在“不计利害地追求客观真理”。请扪心自问,你所称的科学,是否如此淳朴和善良。尤瑟纳尔女士说:“当我计算或写作时,就超越了性别,甚至超越了人类。”请扪心自问,你所称的科学,是否是如此崇高的事业。

  • 当年文化知识不能成为饭碗,也不能夸耀于人,但有一些青年对它还是有兴趣,这说明学习本身就可成为一种生活方式。学习文史知识目的在于“温故”,有文史修养的人生活在从过去到现代一个漫长的时间段里。学习科学知识目的在于“知新”,有科学知识的人可以预见将来,他生活在从现在到广阔无垠的未来。假如你什么都不学习,那就只能生活在现时现世的一个小圈子里,狭窄得很

  • 人的存在是一种自然现象,而不是某种意志的产物。这种现象的内容就包括:人和人是不一样的,有性别之分、贤愚之分,还有同性恋和异性恋之分,这都是自然的现象。把属于自然的现象叫做“丑恶”,不是一种郑重的态度。这段话的意思说白了就是这样的:有些事原本就是某个样子,不以人的意志为转移。

  • 据我所知,学化学的研究生也未必能学到李先生的理论;他们还有个罪名是“追星族”,鬼迷心窍,连杨振宁、李政道、李四光是谁都不知道。据我所知,这三位先生的学问实在高深,中学生根本不该懂,不知道学问,死记些名字,有何必要?更何况记下这些名字之后屈指一算,多一半都入了美国籍,这是给孩子灌输些什么

  • 那些与命运斗争的人,那些做接近自己限度的斗争的人,却天生地接近这种失败。

    一个常常在进行着接近自己限度的斗争的人总是会常常失败的,一个想探索自然奥秘的人也常常会失败,一个想改革社会的人更是会常常失败。只有那些安于自己限度之内的生活的人才总是“胜利”,这种“胜利者”之所以常胜不败,只是因为他的对手是早已降伏的,或者说,他根本没有投入斗争。

    在人生的道路上,“失败”这个词还有另外的含义,即是指人失去了继续斗争的信心,放下了手中的武器。人类向限度屈服,这才是真正的失败。而没有放下手中武器,还在继续斗争,继续向限度挑战的人并没有失败。如此看来,老人没有失败。老人从未放下武器,只不过是丧失了武器。老人没有失去信心,因此不应当说他是“失败了的英雄”。

    那些永远不肯或不能越出自己限度的人是平庸的人。

关于幂等的若干问题

发表于 2020-07-09
  1. 请求已经处理成功,当业务调用方重试时,幂等id不变,但其他参数有变化时。
    • 该返回成功的结果吗?或者说应该对所有的参数进行校验,判断和之前的参数是否一致?
    • 与幂等相关的不可变参数组成签名,并保存到数据库(同时保存当时参与签名的生成规则),后续可依据判断参数是否变化?
    • 个人看法最新更新 - 20240528
      • 对于入参是否变化不应该归于苛刻,因为后续迭代加字段是很正常的
      • 说明接口对某些入参幂等,比如订单号
      • 根据业务实际情况,根据需要标注幂等时效性(比如同个订单号一年内幂等,后续可能返回订单错误等)

程序异常时能返回50x吗?

发表于 2020-07-08

问题描述

  1. 内网nginx配置
    proxy_next_upstream error timeout http_500 http_502 http_503 http_504;
    proxy_upstream_tries 5;
    
    表示超时或50x(500,502,503,504),nginx会进行重试,一共5次
  2. 服务的程序有bug,报NullPointException,实际逻辑已经执行成功
  3. 服务使用springboot,默认异常时返回的HTTP状态是500
  4. nginx收到500之后,进行重试
  5. 服务的接口不幂等

解决

  1. 很难保证服务的所有接口都幂等,并且是外网的幂等
  2. 服务的统一异常拦截,最好把状态码设置成200,调用方通过业务错误码判断,避免nginx误重试。

外网请求如何保证幂等

发表于 2020-07-08

常见的外网请求,通常来自网页或app。一个不幂等的接口,可能会导致用户只点击一次,却产生多次点击的效果。

  1. 如果用户的请求只是修改昵称,那么基本没影响
  2. 但如果用户的请求是扣费,比如送礼,那就会产生多扣费的资金问题。

问题的原因

在互联网上,无论是内网还是外网,网络经常会有不稳定的情况。为应对这些网络问题,通常会有各种重试策略。

  1. 内网不稳定的情况,通常会在代理上(如nginx)配置一些超时重试,或50x重试策略
  2. 外网不稳定的情况,客户端会在超时时进行重试或切域名重试等

这些重试策略,导致了用户端只发出一次请求,实际服务端收到多次请求的情况。

总结:“重试”是产生问题的源头

解决问题的关键

解决问题的关键其实很简单,就是要让服务端能识别接收到的“多个”请求是否是“同个”请求,从而确保业务只执行一次。

如何标识请求的唯一性

本质上是请求的参数中,包含由一个或多个参数组合而成的,唯一且不变的标识。

  1. 像修改用户昵称的请求,业务本身就幂等,因为用户id是唯一且不变的
  2. 像用户的扣费请求,那么通常的做法是使用唯一的订单号,以及相应的唯一ID生成策略

唯一ID算法需要考虑哪些

  1. 有序
    • 唯一ID作为数据的索引,保持有序有助于数据库性能提升
  2. 数据库类型
    • 以MySQL为例,选择bigint(20)还是varchar,前者只有64位,像“2017072809364399365840049582”这种订单号只能用varchar存储
  3. 基因
    • 分库需求,详见:唯一ID的基因
  4. 性能

前端唯一ID生成方案

  1. 使用UUID (缺点:无序)
  2. 通过后端接口获取唯一ID(缺点:多一次外网的网络请求)
  3. 按一定的业务规则生成,如:timestamp+ 用户ID(10-11位)+ 随机3位数字(或递增)(优点:有序且基本保证唯一)

后端唯一ID生成方案

  1. 使用snowflake算法实现的唯一ID服务
  2. 其他业界方案

解决方案

  1. 前端唯一ID使用方案3
    • 后端服务对前端生成的ID进行规则校验,防止恶意伪造不规范的唯一ID
  2. 后端唯一ID
  3. 直接使用前端传入的唯一ID
  4. 从唯一ID服务获取(和前端唯一ID进行映射)

扩展

不同业务唯一ID冲突问题

  • 若后端作为一个基础服务,对接上层业务,每个业务使用的唯一ID规则不一样。那么如何避免业务之间唯一ID冲突?
    1. 使用业务ID+业务唯一ID进行订单唯一性区分
    2. 统一唯一ID的生成规则(统一从基础服务获取或由该服务提供订单ID申请接口)

跨机房的服务幂等问题

  • 若服务部署多个机房(通常每个机房有对应的数据库),如何保证幂等,因为外网请求重试,不一定会到达同一个机房。
    1. 把用户按机房分区
    2. 不考虑跨机房幂等问题(主备架构下同个请求落在不同机房概率不高,收益低)

唯一ID的基因

发表于 2020-07-07

在互联网服务中,经常需要使用唯一ID。其中一个常见的应用场景是作为业务中请求的幂等ID。

分布式唯一ID生成方案

  1. uuid
  2. snowflake
  3. 包含业务属性的唯一ID,如 timestamp+ uid(10-11位)+ 随机3位数字(或递增)
  4. 其他方案

优劣不在这里讨论

业务场景分析

结合目前工作中的现状进行分析

  1. 唯一ID的存储方式:MySQL-bigint(20)
  2. 唯一ID生成方式:snowflake-64位:42位时间戳+5位机器码+5位进程worker标识码+12位自增id(42|5|5|12 = 64)
  • 我们把幂等ID作为数据库的唯一键,从而保证幂等;而当数据量越来越大是,我们对数据按月进行分表,提升处理性能。

  • 我们在数据库中除了有幂等ID(orderId)字段之外,还有添加时间字段(addTime);当请求进来之后,根据addTime找到相应的月表。所以实际上要保证请求处理幂等,依赖的是orderId+addTime 不变(或addTime保证始终落在同一个月)

为什么要依赖addTime保证幂等?

  1. 基础组件提供的唯一ID生成服务,对snowflake进行个性化的改造,只能保证ID是唯一的,然而ID的属性并不明显,无法方便的使用唯一ID进行分库(实际上snowflake算法的前n位是时间戳,可以考虑作为分库的属性)

  2. 一些常见唯一ID的例子

    交易单号:4008722001201707283057762612
    商户单号:2017072809364399365840049582
    订单编号: 60310040822721833
    支付宝交易号: 2017092021001001150522558267
    大众点评订单号36611441412777832
    

像“商户单号”和“支付宝交易号”,很明显可以使用日期“基因”来分库,不过这种唯一ID不能使用bigint 存储,因为超过64位了

总结

一个好的唯一ID算法生成的ID,应该具备易用的“基因”。像UUID这种就不是很满足。

如何处理RPC返回的错误码?

发表于 2020-07-07

RPC调用的返回结果

  1. 成功
  2. 失败
  3. 超时

超时情况是不确定的,需要调用方重试或查询等,根据业务情况进行处理

返回结果表示方法

  1. 使用http协议的状态码
  2. 使用业务错误码(在业务处理中比较常见)
    • 0表示成功
    • 1表示失败
    • 其他错误码代表具体的失败业务场景

失败错误码类型

  1. 业务错误(参数错误,或业务场景校验限制,请求已经处理完成)
  2. 处理速度慢(msg:“请求处理中”)
  3. 限流(msg:“你的操作太快了”)
  4. 未知错误
  5. 其他

针对2或4的场景,很可能提交的RPC实际上已经成功,属于不确定的情况(同超时),业务方不能直接当成失败处理;1的场景中“请求已经处理完成”,实际上已经成功,只是调用方的接口返回不幂等。

错误码处理

定义RPC请求中,成功,失败,不确定三种结果对应的错误码集合(最好可配置),业务针对不同的结果进行相应处理。

  1. 成功(错误码:A,B,C ….)
  2. 失败(错误码:E,F,G ….)
  3. 不确定(错误码:H,I,J …. 和 “超时”)

上线需要做哪些准备?

发表于 2020-07-06

一般常规的上线,需要做哪些准备,确保不会因为遗漏这种低级错误导致线上问题?

  1. 开发中遇到问题标注TODO,避免遗漏,单元测试覆盖业务逻辑
  2. 准备上线checklist,验收方案
  3. 检查上线配置
  4. 是否需要配置定时任务
  5. 重新检查一遍变更的代码

扩展

  1. 涉及app的,需要考虑旧版本兼容和回归测试(重点回归若干个高流量的旧版本)
  2. 若上线后出现问题,先回滚再查问题(适用于大多数场景)
  3. 核心业务要重点测试

使用ThreadContext缓存RPC结果

发表于 2020-07-06
详细介绍如何使用ThreadContext缓存RPC结果以避免重复调用,提高系统性能,包含实际业务场景和解决方案分析。
阅读全文 »

关于服务间一致性和前置校验的思考

发表于 2020-07-05

每个服务都有相应的前置校验,有些前置校验逻辑不统一,就会导致服务间数据的不一致。比如风控拦截,服务A未拦截,而服务B却拦截了,从而导致不一致。

业务描述

  • 下面描述一个仓库送礼的场景
    送礼请求----------> 服务A(送礼服务)
               [1.出仓库礼物]------------>服务B(仓库服务)
               [2.收礼者加分成]------------>服务C(消费服务[含分成服务])

送礼请求----------> 服务A(送礼服务)
             [1.出仓库礼物]------------>服务B(仓库服务)
             [2.收礼者加分成]------------>服务C(消费服务[含分成服务])
                                [查询是否风险用户]------------>服务D(风控服务)

服务C增加查询之后,如果是风险用户,将会拦截加分成的操作。这样就会导致用户仓库礼物出仓了,但是收礼者却无法接收分成

如何解决

服务B也要进行风险用户判断,针对资金操作相关的校验,可以统一逻辑,避免相同逻辑维护多个地方

  1. 服务C增加一个统一判断接口
    送礼请求----> 服务A(送礼服务)
              [1.出仓库礼物]----->服务B(仓库服务)
                            [ 1.1查询用户是否允许资金变动]----->服务C
                                         [1.1.1查询是否风险用户]--->服务D(风控服务)
              [2.收礼者加分成]----->服务C(消费服务[含分成服务])
                                [2.1查询是否风险用户]----->服务D(风控服务)


若用户是风险用户,操作1就会返回失败,不出仓,也无后续的操作2,这样就避免不一致的问题

  1. 收敛消费入口,统一资产类的入口
    方案1的缺点明显,每个服务都要直接或间接的查询一次风控服务,实际上理想情况下,只需要查询一次即可。
    送礼请求----------> 服务A(送礼服务)
              [1.出仓库礼物和加分成]--------->服务C(消费服务)
                                    [1.1查询是否风险用户]------------>服务D(风控服务)
                                    [1.2出仓库礼物]------------>服务B(仓库服务)
                                    [1.3收礼者加分成]


由消费服务统一对资产进行“出”和“入”

  1. 网关服务拦截
    由网关服务统一调风控服务进行判断,并支持业务类型定制(比如资产类接口应该进行哪些判断)
<1…192021>

208 日志
267 标签
RSS
© 2026 Kingson Wu
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4