SparkBook
本文最后更新于87 天前

简介

​小微书是一个尝试修改小红书的应用,包含有帖子,点赞,评论,聊天,排序等基本功能,对事件的存储,分散,治理,容错,安全,理解可以组成一个大型的论坛项目,虽然项目创新点不足,但在收藏项目上可学习相关内容,采用微服务架构,并且对高水平,高可用,高性能够进行全方位的思考和实践,以及接入日志监控和链路追踪,此项目只供个人学习相关查看,前端部分相对简陋,以下设计针对前端进行实现,根据提交内容可以对应的资料,为每个开发阶段制定测试方案,包括单元测试、集成测试和负载测试,确保系统稳定可靠,当前待建设中。

开发环境

IDE🧑‍💻:GoLand

1.22.0

操作系统:Ubuntu 22.04.3 LTS(WSL2)

业务分析

BFF服务

长token设计,长token的7天过渡,普通token的30分钟短过渡,而且redis只存储退出登录用户的jwtToken,使用的时候把它存入ctx中的头部header,x-jwt-token,x-refresh-token以及对应的用户可以获取到UserClaims,这块东西外面有了一个中间件去进行,session检查机制,会根据ssid,并且存储在cookie中,去判断是否当前用户以及下线,这里和jwt的二选一用户代理登录验证用户,更好的实践应该是浏览器指纹那类的装饰器模式,限流的分发,用随机数去进行限流,判断是否进行远程调用,或者本地调用,这里拓展的是不同实例之间的调用支持微信登录,规定好的codeurl和回调,说明存在到cookie里面,这里也做jwt校验

用户服务

安全: 1.密码不采用明文存储,用md5加密后存入数据库

2.访问资源采用jwt,并且这个jwt其实可以外接一个中间件给gin使用,并且可以结合长短token一起使用

3.还请求头携带用户代理进行,需要消耗的可以上升到浏览器指纹这块让前端去操作

4.注册和登录的逻辑采用插入更新,其实就是那句如果遇到id冲突就成更新注册操作需要校验是否在注册过之前,这里不要用先去查一次库的方式,而是直接插入由于手机号是独立的,根据去mysql的错误重复数据1042进行判断创建是否成立的这里面还有一个findById的操作,思路是这里采用查数据库后写进磁盘,触发降级不会走数据库的快慢路径操作先走磁盘,磁盘找不到就从数据库找,找完重新设置进去,如果redis出现错误,就不让他流量直接打到数据库,因此直接返回错误

5.微信登录,先去url让接口弄成二维码,然后扫描后等待微信回调方法等待接受,等待微信回调截图,然后还有一个验证的流程,然后进行注册或者登录操作,主要是里面的unionid和openid

短信服务

短信服务其实就是去套用各种模板,并且得出一个严格的抽象,有趣的是它的内容错怎么处理,提供多种实现,同步转异步,权限验证,超时重试,限流,还有一些固定模板的实现

1.权限:传参数发送的时候,对应的模板id会被jwt加密,需要类似的装饰器模式进行一个解决后得到id

2.限流:设置限流后对短信服务进行拦截,也结合redis和lua的通用

3.failover:这里首先是采用吞吐量开始进行,但是发现流量都在第一个,就用求余法进行操作,还配置了最大超时次数就进行切换短信渠道商,这里切换的时候用的一些cas和原子类的操作,没有去加锁

4.同步转异步:

1.1 使用绝对阈值,这样直接发送的时候,(连续一段时间,或者连续N个请求)响应时间超过了500ms,然后后续请求转异步 1.2 或者发送的错误率大于某个阈值 2.退出异步的方式就是连续请求和错误率都低于了阈值 实现的方式都是用的两个维护滑动窗口的方式,这个也可以渐变动态可配置

对于发送的接口,先抢占,抢占到后发送,发送后记录数据给数据库,处理并发问题,结合数据库的更新锁,就是一个查完更新的操作,更新的同时计数加一并且只查一分钟前的记录,保证发送的时候不会有并发问题,因为时间已经被该实例更新,不会被其他获取

验证码服务

它通常要去调用短信服务,然后在自己维护一个redis,结合lua脚本把输入和里面的正确确认,不要在代码中会带来问题,来验证验证码是否正确,验证码的策略是十分钟有效,并且超过一分钟就可以重新发送,在redis维护两个键就行,并且用户输入的验证码正确,验证次数的标记-1,让他自己有效期然后还有本地服务器的实现方式,验证码这种东西不需要多级服务器,因此在创建的时候可以手动配置

