主从延迟&读写不一致解决方案分析

具体业务场景方案分析

问题背景:

主从延迟&读写不一致解决方案分析_第1张图片

虽然强制走写库,避免了主从延迟带来的读库数据不一致问题,但是增加了写库的QPS,带来了巨大压力,所以通过限流来保护db,但这样会降低QPS。

业务上暂时不清楚,所以现在从技术层面讨论读库能分担压力的可行方案。

结论:

改走读库,但是

采用降低主从延迟 来尽量保证读的数据为最新

并使用 redis缓存标记法 ,来识别 是否走写库

问题分析:

走读库,无法避免的是数据不一致问题,对于这个问题,可以从两个方面并行解决:

具体方案选型参考下文

1.降低主从延迟

主从延迟降的越低,读的时候越不容易读不到最新数据。

降低的方案在下文详细介绍,具体方案分析需要结合业务来分析梳理,这里先不重要讨论

2.走从库,但兜底主库逻辑——即redis缓存标记法

这里重点讨论这个。

这个时候我们主要解决的问题是两个:

  1. 缓存 与写库db  数据一致性的问题
  2. 缓存的数据 与 读库db 的数据哪个是最新的判断问题

redis缓存标记法具体方案分析:

步骤:

写操作 :写redis、更新master

读操作:读redis,读slave

方案:

Aredis上记录标记格式(key=业务代号:数据库:表:主键ID; value = 过期时间)过期时间:预估的主库和从库同步延迟的时间

Bredis上记录标记格式(key=业务代号:数据库:表:主键ID; value = 更新时间)

Credis上记录标记格式(key=业务代号:数据库:表:主键ID; value = 自增ID)

方案A

由于过期时间:预估的主库和从库同步延迟的时间,这个无法准确预估,而现在我们是希望能准确判断读到的数据是否是延迟,因此不考虑。

方案B

主从延迟&读写不一致解决方案分析_第2张图片

对于方案B,我们在redis记录的是这条数据最新更新的时间Tredis_up,而数据库中数据更新的时候会生成数据更新的时间,那么通过比对slave 查出的 数据更新 Tslave_up

之间的大小,就可以判断 是否需要再走写库。

但这里有两个问题,先写redis,还是先写mysql,下面分别分析

B1 先写redis,后写mysql

具体操作:写redis的时候java生成当前的时间Tredis_up写进redis ,然后再更新数据库,这个时候数据库中的更新时间得用Tredis_up

分析:此操作可以保证redis中记录的就是最新的数据,防止写入redis失败的情况。

希望通过Tredis_u==Tslave_up来判断是否延迟,

即认为Tredis_u==Tslave_up代表没有延迟

数据一致性问题:此时其实无法通过判断Tslave_up ==Tredis_u来判断是否有延迟,是否需要走写库,

因为Tslave_up==Tredis_up 时候,可能是延迟(比如高并发场景,Tredis_up值和master数据库中的最新值不一样),也可能没有延迟(理想情况)

B2先写mysql,后写redis

具体操作:先更新数据库,再写redis的时候用这条数据的更新时间

分析:此操作不可以保证redis中记录的就是最新的数据,不能避免写入redis失败的情况。

希望通过Tredis_u==Tslave_up来判断是否延迟,

即认为Tredis_u==Tslave_up代表没有延迟

数据一致性问题:此时其实无法通过判断Tslave_up == Tredis_u来判断是否有延迟,是否需要走写库,

因为 Tslave_up==Tredis_up 时候,可能是延迟(比如redis更新晚了,或者更新失败),也可能没有延迟(理想情况)

方案C

以上方案的问题在于:

无法保证db,缓存数据一致性(就不用谈redis数据和slave数据的比较)

那么就有了方案C,可以设置value的值为自增版本号:versionId,同时存到这数据的字段中,那么就可以比对slave中的versionId和redis中的versionId是否相等即可。

主从延迟&读写不一致解决方案分析_第3张图片

写库具体操作:

1.先通过redis为这条数据生成一个自增字段versionId,并存下来

2.更新db数据,并写入versionId

3.比较新的versionId是否 大于原有的versionId——解决线程不安全问题、并发问题

4.如果新的versionId大于原有的versionId,那就顺利更新,否则重新在redis中生成版本号versionId,并更新

读库:

1.读slave

2.读redis

3.比较versionId:相等ok;不相等就再走写库

tip:这里需要注意一点的是我们的value不能是时间,而应该是redis生成的一个分布式自增Id

这样既解决redis与db数据一致性问题,也可以保证redis中value的值 与 读库db 的数据哪个是最新的判断问题。

问题保障:

1.redis生成分布式自增id——保证分布式环境下id不冲突

2.redis的单线程——保证单机高并发下线程安全

3.自增id——保证redis保存最新id

带来的问题:

