数据库提升性能的方式
- 数据库连接池。使用的是阻塞io,连接池一般结合线程池一起使用。
- 异步请求。使用异步io,需要自己实现mysql的协议。
- 从sql执行处罚,使用sql预编译执行的方式,prepare,跳过词法句法分析、权限验证、优化器等步骤,提高执行效率。
- 读写分离。对于一致性要求不是特别高的场景,也就是最终一致性,并且读多写少的场景,可以使用主数据库写,从数据库读的方式。
数据库主从方式可以解决数据库单点故障的问题。
使用数据库主从方式,如果一致性要求高:
- 使用主从半同步复制;
- 读去主数据库读取。
- 缓存方案,本文介绍的主要内容。
为什么需要数据库缓冲层?
前提
一般大部分项目中,数据库读操作是写操作的10倍左右;单个主节点能支撑项目数据量;数据的主要依据是 mysql。
mysql缓存
mysql本身是有缓存的,它的作用也是用来缓存热点数据,这些数据包括数据文件、索引文件等;mysql的缓存是从自身出发,跟应用层的具体业务无关;缓存策略主要是类似LRU的算法。
mysql数据主要存储在磁盘当中,适合大量重要数据的存储;磁盘当中的数据是远大于缓存中的数据;一般业务场景mysql作为主要数据库。
mysql缓存和缓存数据库的区别
缓存数据库就是内存数据库,包括redis,mamcached等。他们所有的数据都存储在内存当中,当然也可以选择将内存中的数据持久化到磁盘当中。
mysql本身的缓存和缓存数据的数据都是存在内存中。mysql缓存的内容是最近访问的热点数据,不受具体业务的控制;而在业务开发中,业务程序代码可以选择将哪些数据存到redis中去。
总结
- mysql本身的缓存数据不由应用程序控制,而应用程序可以控制redis中存储哪些数据,一般都是缓存应用层自定义的热点数据;
- 磁盘的访问速度相对内存来说比较慢,应尽量将热点数据放在内存中;
- 主要解决读的性能;因为写没必要优化,必须让数据正确的落盘;如果写性能出现问题,那么请使用横向扩展集群方式来解决;
- 项目中需要存储的数据应该远大于内存的容量,同时需要进行数据统计分析,所以mysql应该作为主要的数据依据;
问题
系统中除了关系型数据库mysql,如果加上了缓存数据库redis,那么就有一个很关键的问题,redis中的数据和mysql数据的同步问题。下面的讨论都是基于热点数据的同步问题。
为什么有同步的问题?
系统没有引入redis之前,我们对数据的读写都是基于mysql(将mysql内部作为一个整体,不考虑mysq主从数据库或者集群),所以不存在同步问题;
引入redis后,我们对数据的操作需要分别操作redis和 mysql,那么这个时候数据可能存在几个状态?
- mysql有,redis无,通过策略避免
- mysql无,redis有,不正常,避免
- 都有,但数据不一致,不正常,避免
- 都有,数据一致,最终目标。
- 都没有,正常的,正常。
4和5显然是没问题的,我们现在需要考虑1、2以及3。
核心思想:系统主要数据的依据是mysql,所以mysql数据正确就OK了,只需要将mysql的数据正确同步到redis就可以了;同理,redis有,mysql没有,这比较危险,此时我们可以认为该数据为脏数据;所以我们需要在同步策略中避免该情况发生;同时可能存在mysql和redis都有数据,但是数据不一致,这种也需要在同步策略中避免。
redis不可用的话,系统应该要保持正常工作;
mysql不可用的话,应该停止对外提供服务。
另外可以将问题3转化为问题1。
解决数据同步问题
对于读操作,我们都采用一种策略;对于写操作,我们需要考虑对于数据一致性要求的程度,针对强一致性和最终一致性,制定不同的策略。
读策略
先从redis当中获取数据,如果redis不可用,去mysql获取;如果redis有,直接返回;如果redis没有,转而向mysql请求,如果mysql没有,直接返回;如果mysql有,则返回并将数据回写到redis当中;
写策略
强一致性
会有两次写redis。
缺点是数据不能频繁修改,效率会降低。
最终一致性
最终一致性,有时候是可以接受的。
先修改redis,再写mysql。有个问题,如果在写mysql前,mysql宕机了,如果有客户端去读数据,就会从redis中读,但mysql里面根本没有这个数据。
这个问题是可以接受的,因为mysql如果宕机了,整个系统是需要停止服务的。
重要数据不会作为热点数据,不会存储在redis中。
这种方案性能肯定比上面的方案高。但可能会存在拿到一个错误数据,作为业务逻辑的依据。
主从复制
dml操作会生成两个文件,binlog,redo log。
binlog,Mysql Server层生成的。binlog中存储行信息。
redolog,是事务的,顺序写,物理日志,记录要修改B+树对应磁盘中的起始地址+偏移量+具体内容。是由引擎层Innodb生成的。
mysql主从复制,是通过binlog实现的。
- 主库更新事件(update、insert、delete)通过 io-thread 写到binlog;
- 从库请求读取 binlog,通过 io-thread 写入(write)从库本地 relay log(中继日志);
- 从库通过sql-thread读取(read) relay log,并把更新事件在从库中执行(replay)一遍。
Mysql同步数据到Redis
原理: 主从复制。一个组件(如go-mysql-transfer)伪装成mysql的从数据库,当mysql主数据库数据有更新时,获取更新数据,并同步到redis中。
go-mysql-transfer使用
- mysql和redis的地址;
- 定义热点数据;
- 使用lua实现热点数据的同步;
- 第一次使用.\go-mysql-transfer.exe -stock,进行全量同步;
- 通过go-mysql-transfer.exe启动;
之后对mysql的所有dml操作,都会同步到redis中。
异常情况
以上介绍的都是对mysql和redis数据一致性的正常逻辑处理,对于异常情况,我们并没有考虑。下面来看异常情况。
缓存穿透(Cache Penetration)
假设某个数据redis不存在,mysql也不存在,也就是前面介绍的数据5种状态的状态5。有黑客利用它对系统进行攻击,一直尝试读怎么办?缓存穿透,数据最终压力依然堆积在mysql,可能造成mysql不堪重负而崩溃;
解决方法
- 发现mysql不存在,将redis设置为 <key, nil> 设置过期时间。下次访问key的时候,不再访问mysql。容易造成redis缓存很多无效数据;
- 布隆过滤器,将mysql当中已经存在的key,写入布隆过滤器,当访问mysql中不存在的数据时,直接pass掉。
缓存击穿(Hotspot Invalid)
缓存击穿,某些数据redis没有,但是mysql有;此时当大量这类数据的并发请求,同样造成mysql压力过大;
解决方法
- 通过分布式锁,控制并发请求;
- 将很热的key, 设置为不过期。
缓存雪崩(Cache Avalanche)
一段时间内,缓存集中失效(redis无 mysql 有),导致请求全部走mysql,有可能搞垮数据库,使整个服务失效。
解决方法
- 如果因为redis宕机,造成所有数据涌向mysql;
采用redis高可用的集群方案,如哨兵模式、cluster模式; - 如果因为设置了相同的过期时间,造成缓存集中失效;
设置随机过期值或者其他机制错开失效时间; - 如果因为系统重启的时候,造成缓存数据消失;
重启时间短,redis开启持久化(过期信息也会持久化)就行了; 重启时间长提前将热数据导入redis当中;
总结
mysql缓存方案,主要需要考虑redis和mysql数据的5种状态,结合强一致性和最终一致性,以及mysql数据如何同步到redis中,并需要考虑缓存击穿等异常情况。