一、读写分离的基础逻辑与常见问题

很多做过中大型系统开发的人都有过这样的经历:系统上线初期跑得好好的,用户量上来后,查数据的页面越来越卡,提交操作倒是没受多大影响。后来问运维才知道,系统用了读写分离架构,但查数据的节点(也就是从节点)比主节点慢了好几分钟,导致用户刚改完信息,刷新页面还是旧内容。这就是典型的从节点延迟问题,也是读写分离架构里最容易踩的坑。

先说说读写分离到底是什么。简单讲就是把原来一台数据库干的活拆成两部分:主节点专门管写操作,比如用户注册、提交订单、修改信息这类需要改数据的操作;从节点专门管读操作,比如查商品列表、看个人中心、导出报表这类只需要取数据的操作。这么做的好处很明显,原来一台机器既要处理写的复杂逻辑,又要应付成千上万的查询请求,现在拆分后各干各的,系统整体的承载能力能翻好几倍。

但问题也随之而来:主节点改完数据后,得把修改的内容同步给从节点,这个同步过程如果出问题,就会导致从节点数据和主节点不一致,也就是我们说的延迟。比如用户刚在主节点改了密码,从节点还没同步到新密码,这时候用户去查自己的账号信息,就会看到旧密码,甚至可能出现“刚提交的订单找不到”“修改的个人信息没生效”这类影响用户体验的问题。

二、达梦数据库读写分离的具体实现

达梦数据库是国内常用的关系型数据库,它的读写分离实现和其他数据库类似,但有自己的配置细节。接下来我们就一步步说怎么搭建达梦的读写分离架构,所有示例都基于达梦数据库V8版本,这是目前最常用的稳定版。

2.1 准备工作

搭建之前需要准备两台服务器,一台做主节点,一台做从节点,两台服务器都要安装好达梦数据库V8,并且确保两台服务器之间的网络是通的,防火墙放开达梦的默认端口5236。另外,主节点的数据库实例要提前创建好,从节点可以先不创建实例,后面通过备份恢复的方式同步初始数据。

2.2 主节点的配置

首先要配置主节点的归档模式,因为达梦的同步是基于归档日志的,没有归档日志就没法同步数据。打开达梦的配置文件dm.ini,找到归档相关的配置项,改成如下内容:

# 开启归档模式
ARCH_INI = 1
# 归档文件存放路径
ARCH_DIR = /dm8/arch
# 归档文件大小限制,单位MB
ARCH_SIZE = 1024
# 归档文件最大数量,超过后自动删除最早的
ARCH_HIST = 30

改完配置后,需要重启达梦服务,让归档配置生效。重启后可以登录达梦的管理工具,执行下面的命令查看归档是否开启成功:

-- 查看归档模式状态
SELECT ARCH_MODE FROM V$DATABASE;

如果返回结果是Y,说明归档开启成功。

接下来要配置主节点的同步用户,达梦的同步需要专门的用户来做,不能用默认的SYSDBA用户。执行下面的SQL创建同步用户:

-- 创建同步用户,用户名是sync_user,密码是Sync123456!
CREATE USER sync_user IDENTIFIED BY Sync123456!;
-- 给同步用户授权,需要同步权限和登录权限
GRANT SYNCHRONIZATION TO sync_user;
GRANT CREATE SESSION TO sync_user;

2.3 从节点的配置

从节点的配置比主节点多一步初始数据的同步。首先要把主节点的数据库备份下来,然后恢复到从节点。备份主节点可以用达梦的disql工具,执行下面的命令:

-- 备份主节点的数据库,备份文件存放在/dm8/backup路径下
BACKUP DATABASE FULL TO 'full_backup_20240101' BACKUPSET '/dm8/backup/full_backup_20240101';

备份完成后,把备份文件拷贝到从节点的对应路径下,然后在从节点恢复备份。恢复前要确保从节点的达梦服务是关闭的,然后用达梦的dmserver工具执行恢复命令:

