MySQL多线程insert ... on duplicate key update数据竟然丢了?!

良心公众号

关注不迷路

最近,菜鸡在项目重构优化的路上可谓是一波三折。小伙伴们听到这里,是不是忍不住露出了幸灾乐祸的笑容(手动狗头)。

基本情况

闲话少说,直奔主题!先说一说本次重构优化的基本情况。菜鸡重构优化的项目,主要是以下功能,从Kafka获取数据,根据Apollo配置中心的参数配置进行一定的处理,最后将数据写入相应的MySQL数据库表。

重构优化的原因

听起来非常简单的一个系统。重构优化主要有以下三点原因:

  • 由于业务场景是一个大数据,高并发场景,对数据库表的update操作十分频繁,数据库是主从复制的架构,短时间内大量的update操作造成了比较严重的主从延迟,基于此,DBA根据数据库的性能表现临时开启MySQL的多线程复制,一定程度上降低了主从延迟,但问题并没有从系统层面进行解决。

  • 而且,系统之前的设计没有合理地运用配置,存在一定程度的硬编码

  • 此外,工具类没有经过设计,存在扩展性差等问题。

基于此,菜鸡开始了系统的重构优化之路。

重构的步骤

说来就来。重构的步骤可大致分为以下三步:

  • 菜鸡首先梳理了系统的各个模块以及设计的数据库表,并以此为拆分和设计的依据。

  • 之后,菜鸡将硬编码改为Apollo配置项。这个比较简单,大致思路是设计相应的枚举类,用于代替之前的硬编码。

  • 最后,菜鸡抽取了一个单独的项目,里面主要存放各个项目都会用到的工具类,比如Apollo的配置工具。简单说一下Apollo配置工具抽取的思路,基于观察者模式,设计一个可以引包即用的工具类,避免每个项目都要重写一遍的初始化,读取配置等操作。

优化的步骤

有关重构的工作主要是这些,接下来就是真的MySQL主从延迟过大的问题优化。有经验的小伙伴都了解MySQL的主从复制架构,在频繁更新的时候(也就是写操作频繁的时候),主从延迟会比较大。

这是因为MySQL使用单线程重放RelayLog,而在数据库层面的优化就是DBA所采用的多线程并发重放RelayLog,当然,这其中需要考虑数据一致性问题,但这不是本文的重点,感兴趣的小伙伴可以自行了解。

而菜鸡需要做的是,从代码层面降低数据库更新频率,也就是需要在内存中提前做数据聚合。针对代码中关于数据的处理,可以大致分为以下步骤:

  • Kafka消息读取至阻塞队列

  • consumer定时(或定量)消费阻塞队列中的数据

  • 将数据在内存中进行聚合

  • 多线程批量写入数据库


优化过程的问题及原因

而根据上述步骤进行实现之后,经过测试,菜鸡发现,有大量的数据丢失。

经过仔细排查监控日志,最终将问题定位在多线程批量写入数据库。

为什么会有丢数据的问题呢?答案是在多线程批量写入的时候,用到了insert ... on duplicate key update语法,而更新操作涉及到表组合键的更新,导致出现了死锁

死锁的出现,导致大量的更新失败,从而出现了大量数据丢失的情况。

需要指出的是,菜鸡用的MySQL版本是5.7。

问题解决的思路

那么该如何解决呢?解决问题方案需要考虑哪些问题呢?

  • 经过对更新数据的特性分析,确定大量的更新操作最终是落在对少量数据高频更新上。这样以来,就可以通过在内存中多做一些聚合,然后再单线程更新数据库。这是最简单易行的解决方式,既可以降低更新DB频率,又避免了insert ... on duplicate key update多线程情况下出现死锁的问题。对于菜鸡所涉及的场景,其实这个解决方案就够了,因为该场景对数据的实时性要求并不是很高,所以在内存中多几秒时间进行聚合带来的影响可以忽略。

  • 但如果实时性要求高的情况下该如何是好呢?这时可以考虑从数据源做文章,可以在Kafka的生产者端将数据按相应的key进行分区,这样,由于最终消费数据的各个线程之间所获取的数据之间的组合键没有重合,因此可以避免多线程更新的死锁问题,同时也保证了对实时性的要求。

  • 但这个方案就没有问题了吗?其实仔细想想也是有问题的,问题就是热点数据,极端情况下,如果大量数据全部对应到一个key,超出了消费者的消费能力,就会出现数据囤积。一个可行的改进方案是,在数据源端保持原有的策略不变(也就是不按key进行分区),然后将数据在内存中聚合之后再提交至Kafka的新的topic(按key进行分区),这样就避免了热点数据的问题。

由此可见,方法总是有的,只是哪个更适合当下的业务场景。

关于重构优化的一点体会

经过这次重构优化,菜鸡深深体会到,程序是需要被设计的,如果系统有个很好的底子,那么后来人往往会循着这个良好的设计进行后续的扩展,这是一个良性循环。

但问题往往出在系统发展的某一个结点,在这个结点,有哪怕只有一个人,在开发过程中掉链子了,写出了质量严重低于系统平均水平的代码,我们称之为未经思考的代码。而项目恰好又没有严格的code review机制,那么,这坨烂代码的影响将是深远的,因为它也会被后来人效仿。重构在解决这类问题的时候,约等于重写,付出的代价将是沉重的。

没有一个系统在一开始就是烂系统,烂代码都是在岁月行进过程中逐渐积累的,因此,本着负责的态度,甚至是鸡蛋里挑骨头的态度,形成积极code review的风气,对系统的代码质量是一件天大的好事,对开发人员的成长也有很强的正面作用。

同样,没有一个系统在一开始就是好系统,好系统都是在不断的设计与重构中逐渐变好的。真正好的系统,如果里面有几行烂代码,看上去会很显眼。当然,好没有严格统一的标准。但一个相对比较好的系统一定是一个扩展性好的硬编码少的基础健壮的易于维护的监控全面的系统。

且不说用到多么前沿的知识理念,多么新颖的架构设计,光是以上这五条,很多系统可能都没有完全做到。有时候,一个需求来得及,会给人一种先实现功能,以后可以再重构的幻觉,这种想法是极其危险的,因为系统逐渐腐烂往往是因为这个原因,而不是程序员的水平不能写出更好的代码。

用简单而未加思考的方式实现一个需求是程序员的第一反应,但这个反应就像是刷算法题里的暴力算法,是值得被优化的,虽然在这里的优化不一定带来性能上的提升,但是对系统的健壮性、可扩展性和可维护性一定是有帮助的。暴力实现是战略性懒惰,是伪工作者的习惯,因为不用动脑。也正因为不用动脑,所以没有提高。

一句话总结就是,重构优化一直在路上……

学习 | 工作 | 分享

????长按关注“有理想的菜鸡

只有你想不到,没有你学不到

你可能感兴趣的:(数据库,java,mysql,编程语言,redis)