拉巴力的纸皮箱


  • 首页

  • 标签

  • 归档

  • 关于

  • 搜索

唯一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

在业务开发中,经常会使用RPC请求获取数据。有时候在同一条逻辑链路中,会多次使用RPC返回的数据。

业务场景

请求----------> 服务A
             [1.methodA]----获取用户数据--->服务B(用户服务)
             [2.mehtodB]----获取用户数据--->服务B(用户服务)

上图中,服务A中同一个逻辑链路中包含methodA和mehtodB,两个都会使用到用户数据,因此会导致重复的RPC调用。

解决方案

  1. 将数据实体作为methodB的参数传入
    • 这种方式可以避免调用多次重复的RPC,但是也有缺点:
      a. 如果有mehtodC,methodD等,每个方法都加个参数,不是很优雅
      b. 如果除了获取用户信息,还要获取商品信息等,那么方法形参将越来越多,影响阅读
请求----------> 服务A
             [1.methodA]----获取用户数据--->服务B(用户服务)
             [2.mehtodB(param1: userInfo)]----获取用户数据--->服务B(用户服务)
             [3.mehtodC(param1: userInfo)]----获取用户数据--->服务B(用户服务)
  1. 使用 ThreadContext 缓存RPC结果
    • 可以使用拦截器统一处理服务的所有ThreadContext(因为使用完之后需要remove)
    • 将RPC结果保存到ThreadContext和从ThreadContext获取RPC结果的逻辑,封装在RPC调用方法中
      请求----------> 服务A
             [1.methodA]----获取用户数据--->服务B(用户服务)
             ->> 将RPC结果保存在ThreadContext
             [2.mehtodB]----获取用户数据--->从ThreadContext获取
             [3.mehtodC]----获取用户数据--->从ThreadContext获取

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

发表于 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. 网关服务拦截
    由网关服务统一调风控服务进行判断,并支持业务类型定制(比如资产类接口应该进行哪些判断)

RPC可以和事务绑定吗?

发表于 2020-07-05

在平常编码的时候,经常会看到很多人喜欢把一次RPC调用和数据库事务绑定在一起,RPC调用成功则提交事务,RPC调用失败或超时,则回滚事务。那么这样做是对的吗?

业务场景描述

考虑以下一种业务场景:用户加入群聊需要花费200金币。
服务架构:服务A(群服务),服务B(消费服务),操作都是幂等的。

加群请求----------> 服务A
             [1.新增事务]
             [2.RPC进行扣费]------------>服务B
             [3.执行加群操作(DB操作)]
             [4.提交事务]

业务场景分析

  1. 服务B返回成功,业务无异常
  2. 服务B返回失败,事务回滚,业务无异常
  3. 服务B超时,或服务A RPC调用之后重启等场景,业务异常!

业务异常表现为:用户已经扣费,但是没加群成功(即服务A和服务B数据不一致)

如何解决

  • 服务A通过本地事务表解决。

    1. 服务A在进行RPC调用之前,需先保存一条订单,订单状态为“待扣费”,扣费成功后,更新订单状态为“已扣费”。
    2. 启动定时任务,查询过去某时间范围内(如30分钟前至当前时间1分钟前)的“待扣费”的订单是否已扣费。若已扣费进行加群操作,并更新订单状态;否则,将订单置为无效状态。
    3. 订单状态:[1待扣费,2已扣费,3已加群,4无效]
  • 加群流程图

    加群请求----------> 服务A
               [1.新增“待扣费”订单(DB操作)]
               [2.RPC进行扣费]------------>服务B
               [3.更新订单状态为“已扣费”(DB操作)]
               [4.执行加群操作(DB操作)]
               [5.更新订单状态为“已加群”(DB操作)]


上图的每个操作都是单独的,其中某一步异常都会导致后续中断,因此需要两个定时任务,分别检查“待扣费”和“已扣费”的订单。

  • 定时任务1
    定时任务1---------> 服务A
               [1.查询“待扣费”订单]
               [2.RPC查询是否已扣费]------------>服务B
               |(1)已扣费                         |(2)未扣费
               |                                         |

[3.更新订单状态为“已扣费”(DB操作)] [3.更新订单状态为“无效”(DB操作)]
[4.执行加群操作(DB操作)]
[5.更新订单状态为“已加群”(DB操作)]

  • 定时任务2
    定时任务2---------> 服务A
               [1.查询“已扣费”订单]    
               [2.执行加群操作(DB操作)]
               [3.更新订单状态为“已加群”(DB操作)]


当然定时任务1和2可以放在同一个定时任务执行

  • 解决了问题之后,我们再回头看看一开始的问题:“RPC调用和数据库事务不能绑定吗?”
    • 答案是看具体业务情况而定
    • 比如上述的业务场景。操作1显然是不能绑事务的
    • 但是2,3,4,5操作其实是可以绑事务的,因为定时任务可以进行补偿,而且可以因此减少定时任务2
    • 总结:rpc操作后续可补偿重试或查询,那么就是可以绑事务的,因为服务不会“失忆”;相反数据未落地之前,是不能进行绑事务了,因为一旦异常,数据就会回滚而“丢失”。