# 进入达梦的bin目录
cd /dm8/bin
# 恢复备份,实例名是DM01,备份路径是/dm8/backup/full_backup_20240101
./dmserver /dm8/data/DM01/dm.ini mount
./dmrman CTLSTMT="RESTORE DATABASE '/dm8/data/DM01' FROM BACKUPSET '/dm8/backup/full_backup_20240101';"
./dmrman CTLSTMT="RECOVER DATABASE '/dm8/data/DM01' FROM BACKUPSET '/dm8/backup/full_backup_20240101';"
./dmrman CTLSTMT="RECOVER DATABASE '/dm8/data/DM01' UPDATE DB_MAGIC;"

恢复完成后,打开从节点的dm.ini配置文件,配置归档模式和同步相关的参数,内容如下:

# 开启归档模式
ARCH_INI = 1
# 归档文件存放路径
ARCH_DIR = /dm8/arch
# 归档文件大小限制,单位MB
ARCH_SIZE = 1024
# 归档文件最大数量,超过后自动删除最早的
ARCH_HIST = 30
# 同步模式,这里配置成异步同步,因为同步模式会影响主节点性能
SYNCHRONIZATION = ASYNC
# 主节点的IP和端口,格式是IP:端口
SYNCHRONIZATION_SERVER = 192.168.1.100:5236
# 同步用户的用户名和密码
SYNCHRONIZATION_USER = sync_user
SYNCHRONIZATION_PASSWORD = Sync123456!

改完配置后,启动从节点的达梦服务,然后登录从节点的管理工具,执行下面的命令查看同步状态:

-- 查看同步状态
SELECT SYNCHRONIZATION_STATUS FROM V$SYNCHRONIZATION;

如果返回结果是RUNNING,说明同步已经正常启动。

2.4 应用层的配置

读写分离架构不仅需要数据库配置,应用层也要做对应的修改,把读操作和写操作分别路由到从节点和主节点。这里我们用Java的MyBatis-Plus作为示例,配置多数据源来实现读写分离。

首先在项目的pom.xml里引入MyBatis-Plus和Druid数据源的依赖:

<!-- MyBatis-Plus依赖 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3</version>
</dependency>
<!-- Druid数据源依赖 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.16</version>
</dependency>

然后在application.yml里配置主从数据源:

spring:
  datasource:
    # 主数据源,用于写操作
    master:
      url: jdbc:dm://192.168.1.100:5236/DM01
      username: SYSDBA
      password: SYSDBA
      driver-class-name: dm.jdbc.driver.DmDriver
    # 从数据源,用于读操作
    slave:
      url: jdbc:dm://192.168.1.101:5236/DM01
      username: SYSDBA
      password: SYSDBA
      driver-class-name: dm.jdbc.driver.DmDriver

接下来配置数据源的路由规则,创建一个自定义的数据源注解,用来标记方法是读操作还是写操作:

import java.lang.annotation.*;

// 自定义数据源注解,用来指定操作类型
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    // 数据源类型,MASTER是主节点,SLAVE是从节点
    String value() default "MASTER";
}

然后创建一个数据源的切面,用来拦截方法,根据注解切换数据源:

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class DataSourceAspect {
    // 切点,拦截所有带有DataSource注解的方法
    @Pointcut("@annotation(dataSource)")
    public void pointCut(DataSource dataSource) {}

    // 环绕通知,切换数据源
    @Around("pointCut(dataSource)")
    public Object around(ProceedingJoinPoint point, DataSource dataSource) throws Throwable {
        // 获取注解里的数据源类型
        String type = dataSource.value();
        // 设置当前线程的数据源类型
        DataSourceContextHolder.setDataSourceType(type);
        try {
            // 执行目标方法
            return point.proceed();
        } finally {
            // 清除当前线程的数据源类型,避免污染
            DataSourceContextHolder.clearDataSourceType();
        }
    }
}

最后在Service层的方法上加上对应的注解,比如写操作的方法加@DataSource("MASTER"),读操作的方法加@DataSource("SLAVE"):

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;

    // 写操作,调用主节点
    @DataSource("MASTER")
    public void updateUser(User user) {
        userMapper.updateById(user);
    }

    // 读操作,调用从节点
    @DataSource("SLAVE")
    public User getUserById(Long id) {
        return userMapper.selectById(id);
    }
}

这样配置后,应用层就会自动把写操作路由到主节点,读操作路由到从节点,实现了读写分离。

