线上稳定性保障

  1. 监控
    把各个环节的业务量级、成功失败原因、活动量级上报,这些数据是有因果关系的
    所以我们把这些数据汇总到大盘,提高问题定位的速度。

  2. 隔离
    区分业务,隔离餐团与买单业务,然后区分了环节,隔离查询与交易环节
    最终业务与业务隔离,交易与查询隔离,保障了系统的稳定性。

  3. 降级,首先是确认场景优先级,区分核心功能非核心功能,区分核心依赖非核心依赖

  • 系统容量问题,优先降级非核心功能,限流查询链路
  • 功能异常问题,按业务、场景类型进行降级,避免故障影响扩大
  1. 全链路压测
    需要定期进行全链路压测评估链路流量负载能力。
    压测前,会对压测流量进行ID偏移,防止污染线上,同步压测数据并进行状态改写。
    压测中读写分离,读线上数据,防止污染本地缓存,写影子表防止污染线上表。
    压测后扫描线上数据,确保无压测数据写到线上,然后清理压测数据。

总结:
事前准备,主要做链路梳理,明确强弱依赖,然后进行性能评估,主要进行容量评估和负载评估,然后完善监控,明确因果建设大盘,最后准备降级预案,区分场景,区分优先级。
事中快速响应,通过监控大盘定位问题,按预案进行扩容、限流、降级、熔断等操作。
事后进行性能优化,问题修复并进行复盘总结。

如何实现延迟队列

如何实现一个延迟队列?

(1) 数据库轮询#

例如对于订单支付失效要求比较高的,每2S扫表一次检查过期的订单进行主动关单操作
优点是简单
缺点是每分钟全局扫表,浪费资源,如果遇到表数据订单量即将过期的订单量很大,会造成关单延迟

(2) JDK的延迟队列#

一个无界阻塞队列,该队列只有在延迟期满的时候才能从中获取元素,放入DelayQueue中的对象,是必须实现Delayed接口的
优点:效率高,任务触发时间延迟低。
缺点:(1)服务器重启后,数据全部消失,怕宕机 (2)集群扩展相当麻烦 (3)因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现OOM异常 (4)代码复杂度较高

(3) 时间轮算法#

时间轮算法可以类比于时钟,如上图箭头(指针)按某一个方向按固定频率轮动,每一次跳动称为一个 tick。这样可以看出定时轮由个3个重要的属性参数,ticksPerWheel(一轮的tick数),tickDuration(一个tick的持续时间)以及 timeUnit(时间单位),例如当ticksPerWheel=60,tickDuration=1,timeUnit=秒,这就和现实中的始终的秒针走动完全类似了。
如果当前指针指在1上面,我有一个任务需要4秒以后执行,那么这个执行的线程回调或者消息将会被放在5上。那如果需要在20秒之后执行怎么办,由于这个环形结构槽数只到8,如果要20秒,指针需要多转2圈。位置是在2圈之后的5上面(20 % 8 + 1)

优点:效率高,任务触发时间延迟时间比delayQueue低,代码复杂度比delayQueue低。
缺点:

  • 服务器重启后,数据全部消失,怕宕机
  • 集群扩展相当麻烦
  • 因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现OOM异常

(4) redis zset实现#

利用redis的zset,zset是一个有序集合,每一个元素(member)都关联了一个score,通过score排序来取集合中的值。
zset常用命令
添加元素: ZADD key score member [[score member] [score member] …]
按顺序查询元素: ZRANGE key start stop [WITHSCORES]
查询元素score: ZSCORE key member
移除元素: ZREM key member [member …]

我们将订单超时时间戳与订单号分别设置为score和member,系统扫描第一个元素判断是否超时(zrange key 0 0 [withscores] )

存在一个致命的硬伤,在高并发条件下,多消费者会取到同一个订单号.
解决方案:
(1) 用分布式锁,但是用分布式锁,性能下降了,该方案不细说。
(2) 对ZREM的返回值进行判断,只有大于0的时候 (删除成功),才可消费数据

