文章阅读与实践 - 延迟双删/分库分表/Spring IOC生命周期/Mysql主从一致优化
延迟双删
文章来源:延迟双删如此好用,为何大厂从来不用
大家都推荐“删缓存”,但大厂为啥不用?
数据库改了数据,就顺手把缓存删掉,下次读的时候再从数据库拿,重新写进缓存。这叫 Cache-Aside 模式,听着挺合理,对吧?更高级点的,还有个叫“延迟双删”的招:
- 第一次:数据改完,立刻删缓存;
- 第二次:等个1~2秒,再删一次缓存。
是为了防止:改数据的时候,刚好有人在读旧缓存,然后又把旧数据写回去,导致不一致。但是很多公司根本不这么做。
延迟双删的“坑”在哪?—— 缓存穿透 + 数据库爆炸
想象一下这个场景:你删了缓存 → 用户来查 → 缓存没了 → 直接打到数据库 → 数据库压力暴增;如果这时候系统本来就在高并发(比如双十一、抢票),那一瞬间成千上万请求全冲进数据库,直接把你库干趴。这叫“缓存穿透”——本来用缓存是为减轻数据库压力,结果你一删,反而让数据库扛不住了。
而且,延迟时间很难拿捏:
- 等太久:数据不一致的时间就长;
- 等太短:可能还没改完,第二次删就白删了。
所以,对小系统来说,“延迟双删”还能凑合用;但对大流量系统来说,这招太危险,不能靠它保命。
Facebook的解法:搞个“租约”机制,像排队领号
Facebook 想了个聪明办法,叫 Lease(租约)机制,你可以理解成“谁先申请,谁才能写”。
- 当缓存里没数据时,不是直接让所有人去查数据库,而是:
- 返回一个“token”(令牌),相当于排队号;
- 只有拿到这个号的人,才能去数据库查数据,并写回缓存;
- 其他人只能等着,不能乱动。
这就避免了“大家一起查数据库、大家一起写缓存”的混乱场面。用 Redis 实现的话,要点是:
- 查缓存前,先看有没有“租约”;
- 写缓存时,要验证自己是不是“持号者”;
- 改完数据库后,不仅要删缓存,还要把“租约”也删掉。
好处是 - 减少数据库压力,避免多个请求同时重建缓存(惊群效应)。
缺点呢? 实现复杂,要用 Lua 脚本保证原子性; 代码变麻烦,要处理 token、解析返回值等。但对 Facebook 这种量级来说,这点复杂度值得。
Uber 的做法:加个“版本号”,谁新谁上位
Uber 的思路更直接:给每条数据加个“时间戳”当版本号。每次往缓存写数据前,先比一下版本:
- 如果你要写的这条数据,比缓存里的“版本旧”,那就别写了;
- 只有“新版本”才允许写进去。
这样就能防止:A改了数据 → 删缓存 → B读到旧数据 → 写回缓存” 把旧数据又塞回去。
实现方式:
- 用 Lua 脚本,在 Redis 里存两个 key:
- 一个是真正的数据;
- 一个是它的版本号(比如数据库的时间戳);
- 写缓存时,脚本自动比对版本,只让新的写进去。
Uber还有个叫 CacheFront 的系统,能异步监听数据库变更,自动更新缓存。所以哪怕你不用“删缓存”,它也能保证缓存最终一致。
方案 | 适合场景 | 缺点 |
---|---|---|
删除缓存 + 延迟双删 | 小公司、低流量系统 | 容易穿透缓存,压垮数据库 |
Facebook 租约机制 | 高并发、怕惊群 | 实现复杂,要改代码逻辑 |
Uber 版本号机制 | 数据有时间戳,想精准控制 | 要额外存版本,设计稍复杂 |
分库分表
文章来源:大厂高频面试题:为什么要分库分表?
分表:解决数据量太大的问题
当你发现一张表里的数据太多(比如几千万甚至上亿条),查询变得非常慢,即使你做了优化也不行,这时候就需要考虑分表了。
分表的方法
1.垂直拆分
- 做什么:把一个有很多列的大表拆成几个小表。
- 怎么做:遵循“冷热分离”的原则,把常用的、重要的信息放在主表里,不常用的信息放在扩展表里。
- 举例:用户表可以拆成用户主表(存ID、手机号、密码等常用信息)和用户扩展表(存个人简介、头像、地址等不常用信息)。
- 注意:最好在设计数据库的时候就想好怎么分,以后再改会很麻烦。
2.水平拆分
- 做什么:把一个有很多数据的表按照某种规则分成多个结构相同的表。
- 怎么做:
- 按范围:比如按ID区间分,1-1000万的数据放表1,1001-2000万的数据放表2。
- 按哈希:对某个关键字段(如用户ID)取哈希值,然后根据哈希值决定数据放到哪个表。这种方式最常用,数据分布比较均匀,但扩容有点复杂。
- 按时间:比如订单表可以按月或年创建新表,这样管理和查询都很清晰。
3.小结
分表的目的就是让单个表变小,提高查询速度。
分库:解决并发压力太大的问题
什么时候需要分库?
当一个数据库实例的连接数和每秒查询次数(QPS)太高,超过了硬件和MySQL能承受的上限,导致数据库响应慢甚至宕机,这时候就需要分库了。
怎么做分库?
按业务拆分是最推荐的方式。比如一个电商系统可以拆分成用户库、商品库、订单库、支付库。
好处:
- 资源隔离:一个业务库的高负载不会影响其他业务库。
- 降低耦合:业务边界清晰,方便独立开发和维护。
- 提升并发:把访问压力分散到多个数据库实例上,整体处理能力增强。
小结
分库的目的是把访问压力分散到不同的数据库实例上,提高系统的并发处理能力和稳定性。
总结
分表是为了解决数据量太大导致的查询性能问题,分库是为了解决并发压力太大导致的数据库性能瓶颈。在实际应用中,我们常常会结合使用这两种方法,比如先按业务拆分出订单库,再对订单表进行水平拆分。
当然,分库分表也会带来一些新的挑战,比如分布式事务、跨库JOIN查询、全局唯一ID等问题,这些就需要用更复杂的中间件或架构方案来解决了。
Spring IOC的生命周期
文章来源:再探Spring IOC的生命周期
IOC容器的运作过程可以梳理成四个清晰阶段。
第一阶段:准备阶段 - 收集Bean蓝图
就像建房子前先画设计图,Spring在这个阶段:
-
扫描所有配置(XML、注解、配置类)
-
解析并创建BeanDefinition(Bean的"身份证")
-
将这些定义注册到容器工厂中
-
执行BeanFactoryPostProcessor(可以修改Bean定义的"超级管理员")
关键点:BeanFactoryPostProcessor有优先执行权,可以修改Bean的属性、作用域等元信息,但不能直接操作Bean实例。
第二阶段:创建阶段 - Bean的诞生
这个阶段真正创建Bean实例,步骤很精细:
-
实例化(调用构造函数"出生")
-
属性填充(注入需要的依赖)
-
Aware接口回调(让Bean知道自己叫什么、在哪)
-
初始化前处理(BeanPostProcessor的before方法)
-
执行初始化(InitializingBean接口和@PostConstruct)
-
初始化后处理(BeanPostProcessor的after方法)
有趣发现:通过断点追踪,我发现@Async和@Transactional注解的代理对象创建时机不同,说明不同功能的增强处理时机是有差异的。
第三阶段:使用阶段 - Bean的工作期
-
业务代码通过依赖注入或直接获取使用Bean
-
懒加载的Bean在第一次被访问时才初始化
-
这是Bean"奉献价值"的主要阶段
第四阶段:销毁阶段 - Bean的退休
容器关闭时:
-
先触发@PreDestroy标记的方法
-
再调用DisposableBean接口的destroy方法
-
最后释放资源,等待GC回收
web环境特色:在web应用中,容器关闭是通过ServletContextListener监听的,Spring Boot还提供了/actuator/shutdown端点来优雅停机。
实际案例:RocketMQTemplate的销毁
RocketMQTemplate实现了DisposableBean接口,在destroy()方法中优雅地关闭了生产者和消费者,释放了所有资源,这正是生命周期终点处理的完美示例。
总结
Spring IOC生命周期可以简记为:先注册后处理再创建,使用时才初始化,关闭前先销毁。理解这四个阶段及其扩展点,就像掌握了Spring容器的工作节奏,能够更好地在合适时机介入处理,写出更优雅的Spring应用。
Mysql主从一致优化
文章来源:美团一面:Mysql主从一致性问题如何优化?四种优化方案分享
问题根源:主从延迟
-
主库写完了,从库还没同步完,中间有个时间差。
-
如果你在这段时间内读从库,就会读到“旧数据”。
方案一:接受延迟,不管了
-
做法:不做特殊处理,允许短暂的不一致。
-
比喻:就像发朋友圈,你发了但别人晚几秒才看到,没关系。
-
适用:点赞数、浏览量、非核心数据。
-
优点:简单、快。
-
缺点:可能读到旧数据。
方案二:半同步复制
-
做法:主库写完后,等至少一个从库确认收到日志,再告诉客户端“写好了”。
-
比喻:你发通知后,必须等一个人回复“收到”,你才放心。
-
适用:对数据可靠性要求高,能接受稍微慢一点的写入。
-
优点:数据更安全,一致性更好。
-
缺点:写操作变慢。
方案三:所有读都走主库
-
做法:不管什么情况,读写全部走主库。
-
比喻:所有问题都直接找老板,不找秘书。
-
适用:金融、订单等绝对不能读旧数据的场景。
-
优点:绝对一致。
-
缺点:主库压力大,得加缓存缓解。
方案四:智能判断读主还是读从
-
做法:
-
写的时候,在缓存(如Redis)里打个标记(例如:
这个数据刚改过,5秒后过期
)。 -
读的时候,先查标记:
-
有标记 → 读主库(保证读到最新的)
-
没标记 → 读从库(减轻主库压力)
-
-
-
比喻:你刚更新了简历,HR 会直接找你确认最新情况;过了一段时间,就直接看档案了。
-
适用:既要一致性又要性能的场景。
-
优点:灵活、智能、兼顾性能与一致。
-
缺点:系统变复杂,要写更多代码。