三、从节点延迟的监控方案

就算配置好了读写分离,也不能保证从节点完全没有延迟,所以需要一套监控方案来及时发现延迟问题。达梦数据库提供了很多系统视图可以用来查看同步状态,我们可以基于这些视图来做监控。

3.1 达梦数据库的延迟监控指标

达梦的系统视图里有几个关键的指标可以用来判断从节点的延迟情况,分别是主节点的归档日志序号、从节点的归档日志序号、从节点的同步进度。

首先看主节点的归档日志序号,执行下面的SQL可以查看主节点当前的最大归档序号:

-- 查看主节点当前的最大归档序号
SELECT MAX(ARCH_SEQNO) FROM V$ARCH_LOG;

然后看从节点的归档日志序号,执行下面的SQL可以查看从节点当前接收到的最大归档序号:

-- 查看从节点当前接收到的最大归档序号
SELECT MAX(ARCH_SEQNO) FROM V$SYNCHRONIZATION_RECV;

如果这两个序号的差值大于0,说明从节点还有归档日志没接收,也就是存在延迟。差值越大,延迟越严重。

另外,还可以查看从节点的同步进度,执行下面的SQL可以查看从节点的同步状态和延迟时间:

-- 查看从节点的同步状态和延迟时间
SELECT SYNCHRONIZATION_STATUS, DELAY_TIME FROM V$SYNCHRONIZATION;

DELAY_TIME字段的单位是毫秒,这个值越大,说明从节点的延迟越严重。

3.2 自定义监控脚本

为了方便日常监控,我们可以写一个Shell脚本,定时查询达梦的系统视图,把延迟情况记录下来,超过阈值就发告警。这个脚本基于达梦的disql工具,所有示例都基于达梦数据库V8版本。

脚本的内容如下:

#!/bin/bash
# 达梦数据库延迟监控脚本
# 配置参数
DM_HOME="/dm8" # 达梦数据库安装路径
DM_USER="SYSDBA" # 数据库用户名
DM_PASSWORD="SYSDBA" # 数据库密码
MASTER_IP="192.168.1.100" # 主节点IP
SLAVE_IP="192.168.1.101" # 从节点IP
PORT="5236" # 数据库端口
THRESHOLD=10000 # 延迟阈值,单位毫秒,超过这个值就告警
LOG_FILE="/dm8/monitor/delay_monitor.log" # 日志文件路径

# 函数:执行SQL并返回结果
execute_sql() {
    local sql=$1
    local ip=$2
    # 用disql执行SQL,-s表示静默模式,-L表示登录后执行
    ${DM_HOME}/bin/disql -s -L ${DM_USER}/${DM_PASSWORD}@${ip}:${PORT} << EOF
${sql}
EOF
}

# 函数:记录日志
log() {
    local msg=$1
    echo "$(date '+%Y-%m-%d %H:%M:%S') - ${msg}" >> ${LOG_FILE}
}

# 1. 查询主节点的最大归档序号
master_sql="SELECT MAX(ARCH_SEQNO) FROM V$ARCH_LOG;"
master_seq=$(execute_sql "${master_sql}" "${MASTER_IP}" | grep -v "行号" | grep -v "已选择" | tr -d '[:space:]')

# 2. 查询从节点的最大归档序号和延迟时间
slave_sql="SELECT MAX(ARCH_SEQNO), DELAY_TIME FROM V$SYNCHRONIZATION;"
slave_result=$(execute_sql "${slave_sql}" "${SLAVE_IP}" | grep -v "行号" | grep -v "已选择" | tr -d '[:space:]')
slave_seq=$(echo ${slave_result} | cut -d '|' -f 1)
delay_time=$(echo ${slave_result} | cut -d '|' -f 2)

# 3. 计算归档序号差值
seq_diff=$((master_seq - slave_seq))

# 4. 判断是否超过阈值
if [ ${delay_time} -gt ${THRESHOLD} ] || [ ${seq_diff} -gt 10 ]; then
    log "告警:从节点延迟严重,延迟时间:${delay_time}毫秒,归档序号差值:${seq_diff}"
    # 这里可以加告警逻辑,比如发邮件、发短信、调用企业微信接口等