支付服务

这里用的是微信Native的支付方式,主要是讲一下思路和需要的api问题

思路:当业务方创建订单时,汇总形成对应的bizId和对应的数据,然后会在数据库初始化一条预支付信息,在web端公开后接受微信方的回调后更新对应的时间,然后把数据发送到kakfa上,方便后面进行数据清空洗(未实现),最重要的是给应答的业务方可以进行读取需要的内容,内容错误的思路就是当业务方半小时去没有返回时,需要主动微信本地查,脉冲打开个去查数据库30分钟前的数据然后发送获取应答的结果,后面更新数据。

api:这里占用了微信的开源github.com/wechatpay-apiv3/wechatpay-go可以查看对应的实现,还有需要初始化微信客户端的一些配置,这些放在ioc/wechat.go里面。*notify.Handler可以在web端解析的响应请求& payments.Transaction{} *native.NativeApiService提供一些主动查询的方法

奖励服务

流程:打赏-支付-入账三个服务的调用

预打赏在打赏服务给的二维码颗粒做数据,而不是在支付服务做的数据,先查数据,不创建,调用支付服务,然后写入数据,根据biz和bizId和uid进行查询,防止被人恶意查数据

获取打奖结果只需要用户去关心是否打奖成功,查询订单可以走一个快慢路径,先查本地表的维护状态,否则就去调用支付服务查状态,然后自己更新,出现这种情况是比如但是预支付出现二维码后尚未支付就进行获取奖励

更新状态的时候再去查订单状态,只是对状态是支付成功的操作,这里模拟了平台抽成的操作,然后都发给入账服务进行处理

kafka的清理功能,在支付的时候,把结果发送给kafka,现在的主题是 payment_event,在打赏服务中起一个消费者奖励-%d,只消费和自己数据有关,消费到的数据在进行更新到对应的数据库里面状态,可以做一个二次校验的功能

入账服务

首先知道一个人可能会有多个账号,但是uid是固定的,因此需要建立一张uid和账户记录的表,以及一张入账记录的表,以及对应记录的业务id和对应金额。涉及的一些负数或者小数都可以通过字段解决,这里不做赘述实现,这里目前只维护了一个接口入账信息,并没有多余的审核,校验也是操作,插入数据的时候去开个事务的操作

标签服务

场景是可以给资源打多个标签,并且标签支持搜索,个人也可以拥有多个标签,不支持等级联标签,资源就是某个资源打了某些标签,因此要维护标签表和标签资源表,那么今晚肯定会关联一个逻辑外键出来,这里的处理方式是用gorm的关联外键,并且设置等级联删除,并且打上标签的时候会开事务先删除原有的标签,不支持修改标签。

导入服务器的策略,用户可以有一个异步预加载的操作,对于tob最好可以将多个对应标签写入服务器策略

kafka的用途是把资源发送给es进行读取到inputAny里面,里面存的字段data和对应字符串

客户服务

搜索提取成了一个服务,这样就可以对接各个业务方带来的搜索功能,但是这里没有使用多查询或多匹配之类的写法,比如搜索的内容有用户和文章,那么就会去用两个存储库去搜查的操作,标签也能被搜索。以后需要对接不同业务方的时候需要自己去修改,当然提供InputAny的方式也行,需要有人搜索什么东西,传json数据也行,但是没有索引索引那么好找。

用kafka消费数据的时候就是读到业务方自己的话题的时候,就相当于去调用任何,这也是一种兜底策略

文章服务

文章这块,是有一个三个状态,发表,未发表,自己可见,而且发表和未发表的会有两个数据表,走的思路也是redis进行一个磁盘操作,都是回写磁盘的操作。那么对于内存的折中,对于作者界面,网址磁盘第一页,而文章内容也是磁盘的摘要,并且默认用户会第一篇进入看的次数比较多,如果查第一页的时候返回数据,那么就异步进行预机制加载把第一篇文章给他全部存储起来,并且内容不超过1MB限制,否则查库回写第一页数据。

在作者触发更新文章的时候需要把第一页缓存记得删除

还有一个思路就是把文章内容存储到s3或者mongodb里面,数据库只存那些数据

有了制作库和线上库的概念,不干扰线上生产环境,那么大部分发版的操作是进行事务的,对于插入操作其实就看id是否为0,而在线上库则需要事务控制提交和回滚,提供upsert语义

