读写分离
为什么需要读写分离
Mysql主要是针对磁盘进行操作,虽然Mysql内部有一些缓冲区,但读性能是非常低的,速度慢。而在大多数项目中,读操作是要远远大于写操作的,所以如果将读写分离,可以更好地分别提高读写性能。
但是读写分离可能会造成一些问题,如读和写操作的数据不一致,且部署维护困难。
mysql主从复制
binlog
二进制日志,是在事务提交之后产生的,跟存储引擎没什么关系。而redolog
在事务提交之后也有一个整体的刷盘操作。binlog
记载的是修改的行信息。
- 主库更新事件(update、insert、delete)通过io-thread写到binlog;
- 从库请求读取binlog,通过io-thread写入(write)从库本地 relay log(中继日志);
- 从库通过sql-thread读取(read) relay log,并把更新事件在从库中执行(replay)一遍;
事务提交之后产生binlog
,我们的从数据库会开启一个IO线程,从主数据库复制binlog
到从数据库中。从数据库再把它写到一个中继日志。之后我们再开启另一个线程SQL thread
,它负责将中继日志的内容读出来然后,进行回放,即,将在主数据库中执行过的sql语句再执行一遍。这是主从数据库将保持一致。
如果从数据库越多,同步也就越慢
最终一致性、强一致性
最终一致性
写主读从
强一致性
写主;如果一致性要求高从主数据库中读,如果一致性要求不高从从数据库中读
缓存方案
前提:读多写少,单个主节点能够承受项目数据量
将热点读数据在缓存数据库备份,将热点读操作转移到缓存数据库
应用场景分析
- 内存访问速度是磁盘访问速度的十万倍
- 大所属业务场景下读次数是写次数的十倍以上
所以我们需要去优化它的读性能
mysql自带内部的缓存,但这个缓存主要是针对mysql自身的需求的。而mysql主要需要解决的是内存访问与磁盘访问的速度差异,跟我们的业务需求不同。
例如在整点秒杀活动中,我们可以提前缓存一些用户数据,以防用户大量登陆造成的mysql读性能下降
通常使用关系型数据库作为主数据库,便于分析(因为内存比磁盘小得多)
使用缓存数据库(内存数据库)存放热点数据
同步问题分析
因为引入了缓存数据库,所以需要引入同步策略,将缓存数据库和mysql的数据保持一致
缓存可以宕机,出现异常,但需要保证我们系统必须可以继续运行。
存在状态
mysql有,缓存无
这种情况下可以接受,但是需要通过策略,将mysql中的数据同步到缓存中,这样方便我们下一次直接从缓存中取数据
mysql无,缓存有
不可接受,因为mysql是作为我们主要数据的依据、基准。如果mysql中没有数据,那么其他的,比如先访问缓存数据库的话,缓存中有,会造成我们整个系统的数据不一致。所以我们需要通过某种策略去避免这种情况的发生
mysql有,缓存有,但是数据不一致
同样不可接受,原因同上。也同样需要通过策略去避免。
mysql和缓存都有数据,且一致
可接受
mysql和缓存都没有数据
可接受
目标场景
单个数据中心
部分游戏业务场景,有单独的DBServer
多个数据中心
大部分web项目,server端不缓存数据,直接操作数据库;如果缓存数据可能造更新丢失
最终一致性解决方案
读写分离,主库将数据同步到从库,是需要时间,那么在同步期间,主从之间数据有差异。
读方案:先读缓存,缓存存在直接返回;缓存不存在,去访问mysql获取,再写入Redis。
这里有写两种方案:
- 直接写mysql,等待mysq|同步数据到redis
- 先写redis,设置key的过期时间为200ms(经验值),等待mysq|回写redis,覆盖key,设置更长的过期时间(因为,如果这时有其他人需要访问这个数据,就可以直接从Redis中读取,不需要去mysql,加快速度);200ms默认的是写mysq|到mysq同步到redis的时长;这个需要根据实际环境进行设置。可能存在多次访问mysql(即超过了200ms没有返回,则返回时key不存在,所以会造成Redis再去访问mysql ),也可能存在更新丢失(即多个服务器操作同一流程,我们在Redis写完,等它同步过来的时候,又有另一个服务器进行修改,会造成内存中的数据不一致)
强一致性解决方案
读方案:和同步一致性一样,先读缓存,缓存存在直接返回;缓存不存在,去访问mysql获取,再写入Redis
写方案:先删除缓存,再写mysql,等待mysql同步到缓存(否则在还未同步成功时,会出现数据不一致)
数据同步方案
不管强一致性还是最终一致性,都需要一个同步的流程,把mysql的数据同步到Redis中。接下来解释同步中的几种方案。
方案一(canel)
依照主从复制的原理,这个canel
伪装成从数据库,实现了如下的部分(红),即读取binlog
。
而canel client
则实现了将binlog
这个二进制文件解析成可读的格式
方案二
不建议使用,有事务的场景容易出错;虽然保证了真正的强一致性;
这个实现每次插入修改都需要重新简历redis连接,操作完后又释放redis连接;
缓存故障
下面三个问题都是读缓存没有,然后再去读mysql的过程中出现了问题
缓存穿透
问题:当缓存和mysql都不存在:先读缓存,如果缓存不存在再读mysql,如果mysql不存在则直接返回。如果黑客利用mysql中不存在的数据,它就会一直请求mysql中不存在的数据,压力会全部堆积在mysql中,那么这时候可能会造成mysql的崩溃(包括假死,数据库无法提供服务的这种状态)。
解决:
- 缓存中设置
<key, nil>
与过期时间(防止堆积过多<key, nil>
),下次访问key的时候,不再访问mysql。 - 部署布隆过滤器(将mysql中存在的key写入布隆过滤器)(布隆过滤器不支持删除的功能,只支持添加的功能,所以限制过多)
缓存击穿
问题:某些数据Redis没有,但mysql有,此时有大量这类数据的并发请求,同样会造成mysql的压力过大。(这种很热的key过期了)
解决:
- 加锁:从mysql请求数据的时候获取锁,如果获取成功,则进行接下来的操作;如果获取失败,则休眠一段时间(200ms)再去获取;获取成功,则同步到Redis中,最后再释放锁。释放锁之后,其他的进程可以并发的访问Redis,并发访问Redis并没有问题。
- 将很热的key设置不过期
缓存雪崩
问题:表示一段时间内,大量缓存集中失效(redis无 mysql有),导致请求全部走mysql,有可能搞垮数据库,使整个服务失效
解决:
- 如果因为缓存数据库宕机,造成所有数据涌向mysql; 采用高可可用的集群方案,如哨兵模式(通过选举会选举出另外一个节点,作为缓存主数据库)、cluster模式;
- 如果因为设置了相同的过期时间,造成缓存集中失效;设置随机过期值或者其他机制错开失效
- 如果因为系统重启的时候,造成缓存数据消失(Redis是内存数据库);重启时间短,redis开启持久化(过期信息也会持久化)就行了;重启时间长提前将热数据导入redis当中;