数据库架构设计

数据库架构设计是针对海量数据的数据库,通过数据结构、存储形式和部署方式等方面的规划和设计,以解决数据库服务的高并发、高可用、一致性、可扩展以及性能优化等问题。

 

一、可用性设计

可用性是指在某个考察时间,系统能够正常运行的概率或时间占有率的期望值。通常,我们都要求某个系统具备“高可用性”。

所谓 “高可用性”(High Availability)是指系统经过专门的设计,从而减少停工时间,保持其服务的高度可用。

数据库的高可用,通常采用的解决方式为:复制+冗余。

 

1. 保证 “读” 高可用的方法

数据库主从复制,冗余数据。如图所示:

数据库主从复制,冗余数据

主库用于写数据,从库用于读数据。在一主多从的数据库架构中,多份从库数据保证了读数据高可用。

数据库主从复制可能带来的问题:主从数据不一致。

 

2. 保证 “写” 高可用的方法

双主模式,即复制主库,冗余数据。如图所示:

双主模式,复制主库,冗余数据

很多公司采用单主模式,这是无法保证数据库写的高可用性。

数据库双主模式可能带来的问题:双主同步 key 冲突,引起数据不一致。

解决方案:

a)方案一:由数据库或者业务层保证 key 在两个主库上不冲突。

b)方案二:“双主” 当 “主从” 用,不做读写分离,当主库挂掉时,启用从库。如图下图:

优点:读写都到主,解决了一致性问题;“双主”当“主从”用,解决了可用性问题

带来的问题:读性能如何扩充?解决方案见下文

 

 

二、读性能设计:如何扩展读性能

1. 建立索引

建立太多的索引,会带来以下问题:

  • a)降低了写性能。
  • b)索引占用内存多了,内存存放的数据就会减少,数据命中率降低,IO次数随之增加。

对于索引过多的问题,有以下解决方案:

  • 不同的库可以建立不同索引。
  • 主库只提供写,不建立索引。

建立索引

online 从库只提供 online 读,建立 online 读索引。

offline 从库只提供 offline 读,建立 offline 读索引。

2. 增加从库

增加从库会引发主从不一致问题,从库越多,主从时延越长,不一致问题越严重。这种方案很常见,但我们在生产环境中没有采用。

增加从库

3. 增加缓存

传统缓存的使用方案:

  • a)发生写请求时,先淘汰缓存,再写数据库。
  • b)发生读请求时,先读缓存,hit则返回,miss则读数据库并将数据入缓存(此时可能旧数据入缓存)。
  • 建立索引

增加缓存会带来的两个问题:

  • a)数据复制会引发一致性问题,由于主从延时的存在,可能引发缓存与数据库数据不一致。
  • b)业务层要关注缓存,无法屏蔽“主+从+缓存”的复杂性。

我们缓存的使用方案

服务+数据+缓存。

服务+数据+缓存

这种方案带来的好处:

  • a)引入服务层屏蔽“数据库+缓存”
  • b)不做读写分离,读写都到主的模式,不会引发不一致

 

三、一致性设计