交互服务

这里设计对应的点赞和收藏业务逻辑

表格的设计,有交互表,点赞表,收藏表,定义了是否点赞和收藏的逻辑,因此有一些级联操作,然后也有很多双写入操作,采用的策略是事务,然后也定义了双写连接池自动提交的操作

缓存设计只缓存文章的点赞和收藏数量,采用结构是HMSet,并且读取量用kafka进行异步批量消费削峰

数据迁移也在这里做,这里定义了的是同结构数据迁移,采用migrator进行数据迁移,结合canal进行数据的监听,然后发送到kafka里面进行异步消费

热榜服务

这里定义了两个接口,一个是计算接口,一个是获取接口

对于热榜分数的计算采用点赞量和时间进行衰减,需要调用文章服务去分批获取所有的文章,然后根据每个文章去调用交互服务获取对应的点赞量,最后计算出该文章相应的热榜分数,用一个优先队列来保存获取的热榜数据,借用了ekit包里面的NewPriorityQueue,把大的分数都放在前面就解决了,查找的时候也是根据utime排序后查找,不够前面的有对应的处理,并且防止算数问题,查表的时候会有一个起始时间的限制

旁路模式双服务器设计,更新数据的时候先更新本地,再更新redis,获取的数据的时候,先从本地服务器获取,如果有问题再获取redis服务器,再把redis获取的数据回写到本地服务器里面,如果redis出问题,就从本地服务器里面强制获取老的数据内容错

关注服务

表设计只有关注和被关注的表,对于粉丝量和关注量这种设计并不做持久化,只放在服务器上,需要时做计数查询

对于业务来说,当一个人进行关注时,那么关注的数量就会增加一个,同时被关注者的粉丝也随之增加,对于粉丝量和关注量,采用缓存里面去设计,并且启用reids的管道监控进行操作,由于查看自己的粉丝和关注是少量操作,不是缓存,如果需要查就做一个计数查询,然后缓存到redis里面,后面的增加操作就是直接备份,当然这里设计还可以更好的操作。

需要理清楚关注者和被关注者,用表查询解决,这里相对来说只是业务设计

评论服务

评论的设计有清晰的方式,一种是父评论和子评论id树型设计,还有嵌套集进行洼地设计,还有闭包类型的两周桌设计,这里采用第一种设计模式,对应有根评论,父评论,子评论队列,相互洼地的

级联删除,删除的时候要把父评论的回复一起删掉了,可以理解成PID找不到的id的时候就会把自己也删掉

查找子评论的时候并发访问三条子评论,并且存在降级策略,上游ctx可以控制是否触发降级,若触发则不再查询子评论,找子评论的方式是循环并发每个子评论,并且注意闭包抓取的问题

查询的时候增加minId和maxId的操作,防止出现的分页重复读取评论问题

通过是否id为0来判断是查询当前rootid主评论还是查找主评论及其所有子评论

自定义扩展包

logger:这个主要是采用zapLogger现扩展,在不适用其他日志构建框架的前提下,简单使用接口定义出对应常用的方法,并且采用字段的字段约束,并且用这个给gorm和gin进行扩展打印日志,在其他业务代码中也采用这个

saramax:通过sarama的一些api进行二次封装,主要是对消费者去实现ConsumerGroupHandler就可以自己编写消息的前后和处理的逻辑,并引入泛型进行序列化读取操作,主要有批量消费和重试机制

ginx

1.主要使用的是*gin.HandlerFunc主要做对请求体的绑定到对应结构体和对claim进行校验的流程,同样采用泛型外部传入,然后进行处理走对应的函数流程,完成后返回统一的结果。

2.还有有限流插件的操作,在一定时间段限制对应的客户端ip流量通过,主要是按照ip进行建key,然后走lua脚本用zset的数据结构,基本和其他限流操作差不多。

3.还有对请求和响应的打印日志,aop思想,扩展插件可以显式配置是否需要打印响应的请求体和返回数据,下面难点在需要gin.ResponseWriter操作的http.ResponseWriter,对返回数据进行操作。

4.此外还统计激活请求书和返回响应时间到Prometheus里面任选

grpcx

1.采用自己设计的平滑的带权重的负载均衡,主要是要注意balancer.Builder和sbalancer.Picker分别两个结构去体实现这两个接口,以etcd作为注册中心,并且加锁防止不同的节点计算,balancer.SubConn作为节点存储,其中要注意注册中心转发数据后整会变浮点的

