1、为何单体架构中要做出成分布式延迟队列
大家常说的单体架构 只是业务形态,而分布式延迟队列 说的是调度模式与消费机制是否支持多实例、幂等、安全抢占。
即使是单体服务,也可进行多副本进行部署:
go
Nginx / SLB
-> manpao-service 实例 A
-> manpao-service 实例 B
-> manpao-service 实例 C
这种属于水平面拓展,虽然依旧不是分布式系统,但是这是确保架构高可用性的必要手段。
2、为什么要采用outbox模式
其实这个点,我一直想要说出来。
不论是在异步MQ的时候,还是做延迟队列的时,我的项目都采用outbox作为基石。
为什么?
他是发信箱模式 ,他确保的是最终一致性。
他解决的是业务数据写入成功 与消息/事件发送成功 两者之间尽量保持一致。
为了防止数据入库了,但是消息却因为网络原因或MQ挂了,最后导致发送失败。
并且他的拓展性挺高的。
若后期存储的数据量变大 的话,我可以根据不同功能分表
若后期吞吐量变大 的话,我可以采用cdc这种监听日志的组件,去mysql的binlog日志。
若后期要保证最终一致性,我可以采用indox表,作为消费端的消费表。用来做幂等性控制。
3、为何我要做分布式延迟队列
因为做分布式延迟队列,主要是为了解决订单超时问题。并且要确保多实例下任务调度依旧可靠。
并且只放做一个定时器关闭的话,服务重启就没了。
因为属于异步写入,所以可能会出现一种情况,订单处理成功,但是redis没写入成功。
所以我采用了outbox模式。
并且 zset 天然适合这种排序的。具体的负载我放到hash中,可以降低内存开销。
同时我在抢占的时候,不是直接用zrangebyscore,而是采用lua脚本,进行任务抢占、状态修改、可见性时间修改、以及发放 预订令牌 等操作,来避免被重复抢占。
其中,通过修改可见性时间,是增加score时间,暂时对其他实例不可见。
预订令牌,是对每个取出的数据发放的令牌。具有唯一性,是为了保证幂等操作的。只要持有令牌的的才能够被正常消费。
在设计的时候,其实考虑了很多问题
- 采用outbox是不得已的,毕竟redis与mysql属于不同系统,除了引入分布式事务,几乎没法回滚。
- 既然采用了outbox,就可能会出现重复投递,因为我可能改变本地状态的操作失败。
- outbox扫表很慢,是避免不了的,而我采用的是索引优化、分页描扫、后期还可以采用cdc技术。
- 我的score设计的是毫秒级的,为了防止时间戳问题,我还专门统一了时区。
- 如果后期数据量特别大的话,还可以考虑分片,分几个不同的key,避免全部积累在一处。
- 如果可见性时间到期了,然后我又取了一次,这时就以zset+hash中,拿着的新令牌为准。只有合法token才能提交结果。
- 我的令牌token,只能防并发,最多只能算是链路幂等,不能说是业务幂等。
- 不用MQ的原因,就是因为MQ太重,太笨。不灵活。
4、我的微服务采用的架构:
采用的 Clean Architecture ,其实就是外层依赖 内层的一种思想。
一般是配合DDD架构搭配起来的。
5、图片管理模块设计
我在设计图片管理模块时,首先抽象了存储驱动的接口,对外屏蔽细节,只暴露了Upload/Delete能力。上层可以通过具体的配置参数,来选择驱动。我们平时开发环境走本地,生产环境走oss对象存储。
然后针对大文件上传,我设计了一套服务器中转方式 的分片上传 流程。
首先建立一个临时目录,并初始化一份manifest.json文件,用来记录分片的元数据。如预计分片个数、过期时间等一些必要内容。这样前端可以在断网后,根据元数据,补充缺失的分片。
最终所有到齐之后,在统一合并起来。
并且为了防止并发严重,我这里用带缓冲的 chan 做了一个全局限流。同时限定上传图片或分片 的数量与同时合并的数量。避免服务器被打爆。
我merge的实际操作, 就是先根据manifest.json中的元数据,找到写入顺序,然后通过io.copy进行写入一个文件。
其实在设计之初,我已经知道我此时做的是不完美的。
- 我采用的是服务器中转,他的缺点我非常清楚,回到是磁盘IO增加、并且占用网络带宽。我们这样做是因为数据量比较小,最重要的是我们这个暂时还支持本地。如果后期数据量大的话,我会让前端来做这些事情,我这里只需要给凭证,记录状态即可。
- 我的manifest.json 暂时是存到了本地 ,等以后多实例 的时候,我会考虑到存放到数据库 / redis里面,以应对多实例 / 多节点的情况。
- 我才用带缓冲的channel作为并发限流,是因为他很轻量。
- 我才用的是io.copy这种流式拷贝,为的就是避免分片合并时,一次性直接输入占用大量内存。
- 为了保证我的分页没坏,我会变拷贝变计算hash,最终做一个对比,看对于否。
- 如果分片上传到一半时断网了,那没关系,因为他的后缀是.temp,所以我不会用它,只能重传。
- 我的孤儿文件分为了两种,一种是过期的,另一种是上传到oss了,但是没有入数据库。所以最终会被清扫掉。
- 最大的缺点就是占用网络宽带与磁盘io,并且此时是无法做到多实例。
6、我是如何搭建消息通知链路的
其实这个主要就是做范围的群公告用 ,比如给所有的学校管理员发消息,或者说给某个学校内的所有人发消息,并且要知道他们的已读未读的数量。
我首先采用的就是读扩散,只记录一条公告,与发送范围,而不是直接给每个人都记录一条信息,然后通过websocket给所有人发布一个通知信号,前端收到信号后,主动通过http,去拉取或者刷新可见信息。
此时在单独另存一个表,用来存放谁读了、谁删除了。
如果没有任何记录,就代表没有浏览过。
这样设计,可以极大程度上减少存储成本。
我当时为了这样设计,也思考了许久。
- 为啥采用读扩散,而不采用写扩散,是因为读扩散只需要O(1)时间写入,只有读取的时候麻烦。
- 我的范围是通过学校、角色、城市这些范围去判断过滤的。
- 如果后期我的表格非常大 的情况下,我会采用冷热分离的模式,把最近30天的放在一个表中,剩下其余的放在其他表中。
- 我现在没有支持已撤回这一点,但是如果后期非要支持已撤回的话,我会新增一个状态字段,以后过滤一下就行。
- 我现在不支持像钉钉那样的精确读,读扩散非常适合的是群公告一类的。
- 如果后期的写入压力非常大,也可以采用分库分表等优化方向。
7、我是如何设计多租户体系的
首先就是在jwt内部携带用户id,当作用户的什么凭证。
短token经常传输、被应用,所以把他设计的生命周期很短。
而长token,被存到前端本地,比较难被获取,所以生命周期长,长用来刷新短token。
此外
我的组织、角色、用户、以及api、路由组、菜单等各种关系我是存在数据库中的。
casbin是授权判断引擎 ,是存到内存中的,用来频繁判断权限会很快,且性能好。
我会在项目开启时,把本地数据库内的所有权限功能同步到casbin中。
若有用户权限发生改变,我则会发布事务去通知所有的实例,去更新。
我说了三级授权,他其实是:
在不同组织上下文隔离 的情况下,对属于该组织的角色,进行前端组件/后端接口/资源点的授权。
其中,当给某个用户分配新角色,会触发casbin_rule进行增量添加。然后需要把这件事情通知给所有的权限进行重载。因为他只影响了用户一人。所以可以采用增量。
但若是时给角色新增权限,则需要casbin_rule规则进行重建。然后其他实例一起重载。因为这个影响了所有人。
用户进行访问的时候,会优先走redis获取他此时组织与角色,只有过期的时候,才会打到mysql上。
8、我是如何实现分布式锁的
我的分布式锁,是通过:
set lock:xxx NX EX ttl
这样实现的,nx就是只有key不存在才能写入,互斥用的,EX是过期时间。
同时我才用的lua脚本进行原子操作,并且专门设计一个看门狗进行续期。
9、我是如何设计学生认证模块的?
我首先设计一套ocr接口,用户屏蔽内部细节,上层调用我的接口就行。
然后我设置了一套工厂模式,优先选用腾讯云的ocr,当其出错了、额度用完、或者置信度太低,则会降级到阿里云的ocr。
此外我还基于redis设计了一套熔断器 。
最初是关闭状态,当连续失败数量达到阈值,或者错误率到达50%后,会进行熔断。
不再打到主ocr上,会将请求放到备用ocr上。
等到30秒后,才会默默的允许放出一点探针进行试探。