背景:
公司某业务模块的用户通讯记录和通话记录的表,单表记录过亿了,mysql很坚挺好不好!!!!
其中某表是205773235 约2.0亿
另外某表是575213155 约5.7亿
用户数量 约100W
其中通讯录有的用户是有很多重复记录的,之前设计数据库的同事按条来存的,也就是说一个用户会有多条记录,还有重复的记录(没有去重),历史遗留问题那就不废话了,我们存在的意义就是把不合理的设计去规范化来
如: id:1 uid:10086 name: 马云爸爸 num:88888
id:2 uid:10086 name: 马化腾爸爸 num:99999
这样的数据表设计在数据量少的时候是没有问题的,不去重这点会占用额外的空间,其实这些不太常用而且有很多一对多关系的数据可以存json或者直接用OSS来存的(ps我们技术顾问推荐的存储方式)
大概思路:
1.是把100W的用户拆分成10份,每次循环10W个用户的数据上传
2.打通oss接口,文件保存按照日期+小时这样子保存文件
3.存储方式json文件方式保存
4.数据安全方面,用动态密匙加密json,动态的密匙需要按规律保存好(因为文件要开放公共读的,处于对用户数据的安全考虑得加密)
5.上传成功返回url保存好
异常处理:
1.文件上传必然会有上传失败的,每个文件循环上传,如果上传成功,则跳出循环,否则程序休息0.5秒继续上传,最大限度循环10次,如果还是上传失败,把这个用户的uid push到redis的链表里面
2.文件上传完成后保存上传路径,同理保存也有可能保存失败,也是上面的套路,做一个循环,如果保存一次成功了就跳出循环,否则信息0.5秒继续发起保存sql,成功推出循环,失败10次保存uid到redis上面
3.每10分钟检查一次redis里面的上传失败或者保存用户数据失败的用户,如果超过500条则继续上传,这里的套路和1.2点同理,如果还是失败,到这里还失败的,其实用户已经失败了20次了,那么应该属于重灾区,把这些用户的uid保存到数据库里面
4.1.2.3步骤都有和文件日志结合的,我的目标是不要有问题,少有问题!
最终实践:
1.其实我没有直接拿用户表的uid,因为有小部分的用户是没有数据的,我创建了一张表,专门来保存符合调教的用户的uid,导入适合的数据的命令如花:
INSERT INTO 某临时表 (uid)
select * from (SELECT ct.uid FROM (select max(id) as id from 某符合条件的表啦 group by uid ) pct join 某符合条件的表啦 ct on ct.id=pct.id ) as xxx
2.一步步找到之前的保存数据的方式改成现在的模式
3.各种测试方法的可行性
具体迁移方法:
/**
* 通话记录迁移 written : yangxingyi date: 2017-09-11
*/
public function send_phone()
{
ini_set('memory_limit','10240M');//限制内存10GB
$start = trim(I('start'));
$end = trim(I('end'));//2017-09-18 15:47 区间1000-100000
($start>$end)&&exit("输入范围有误!");
(!$start||!$end)&&exit("没有输入条件!");
$uids = 某用户表模型->where("id between $start and $end")->field('uid')->select();
$uids||exit('没有符合条件的数据!');
file_log('send_phone','正常数据的开始时间','phone');
$oss = new OssCore();
$que = 实例化redis;
$t1 = $t2 = time();//记录耗时
$bucket = self::$contact_bucket; // 存储空间
$result = $save_res ='';
$success_count = $false_count = 0;//记录成功失败条数
$total_count = count($uids);//记录总条数
while(list($index,$item)=each($uids))
{
$uid = $item['uid'];
$exist_oss_path = 某url表->where(array('uid' => $uid))
->order("id desc")->field('oss_path')->find();
if($exist_oss_path['oss_path']){
file_log('send_phone','已上传通讯录的uid: '.$uid,'phone');
continue;//跳过已经上传过内容的用户
}
$info = 某通话记录表->where(array('uid' => $uid))->field('name,number')->select();
if(!$info||!is_array($info)){
file_log('send_phone','没有通讯录的uid: '.$uid,'phone');
continue;//跳过没有内容的用户
}
$path = 'phone/' . date("Ymd") . '/' . date('H') . '/' . $uid;
$content = 某加密函数(json_encode(make_array_uinque($info,'name','number')),self::$contact_key_prefix.某随机字符串);
//组装数据,并且根据name,number字段去重
for($i=0;$i<10;$i++){
$result = $oss->uploadToOss($path, $content,$bucket );
//uploadToOss(路径,内容,$bucket="存储空间")
if($result){
break;
}else{
usleep(500);file_log('send_phone','上传oss失败uid: '.$uid." 次数".$i,'phone');
}
}
//echo $result['info']['url']."
";oss返回的url
for($i=0;$i<10;$i++){
$save_res = $this->save_oss_path('',$result['info']['url'],$uid);//id是pk(这里没pk),传第二,第三个参数
if($save_res){
break;
}else{
usleep(500);file_log('send_phone','修改数据库失败uid: '.$uid." 次数".$i,'phone');
}
}
if(($index>0&&$index%1000)==0){
$t1000 = time()- $t1;
file_log('send_phone','千条耗时(秒): '.$t1000,'phone');
$t1 = time();
}
if(($index>0&&$index%10000)==0){
$t10000 = time()- $t2;
file_log('send_phone','万条耗时(秒): '.$t10000,'phone');
$t2 = time();
}
//异常处理
if (!$result||!$save_res)
{
file_log('send_phone','上传失败的uid: '.$uid,'phone');
$que->lpush('send_phone_ids_error', $uid);
$false_count++;
}else{
$que->incr('send_phone');//ok的数目
$success_count++;//成功条数
}//异常处理完毕
unset($uids[$index]);//悉放内存
}
$tt = time()-$t1;
file_log('send_phone','结束时间,已上传总条数:'.$que->get('send_phone').
",本次总条数:$total_count,成功:$success_count ,失败:$false_count 消耗时间(秒):$tt,id区间$start-$end",'phone');
}
异常处理函数,每10分钟执行一次:
/**
* 保存数据和异常处理 written : yangxingyi date: 2017-09-12 11:45 Email: [email protected]
*/
public function handle_exception(){
$oss = new OssCore();
$que = 实例化redis;
$bucket = self::$contact_bucket; // 存储空间
$list_count = $que->llen('send_phone_ids_error');
$time = time();
if ($list_count > 500){
$error_ids = $que->lrange('send_phone_ids_error', 0, -1);//返回来的一维数组
$que->del('send_phone_ids_error');//返回来数据就干掉它
file_log('handle_exception','大于500条异常数据的开始时间','phone');
$result = $save_res = '';
foreach($error_ids as $item)
{
$uid = $item;
$info = 某源数据表->where(array('uid' => $uid))->field('name,number')->select();
if(!$info||!is_array($info)){
continue;//跳过没有内容的用户
}
$path = 'phone/' . date("Ymd") . '/' . date('H') . '/' . $uid;
$content = 加密函数(json_encode(make_array_uinque($info,'name','number')),self::$contact_key_prefix.什么鬼字符串);//组装数据
for($i=0;$i<10;$i++){
$result = $oss->uploadToOss($path, $content,$bucket );//uploadToOss(路径,内容,$bucket="存储空间")
if($result){
break;
}else{
sleep(1);
}
}
for($i=0;$i<10;$i++){
$save_res = $this->save_oss_path('',$result['info']['url'],$uid);
//id是主键索引(这没有),传递两个参数,需要传第三个参数uid
if($save_res){
break;
}else{
sleep(1);
}
}
//第二次的异常处理
if ($result&&$save_res)
{
$que->incr('send_phone');
}else{
file_log('handle_exception','第二次的异常处理uid: '.$uid,'phone');
保存错误的uid表->add(array('uid'=>$uid,'time'=>$time));
}
}
}
}
二维数组去重的方法:
function make_array_uinque($arr,$type1='username',$type2='uid')
{
$arr_out = $arr_wish = [];
foreach($arr as $k => $v)
{
$key_out = $v["$type1"]."-".$v["$type2"];//提取内部一维数组的key(任意值)作为外部数组的键
if(array_key_exists($key_out,$arr_out)){
continue;
}else{
$arr_out[$key_out] = $v; //以key_out作为外部数组的键
$arr_wish[$k] = $v; //实现二维数组唯一性
}
}
return $arr_wish;
}
如果您读到了这里,谢谢各位看官,看完这么枯燥的文章!!!方法有不足的地方,欢迎各位大神指教!!!如果觉得对您有用的话欢迎转载,如果您有什么好文章也欢迎跟我分享!!!小弟不胜感激!!!!