其他解决方式

  1. 业务对账,补偿机制。即接入第三方服务进行业务流水对账,发现不一致,进行修正;具体修正方式需根据具体业务场景,是退币,还是使加群成功。
  2. 使用其他分布式事务解决方案(tcc,seata等)

扩展

  • 重试是 “写”还是“读”?比如上述扣费操作,超时情况下,服务A重试是查询还是重新执行扣费?
    • 其实还是取决于业务场景
    • 一般情况下,超时情况下,返回用户是失败,所以使用查询比较合适,不系统自动为用户扣费,这时候应该是“读”
    • 如果需要使用rpc调用进行加群,用户扣费了就必须要加群成功,那么加群RPC重试应该是“写”
    • 对于金钱业务来说,一般情况下,加币操作(充值,活动赠币)是“写”,扣币操作(送礼,用户触发)是“读”;入仓是“写”,出仓是“读”
  • 补偿操作是“正向”还是“逆向”?比如上述扣费操作,超时情况下,服务A重试查询到用户已经扣费成功,这是是应该给用户退币,还是加群成功?
    • 一样还是取决于业务场景
    • 如果用户只是想加群,那么只需要正向操作就好了,不影响用户利益,只是加群操作有些延迟,这时候应该是“正向”
    • 如果用户扣费之后,可以进行抽奖等,相当于用户的权益是具备实时性的,因为扣费超时(实际成功)导致无法抽奖,那么应该是退币才对的,即“逆向”
  • “无效”状态的订单应该支持重试,因为无效是因为未扣费,若作为一个基础服务,上次服务可能会使用同一个订单号进行重试,那么不能告诉业务方,操作失败等,应该作为一个全新的请求处理。可以通过增加订单的流水日志解决数据覆盖问题。
  • 上述的处理方式中,余额不足的扣费,会导致比较多的无效订单,因此在扣费之前,可以增加一次RPC查询,判断是余额是否足够

发版过程的兼容性考虑

发表于 2020-03-29

  • 一般情况下,服务一般不是仅有一台服务器提供服务的,因此服务上线的过程中其实会经历一个灰度过程

如上所示,服务器A是新代码,走的是新逻辑,服务器B和C未发版,走的旧逻辑。

  • 如果新上线的变更只涉及读逻辑的,那么这样的灰度上线是没问题的。
  • 但如果上线的内容涉及写逻辑的变更,甚至还关联到异步处理的变更。那么这样的上线方式可能是不兼容的,会导致处理异常。
  • 在服务全量发版之后,新的流量可能正常,但发版过程中导致一定的脏数据。

(图不画了,自行想象)

解决方案

  • 开关!用配置中心开关控制,走新逻辑还是旧逻辑。默认旧逻辑。
  • 代码需要保留两套,通过开关控制走哪个分支。
  • 全量发版之后,此时线上全是新代码了,通过开关灰度(此时是业务灰度而不是机器灰度了)

表结构变更是否需要处理历史月表?

