背景
公司合规要求,将Redis定位为缓存加速,不再允许直接将Redis作为数据库使用;经过简单的技术评估,决定将Redis 切换为TiDB,因此需要将Redis存量数据迁移到TiDB中。
通过DBA同事了解到暂无合适的工具后(若公司环境有现成迁移工具,建议使用工具),我们决定自行开发一个接口上线,通过手工触发的形式实现数据的迁移。
实战
环境:
| 内容 | 版本 | 备注 |
|---|---|---|
| java | java8 | |
| redis | 5.0.3 | 集群模式 |
| jedis | 3.10.0 | |
| springboot | 2.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);
}
}
}
其他
- 以上代码只是最基本的实现,如果需要断点续传等"高级"功能,还需自行完善。
- scan命令对于Redis来说比较友好,性能消耗不大,但具体影响请自行评估。
- 迁移速度很快(实测测试环境44分钟,扫描了5068w个key,同时经过过滤后插入了126w条数据),建议迁移期间中断Redis的写入,避开额外的处理。
- Redis的访问速度优势明显,迁移后单次访问db的时长会大幅增加,也需进行性能评估与测试。
- 本次还有个额外的经验,不能过分相信AI,上述代码在和AI大战多轮都未能完全跑通,AI主要的问题集中在循环集群各个节点的部分,最后学习了一些历史代码才解决。

719

被折叠的 条评论
为什么被折叠?