2.也对进行日志打印的操作,通过判断是否在panic掉之前的数据来获取对应的错误,然后打对应的一些日志,方法,响应时间,对端应用名和ip等。

gormx

1.利用回调机制增加对sql查询的时间的Prometheus的操作

2.对交换数据库的时候双写方案,双写的一致性保证,使用方法在测试用例里面,手动拓展进行事务的提交和回滚也支持双数据源,主要利用的是gorm.SubConn和sql.Tx,晚上可以更准确的控制不同数据源的提交和回滚

canalx、redis:都是对一些操作的日志打印到Prometheus集成和一些通用东西的输出

cronjobx:对定时任务的统一管理,并且也加入队列追踪和日志打印的流程,主要还是去实现Job实现,这里引出了一个新的服务,自定义了任务的数据库板块,实现方式参考xxl-job,并且以grpc的方式引入,不做服务注册

这里多一个cronjob的服务实现,对于任务采用抢占式调用,根据cron表达式设置下次调用时间字段,循环扫描查找下次调用​​时间小于当前的时间的就进行抢占标记,并更新使用时间,这个抢占过程采用的是CAS乐观锁去实现,查看是否影响数据

ratelimit:利用redis和lua脚本实现动画的限流操作

migrator:做的是同结构不进行数据迁移相关。数据校验和数据修复

1.首先不升级迁移的方案首先对于两个数据库有四个状态,分别是src,src优先,dst优先,dst,简单读写和写的操作,这里需要结合之前的双写策略保证对于两个数据源的数据一致性

2.数据校验,分割增量校验,增量需要注意offset的校验量问题,可以使用utime或者canal进行解决,全校验量,在这个修复的策略用定时任务去处理,并且发送给kafka去异步进行修复,最大程度保证数据一致性。先去查一下对应的表,记录对应的列字段。

3.修复数据,异构问题需要考虑,在修复的同时,会有源库存在没有被同步到目的库,这里走选择用于更新插入的语义才能解决,但是有一个极端比如他同步的时候有删除源库数据,那么对于源库修复的时候也需要进行删除

4.支持http调用动态改变四个状态以及对校验的启动和停止,具有高算数思考

5.解决offset和limit带来的灾过或者重复读取问题,方案是canal或者gautime

技术栈

  • Node.js
  • Docker
    • 镜像源(还是挂代理方便)
    • mysql – 开源关系数据库管理系统 (RDBMS)
    • redis – 开源内存存储
    • etcd – 分布式键值存储,旨在跨集群安全地存储数据
    • mongo – MongoDB 文档数据库提供高可用性和易扩展性
    • kafka – Apache Kafka 是一个用于构建实时应用程序的分布式流媒体平台
    • prometheus – Prometheus 监控系统和时间序列数据库
    • grafana – 开放的可观察性平台
    • zipkin – 分布式跟踪系统
    • Canal – 阿里巴巴的开源项目,用于捕获数据库更改(binlog)并同步到其他系统。它通常用于数据复制和实时流传输。
    • ELK – 一组用于记录、搜索和可视化数据的工具:
      • Elasticsearch:一个分布式、RESTful 搜索和分析引擎
      • Logstash:服务器端数据处理管道
      • Kibana:Elasticsearch 数据可视化工具
  • Kubernetes
  • wrk – 现代 HTTP 基准测试工具
  • protobuf – 协议缓冲区 – Google 的数据交换格式
  • buf – 使用协议缓冲区的最佳方式

编程能力

分包架构

这体现在对于各种服务之间的管理和应用,并且服务内遵循dao- repository-service-handler ,并且使用mock和wire对里面进行测试和依赖注入,并且采用DDD和TDD的一些实际运用,各个服务之间都采用rpc调用

面向失败编程