else
    log "正常:从节点延迟时间:${delay_time}毫秒,归档序号差值:${seq_diff}"
fi

脚本写完后,给脚本加执行权限,然后把脚本加到crontab里,定时执行,比如每分钟执行一次:

# 给脚本加执行权限
chmod +x /dm8/monitor/delay_monitor.sh
# 编辑crontab
crontab -e
# 加入下面的内容,每分钟执行一次
* * * * * /dm8/monitor/delay_monitor.sh

这样就能定时监控从节点的延迟情况,及时发现问题。

3.3 监控方案的优缺点

这个自定义监控方案的优点是简单、灵活,不需要额外安装监控工具,基于达梦自带的系统视图就能实现,成本低,适合中小团队使用。缺点是功能比较基础,只能监控延迟情况,不能监控同步的其他问题,比如同步中断、归档日志丢失等,而且告警逻辑需要自己实现,需要一定的开发能力。

如果是大型团队,还可以用Prometheus+Grafana来做监控,把达梦的监控指标接入Prometheus,然后在Grafana上做可视化的仪表盘,这样就能更直观地看到延迟情况,还能设置更复杂的告警规则。

四、读写分离架构的应用场景与注意事项

4.1 应用场景

读写分离架构适合大部分中大型系统,尤其是读多写少的系统,比如电商平台、社交平台、内容管理系统、报表系统等。这些系统的读操作占比很高,一般能达到80%以上,用读写分离架构能大幅提升系统的承载能力和响应速度。

比如电商平台的商品列表查询、用户中心的信息查看、订单列表查询等都是读操作,占了系统操作的大部分,把这些操作路由到从节点,就能减轻主节点的压力,提升系统的整体性能。

4.2 技术优缺点

读写分离架构的优点很明显:第一,提升系统的承载能力,把读和写拆分到不同的节点,各节点的压力都减轻了,能支持更多的用户;第二,提升系统的可用性,主节点出问题的时候,从节点还能继续提供读服务,不会导致系统完全不可用;第三,提升系统的扩展性,需要提升读能力的时候,只需要增加从节点的数量就行,不需要改动主节点。

缺点也很明显:第一,增加了系统的复杂度,原来的单节点架构变成了多节点架构,需要配置同步、路由、监控等,增加了运维和开发的难度;第二,存在延迟问题,从节点的数据和主节点的数据不是完全实时的,需要处理延迟带来的业务问题;第三,增加了成本,多节点架构需要更多的服务器资源,增加了硬件成本。

4.3 注意事项

第一,要根据业务的实际情况选择同步模式,达梦数据库有同步模式和异步模式,同步模式是主节点写完数据后,等从节点同步完成再返回,这样延迟小,但会影响主节点的性能;异步模式是主节点写完数据后直接返回,从节点异步同步,这样性能好,但延迟大。如果业务对数据一致性要求高,比如金融系统、支付系统,就要用同步模式;如果业务对数据一致性要求不高,比如电商的商品列表、新闻资讯,就可以用异步模式。

第二,要处理延迟带来的业务问题,比如用户刚提交的订单,需要马上查看,这时候如果从节点还没同步到,就会出现找不到订单的情况。这时候可以在业务层面做处理,比如刚提交的订单先从主节点查询,过一段时间后再从从节点查询,或者在查询的时候加一个参数,指定从主节点查询。

第三,要做好监控和告警,延迟问题是读写分离架构的常见问题,必须要有一套完善的监控和告警方案,及时发现问题,及时处理,避免影响用户体验。

第四,要做好数据备份,多节点架构的备份比单节点复杂,要定期备份主节点的数据,避免数据丢失。

五、文章总结

读写分离架构是提升系统性能和承载能力的常用方案,达梦数据库的读写分离实现相对简单,通过配置主从节点的归档模式、同步参数,再加上应用层的多数据源配置,就能实现读写分离。但从节点延迟是读写分离架构的常见问题,需要通过监控方案及时发现和处理。

在实际使用中,要根据业务的实际情况选择合适的同步模式,处理延迟带来的业务问题,做好监控和备份,这样才能充分发挥读写分离架构的优势,提升系统的性能和可用性。