【实战】Redis集群数据迁移TiDB

背景

​ 公司合规要求,将Redis定位为缓存加速,不再允许直接将Redis作为数据库使用;经过简单的技术评估,决定将Redis 切换为TiDB,因此需要将Redis存量数据迁移到TiDB中。

​ 通过DBA同事了解到暂无合适的工具后(若公司环境有现成迁移工具,建议使用工具),我们决定自行开发一个接口上线,通过手工触发的形式实现数据的迁移。

实战

环境:

内容版本备注
javajava8
redis5.0.3集群模式
jedis3.10.0
springboot2.7.2

声明:以下代码为了合规,屏蔽了一些业务细节,可能无法直接运行,稍微调一下就OK。解说都放在代码注释中。

/**
 * redis相关处理
 */
@Slf4j
@Service
public class RedisServiceImpl implements RedisService {

    //这里我默认已经做好了redis的配置,毕竟之前服务肯定在用redis
    @Autowired
    @Qualifier("redisTemplate")
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 获取redis集群信息
     */
    @Override
    public JedisCluster getJedisCluster() {
        return (JedisCluster)redisTemplate.getConnectionFactory().getConnection().getNativeConnection();
    }

    /**
     * 通过redis获取到需要入库的dto
     */
    @Override
    public List<RedisMidDataDTO> getHashData(final List<String> keys, final Jedis jedis) {
        List<RedisMidDataDTO> result = new ArrayList<>(keys.size());
        // 使用pipeline批量获取,效率更高
        Pipeline pipeline = jedis.pipelined();
        Map<String, Response<Map<String, String>>> responses = new HashMap<>();
        for (String key : keys) {
            //我的key是hash格式,其他格式也可处理
            responses.put(key, pipeline.hgetAll(key));
        }
        // 执行 Pipeline
        pipeline.sync();
        // 转换结果
        for (Map.Entry<String, Response<Map<String, String>>> entry : responses.entrySet()) {
            Map<String, String> value = entry.getValue().get();
            //具体的处理逻辑,封装得到对应dto
            result.add(RedisMidDataDTO.builder().build());
        }
        return result;
    }
}

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.ScanParams;
import redis.clients.jedis.ScanResult;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 迁移的实现类,由controller调起
 */
@Slf4j
@Service
public class MigrateServiceImpl implements MigrateService {

    //扫描和写入的批次大小,1000是比较合理的
    private static final int SCAN_BATCH_SIZE = 1000;

    @Autowired
    RedisService redisService;

    @Autowired
    MidDataSupport midDataSupport;

    /**
     * 数据迁移实现入口函数
     * 可能不需要入参,根据自己的实际情况处理
     */
    @Override
    public void migrateMidData() {
        try {
            List<String> keys = new ArrayList<>();
            long totalCount = 0;
            //集群模式的数据,是无法直接开扫的,需要按节点分别进行扫描
            JedisCluster jedisCluster = redisService.getJedisCluster();
            Map<String, JedisPool> clusterNodes = jedisCluster.getClusterNodes();
            for (Map.Entry<String, JedisPool> entry : clusterNodes.entrySet()) {
                Jedis jedis = entry.getValue().getResource();
                // 检查是否是主节点,集群模式非主节点无需扫描
                if (!"master".equals(jedis.info("replication").split("\\r?\\n")[1].split(":")[1])) {
                    continue;
                }
                //cursor是redis自带的游标,为0则表示扫描完毕,细节无需深究
                String cursor = "0";
                do {
                    //遍历是可以模糊匹配的,此处是全扫
                    ScanResult<String> scanResult = jedis.scan(cursor, new ScanParams().count(SCAN_BATCH_SIZE));
                    keys.addAll(scanResult.getResult());
                    totalCount += scanResult.getResult().size();
                    log.info("总扫描计数:{}", totalCount);
                    List<RedisMidDataDTO> redisData = redisService.getHashData(keys, jedis);
                    processKeyBatch(redisData, type);
                    keys.clear();
                    cursor = scanResult.getCursor();
                } while (!"0".equals(cursor));
            }
        } catch (Exception e) {
            log.error("迁移数据失败", e);
        }
    }

    /**
     * 写入TiDB
     *
     * @param redisData
     */
    private void processKeyBatch(List<RedisMidDataDTO> redisData) {
        if (redisData.isEmpty()) {
            return;
        }
        try {
            // 批量写入TiDB,具体实现略,根据业务场景自行决定是insert还是upsert
            midDataSupport.batchUpsertRtMidData(redisData);
        } catch (Exception e) {
            log.error("TiDB写入失败", e);
        }
    }
}

其他

  1. 以上代码只是最基本的实现,如果需要断点续传等"高级"功能,还需自行完善。
  2. scan命令对于Redis来说比较友好,性能消耗不大,但具体影响请自行评估。
  3. 迁移速度很快(实测测试环境44分钟,扫描了5068w个key,同时经过过滤后插入了126w条数据),建议迁移期间中断Redis的写入,避开额外的处理。
  4. Redis的访问速度优势明显,迁移后单次访问db的时长会大幅增加,也需进行性能评估与测试。
  5. 本次还有个额外的经验,不能过分相信AI,上述代码在和AI大战多轮都未能完全跑通,AI主要的问题集中在循环集群各个节点的部分,最后学习了一些历史代码才解决。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值