1.由于引入了redis,所以吞吐量会降低一点,——但是考虑到大流量场景下,从库分担压力 带来效益会高于 redis引入带来的负面影响

其他待优化:~~~~~~~~~~~

综上,方案C可行,可以保证缓存,db一致性,且缓存的数据为最新的。因为版本号递增。

——————————————————————————————

主从延迟问题

在高并发场景下,从库的数据一定会比主库慢一些,是有延时的。所以经常出现,刚写入主库的数据可能是读不到的,要过几十毫秒,甚至几百毫秒才能读取到。

这里补充实际延迟情况:比如多少QPS~延迟多久

1)、MySQL数据库主从同步延迟原理mysql主从同步原理:主库针对写操作,顺序写binlog,从库单线程去主库顺序读”写操作的binlog”,从库取到binlog在本地原样执行(随机写),来保证主从数据逻辑上一致。mysql的主从复制都是单线程的操作,主库对所有DDL和DML产生binlog,binlog是顺序写,所以效率很高,slave的Slave_IO_Running线程到主库取日志,效率比较高,下一步,问题来了,slave的Slave_SQL_Running线程将主库的DDL和DML操作在slave实施。DML和DDL的IO操作是随即的,不是顺序的,成本高很多,还可能可slave上的其他查询产生lock争用,由于Slave_SQL_Running也是单线程的,所以一个DDL卡主了,需要执行10分钟,那么所有之后的DDL会等待这个DDL执行完才会继续执行,这就导致了延时。有朋友会问:“主库上那个相同的DDL也需要执行10分,为什么slave会延时?”,答案是master可以并发,Slave_SQL_Running线程却不可以。

2)、MySQL数据库主从同步延迟是怎么产生的?当主库的TPS并发较高时,产生的DDL数量超过slave一个sql线程所能承受的范围,那么延时就产生了,当然还有就是可能与slave的大型query语句产生了锁等待。首要原因:数据库在业务上读写压力太大,CPU计算负荷大,网卡负荷大,硬盘随机IO太高次要原因:读写binlog带来的性能影响,网络传输延迟。

延迟的原因:

1、内存配置过小或者 iops 配置(这个指的是 io capacity,sas 盘和 ssd 盘配置有区别)不当。

2、主库 TPS 过高,主库写入频繁,从库压力跟不上导致延时

产生的DDL数量超过slave一个sql线程所能承受的范围

4、主库执行大事务导致延迟
比如在主库执行一个大的 update、delete、insert … select 的事务操作,产生大量的 binlog 传送到只读节点,只读节点需要花费与主库相同的时间来完成该事务操作,进而导致了只读节点的延迟。

解决方法:
拆分大事务:增加缓存,异步写入数据库,减少直接对db的大量写入,减少大事务

5.与slave的大型query语句产生了锁等待

数据库在业务上读写压力太大,CPU计算负荷大,网卡负荷大,硬盘随机IO太高次要原因:读写binlog带来的性能影响,网络传输延迟。

6、其它情况,如对无主键表的删除。
用户在删除数据的时候,由于表主键的缺少,同时删除条件没有索引,或者删除的条件过滤性极差,导致 slave 出现 hang 住,会严重的影响生产环境的稳定性。

只读实例出现延迟后的排查思路

  • 看只读节点 IOPS 定位是否存在资源瓶颈
  • 看只读节点的 binlog 增长量定位是否存在大事务
  • 看只读节点的 comdml 性能指标,对比主节点的 comdml 定位是否是主库写入压力过高导致
  • 看只读节点 show full processlist,判断是否有 Waiting for table metadata lock 和 alter,repair,create 等 ddl 操作。

主从延迟的解决方案:

1)、架构方面

1.业务的持久化层的实现采用分库架构,mysql服务可平行扩展,分散压力。

2.单个库读写分离,一主多从,主写从读,分散压力。这样从库压力比主库高,保护主库。

3.服务的基础架构在业务和mysql之间加入memcache或者redis的cache层。降低mysql的读压力。

4.不同业务的mysql物理上放在不同机器,分散压力。

5.使用比主库更好的硬件设备作为slave总结,mysql压力小,延迟自然会变小。

2)、硬件方面

1.采用好服务器,比如4u比2u性能明显好,2u比1u性能明显好。

2.存储用ssd或者盘阵或者san,提升随机写的性能。

3.主从间保证处在同一个交换机下面,并且是万兆环境。

总结,硬件强劲,延迟自然会变小。一句话,缩小延迟的解决方案就是花钱和花时间。

3)、数据库自身特性

1、sync_binlog在slave端设置为0

2、–logs-slave-updates 从服务器从主服务器接收到的更新不记入它的二进制日志。

3、直接禁用slave端的binlog

4、slave端,如果使用的存储引擎是innodb,innodb_flush_log_at_trx_commit =2