优点:
(1) 由于使用Redis作为消息通道,消息都存储在Redis中。如果发送程序或者任务处理程序挂了,重启之后,还有重新处理数据的可能性。
(2) 做集群扩展相当方便
(3) 时间准确度高
缺点:
(1) 需要额外进行redis维护

(5)使用消息队列#

我们可以采用rabbitMQ的延时队列。RabbitMQ具有以下两个特性,可以实现延迟队列

RabbitMQ可以针对Queue和Message设置 x-message-tt,来控制消息的生存时间,如果超时,则消息变为dead letter
lRabbitMQ的Queue可以配置x-dead-letter-exchange 和x-dead-letter-routing-key(可选)两个参数,用来控制队列内出现了deadletter,则按照这两个参数重新路由。

优缺点
优点: 高效,可以利用rabbitmq的分布式特性轻易的进行横向扩展,消息支持持久化增加了可靠性。缺点:本身的易用度要依赖于rabbitMq的运维.因为要引用rabbitMq,所以复杂度和成本变高

报表设计学习

es + clickhouse 搭配

总结#

es适合做全文检索需求,ch适合做大数据分析需求,本需求使用ch建立es视图表兼容es的维表,将维表数据和事实表数据分开处理,使用时连表查询,充分利用了两种引擎的特性,既支持全文检索,也保证了查询性能

es & clickhouse#

clickhouse除了不能全文检索,其他方面完全超过es, 尤其是在事实表,这种日志类型不断增加的业务类型上,主要在查询性能,稳定性,磁盘使用率 上要比es优秀很多,

因为ch本身的一个列式存储,编码压缩,向量化执行等特性。而且ch支持sql语言。

我们这个业务实际没有导入ch,而是ch上建立了一个es的视图表,从ch层兼容了es。

账户维度数据使用es,便于这块数据的更新,因为ch在update原数据方面支持的不算很好,且es支持全文检索
消耗事实数据使用ch,这部分量非常大,每个account每分钟一行, 且是纯insert, 适合ch处理。

es的存储特性,让他在全文检索的时候不能替代。

权限管理模型学习

本质上可以抽象出用户(User)、对象(Object)、行为(Action)和策略(Policy)的四个基本要素。策略(Policy)决定了用户(User)在一定的条件下对指定的对象(Object)有什么样的行为(Action)。

ACL#

ACL即访问控制列表,是基于资源的一种权限控制管理方式。实现原理非常简单,每一项资源都有一个访问列表,记录哪些用户可以对这个资源做CRUD的操作。当用户访问某个资源的时候,系统会检测用户是否在访问列表里,从而确定用户是否有对资源的操作权限

RBAC#

RBAC, 相比与ACL的直接用户上赋予权限

引入角色:
角色 —> 权限的集合,批量操作权限,权限可以统一分配和回收,角色可以和企业的岗位对应,能更加好理解和维护。

引入组织架构:
组织部门 —> 人员的集合, 批量操作人员 (eg: 赋予部门权限,或判断用户是否在组织中)

RBAC的特点如下:
优点是权限只和角色关联,可以统一分配和回收,使用简单,也降低了维护成本。角色可以和企业的岗位对应,能更加好理解和维护。
缺点是当某个角色下的某些用户需要做特别的权限限定,例如去除一些权限,就无能为力。另外,角色规划不好的情况下容易造成角色爆炸和混乱。

ABAC#

ABAC是基于属性的权限验证控制,动态的计算一组属性从而判断当前用户是否有操作权限。属性可以分为用户属性(岗位)、环境属性(公司、家里、时间)、对象属性(对象的类型,比如服务器、楼宇等)、操作属性(启动、创建、读取等)。一组属性可以叫做一个策略,策略可以配置在用户实体上,也可以配置在组织架构上

ABAC的特点如下:
非常灵活,可以根据不同的维度、粒度进行权限管控
无需预置判定逻辑,可以根据需要随时调整,在经常变化的系统中可以很好的支持
策略配置复杂时,会给管理者和问题追查时带来困难
策略判定需要实时计算,会带来性能损耗
从策略上不能很直观的看到人和对象的关系

