背景:
书接前文: 轻松应对高并发和大流量:一套简单可靠的后端缓存系统
讲述了如何构建一个简单可靠还高性能的缓存系统,文中略过了一个重要的点:保证先写缓存,后写持久化数据库(Mysql)一定会成功。
本文将讨论,异步写 Mysql 的业界一般实现和一些注意事项。
系统架构(技术栈)
- DB:Mysql
- MQ:Kafka
- Cache:Redis
流程
流程如上图,这里很容易提出三个问题:
- 异步写 Mysql ,如何提前获取索引ID?
- 写 Kafka 失败了怎么办?
- 消费 Kafka 失败了怎么处理?
分布式ID 预生成处理
接上文,举个例子,比如: 两个请求都同时 insert 同一张表,那么因为是异步,无法使用自增ID,那么如何确认各自在表的行ID呢?
假设有上万用户同时申请创建用户操作,是否能够 Cover 住,让每一个用户都能分配到唯一不重复的ID?
业界常见做法可以参考:tech.meituan.com/2017/04/21/…
此处简单总结:
- (类)雪花算法:美团 leaf 、百度 uid-generator
- 基于号段的ID生成算法: 美团leaf 、滴滴tinyid
其中,为了实现高可用, 滴滴tinyid 的实现很有意思,在基于基础号段的做法上 :
接入两个(或多个)独立的Mysql,在每一个Mysql服务上有不同的表,但是其中一个只生产奇数,另一台只生产偶数。
总结: 轮子早已经造好,用就完了。
健壮性拓展
下游的基础服务时不时抖动一下是非常正常的, 健壮性好的服务需要 cover 住这种情况,不然就会丢失用户的请求,造成数据丢失,甚至资产损失。
那么回到问题:如果处理写 Kafka 失败的情况?———— 很简单,重试呗
- 容器内重试
当写失败时,将kafka msg 序列化好,并保存到容器内的 channel、log 中
run起来另一个协程,不停地对上述的 channel 或 log 进行消费,将里面的消息重新发送kafka。
缺点:因为现在大多服务都是采用容器化技术,容器重启就会丢失未执行完的消息
- 持久化
为了不丢失数据,所以这些失败需要重试的日志需要能持久化保存。
1 可以用Mysql等关系型保存。
2 或者是其他非关系型KV数据库。
3 再或者能把日志写到一个不会丢失磁盘上也行。
总之能够确保这些失败的消息能够保存。推荐用 Mysql 实现: 比较简单 且 不引入新的依赖服务
如果这里持久化失败了怎么办?
这种情况说明 Mysql、Kafka 都出现了问题,代码能做的其实已经不多了。
- 先上报监控并触发告警,让开发者注意到问题
- 打印日志。 (日志平台一般能够采样)
- 下载日志并手动修复。
NOTE:此时 Redis 数据依然是正确的,在 Redis 未过期的时间内,依然有缓冲。所以这也是为什么建议将 Redis 过期时间设置得稍长
- 消费
限速 ;由于大流量,上游生产 Kafka 消息的速度非常快,Mysql 是不能承受的,因此这里如果触发了限速,那么就需要等待。
重试
这里失败说明 Mysql 暂时是不可用的,因此这里的重试选择持久化到日志文件,并由另一个服务/协程进行重试
如果重试失败,同上面一样,也是 监控+告警+手动重试
PS: 实际上在线上,几年来都没有出现过此情况。
另外也可以再引入一个第三方的依赖来进一步增强健壮性。
关于读请求的优化
至此,写请求都被改造成异步,但是读请求依然是同步,虽然 Mysql 对读请求的性能大大高于写请求,但是如果是用户数量过高,Mysql 依然无法扛住,即 缓存穿透引发雪崩
对此一般有以下几种做法: 后端:
- 提前进行缓存预热,即将很可能来的用户提前加载 Redis 缓存
- 限流,对存在较多读取 Mysql 的接口进行限流保护
- 适当的提高Redis的过期时间,比如在 前文所采用的 Redis 架构中,Redis Key 的过期时间为7天
前端:
- 错峰,手动削峰用户流量
- 对于某些页面,增加静态资源展示,减少对后端接口请求
中间件
- Mysql 读写分离,用从库提升读请求的承载能力,以及根据流量考虑是否需要扩容。
- 分库分表
其他问题
- 重复消费和并发消费问题 ———— Mysql 表中里面一般会保存 Modify Time,一个简单的做法是,每次更新 Mysql 时,都仅在当前消息更领先的时候更新,即 Msg.ModifyTime > DB.ModifyTime
- 待补充
附带代码:
func WriteRequest(){
info, err := cache.GetInfo(ctx, uid)
if err != nil {
return err
}
// 修改info
err = cache.SetInfo(ctx, info)
if err != nil {
return err
}
//
msg := &KafkaMessage{}
err := mq.GetProducer().Publish(ctx, msg)
if err != nil {
// 记录到Mysql
recordErr = mysql.RecordKafkaMsg(ctx, msg)
if recordErr != nil {
// 监控、日志
log.Error()
reporter
}
}
return nil
}
func ConsumeKafkaMsg(kafkaMsg){
//
err := mysql.UpdateDB(kafkaMsg)
if err != nil {
// 写文件
writeToFileErr := WriteToFile(kafkaMsg)
if writeToFileErr != nil {
reporter
log.Error()
// 或者是再引入第三方增强扩展性
}
}
return nil
}
// 监听文件,进行重试
func WatchFile(){
for {
kafkaMsgBytes = reader.ReadLine()
kafkaMsg = json(kafkaMsgBytes)
err := mysql.UpdateDB(kafkaMsg)
if err !=nil {
log
// 重新写回文件
writeToFileErr := WriteToFile(kafkaMsg)
if writeToFileErr!=nil{
// 监控
// 日志
}
}
}
}