可以通过如下的一些方式来优化数据库,查询缓存的使用、数据库连接池化设计、大表优化、分库分表,核心想说的其实是水平拆分,也就是分库分表,更加核心想要讨论的问题实际上是在水平拆分的时候如何保证分布式一致性,通过Redis自增的方式还是雪花算法,整体来了解一遍。
查询缓存的使用
执⾏查询语句的时候,会先查询缓存。不过,MySQL 8.0 版本后移除,因为这个功能不太实⽤,但是在之前的版本中还是作为一个优化方案存在的,
- 开启查询缓存后在同样的查询条件以及数据情况下,会直接在缓存中返回结果。这⾥的查询条件包括查询本身、当前要查询的数据库、客户端协议版本号等⼀些可能影响结果的信息。任何两个查询在任何字符上的不同都会导致缓存不命中。
- 如果查询中包含任何⽤户⾃定义函数、存储函数、⽤户变量、临时表、MySQL库中的系统表,其查询结果也不会被缓存。
- 缓存建立之后,MySQL的查询缓存系统会跟踪查询中涉及的每张表,如果这些表(数据或结构)发⽣变化,那么和这张表相关的所有缓存数据都将失效。
缓存虽然能够提升数据库的查询性能,但是缓存同时也带来了额外的开销,每次查询后都要做⼀次缓存操作,失效后还要销毁。 因此,开启缓存查询要谨慎,尤其对于写密集的应⽤来说更是如此。如果开启,要注意合理控制缓存空间大小,⼀般来说其⼤⼩设置为⼏⼗MB⽐较合适
数据连接池化设计
池化设计应该不是⼀个新名词。我们常⻅的如java线程池、jdbc连接池、redis连接池等就是这类设计的代表实现。这种设计会初始预设资源,解决的问题就是抵消每次获取资源的消耗,如创建线程的开销,获取远程连接的开销等。池化设计还包括如下这些特征:池⼦的初始值、池⼦的活跃值、池⼦的最⼤值等,这些特征可以直接映射到java线程池和数据库连接池的成员属性中。
数据库连接本质就是⼀个 socket 的连接。数据库服务端还要维护⼀些缓存和⽤户权限信息之类的 所以占⽤了⼀些内存。我们可以把数据库连接池是看做是维护的数据库连接的缓存,以便将来需要对数据库的请求时可以重⽤这些连接。为每个⽤户打开和维护数据库连接,尤其是对动态数据库驱动的⽹站应⽤程序的请求,既昂贵⼜浪费资源。
- 在连接池中,创建连接后,将其放置在池中,并再次使⽤它,因此不必建⽴新的连接。
- 如果使⽤了所有连接,则会建⽴⼀个新连接并将其添加到池中。
连接池减少了⽤户必须等待建⽴与数据库的连接的时间
大表优化
首先想办法对单表的数据进行优化,在此基础上,如果数据量实在大,就得考虑拆分了。
单表优化
除非单表数据未来会一直不断上涨,否则不要一开始就考虑拆分,拆分会带来逻辑、部署、运维的各种复杂度,一般以整型值为主的表在千万级以下,字符串为主的表在五百万以下是没有太大问题的。而事实上很多时候MySQL单表的性能依然有不少优化空间,甚至能正常支撑千万级以上的数据量
- 字段设计层面的优化方案,总而言之就是能用整数表示就别用字符串
- 尽量使用TINYINT、SMALLINT、MEDIUM_INT作为整数类型而非INT,如果非负则加上UNSIGNED,避免不必要空间的浪费
- VARCHAR的长度只分配真正需要的空间,虽然是可变长的,但是规范下长度
- 使用枚举或整数代替字符串类型,枚举和整数占用空间少
- 尽量使用TIMESTAMP而非DATETIME,时间戳字符串长度会比DATETIME小点儿
- 单表不要有太多字段,建议在20以内
- 避免使用NULL字段,很难查询优化且占用额外索引空间
- 用整型来存IP
- 索引设计层面的优化方案,总而言之就是不要胡乱创建索引,少用外键和唯一约束,因为也会有索引
- 索引并不是越多越好,要根据查询有针对性的创建,考虑在WHERE和ORDER BY命令上涉及的列建立索引,可根据EXPLAIN来查看是否用了索引还是全表扫描索引的维护和存储也耗费性能,所以别乱建索引
- 应尽量避免在WHERE子句中对字段进行NULL值判断,否则将导致引擎放弃使用索引而进行全表扫描
- 值分布很稀少的字段不适合建索引,例如"性别"这种只有两三个值的字段,索引底层实现是B+树,杀鸡不能用牛刀
- 字符字段只建前缀索引
- 字符字段最好不要做主键
- 不用外键,由程序保证约束
- 尽量不用UNIQUE,由程序保证约束
- 使用多列索引时注意顺序和查询条件保持一致,同时删除不必要的单列索引
- 查询SQL语句层面的优化方案,总而言之就是精细化查找,避免用不上索引
- 不做列运算:任何对列的操作都将导致表扫描,它包括数据库教程函数、计算表达式等等,查询时要尽可能将操作移至等号右边
- sql语句尽可能简单:一条sql只能在一个cpu运算;大语句拆小语句,减少锁时间;一条大sql可以堵死整个库
- 不用
SELECT *
OR改写成IN
:OR的效率是n级别,IN的效率是log(n)级别,in的个数建议控制在200以内- 不用函数和触发器,在应用程序实现
- 避免%xxx式查询
- 少用JOIN
- 使用同类型进行比较,比如用’123’和’123’比,123和123比
- 尽量避免在WHERE子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描
- 对于连续数值,使用BETWEEN不用IN:SELECT id FROM t WHERE num BETWEEN 1 AND 5
- 列表数据不要拿全表,要使用LIMIT来分页,每页数量也不要太大
以上都是在字段设计层面、索引设计层面以及日常操作层面能做的一些优化,而事实上,如果数据量特别大的时候,还是得做一些拆分。关于更多的高性能优化建议,可以参照这篇blog:Mysql高性能优化规范建议
读写分离
经典的数据库拆分⽅案,主库负责写,从库负责读。在我的另一个关于Redis的blog里有介绍到。
垂直拆分
垂直分库是根据数据库里面的数据表的相关性进行拆分,比如:一个数据库里面既存在用户数据,又存在订单数据,那么垂直拆分可以把用户数据放到用户库、把订单数据放到订单库。垂直分表是对数据表进行垂直拆分的一种方式,常见的是把一个多字段的大表按常用字段和非常用字段进行拆分【参照数据库第三范式】,每个表里面的数据记录数一般情况下是相同的,只是字段不一样,使用主键关联
垂直拆分的优点是:
- 可以使得行数据变小,一个数据块(Block)就能存放更多的数据,在查询时就会减少I/O次数(每次查询时读取的Block 就少)
- 可以达到最大化利用Cache的目的,具体在垂直拆分的时候可以将不常变的字段放一起,将经常改变的放一起
- 数据维护简单
垂直拆分的缺点是:
- 主键出现冗余,需要管理冗余列
- 会引起表连接JOIN操作(增加CPU开销)可以通过在业务服务器上进行join来减少数据库压力
- 依然存在单表数据量过大的问题(需要水平拆分)
- 事务处理复杂,因为锁是行级别的
其实综合来看垂直拆分的场景非常少,基本上在设计初期就不会在一张表上设计过多字段
水平拆分
保持数据表结构不变,通过某种策略存储数据分⽚。这样每⼀⽚数据分散到不同的表或者库中【分库分表】,达到了分布式的⽬的。 ⽔平拆分可以⽀撑⾮常⼤的数据量,这个才是我们一定会面临的随着业务不断增大而产生的问题
- 库内分表仅仅是单纯的解决了单一表数据过大的问题,由于没有把表的数据分布到不同的机器上,因此对于减轻MySQL服务器的压力来说,并没有太大的作用,大家还是竞争同一个物理机上的IO、CPU、网络,这个就要通过分库分表来解决
所以说分库分表是我们最常用的解决方案
- 水平拆分的优点是:不存在单库大数据和高并发的性能瓶颈;应用端改造较少;提高了系统的稳定性和负载能力
- 水平拆分的缺点是:分片事务一致性难以解决;跨节点Join性能差,逻辑复杂;数据多次扩展难度跟维护量极大
水平拆分最大的问题就是如何保证分布式的主键id唯一性!因为要是分成多个表之后,每个表都是从 1 开始累加,这样是不对的,我们需要⼀个全局唯⼀的 id来⽀持。⽣成全局 id 有下⾯这⼏种⽅式:
- UUID:不适合作为主键,因为太⻓了,并且⽆序不可读,查询效率低。⽐较适合⽤于⽣成唯⼀的名字的标示⽐如⽂件的名字。
- 数据库⾃增 id : 两台数据库分别设置不同步⻓,⽣成不重复ID的策略来实现⾼可⽤。这种⽅式⽣成的 id 有序,但是需要独⽴部署数据库实例,成本⾼,还会有性能瓶颈。
- 利⽤ redis ⽣成 id : 性能⽐较好,灵活⽅便,不依赖于数据库。但是,引⼊了新的组件造成系统更加复杂,可⽤性降低,编码更加复杂,增加了系统成本
- Twitter的snowflake【雪花】算法 :Github 地址:https://github.com/twitter-archive/snowflake。
- 美团的Leaf分布式ID⽣成系统 :Leaf 是美团开源的分布式ID⽣成器,能保证全局唯⼀性、趋势递增、单调递增、信息安全,⾥⾯也提到了⼏种分布式⽅案的对⽐,但也需要依赖关系数据库、
- Zookeeper等中间件
其中应用比较多的是Redis的分布式自增主键和雪花算法,Redis的分布式自增主键在另一篇blogRedis的应用场景提到过,这里主要看看雪花算法是怎么实现的
雪花算法
这种方案大致来说是一种以划分命名空间来生成ID的一种算法,这种方案把64-bit分别划分成多段,分开来标示机器、时间等。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号,最后还有一个符号位,永远是0。
单机每秒内理论上最多可以生成1024*(2^12),也就是409.6万个ID(1024 X 4096 = 4194304)
- 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0
- 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截) 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69
- 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId。10-bit机器可以分别表示1024台机器。如果我们对IDC划分有需求,还可以将10-bit分5-bit给IDC,分5-bit给工作机器。这样就可以表示32个IDC,每个IDC下可以有32台机器,可以根据自身需求定义。
- 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号。12个自增序列号可以表示2^12个ID,理论上snowflake方案的QPS约为409.6w/s,这种分配方式可以保证在任何一个IDC的任何一台机器在任意毫秒内生成的ID都是不同的
整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右,但是也有一个缺点就是,强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态