个人总结:
ACL太复杂了,容易乱,操作起来级联严重
RABC, 适合公司类型,组织架构、人员角色的权限固定管理。
ABAC, 策略过于灵活,适合变化非常多的,策略复杂的

分布式锁

1、 基于MySQL的分布式锁实现
原理:直接创建一张锁表,然后通过操作该表中的数据来实现了.
。利用唯一索引的排他性来实现分布式锁,只有抢到锁的线程才能插入成功,释放锁删除该条记录即可

缺点:
1、强依赖数据库的可用性,存在单点问题。
2、数据库在高并发的场景下,性能上表现欠佳

2、基于Redis的分布式锁实现
可以通过set nx的特性来实现分布式锁

3、基于Zookeeper的分布式锁实现
可以利用临时有序节点的特性来实现分布式锁。
原理:1、先建一个目录lock 2、线程A想获取锁就在lock目录下创建一个带顺序的临时节点 3、然后获取比当前顺序号小的顺序号,获取到则获取锁失败,获取不到则获取锁成功。 4、解锁则是删除这个临时节点。

临时节点是根据客户端维护的session会话来维持生命周期的,当客户端不在维护session时,或者说客户端断开连接时,session消失,那么临时节点就会消失,这种特性正好满足了分布式锁要有过期时间的需求,不会产生死锁问题

缺点:抗不了多少并发,内存无法横向扩展

库存服务提性能

提性能#

读优化#

多级缓存:让数据靠近计算
链路优化:缩短耗时
周期库存⽅案:提⾼周期库存查询性能

写优化#

流量漏⽃:缓存预扣减少数据库的⽆效访问
分库分表:提升数据库的整体写⼊能⼒( 窄表设计下单分片物理机可承载4500,云服务器可承载3000,仅供参考,具体以业务测试为准 )
并⾏扣减:降低库存扣减的响应时间
分库存:解决单行更新能力不足问题,(一般单行更新的QPS在500以内)
合并请求:异步批量,将多次扣减累计计数,集中成一次扣减,从而实现了将串行处理变成了批处理。大大减轻更新压力。

当前峰值TPS超3000,年增⻓50%,未来5年期望⽀撑量级1万+;
综合以上分8库,部署2个集群,每个集群4个库

1、如果某个sku_id的库存扣减过热,单台实例支撑不了(mysql官方测评:一般单行更新的QPS在500以内),可以考虑将一个sku的大库存拆分成N份,放在不同的库中(也就是说所有子库的库存数总和才是一件sku的真实库存),由于前台的访问流量非常大,按照均分原则,每个子库分到的流量应该差不多。上层路由时只需要在sku_id后面拼接一个范围内的随机数,即可找到对应的子库,有效减轻系统压力。

2、单条sku库存记录更新过热,也可以采用批量提交方式,将多次扣减累计计数,集中成一次扣减,从而实现了将串行处理变成了批处理,也可以大大减轻数据库压力。

3、引入RocketMQ消息队列,经过前置校验后,如果有剩余库存,则把创建订单的操作封装成消息发送给MQ,订单系统从RocketMQ中以特定的频率消费,创建订单,该方案有一定的延迟性。

pcursor基于游标的分页

一、基于偏移量分页&基于游标分页#

1.基于偏移量的分页#

适用于内容是静态的,或者不用实时返回数据最新的变化。特点:1.只要有新增或删除,就会有大量的数据偏移量的改变,造成重复展示或者漏展示。2.存在数据量大时的offset慢查询问题

img

2.基于游标的分页#

适用于查询结果在用户浏览过程中是变化的。特点:1.时间线里出现新的数据或删除数据,这些变化都可以在 “前一页” 或者 “后一页” 体现出来。 2.没有offset问题

eg: 发现页 下拉获取最新,上拉获取更久

二、接口实现#

请求参数

参数名 类型 逻辑
参数名 类型 逻辑
pcursor String 初始值传 “” , 后续值使用后端返回值
count Integer 本次想获取的数据量

响应信息

参数名 类型 逻辑
pcursor String 透传给下一页查询 调用方判断是否为 “no_more”, 若是则不再调用,否则永远停不下来了

三、底层实现#

根据更新时间排序

