问题
接到一个需求,领导说我们这个发短信的业务因为用户标签需要查询数据库导致服务消费kafka数据过慢,你把这几个标签数据从mysql迁移到redis,数据量大概有三四千万做好统计一下导入数据的时间效率,和redis占用内存情况,做好发给我看一下。作为一个外包菜鸟开发,我以为做一些增删改查就可以了,没想到这么有难度的事情交给我。
只能寻找解决方案。
寻找解决方案
1,首先,找到的就是利用redis管道的特性执行以下命令,因为数据量有点大用sql查询效率太低,即使多线程,sql优化做好,也不行。因为每次查询100到1000条这样循环查询随着条数增加,大概500万时就已经很慢了。
mysql -h127.0.0.1 -uusername-p'password' -Ddatabase --skip-column-names --raw < mysql_to_reds.sql | redis-cli -h 127.0.0.1 -p 6379 --pipe
mysql_to_reds.sql这个文件中的内容代表对mysql的查询的一个转译让redis可以识别
SELECT CONCAT(
"*8\n",
'$', LENGTH(redis_cmd), '\n', redis_cmd, '\n',
'$', LENGTH(redis_key), '\n', redis_key, '\n',
'$', LENGTH(hkey0), '\n', hkey0, '\n',
'$', LENGTH(hval0), '\n', hval0, '\n',
'$', LENGTH(hkey1), '\n', hkey1, '\n',
'$', LENGTH(hval1), '\n', hval1, '\n',
'$', LENGTH(hkey2), '\n', hkey2, '\n',
'$', LENGTH(hval2), '\n', hval2, '\n'
)
FROM (
SELECT
'HMSET' AS redis_cmd,
concat('id' ,id) AS redis_key,
'username' AS hkey0, username AS hval0,
'password' AS hkey1, password AS hval1,
'roles' AS hkey2, roles AS hval2
FROM admin
) AS t
然后就可以愉快的迁移数据了,好像reids和mysql需要在同一服务器下。
2,即使不行也可以先用工具将mysql数据导出来生成文件然后处理一下,直接执行命令读取一下文件,也可以速度也不错。
否定该方案
当写好执行文件,向领导反馈是却收到了否定,因为通过命令不利于保存日志,以及通过命令方式,不利于操作,因为这是交由客户使用的,人家需要这个功能,你却给了一个命令,不是一个应用而且还是搜索得到的,一看就不靠谱,而且也不利于操作,完全没有价值,最好做个应用打个jar包启动一下自动迁移数据。并打印下导入过程的数据日志。便于监控。你来看一下做之前开发了一个导入的类似于这种,然后我看了一下,一个while循环每次从mysql查100条然后hset进redis中,我想这靠谱吗?这不到100万数据都不行了吧?我来个线程池开个多线程来个100个线程能搞吗?我。。。。。。这难搞啊,我只能领任务去想解决方案去了
开发中
1,首先单线程,每次100条导入,用个线程池,开个100个线程,redis导入就用hset。心想这样肯定不行,这个先放下,这个启动jar包就执行代码的方案,由于需要引用配置文件中的变量,如@value @Autowired等所以寻找到了一种解决方案实现ApplicationRunner接口
2,最终打成jar包测试,由于自己这边数据量太小,所以交给驻场运维测试,发现执行了一夜,只搞了八九百万数据,查看日志发现,当导入数据到500万时那每次循环执行需要两三分钟,这怎么用。。。。。。。
3,优化sql寻找优化方案后sql
select a.phone_no as phoneNo,
a.age as ages,
a.pre_day_county_id as dayCountyId,
a.pre_night_county_id as nightCountyId
from t_sms_phone_tag a
JOIN(select phone_no from t_sms_phone_tag limit #{limitStarts},#{page}) b
ON b.phone_no = a.phone_no
由于数据id是电话号码,并不是自动生成的有规律id所以寻找使用了上边这种优化方案。
4,测试还是不行,之比之前好了一点,一夜能导1000万了。这有什么用,
5,只想放弃,跟领导说这种方案不行,还是用之前命令的方案吧。
6,反正就要放弃了试试每次循环导入1万数据,也就是mysql查询时page设置为1万试试发现每次循环只是,多了一点点时间而已,10万试试,可以承受,20万,50万,100万发现情况好了起来,每次循环并没有多多少时间最大为每次循环为70秒,大概45分钟导入了3800万数据,可以用这个方案了
7,开发好,打完jar先备着,就先以这个方案,领导应该能接受。
8,再优化优化,因为是redis集群不能直接用代码中jedisCluster.pipline也就是基于管道命名所产生的API,所以寻找一下redis集群使用pipline的工具类,最终发现了一个
redis集群客户端JedisCluster优化 - 管道(pipeline)模式支持
使用这个工具类,再次使得效率提升提升到最大30秒左右大概一次100万大概15分钟就导入完成。
贴上代码希望对大家有帮助
配置就不写了,正常的mysql,redis集群,只是page参数根据数据量不同设置大小不同我这次设置的是100w
public class ApplicationToRedis implements ApplicationRunner {
private Logger logger = LoggerFactory.getLogger(ApplicationToRedis.class);
@Autowired
TagMapper tagMapper;
@Autowired
JedisCluster jedisCluster;
@Value("${page}")
Integer page;
@Value("${limitStart}")
Integer limit;
/** * value有3部分组成
* * 第一部分是age,
* * 第二部分pre_day_county_id,
* * 第三部分是pre_night_county_id,
* * 三部分之间以“;”来进行分割
*/
public static final String LAC_PHONE_TAG_HASH = "phone:tag:hash:";
public static final String SEMICOLON = ";";
@Override
public void run(ApplicationArguments args) throws Exception {
/*
* ---1---
* 删除程序
long start = System.currentTimeMillis();
//先删除之前redis中的数据
for (int i = 0; i < 80; i++){
jedisCluster.del(LAC_PHONE_TAG_HASH + i);
}
long end = System.currentTimeMillis();
logger.info("用时"+ (end - start) + "ms");*/
/*
* ---2---
* 查询redis数据数量程序
long start = System.currentTimeMillis();
long a = 0;
for (int i = 0; i < 80; i++){
long b = jedisCluster.hlen(LAC_PHONE_TAG_HASH + i);
logger.info(LAC_PHONE_TAG_HASH + i + "---:"+ b + "条");
a+=b;
}
logger.info("总数:" + a + "条");
long end = System.currentTimeMillis();
logger.info("用时"+ (end - start) + "ms");*/
/*
* ---3---
* 导入程序
*/
long start = System.currentTimeMillis();
//先删除之前redis中的数据
for (int i = 0; i < 80; i++){
jedisCluster.del(LAC_PHONE_TAG_HASH + i);
}
//分页起点
int limitStart = 0;
if (limit != null && limit != 0){
limitStart = limit;
}
//分页查询容量
int limitEnd = 3;
if (page != null && page != 0){
limitEnd = page;
}
//循环控制
boolean flag = true;
int redisNum = 0;
int whileNum = 0;
JedisClusterPipeline jcp = JedisClusterPipeline.pipelined(jedisCluster);
jcp.refreshCluster();
while (flag){
long start1 = System.currentTimeMillis();
List<Tag> tag = tagMapper.getTag(limitStart, limitEnd);
if (ObjectUtils.isEmpty(tag) || null == tag){
flag = false;
break;
}
String key = "";
String mapValue = "";
for (Tag t : tag) {
//redis中的key
key = LAC_PHONE_TAG_HASH + phoneHashCode(String.valueOf(t.getPhoneNo()));
//phone所对应的value
mapValue = t.getAges() + SEMICOLON + t.getDayCountyId() + SEMICOLON + t.getNightCountyId();
jcp.hset(key,String.valueOf(t.getPhoneNo()),mapValue);
redisNum++;
}
jcp.sync();
jcp.close();
whileNum++;
long end1 = System.currentTimeMillis();
logger.info("导入中---limitStart:" + limitStart + ";循环次数为第" + whileNum + "次"+";循环用时" + (end1-start1));
limitStart = limitStart += limitEnd;
}
long end = System.currentTimeMillis();
logger.info("用时"+ (end - start) + "ms" +" 导入条数" + redisNum );
}
/**
* 计算hash
*/
public int phoneHashCode(String pho) {
int res = Math.abs(pho.hashCode()) % 80;
return res;
}
}
最后,不知道我开发的符合规范不,希望请大家指点一下代码中不适当之处,大家多交流指点一下。