发表于 2020-03-29

  • 目前针对历史流水表,历史订单表,采用的以月为维度的方式建表,如以下表结构所示
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    CREATE TABLE `t_order_202002` (
    `order_id` varchar(128) NOT NULL COMMENT '订单id',
    `coin` decimal(32,2) NOT NULL DEFAULT '0.00' COMMENT '支付币数',
    `user_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '用户id',
    `create_time` datetime NOT NULL COMMENT '创建时间',
    `update_time` datetime NOT NULL COMMENT '更新时间',
    PRIMARY KEY (`order_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表'

    CREATE TABLE `t_order_202003` (
    `order_id` varchar(128) NOT NULL COMMENT '订单id',
    `coin` decimal(32,2) NOT NULL DEFAULT '0.00' COMMENT '支付币数',
    `user_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '用户id',
    `create_time` datetime NOT NULL COMMENT '创建时间',
    `update_time` datetime NOT NULL COMMENT '更新时间',
    PRIMARY KEY (`order_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表'

    CREATE TABLE `t_order_202004` (
    `order_id` varchar(128) NOT NULL COMMENT '订单id',
    `coin` decimal(32,2) NOT NULL DEFAULT '0.00' COMMENT '支付币数',
    `user_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '用户id',
    `create_time` datetime NOT NULL COMMENT '创建时间',
    `update_time` datetime NOT NULL COMMENT '更新时间',
    PRIMARY KEY (`order_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表'
    现要对表结构增加一个字段 room_id
    1
    2
    3
    4
    ALTER TABLE t_order_202003
    ADD COLUMN `room_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '动作发生的所在房间ID';
    ALTER TABLE t_order_202004
    ADD COLUMN `room_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '动作发生的所在房间ID';
    那么久的历史表可以不处理吗?
  1. 假如全部的查询语句都是select *, 那么久的历史表可以不处理,但是这样违反了按需查询原则
  2. 如果查询语句有指定字段如select order_id, room_id,那么查询到旧的表时会报错

解决方法

  1. 历史表和当前的表结构保存一致,在进行表结构变更时一并处理,避免后续的坑(没精力折腾的建议这样处理,可以增加一些监控手段)
  2. 历史表不处理,在程序代码上兼容。这样会增加代码复杂度,难以维护,如果有必要最好统一sdk中处理。
  3. 运维层面,统一处理,统一月表的结构,需要DBA工具支持

移动端下一种折中的分页方法

发表于 2020-03-26

  • 移动互联网下微服务大行其道,服务之间按业务拆分精细,各司其职,通过服务发现rpc相互调用,实现各自的需求场景。

  • 一个简单的列表,如果不依赖其他服务,只依赖自身的数据库,可以简单的通过sql即可查询出来。

  • 而如果数据来源于其他服务,根据特定的需求场景,可能就需要调用多个服务,获取数据,判断属性,过滤数据,再展示,那么可能未免会出现分页的问题。

  • 以下面的场景为例:

  • 业务服务依赖两个底层服务的数据进行聚合,导致返回客户端的数据小于请求值10,甚至为空,客户端认为已经没数据了,从而错误提示用户“已经到底了”。
  • 解决方案:客户端不依赖返回数据的数量判断是否还有数据,由服务端返回”hasNext=1”来判断是否有数据,控制用户是否可继续滑动查看

  • 方案并不完美,极端情况下可能因为数据被过滤,导致返回前端数据太少设置没数据,影响体验。所以此方案只是不考虑极端场景的折中方案,另外的优化手段是初始请求size控制在一个合适的值,比如size=30

电影摘要

发表于 2020-01-01

《战争之王》

  • https://movie.douban.com/review/8808554/
  • Let me tell you what’s gonna happen.This way you can prepare yourself.Soon there’s gonna be a knock on that door and you will be called outside. In the hall there will be a man who outranks you.First,he’ll compliment you on the fine job you’ve done, that you’re making the world a safer place,that you’re to receive a commendation and a promotion.And then he’s going to tell you that I am to be released. You’re going to protest.You’ll probably threaten to resign.But in the end I will be released.The reason I’ll be released is the same reason you think I’ll be convicted.I do rub shoulders with some of the most vile,sadistic men calling themselves leaders today.But some of those men are the enemies of your enemies.And while the biggest arms dealer in the world is your boss, the President of the United States, who ships more merchandise in a day than I do in a year,sometimes it’s embarrassing to have his fingerprints on the guns. Sometimes he needs a freelancer like me to supply forces he can’t be seen supplying. So , you call me evil. But unfortunately for you,I’m a necessary evil.
      - 让我告诉你将会发生什么事情,好让你有个心理准备。很快会有人来敲门叫你出去,大厅里有个比你官衔高的人。首先,他会称赞你所做的一切,世界因为你变得更安全,你会获得表扬或晋升。然后他会告诉你,我会被释放。你会反对,也许还威胁要辞职,但最后我还是会被释放。我被释放的理由跟你认为我会被定罪的理由一样,我和世界上称自己为领导人的人打交道。这些人其中有些是你敌人的敌人,世界上最大的军火商是你的老板,美国的总统,他一天卖的比我一年还多,有时候在枪支上找到他的指纹是一件很尴尬的事,有时他需要像我这样的自由工作者供应一些他不方便出面供应的货物。所以,你说我是恶魔,但不幸的是,对你我是一个必须存在的恶魔。

《点球成金》

  • I made one decision in my life based on money. (我为了钱 曾做过一次人生重大的决定)
  • And I swore I would never do it again. (我发誓我再也不会那么做)
  • You’re not doing it for the money. (你那样做不是为了钱)
  • You’re doing it for what the money says. (你是为这笔钱代表的价值而做)
  • And it says what it says to any player that makes big money. (每个领高薪的球员也是一样)
  • That they’re worth it. (他们值得)
  • 拿高薪不是为了钱,是为了证明自己的价值

  • https://baijiahao.baidu.com/s?id=1597282786381213634

  • 如果Billy接受这笔钱,就可以一洗之前的“屈辱”,不但个人生活有了保障,还能在新的球队自由发挥,不再被金钱束手束脚。但是Billy没有接受,意料之中又意料之外。离开自己带了几十年的球队不容易,离开自己一直居住的加州不容易,最重要的是离开自己可爱懂事的女儿不容易。当钱的问题终于解决,才发现事情的关键并不是钱,说白了,一个人的成就感并不能用钱来衡量。

  • 如果真要把这部电影当成励志片,它告诉我的并不是“努力就能创造奇迹”,而是人要有勇气作出改变。Billy一直拒绝跟球员亲近,理由是如果投入太多情感,想炒人的时候会下不了手,可却在40几岁的时候,放下了这些防御,主动跟球员沟通,为他们加油打气。在遇到小胖之前,Billy一辈子都在听老一辈人的经验之谈,十八九岁的时候,听了这帮人的游说,放弃了全奖的斯坦福,直接加盟了职业联赛,结果十年也没出成绩,到后来当上经理人,也要听这帮人的建议来买卖队员经营球队,可却在40几岁的时候,选择力排众议,听一个25岁年轻人的建议。如果相信,就要拿出勇气改变。这就是我所学到的。

<1…141516>

153 日志
165 标签
RSS
© 2025 Kingson Wu
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4