1、sync_binlog=1 oMySQL提供一个sync_binlog参数来控制数据库的binlog刷到磁盘上去。默认,sync_binlog=0,表示MySQL不控制binlog的刷新,由文件系统自己控制它的缓存的刷新。这时候的性能是最好的,但是风险也是最大的。一旦系统Crash,在binlog_cache中的所有binlog信息都会被丢失。

如果sync_binlog>0,表示每sync_binlog次事务提交,MySQL调用文件系统的刷新操作将缓存刷下去。最安全的就是sync_binlog=1了,表示每次事务提交,MySQL都会把binlog刷下去,是最安全但是性能损耗最大的设置。这样的话,在数据库所在的主机操作系统损坏或者突然掉电的情况下,系统才有可能丢失1个事务的数据。但是binlog虽然是顺序IO,但是设置sync_binlog=1,多个事务同时提交,同样很大的影响MySQL和IO性能。虽然可以通过group commit的补丁缓解,但是刷新的频率过高对IO的影响也非常大。

对于高并发事务的系统来说,“sync_binlog”设置为0和设置为1的系统写入性能差距可能高达5倍甚至更多。所以很多MySQL DBA设置的sync_binlog并不是最安全的1,而是2或者是0。这样牺牲一定的一致性,可以获得更高的并发和性能。默认情况下,并不是每次写入时都将binlog与硬盘同步。因此如果操作系统或机器(不仅仅是MySQL服务器)崩溃,有可能binlog中最后的语句丢失了。要想防止这种情况,你可以使用sync_binlog全局变量(1是最安全的值,但也是最慢的),使binlog在每N次binlog写入后与硬盘同步。即使sync_binlog设置为1,出现崩溃时,也有可能表内容和binlog内容之间存在不一致性。

2、innodb_flush_log_at_trx_commit (这个很管用)抱怨Innodb比MyISAM慢 100倍?那么你大概是忘了调整这个值。默认值1的意思是每一次事务提交或事务外的指令都需要把日志写入(flush)硬盘,这是很费时的。特别是使用电池供电缓存(Battery backed up cache)时。设成2对于很多运用,特别是从MyISAM表转过来的是可以的,它的意思是不写入硬盘而是写入系统缓存。日志仍然会每秒flush到硬 盘,所以你一般不会丢失超过1-2秒的更新。设成0会更快一点,但安全方面比较差,即使MySQL挂了也可能会丢失事务的数据。而值2只会在整个操作系统 挂了时才可能丢数据。

5.设置主从同步方式为半同步(得至少一个slave响应) & 并行复制(多线程拉binlog)

缺点:写请求的时延将会增加,吞吐量将会降低,但因为是半同步,不是全同步,所以可以衡量一下影响

4)应用层面

1.大事务拆分成为小事务进行批量提交,这样只读节点就可以迅速的完成事务的执行,不会造成数据的延迟

2.分库分表降低数据库压力

3.mysql中的sql优化、锁优化、事务隔离级别优化,降低压力

读写分离数据不一致解决方案:

A降低主从延时方案

主从延迟&读写不一致解决方案分析_第4张图片

B延时无法改变时的方案:

1.避免插入后就马上读。

插入数据时立马查询可能查不到,重写代码。

2.业务能够接受可以不解决 ,sleep几毫秒再查

3.选择性强制读主

对于需要强一致的场景,我们可以将其的读请求都操作主库

4.借助redis 缓存标记法

主从延迟&读写不一致解决方案分析_第5张图片

1)A发起写请求,更新了主库,但在缓存中设置一个标记,代表此数据已经更新,标记格式(业务代号:数据库:表:主键ID)根据自己业务场景。

2)设置此标记,要加上过期时间,可以为预估的主库和从库同步延迟的时间

3)B发起读请求的时候,先判断此请求的业务在缓存中有没有更新标记

4)如果存在标记,走主库;如果没有走从库。

5.本地缓存标记

1)用户A发起写请求,更新了主库,并在客户端设置标记,过期时间,如:cookies

2)用户A再发起读请求时,带上这个本地标记在后端

3)后端在处理请求时,获取请求传过来的数据,看有没有这个标记(如:cookies)

4)有这个业务标记,走主库;没有走从库。

这个方案就保证了用户A的读请求肯定是数据一致的,而且没有性能问题,因为标记是本地客户端传过去的。

但其他用户在本地客户端是没有这个标记的,他们走的就是从库了。那其他用户不就看不到这个数据了吗?说的对,其他用户是看不到,但看不到的时间很短,过个1~10秒就能够看到。

但这个方案解决了当前用户的数据一致性的问题,如上面举的例子,写文章,然后到文章列表,本用户是能够看到的。其他用户暂时看不到是没有关系的。

你可能感兴趣的:(java,mysql,数据库,redis,java)