初心
数据库基本服务就是提供数据的读和写,不要被海量技术淹没。不管怎么存储都需要落盘,那落盘的瓶颈在哪里?一个是寻址时间,另一个是内核态用户态的切换以及数据拷贝。所以改进点就在于,减少寻址时间,对应的就是顺序的写。
不忘原理和目标,才能找到更准确的方法。很多人一谈到数据库优化,就是分库分表,恨不得把所有的新技术都用上,但是要知道,程序世界是没有银弹的,每种技术都会有他的弊端,要找准原因,兴许只是少加了个索引呢?
优化方案
先盘查性能瓶颈,看是查询慢还是更新慢。看业务是写多还是读多,是因为表的设计导致的复杂查询还是业务导致的。
测量任务所花费的时间,对结果进行统计和排序,将重要的任务排在前面。
顺序
表结构、索引数据库参数redis,业务优化扩展(垂直或水平)
写少读多
垂直拓展,先将单体服务器性能优化到极致。优化SQL和表结构。业务梳理,减少不必要的查询。使用缓存,Redis套起来。读写分离(水平扩展)。 写多读少
分库分表(水平扩展)
读写分离
先要确定写不是瓶颈,不然白干。 适用于一次写多次读的场景,比如博客。
核心:将读和写分离开来。 实现:
客户端实现,也是就是改写哪个,改读哪个由程序控制。服务端实现,也就是加中间件,将读写封装起来。
问题
主从复制延迟,主库的写,在从库没读到。 解决:
并行复制,适用于量不是特别大的情况,也就是加快了复制频率。代码控制,将延迟要求高的查询直接查主库。但是读写分离失去了部分意义。二次读取,读从机失败,再读一次主机。
没有银弹
这就是前面说到的,每个技术都会有他的弊端。可能平时只是卡一下,慢一点,一加主从同步,一个用户注册了,查询失败,直接没注册了,注册逻辑崩溃,系统直接不可用,哪个问题大?
所以用一个技术前一定要谨慎。不到万不得已,能用杀鸡刀就不要用宰牛刀。
分库分表
分库
最简单也最复杂。 简单是因为直接将不同的业务连接到不同的库即可。 复杂是因为代码要充分解耦,甚至重写。
分表
垂直分表
适用于列特别多的表。将大表拆分成小表。 问题:增加了查询次数。
水平分表
适用于行特别多的表。 问题:
不支持一些操作,比如count(),join,order by
怎么分?(数据路由)
按照范围,userid、时间、地区、生日等。好处是很好扩展,比如按时间,一个月新建一个表就好了,但是不能分担压力,大部分都会压在新数据上。hash路由,通过算法匹配到不同的表。好处是平均了压力,但是不好扩展,适合一步到位的情况,比如32 库 * 32 表。配置路由,就是自己写判断。
主键怎么处理
设置数据库 sequence 或者表自增字段步长。比如8个表,起点分别是0-7,步长都是8,这样大家主键自动增长就不会冲突了。但是不好扩展。UUID,好处就是本地生成,不要基于数据库来了;不好之处就是,UUID 太长了、占用空间大,作为主键性能太差了。更重要的是,UUID 不具有有序性,会导致 B+ 树索引在写的时候有过多的随机写操作(连续的 ID 可以产生部分顺序写),还有,由于在写的时候不能产生有顺序的 append 操作,而需要进行 insert 操作,将会读取整个 B+ 树节点到内存,在插入这条记录后会将整个节点写回磁盘,这种操作在记录占用空间比较大的情况下,性能下降明显。 适合的场景:如果你是要随机生成个什么文件名、编号之类的,你可以用 UUID,但是作为主键是不能用 UUID 的。获取系统当前时间 获取系统当前时间这个就是获取当前时间即可,但是问题是,并发很高的时候,比如一秒并发几千,会有重复的情况,这个是肯定不合适的。基本就不用考虑了。 适合的场景:一般如果用这个方案,是将当前时间跟很多其他的业务字段拼接起来,作为一个 id,如果业务上你觉得可以接受,那么也是可以的。你可以将别的业务字段值跟当前时间拼接起来,组成一个全局唯一的编号。snowflake 算法,雪花算法。 0 | 0001100 10100010 10111110 10001001 01011100 00 | 10001 | 1 1001 | 0000 00000000 snowflake 算法是 twitter 开源的分布式 id 生成算法,采用 Scala 语言实现,是把一个 64 位的 long 型的 id,1 个 bit 是不用的,用其中的 41 bit 作为毫秒数,用 10 bit 作为工作机器 id,12 bit 作为序列号。
1 bit:不用,为啥呢?因为二进制里第一个 bit 为如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。41 bit:表示的是时间戳,单位是毫秒。41 bit 可以表示的数字多达 2^41 - 1,也就是可以标识 2^41 - 1 个毫秒值,换算成年就是表示69年的时间。10 bit:记录工作机器 id,代表的是这个服务最多可以部署在 2^10台机器上哪,也就是1024台机器。但是 10 bit 里 5 个 bit 代表机房 id,5 个 bit 代表机器 id。意思就是最多代表 2^5个机房(32个机房),每个机房里可以代表 2^5 个机器(32台机器)。12 bit:这个是用来记录同一个毫秒内产生的不同 id,12 bit 可以代表的最大正整数是 2^12 - 1 = 4096,也就是说可以用这个 12 bit 代表的数字来区分同一个毫秒内的 4096 个不同的 id。
怎么平滑过渡
停机,最简单也最稳妥。
平滑迁移
简单来说,就是在线上系统里面,之前所有写库的地方,增删改操作,除了对老库增删改,都加上对新库的增删改,这就是所谓的双写,同时写俩库,老库和新库。 然后系统部署之后,新库数据差太远,用之前说的导数工具,跑起来读老库数据写新库,写的时候要根据 gmt_modified 这类字段判断这条数据最后修改的时间,除非是读出来的数据在新库里没有,或者是比新库的数据新才会写。简单来说,就是不允许用老数据覆盖新数据。 导完一轮之后,有可能数据还是存在不一致,那么就程序自动做一轮校验,比对新老库每个表的每条数据,接着如果有不一样的,就针对那些不一样的,从老库读数据再次写。反复循环,直到两个库每个表的数据都完全一致为止。 接着当数据完全一致了,就 ok 了,基于仅仅使用分库分表的最新代码,重新部署一次,不就仅仅基于分库分表在操作了么,还没有几个小时的停机时间,很稳。