分布式环境中解决主从延时的一些思路

目录标题

MySQL主从复制复习

主从复制,是指建立一个和主数据库完全一样的数据库环境(称为从数据库),并将主库的操作行为进行复制的过程:将主数据库的DDL和DML的操作日志同步到从数据库上,然后在从数据库上对这些日志进行重新执行,来保证从数据库和主数据库的数据的一致性。

为什么要做主从复制?

1、在复杂的业务操作中,经常会有操作导致锁行甚至锁表的情况,如果读写不解耦,会很影响运行中的业务,使用主从复制,让主库负责写,从库负责读。即使主库出现了锁表的情景,通过读从库也可以保证业务的正常运行。

2、保证数据的热备份,主库宕机后能够及时替换主库,保障业务可用性。

3、架构的演进:业务量扩大,I/O访问频率增高,单机无法满足,主从复制可以做多库方案,降低磁盘I/O访问的频率,提高单机的I/O性能。

4、本质上也是分治理念,主从复制、读写分离即是压力分拆的过程。

5、读写比也影响整个拆分方式,读写比越高,主从库比例应越高,才能保证读写的均衡,才能保证较好的运行性能。读写比下的主从分配方法下:

读写比 主库 从库
50:50 1 1
66:33 1 2
80:20 1 4

主从复制的原理

当在从库上启动复制时,首先创建I/O线程连接主库,主库随后创建Binlog Dump线程读取数据库事件并发送给I/O线程,I/O线程获取到事件数据后更新到从库的中继日志Relay Log中去,之后从库上的SQL线程读取中继日志Relay Log中更新的数据库事件并应用,

如下图所示:

细化一下有如下几个步骤:

1、MySQL主库在事务提交时把数据变更(insert、delet、update)作为事件日志记录在二进制日志表(binlog)里面。

2、主库上有一个工作线程 binlog dump thread,把binlog的内容发送到从库的中继日志relay log中。

3、从库根据中继日志relay log重做数据变更操作,通过逻辑复制来达到主库和从库的数据一致性。

4、MySQL通过三个线程来完成主从库间的数据复制,其中binlog dump线程跑在主库上,I/O线程和SQL线程跑在从库上。拥有多个从库的主库会为每一个连接到主库的从库创建一个binlog dump线程。

主从延迟的原因?

MySQL主从复制,读写分离是我们常用的数据库架构,但是在并发量较大、数据变化大的场景下,主从延时会比较严重。

延迟的本质原因是:系统TPS并发较高时,主库产生的DML(也包含一部分DDL)数量超过Slave一个Sql线程所能承受的范围,效率就降低了。

我们看到这个sql thread 是单个线程,所以他在重做RelayLog的时候,能力也是有限的。

解决思路

在分布式环境下,主从数据库的延时是一个常见的问题,尤其是在高并发场景下。以下是一些解决主从延时的思路和实践经验,并附上具体的例子:

1. 读写分离与延迟容忍

思路:将读操作分配到从库,写操作分配到主库。通过设置合理的延迟容忍时间来处理短暂的主从延时。

实践经验

  • 在应用层实现读写分离逻辑。
  • 设置一个可接受的延迟容忍时间(例如5秒),在此时间内允许数据不一致。

例子

java 复制代码
public class UserService {
    private final JdbcTemplate masterJdbcTemplate;
    private final JdbcTemplate slaveJdbcTemplate;
    private static final int TOLERANCE_DELAY = 5000; // 5秒

    public User getUserById(int id) {
        // 从从库读取数据
        User user = slaveJdbcTemplate.queryForObject("SELECT * FROM users WHERE id = ?", new Object[]{id}, new BeanPropertyRowMapper<>(User.class));
        
        // 获取当前时间和上次更新时间
        long currentTimeMillis = System.currentTimeMillis();
        long lastUpdatedTimeMillis = user.getLastUpdated().getTime();

        // 计算时间差
        long timeDifference = currentTimeMillis - lastUpdatedTimeMillis;

        // 如果时间差小于延迟容忍时间,则认为数据是一致的
        if (timeDifference < TOLERANCE_DELAY) {
            return user;
        } else {
            // 否则从主库读取数据
            return masterJdbcTemplate.queryForObject("SELECT * FROM users WHERE id = ?", new Object[]{id}, new BeanPropertyRowMapper<>(User.class));
        }
    }
}

2. 异步复制优化

思路:优化主从复制的配置,减少复制延迟。可以使用半同步复制或并行复制来提高效率。

实践经验

  • 使用MySQL的半同步复制(semi-sync replication)确保至少有一个从库接收到事务后才返回成功。
  • 配置并行复制以加快复制速度。

例子

