这篇文章主要分享了我工作1年来总结的一些在业务中使用技术的经验。
数据库设计
实际业务中,大部分场景属于OLTP(联机事务处理),通常采用以MySQL为代表的关系型数据库,这里简单归纳了一些通用的设计原则。
编号 | 建议 | 说明 |
---|---|---|
1 | 当要表示一对一关系时,直接将关联存储在实体表中。 | 在一对一关系中,将两个关联对象合并为同一个数据实体,以简化数据管理。 |
2 | 当要表示一对多或多对多关系时,设计单独的关联表,并对被关联实体表的主键id组合建立唯一约束。 | 采用关联表存储不同实体的关系,可以屏蔽被关联实体结构变化对关联关系的影响。 |
3 | 在关联表中,对于形成关联的外部数据实体,只存储不可变标识字段。 | 保证服务存储数据与关联的外部数据实体之间的一致性,减少更新操作。 |
4 | 实体表的主键采用分布式唯一ID,关联表的主键采用自增ID。 | 业务实体数据需要保证分布式场景下的唯一性,实体间关系不通过表主键而是通过被关联实体主键的组合来保证唯一性。 |
5 | 在纵向拆分宽表时,实体表中可以聚合的一组相关字段建议归到同一张子表中。 | 基于业务逻辑拆分数据表,简化服务的数据操作处理。 |
6 | 需要动态创建表结构时,用一张实体表存储描述表结构的元数据,实体为可变表对象。 | 适用于一些特殊的定制化功能场景的解决方案。 |
7 | 表中的字段设计必须完整覆盖全部业务场景。 | 服务对业务流程的控制需要基于实体数据的完整性。 |
8 | 字段取值范围固定时,使用合适的整型存储,用数值表示字符串的枚举。 | 与varchar相比:节约空间、查询更快<br/>与enum相比:避免不同数据库中的兼容性问题。<br/>与set相比:提供了更高的写操作灵活性,保障了部分匹配的查询性能。 |
9 | 存储表示时间的字段,使用bigint代替timestamp。 | 提高存储的时间戳精度和范围,屏蔽不同时区的差异。 |
10 | 当数值表示的枚举值需要动态变更时,新增一张关联表存储枚举标识与值的映射。 | 随着业务的迭代,同一字段表示的枚举值可以灵活地迁移。 |
缓存使用
在高并发场景下,关系型数据库的查询容易成为接口性能的瓶颈,合理使用缓存是性能优化的关键。
实践归纳
编号 | 建议 | 说明 |
---|---|---|
1 | 全量获取列表接口有性能要求,且列表项做需要差量更新时,使用redis的hash类型。 | 在并发写场景下,避免全量更新导致先写入缓存更新丢失的问题。 |
2 | 只需要全量获取和更新数据项时,建议直接使用redis的string类型。 | 由于redis是单线程处理读写操作的,使用redis的能力处理数据会降低其吞吐量。<br/>读写redis产生的网络IO比在服务进程内处理数据更容易成为性能瓶颈。 |
3 | 对于难以及时同步更新的外部数据缓存,设置较短的过期时间。 | 服务难以实时同步外部数据,控制缓存与持久化数据不一致的时间窗口,实现最终一致性。 |
4 | 根据业务场景的需求合理选用缓存策略。 | 缓存操作和数据库操作无法通过事务保证原子性,从而衍生出不同的缓存策略。 |
方案对比
前提说明
- 在实际生产架构中,基础设施和业务系统是解耦的,作为业务研发人员没有改动基础设施的权限,因此这里讨论的方案均为Cache-Aside模式。
- 监听binlog同步更新缓存的一致性保障策略强依赖于MySQL,而实际业务场景中需要适配多种国产信创数据库。
- 现实背景下,很多高并发场景可能出现的问题只是出于猜想和假设,处理并发问题和保障代码可维护性需要根据实际业务情况来取舍。
方案 | 写操作 | 读操作 | 适合场景 |
---|---|---|---|
1 | 先更新数据库,再删除缓存 | 先查询缓存,命中直接返回结果;未命中则查询数据库并生成缓存 | 读多写少,并发问题出现概率小,通常情况下首选 |
2 | 先删除缓存,再更新数据库 | 同1 | 读多写少,不考虑并发问题时的强一致性 |
3 | 先更新数据库,再更新缓存 | 同1 | 读多写少,全量获取+差量更新场景规避多写并发问题 |
4 | 只更新数据库 | 先查询缓存,命中直接返回结果;未命中则查询数据库并生成带过期时间的缓存 | 读多写少,强一致性,对缓存宕机有一定容错性;服务引用外部数据时,持久化数据由其它服务管理,如果没有消息通知更新,则无法及时同步更新缓存,只能采用此策略。 |
5 | 先更新缓存,再更新数据库 | 先查询缓存,命中直接返回结果;未命中则查询数据库 | 读多写少,强一致性 |
6 | 先更新缓存,再异步更新数据库 | 同5 | 写多读少,弱一致性,对数据库宕机有一定容错性 |
7 | 先更新数据库,再加锁并删除缓存 | 先查询缓存,命中直接返回结果,未命中则查询数据库并获取锁,加锁成功则生成缓存,加锁失败则直接返回 | 读多写少,强一致性,解决了2的并发问题 |
补充说明:以上除了方案4以外,其它策略理论上不需要为缓存设置过期时间,但实际生产中,缓存宕机时,持久化数据可能已经更新,因此通常设置一个较长的过期时间作为兜底来保证缓存服务宕机时缓存数据与持久化数据的最终一致性。
排序功能
在实际业务场景中,经常遇到对列表中的配置对象进行排序的功能需求,下面列举并分析不同的实现方案。
编号 | 方案 | 处理逻辑 | 分析 |
---|---|---|---|
1 | 接口参数:被排序对象id列表。 数据存储:实体表直接存储索引。 | 服务内查表获取被排序对象的旧索引,然后按照传入参数中的顺序为这些对象匹配新索引。 | 适合处理差量排序,但无法处理嵌套列表排序;处理大量对象排序时,请求数据传输成为接口性能瓶颈。 |
2 | 接口参数:被排序对象目标索引、原索引、目标父级对象id。 数据存储:实体表直接存储索引和父级对象id。 | 服务内前移被排序目标原索引位置后的同级对象,后移目标索引位置后同级对象,无父级对象时采用一个常量表示根父级id。 | 适合处理嵌套列表排序,但无法处理差量排序;处理大量对象排序时,处理副作用带来的性能开销可能无法忽略。 |
3 | 接口参数:被排序对象目标索引、原索引、目标父级对象id。 数据存储:实体表存储父级对象id和前一个同级对象id。 | 服务内更改目标对象所属父级索引和前置节点id、原后随节点的前置节点id、目标索引位置原对象的前置节点id。 | 方案2的改进,解决了处理副作用带来性能损耗的问题,但仍然无法处理差量排序。 |
4 | 接口参数:被排序对象id及其子级对象排序组成的列表。 数据存储:实体表直接存储索引和父级对象id。 | 服务内查表获取被排序对象的旧索引和父级对象id,然后按照传入参数中的顺序为这些对象匹配新索引和父级对象id。 | 方案1的改进,解决了无法处理嵌套列表的问题,但仍然无法避免请求数据传输带来的接口性能瓶颈问题。 |
图片存储
在产品客户端的各种功能首页,可能会涉及需要获取展示包含图标的配置列表的场景,这里提供了一套规划中的方案设计供大家讨论和参考。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。