排序规则:order by update_time desc, id desc 原因: 更新时间 + id 的排序, 可以为所有数据确定顺序,即使更新时间相同,即确定每条记录的游标,游标为更新时间+id,只要查询条件不变,即游标可在该顺序下唯一确定一个位置

1. pcursor游标如何拼接#

  • 根据上面排序规则查询出数据
  • 取最后一个数据last,即为该排序下此次查询的终止位置
  • 根据last数据拼接成pcursor
    pcursor = encrypt ( (“updateTime”, long, 1589439219430); (“id”, long, 95424 ); )

2.pcursor游标如何使用#

  • pcursor解析为 上次查询last的 last.updateTime, last.id
  • 本次查询where条件中,updateTime < last.updateTime or (update_time = last.updateTime and id < last.id) order by update_time desc, id desc;
    如下表格,为order by update_time desc, id desc,上次查询游标为last游标位置,则下次查询 updateTime < 1555500001 or (updateTime = 1555500001 and id < 32) order by update_time desc, id desc, 即为后三行数据
update_time id
1555500001 33
1555500001 32 last 游标位置
1555500001 31
1555500000 44
1555500000 42

3. 生成查询SQL#

若查询条件为 userId, status, updateTime 则查询语句为

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT *
FROM
ad_xxx_info
WHERE
user_id = xxx
AND STATUS = xxx
AND (
update_time < xxx
OR ( update_time = xxx AND id < xxx )
)
ORDER BY
update_time DESC, id DESC
LIMIT 20;

创建联合索引 (user_id, status, update_time) 查询即可全部走索引。注:主键也是索引的一部分,Extra为Using where。using where is fine https://stackoverflow.com/questions/9533841/mysql-fix-using-where

幂等设计

请求幂等性总结:

1.是否需要幂等。比如查询,insert含唯一索引的表,update set数据,delete by id 等简单场景是天然幂等。不需要额外做幂等操作。无法这样的比如要做数据的累加,就要做幂等。

2.如何判断重复。

业务唯一键,查记录表或流水表,根据记录的有无和状态来判断。

3.实现。

  1. 简单的话接口直接实现, 但通常幂等逻辑是有通用性的
  2. 如果服务多接口使用写幂等工具类
  3. 如果多服务使用一套幂等逻辑,写sdk
  4. 最复杂的多服务间幂等,还无法都获取到记录结果,就要在sdk统一读写幂等表,读写相同的幂等记录做幂等

4.并发处理

  1. 单机情况下,使用java锁synchronized
  2. 使用数据库锁,悲观锁:select for update,乐观锁:update set XXX and version = 2 where version = 1
  3. 使用分布式锁:
    redis锁:1.过期时间导致并发问题,2.主从切换时锁丢失
    zookeeper锁:写能力不如redis