sql 复制代码
-- 启用半同步复制
CHANGE MASTER TO MASTER_HOST='master_host', MASTER_PORT=3306, MASTER_USER='repl_user', MASTER_PASSWORD='repl_password', MASTER_AUTO_POSITION=1, MASTER_CONNECT_RETRY=10, MASTER_HEARTBEAT_PERIOD=10, MASTER_DELAY=0 FOR CHANNEL 'group_replication_recovery';

-- 配置并行复制
[mysqld]
slave_parallel_workers=4
slave_parallel_type=LOGICAL_CLOCK

3. 缓存机制(常用)

思路:使用缓存来减轻数据库的压力,并减少主从延时的影响。对于频繁读取的数据,可以先从缓存中读取。

实践经验

  • 使用Redis或Memcached等缓存系统。
  • 在数据更新时同时更新缓存,确保缓存与数据库的一致性。

例子

java 复制代码
public class UserService {
    private final JdbcTemplate jdbcTemplate;
    private final RedisTemplate<String, User> redisTemplate;

    public User getUserById(int id) {
        // 先从缓存中读取
        User user = redisTemplate.opsForValue().get("user:" + id);
        if (user == null) {
            // 缓存中没有,从数据库读取
            user = jdbcTemplate.queryForObject("SELECT * FROM users WHERE id = ?", new Object[]{id}, new BeanPropertyRowMapper<>(User.class));
            // 更新缓存
            redisTemplate.opsForValue().set("user:" + id, user, 1, TimeUnit.HOURS);
        }
        return user;
    }

    public void updateUser(User user) {
        // 更新数据库
        jdbcTemplate.update("UPDATE users SET name = ?, email = ? WHERE id = ?", user.getName(), user.getEmail(), user.getId());
        // 更新缓存
        redisTemplate.opsForValue().set("user:" + user.getId(), user, 1, TimeUnit.HOURS);
    }
}

4. 最终一致性方案(常用)

思路:采用最终一致性方案,允许短期内的数据不一致,但保证最终数据会一致。

实践经验

  • 使用消息队列(如Kafka、RabbitMQ)来异步处理数据同步。
  • 通过事件驱动架构来处理数据变更通知。

例子

java 复制代码
// 生产者发送用户更新事件
kafkaTemplate.send("user-update-topic", user.getId(), user);

// 消费者处理用户更新事件
@KafkaListener(topics = "user-update-topic")
public void onUserUpdate(ConsumerRecord<Integer, User> record) {
    User user = record.value();
    // 更新从库
    jdbcTemplate(slaveDataSource).update("UPDATE users SET name = ?, email = ? WHERE id = ?", user.getName(), user.getEmail(), user.getId());
}

5. 主从切换与自动故障恢复(DBA常用)

思路:通过自动化工具(如MHA、Orchestrator)实现主从切换和自动故障恢复,减少因主库故障导致的延时。

实践经验

  • 部署MHA或Orchestrator来监控主从状态。
  • 自动切换主库并在新的主库上继续服务。

例子

bash 复制代码
# 安装MHA
apt-get install mha4mysql-manager

# 配置MHA
cat > /etc/masterha/app1.cnf <<EOF
[server default]
manager_workdir=/var/log/mha/app1
manager_log=/var/log/mha/app1/manager.log
ssh_user=root
repl_user=repl
repl_password=repl_password

[server1]
hostname=192.168.1.1
port=3306

[server2]
hostname=192.168.1.2
port=3306
EOF

# 启动MHA
masterha_manager --conf=/etc/masterha/app1.cnf

当然,除了之前提到的方法,还有几种常见的解决主从延时的思路和实践经验。以下是更多的一些方法及其具体例子:

6. 使用中间件或代理

思路:使用中间件或代理来管理主从数据库之间的读写分离和负载均衡,减少应用层的复杂性。

实践经验

  • 使用如MaxScale、ProxySQL等中间件。
  • 配置中间件以智能路由读写请求,并提供延迟监控和故障切换功能。

例子

sql 复制代码
-- ProxySQL配置示例
INSERT INTO mysql_servers(hostgroup_id, hostname, port) VALUES (10, 'master_host', 3306);
INSERT INTO mysql_servers(hostgroup_id, hostname, port) VALUES (20, 'slave1_host', 3306);
INSERT INTO mysql_servers(hostgroup_id, hostname, port) VALUES (20, 'slave2_host', 3306);

-- 设置读写分离规则
INSERT INTO mysql_query_rules(rule_id, active, match_pattern, destination_hostgroup, apply) 
VALUES (1, 1, '^SELECT.*', 20, 1); -- 读操作路由到从库
INSERT INTO mysql_query_rules(rule_id, active, match_pattern, destination_hostgroup, apply) 
VALUES (2, 1, '^INSERT.*|^UPDATE.*|^DELETE.*', 10, 1); -- 写操作路由到主库