面向失败编程(Failure-oriented Programming,FOP)是一种编程范式,强调在编写逻辑时尽可能考虑边界条件和失败情况,以增强程序的稳定性。 简单来说,就是时刻考虑系统可能会崩溃。无论是系统本身、依赖的服务还是依赖的数据库,都可能会崩溃。 面向失败编程不仅仅是对输入进行校验,它还包括:

  • 错误处理:需要严密处理各种可能的错误情况
  • 容错设计:长期培养的能力是针对业务和系统特征设计容错策略。这通常是较难掌握的,而其余部分可以通过规范来达成。 在面向失败编程中,需要长期培养的能力是针对业务和系统特征设计容错策略。其他方面较容易掌握,或者公司可以通过规范来达成。 在项目中,讨论了许多容错方案,包括:
  • 重试机制:需要考虑重试的间隔和次数,以及最后可能需要人工介入。
  • 监控与告警:在追求高可用时,还要考虑自动修复的程度
  • 限流:用于保护系统本身。
  • 下游服务治理:如果下游服务可能崩溃,需使用一些治理技巧:
    • 轮询:可以是每次都轮询,也可以针对某个下游节点失败后的限流。
    • 客户端限流:限制客户端的请求速率以保护系统资源。
    • 同步转异步:在转为异步后,必须保证请求会被处理而不会遗漏
  • 考虑安全性:例如,防止 token 泄露以增强系统的安全性。 在设计容错方案时,尽可能在平时收集别人使用的容错方案,以了解各种处理方式。根据自己实际处理的业务设计合适的容错方案。简单地生搬硬套别人的方案,效果可能不佳。

灵活的缓存方案

在整个单体应用中,已经充分接触了缓存方案。相比传统的缓存方案,项目中的缓存方案更具“趣味性”。在实践中,除非逼不得已,通常不会使用看起来非常特殊的缓存方案。 使用过和讨论过的缓存方案包括:

  • 只使用 Redis:更新缓存的常见方案是更新数据库后删除缓存。
  • 本地缓存与 Redis 缓存结合使用。大多数系统完成这些步骤即可,
    • 查找顺序:本地缓存 – Redis – 数据库
    • 更新顺序:数据库 – 本地缓存 – Redis
  • 根据业务特征动态设置缓存的过期时间。例如,如果能判定某个用户是大 V,则他的数据过期时间应设得更长。
  • 淘汰对象:根据业务特征来淘汰缓存对象。
  • 缓存崩溃:需要考虑缓存崩溃的问题。在实践中,缓存崩溃可能导致数据库也一起崩溃。 在上述缓存方案的基础上,需要能够举一反三,根据业务特征设计针对性的解决方案。在整个职业生涯中,如果能有效使用缓存,就能解决 90% 的性能问题。剩下的 10% 则需要依靠各种技巧和优化手段。

注意并发问题

无论是代码中的 Go 实例,还是外部数据库,在实现任何功能时操作对象或 Redis 缓存数据时,都必须考虑并发问题。具体来说,需要关注是否有多个 goroutine 在同一时刻读写对象,这些 goroutine 可能在不同的实例(机器)上,也可能在同一实例(机器)上。 在项目中,使用了多种方法来解决并发问题:

  • SELECT FOR UPDATE:用于确保读取的数据在操作期间不会被修改,简单且有效。
  • 分布式锁:用于保证同一时刻只有一个 goroutine 可以执行特定操作。
  • Lua 脚本:在 Redis 中使用 Lua 脚本来确保在执行多个操作时没有其他 goroutine 修改 Redis 数据。
  • 乐观锁:使用数据库 version 加 CAS(Compare and Swap)机制来保证在修改数据时,数据未被其他操作修改过。
  • Go 对象锁:使用 sync.Mutex 和 sync.RWMutex 来管理对 Go 对象的并发访问,在某些情况下,还可以使用原子操作(atomic 包)来处理简单的并发问题。 在实践中,只能通过长期训练来培养并发意识。在项目开始时,就应有意识地培养自己对并发问题的关注和敏感度。

依赖注入

首先要整体上领悟依赖注入和面向接口编程的优势,这些优点在项目中体现得非常明显:

  • 依赖注入完全达成了控制反转的目标。不再关心如何创建依赖对象。例如,在 cache 模块中,虽然使用了 Redis 客户端,但 cache 实现并不关心具体的实现或客户端的相关参数。
  • 依赖注入提高了代码的可测试性。可以在单元测试中注入由 gomock 生成的实例。在集成测试阶段,为了节省公司资源,第三方依赖通常被替换为内存实现或 mock 实现。
  • 依赖注入叠加面向接口编程后,装饰器模式效果更佳。在 sms 模块中,有各种装饰器的实现,这些实现都是基于面向接口编程和依赖注入的。这使得装饰器可以自由组合,提升了系统的灵活性和扩展性。

用于个人学习,未授权严禁转载!
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