原文:

  1. 维度1 : 是否需要幂等处理?

    1. 查询场景

      select 字段 from 表 where 条件 ,这种是不会对数据产生任何变化,所以查询场景具备天然幂等性;注意这里的幂等是对系统资源的改变,而不是返回数据的结果,即使返回结果不相同但是该操作本身没有副作用,所以幂等

    2. 新增场景

      insert into 表 (order_id,字段) value(1,字段) :

      1)如果order_id为唯一键,重复操作上面的业务,只会插入一条数据,具备天然幂等性。

      2)如order_id不是唯一键,那上面业务多次操作,数据都会新增多条,不具备幂等性。

    3. 修改场景

      1)直接赋值场景:update 表 set price = 20 where id=1 ;分析: 这种场景不管执行多少次,price都一样,具备天然幂等性;

      2)计算赋值场景:update 表 set price = price + 20 where id=1,每次操作price数据都不一样,不具备幂等性;

    4. 删除场景

      1)delete from 表 where id=1 ,多次操作,结果一样,具备天然幂等性

      2)delete from 表 where price > 20,多次操作,结果一样,具备天然幂等性

      总结:单纯的查询接口,删除接口,没有计算的更新接口,每次执行结果是天然幂等的,其他情况则可能需要做幂等

  2. 维度2 : 是否需要并发控制?

    1. 场景1: 唯一键数据的重复插入

      此场景不需要并发控制,天然并发安全,但需要业务方处理DuplicateKeyException异常

    2. 场景2:转账,若 a >= 100, a -100, b+100

      此场景为了保证数据一致性,事务原子性,需要顺序执行

      总结:除了极其简单的场景外,大部分幂等场景都需要并发控制,需要强锁还是弱锁

  3. 维度3 : 如何判断是重复请求?

    1. 场景1: 无业务主键的新增

      此场景下需要服务端颁发token,通过token判断是否重复,做防重复提交

    2. 场景2: 有业务唯一键的新增

      此场景下可以使用业务唯一键判断是否重复

    3. 场景3: 有业务唯一键的更新

      此场景下可以使用业务数据对应的状态判断是否重复

  4. 维度4 : 重复请求处理方式?

    1. 场景1:消息重复消费

      此场景下,不需要给上游一个返回,重复执行只需要丢弃

    2. 场景2:前端重复提交

      此场景下,需要给用户一个提示,那么重复请求可以直接抛出一个DuplicateReqeustException异常,由上层捕获,前端展示“请不要重复请求”

    3. 场景3: 底层的转账接口

      此场景下,同一笔转账,重复调用第一次若成功了,第二次应该返回相同“成功”,若第一次失败了,第二次返回业务执行结果

      结论:根据不同场景,应该有不同的结束方式,应该由业务方自己决定是丢弃,抛异常,直接返回,还是执行业务逻辑

  5. 维度5 : 重复请求返回是否相同?

    1. 场景1:第一次执行成功,第二次返回应该与第一次相同

    2. 场景2:第一次执行因为代码原因导致的异常而失败,第二次返回应该与第一次相同

    3. 场景3:第一次执行因为网络原因失败,第二次应该执行业务逻辑,返回结果可能不一致

      总结:根据第一次执行的成功和失败,判断第二次应该执行业务逻辑还是直接返回相同结果

  6. 维度6 : 重复请求调用下游如何处理?

    1. 场景1: 业务逻辑中,有调用下游rpc接口,第一次执行失败,第二次重复执行业务逻辑再次调用下游接口

      总结:此场景本质上是一个分布式一致性问题,需要业务方自己解决,或者保证下游接口也是幂等的

  7. 识别相同请求

    1. token机制:每次提交上来的请求,都带上一个token标识,同一个token只处理一次,例如:新增场景的防止重复提交
    2. 业务唯一标识:使用id或者unique key,例如:支付后,支付凭证落库
    3. 业务唯一标识 + 状态:支付后,通过支付凭证号和当前订单状态,更新订单状态
    4. 在业务侧建立同库幂等数据表,记录请求唯一键和执行状态,执行成功后更新状态为成功
  8. 并发处理方式

    1. 单机情况下,使用java锁synchronized

    2. 使用数据库锁,悲观锁:select for update,乐观锁:update set XXX and version = 2 where version = 1

    3. 使用分布式锁:

      redis锁:1.过期时间导致并发问题,2.主从切换时锁丢失

      zookeeper锁:写能力不如redis

  9. 一致性保证

    1. 业务方需保证原子性,本地更新都在一个事务里
    2. 若无法保证分布式事务,下游接口需是幂等的,且本地事务必须重试达到最终一致

SDK设计#

  1. 并发控制
    1. 提供注解,按业务自动划分,提供指定幂等唯一键
    2. 提供并发控制,分布式锁服务(redis或zk),指定分布式锁时间
    3. 提供并发时幂等处理方式,丢弃/抛异常/等待锁
    4. 提供降级选择,不强依赖于锁服务
    5. 提供token注解,防止重复提交
  2. 判断重复请求

处理方式 优点 缺点

方案一 业务方自己判断 业务方需要自己考虑判重

方案二 sdk提供判重接口,由业务方实现 可以实现重复请求的通用处理

方案三 幂等表:在业务侧建立同库幂等数据表,记录请求唯一键和执行状态,执行成功后更新状态为成功 业务方无需再关心判重问题 数据量大,开发量大,复杂度较高

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×