-- 加载规则
LOAD MYSQL QUERY RULES TO RUNTIME;
SAVE MYSQL QUERY RULES TO DISK;

7. 数据预热(营销常用)

思路:在业务低峰期预先加载数据到从库,减少高峰时段的数据同步压力。

实践经验

  • 在夜间或其他低峰时段进行批量数据同步。
  • 使用定时任务或调度系统(如Cron)来执行数据预热。

例子

bash 复制代码
# 定时任务脚本
#!/bin/bash

# 每天凌晨2点执行数据同步
0 2 * * * /usr/local/bin/pt-table-sync --sync-to-master h=master_host,u=repl_user,p=repl_password

8. 优化网络和硬件

思路:优化网络连接和硬件配置,提高数据传输速度和处理能力。

实践经验

  • 使用高速网络连接(如10Gbps)。
  • 升级服务器硬件,特别是CPU和内存。
  • 使用SSD硬盘以提高I/O性能。

例子

  • 确保主从服务器之间的网络带宽足够高,减少网络延迟。
  • 使用高性能的SSD硬盘替换传统的HDD硬盘。

9. 多活架构

思路:采用多活架构,多个数据中心同时提供服务,每个数据中心都有自己的主从集群。

实践经验

  • 在多个地理区域部署独立的主从集群。
  • 使用全局负载均衡器(如DNS轮询、GSLB)将请求分发到最近的数据中心。

例子

  • 在北京、上海、广州分别部署一套主从集群。
  • 使用阿里云的全球负载均衡服务(GSLB)将用户请求分发到最近的数据中心。

10. 数据校验与修复

思路:定期进行数据校验,发现并修复主从数据不一致的问题。

实践经验

  • 使用工具如pt-table-checksum和pt-table-sync进行数据一致性检查和修复。
  • 定期运行数据校验任务,并记录日志以便追踪问题。

例子

bash 复制代码
# 数据一致性检查
/usr/local/bin/pt-table-checksum --host=master_host --user=repl_user --password=repl_password

# 数据修复
/usr/local/bin/pt-table-sync --sync-to-master h=master_host,u=repl_user,p=repl_password

11.少量读业务直连主库

业务量不多的情况下,不做主从分离.既然主从延迟是由于从库同步写库不及时引起的,那我们也可以在有主从延迟的地方改变读库方式,由原来的读从库改为读主库。当然这也会增加代码的一些逻辑复杂性。

这边需要注意的是,直接读主库的业务量不宜多,而且是读实时一致性有刚性需求的业务才这么做。否则背离读写分离的目的。

12.适当的限流、降级

任何的服务器都是有吞吐量的限制的,没有任何一个方案可以无限制的承载用户的大量流量。所以我们必须估算好我们的服务器能够承载的流量上限是多少。

达到这个上限之后,就要采取缓存,限流,降级的这三大杀招来应对我们的流量。这也是应对主从延迟的根本处理办法。

通过这些方法,可以进一步减少主从延时,提高系统的稳定性和性能。选择哪种方法取决于具体的业务需求、技术栈以及资源条件。综合运用多种方法通常能取得更好的效果。

相关推荐
指尖下的技术2 分钟前
Mysql面试题----为什么B+树比B树更适合实现数据库索引
数据结构·数据库·b树·mysql
Ciderw3 分钟前
MySQL为什么使用B+树?B+树和B树的区别
c++·后端·b树·mysql·面试·golang·b+树
峰子201225 分钟前
B站评论系统的多级存储架构
开发语言·数据库·分布式·后端·golang·tidb
weisian15125 分钟前
消息队列篇--原理篇--Pulsar和Kafka对比分析
分布式·kafka
无锡布里渊1 小时前
分布式光纤应变监测是一种高精度、分布式的监测技术
分布式·温度监测·分布式光纤测温·厘米级·火灾预警·线型感温火灾监测·分布式光纤应变
40岁的系统架构师1 小时前
15 分布式锁和分布式session
分布式·系统架构
斯普信专业组1 小时前
云原生时代,如何构建高效分布式监控系统
分布式·云原生·prometheus
贾贾20231 小时前
主站集中式和分布式的配电自动化系统区别在哪里?各适用于什么场所?一文详解
运维·分布式·考研·自动化·生活·能源·制造
胡耀超2 小时前
CentOS 7.9(linux) 设置 MySQL 8.0.30 开机启动详解
linux·mysql·centos
计算机学姐4 小时前
基于微信小程序的民宿预订管理系统
java·vue.js·spring boot·后端·mysql·微信小程序·小程序