1. 主从不一致的解决方案

  • a)方案一:引入中间件
  • 主从不一致的解决方案

    中间件将key上的写路由到主,在一定时间范围内(主从同步完成的经验时间),该key上的读也路由到主库。

  • b)方案二:读写都到主
  • 主从不一致的解决方案

    我们采用的方案,不做读写分离,数据不会不一致。

    2. 数据库与缓存不一致的解决方案

    两次淘汰法:

    两次淘汰法

    异常的读写时序,或导致旧数据入缓存,一次淘汰不够,要进行二次淘汰。

    • a)发生写请求时,先淘汰缓存,再写数据库,额外增加一个timer,一定时间(主从同步完成的经验时间)后再次淘汰。
    • b)发生读请求时,先读缓存,hit则返回,miss则读数据库并将数据入缓存(此时可能旧数据入缓存,但会被二次淘汰淘汰掉,最终不会引发不一致)。

     

    四、扩展性设计

    (4.1)58同城秒级别数据扩容 需求:原来水平切分为N个库,现在要扩充为2N个库,希望不影响服务,在秒级别完成 10 最开始,分为2库,0库和1库,均采用“双主当主从用”的模式保证可用性 11 接下来,将从库提升,并修改服务端配置,秒级完成扩库 由于是2扩4,不会存在数据迁移,原来的0库变为0库+2库,原来的1库变为1库和3库 此时损失的是数据的可用性 12 最后,解除旧的双主同步(0库和2库不会数据冲突),为了保证可用性增加新的双主同步,并删除掉多余的数据 这种方案可以秒级完成N库到2N库的扩容。 存在的问题:只能完成N库扩2N库的扩容(不需要数据迁移),非通用扩容方案(例如3库扩4库就无法完成) (4.2)非指数扩容,数据库增加字段,数据迁移 [这些方法在(上)篇中都已经介绍过,此处不再冗余,有兴趣的朋友回复“同城”回看(上)篇] 方案一:追日志方案 方案二:双写方案 (4.3)水平切分怎么切 四类场景覆盖99%拆库业务 a)“单key”场景,用户库如何拆分: user(uid, XXOO) b)“1对多”场景,帖子库如何拆分: tiezi(tid, uid, XXOO) c)“多对多”场景,好友库如何拆分: friend(uid, friend_uid, XXOO) d)“多key”场景,订单库如何拆分:order(oid, buyer_id, seller_id, XXOO) [这些拆库方案在(上)篇中都已经介绍过,此处不再冗余,有兴趣的朋友回复“同城”回看(上)篇] (5)海量数据下,SQL怎么玩 不会这么玩 a)各种联合查询 b)子查询 c)触发器 d)用户自定义函数 e)“事务”都用的很少 原因:对数据库性能影响极大 拆库后,IN查询怎么玩[回复“同城”回看(上)篇] 拆库后,非Partition key的查询怎么玩[回复“同城”回看(上)篇] 拆库后,夸库分页怎么玩?[回复“同城”回看(上)篇] 问题的提出与抽象:ORDER BY xxx OFFSET xxx LIMIT xxx 单机方案:ORDER BY time OFFSET 10000 LIMIT 100 分库后的难题:如何确认全局偏移量 分库后传统解决方案:查询改写+内存排序 a)ORDER BY time OFFSET 0 LIMIT 10000+100 b)对20200条记录进行排序 c)返回第10000至10100条记录 优化方案一:增加辅助id,以减少查询量 优化方案二:模糊查询 a)业务上:禁止查询XX页之后的数据 b)业务上:允许模糊返回 => 第100页数据的精确性真这么重要么? 最后的大招!!!(由于时间问题,只在DTCC2015上分享了哟) 优化方案三:终极方案,业务无损,查询改写与两段查询 需求:ORDER BY x OFFSET 10000 LIMIT 4; 如何在分库下实现(假设分3库) 步骤一、查询改写: ORDER BY x OFFSET 3333 LIMIT 4 [4,7,9,10] <= 1库返回 [3,5,6,7] <= 2库返回 [6,8,9,11] <= 3库返回 步骤二、找到步骤一返回的min和max,即3和11 步骤三、通过min和max二次查询:ORDER BY x WHERE x BETWEEN 3 AND 11 [3,4,7,9,10] <= 1库返回,4在1库offset是3333,于是3在1库的offset是3332 [3,5,6,7,11] <= 2库返回,3在2库offset是3333 [3,5,6,8,9,11] <= 3库返回,6在3库offset是3333,于是3在3库的offset是3331 步骤四、找出全局OFFSET 3是全局offset3332+3333+3331=9996 当当当当,跳过3,3,3,4,于是全局OFFSET 10000 LIMIT 4是[5,5,6,6] 总结:58同城数据库架构设计思路 (1)可用性,解决思路是冗余(复制) (1.1)读可用性:多个从库 (1.2)写可用性:双主模式 or 双主当主从用(58的玩法) (2)读性能,三种方式扩充读性能 (2.1)增加索引:主从上的索引可以不一样 (2.2)增加从库 (2.3)增加缓存:服务+缓存+数据一套(58的玩法) (3)一致性 (3.1)主从不一致:引入中间层 or 读写都走主库(58的玩法) (3.2)缓存不一致:双淘汰来解决缓存不一致问题 (4)扩展性 (4.1)数据扩容:提升从库,double主库,秒级扩容 (4.2)字段扩展:追日志法 or 双写法 (4.3)水平切分 (单key)用户库如何拆分:, user(uid XXOO) (1对多)帖子库如何拆分: tiezi(tid, uid, XXOO) (多对多)好友库如何拆分: friend(uid, friend_uid, XXOO) (多key)订单库如何拆分:order(oid, buyer_id, seller_id, XXOO) (5)SQL玩法 (5.0)不这么玩:联合查询,子查询,触发器,自定义函数,事务 (5.1)IN查询:分发MR or 拼装成不同SQL语句 (5.2)非partition key查询:定位一个库 or 分发MR (5.3)夸库分页 (5.3.1)修改sql语句,服务内排序 (5.3.2)引入特殊id,减少返回数量 (5.3.3)业务优化,允许模糊查询 (5.3.4)查询改写,二段查询