高併發簡單解決方案————redis佇列快取+mysql批量入庫(ThinkPhP)
原始碼地址:https://github.com/Tinywan/PHP_Experience
問題分析
- 問題一:要求日誌最好入庫;但是,直接入庫mysql確實扛不住,批量入庫沒有問題,done。【批量入庫和直接入庫效能差異】
- 問題二:批量入庫就需要有高併發的訊息佇列,決定採用redis list 模擬實現,而且方便回滾。
- 問題三:日誌量畢竟大,儲存最近30條足矣,決定用php寫個離線統計和清理指令碼。
一、設計資料庫表和儲存
- 考慮到log系統對資料庫的效能更多一些,穩定性和安全性沒有那麼高,
儲存引擎自然是隻支援select insert 沒有索引的archive
。如果確實有update需求,也可以採用myISAM。 - 考慮到log是實時記錄的所有資料,數量可能巨大,
主鍵採用bigint,自增即可
。 - 考慮到log系統
以寫為主,統計採用離線計算,欄位均不要出現索引
,因為一方面可能會影響插入資料效率,另外讀時候會造成死鎖,影響寫資料。
二、redis儲存資料形成訊息佇列
/** * 使用佇列生成reids測試資料 * 成功:執行 RPUSH操作後,返回列表的長度:8 */ public function createRedisList($listKey = `message01`) { $redis = RedisInstance::MasterInstance(); $redis->select(1); $message = [ `type` => `say`, `userId` => $redis->incr(`user_id`), `userName` => `Tinywan` . mt_rand(100, 9999), //是否正在錄影 `userImage` => `/res/pub/user-default-w.png`, //是否正在錄影 `openId` => `openId` . mt_rand(100000, 9999999999999999), `roomId` => `openId` . mt_rand(30, 50), `createTime` => date(`Y-m-d H:i:s`, time()), `content` => $redis->incr(`content`) //當前是否正在打流狀態 ]; $rPushResul = $redis->rPush($listKey, json_encode($message)); //執行成功後返回當前列表的長度 9 return $rPushResul; }
三、讀取redis訊息佇列裡面的資料,批量入庫
第一種思路:
/** * 訊息Redis方法儲存到Mysql資料庫 * @param string $liveKey */ public function RedisSaveToMysql($listKey = `message01`) { if (empty($listKey)) { $result = ["errcode" => 500, "errmsg" => "this parameter is empty!"]; exit(json_encode($result)); } $redis = RedisInstance::MasterInstance(); $redis->select(1); $redisInfo = $redis->lRange($listKey, 0, 5); $dataLength = $redis->lLen($listKey); $model = M("User"); while ($dataLength > 65970) { try { $model->startTrans(); $redis->watch($listKey); $arrList = []; foreach ($redisInfo as $key => $val) { $arrList[] = array( `username` => json_decode($val, true)[`userName`], `logintime` => json_decode($val, true)[`createTime`], `description` => json_decode($val, true)[`content`], `pido` => json_decode($val, true)[`content`] ); } $insertResult = $model->addAll($arrList); if (!$insertResult) { $model->rollback(); $result = array("errcode" => 500, "errmsg" => "Data Insert into Fail!", `data` => `dataLength:` . $dataLength); exit(json_encode($result)); } $model->commit(); $redis->lTrim($listKey, 6, -1); $redisInfo = $redis->lRange($listKey, 0, 5); $dataLength = $redis->lLen($listKey); } catch (Exception $e) { $model->rollback(); $result = array("errcode" => 500, "errmsg" => "Data Insert into Fail!"); exit(json_encode($result)); } } $result = array("errcode" => 200, "errmsg" => "Data Insert into Success!", `data` => `dataLength:` . $dataLength . `liveKey:` . $listKey); exit(json_encode($result)); }
第二種思路(供參考,非框架)
<?php $redis_xx = new Redis(); $redis_xx->connect(`ip`, port); $redis_xx->auth("password"); // 獲取現有訊息佇列的長度 $count = 0; $max = $redis_xx->lLen("call_log"); // 獲取訊息佇列的內容,拼接sql $insert_sql = "insert into fb_call_log (`interface_name`, `createtime`) values "; // 回滾陣列 $roll_back_arr = array(); while ($count < $max) { $log_info = $redis_cq01->lPop("call_log"); $roll_back_arr = $log_info; if ($log_info == `nil` || !isset($log_info)) { $insert_sql .= ";"; break; } // 切割出時間和info $log_info_arr = explode("%", $log_info); $insert_sql .= " (`" . $log_info_arr[0] . "`,`" . $log_info_arr[1] . "`),"; $count++; } // 判定存在資料,批量入庫 if ($count != 0) { $link_2004 = mysql_connect(`ip:port`, `user`, `password`); if (!$link_2004) { die("Could not connect:" . mysql_error()); } $crowd_db = mysql_select_db(`fb_log`, $link_2004); $insert_sql = rtrim($insert_sql, ",") . ";"; $res = mysql_query($insert_sql); // 輸出入庫log和入庫結果; echo date("Y-m-d H:i:s") . "insert " . $count . " log info result:"; echo json_encode($res); echo "</br> "; // 資料庫插入失敗回滾 if (!$res) { foreach ($roll_back_arr as $k) { $redis_xx->rPush("call_log", $k); } } // 釋放連線 mysql_free_result($res); mysql_close($link_2004); } $redis_cq01->close(); ?>
四、獲取Redis資料快取資料
/** * [0]檢查當前Redis是否連線成功 * [1]獲取資料,首先從Redis中去獲取,沒有的話再從資料庫中去獲取 */ public function findDataRedisOrMysql($listKey = `message01`) { //Check the current connection status 檢視服務是否執行 if (RedisInstance::MasterInstance() != false) { $redis = RedisInstance::MasterInstance(); $redis->select(2); /** * 首先從Redis中去獲取資料 * lRange 獲取為空的話,則表示沒有資料,否則返回一個非空陣列 */ $redisData = $redis->lRange($listKey, 0, 9); $resultData = []; if (!empty($redisData)) { $resultData[`status_code`] = 200; $resultData[`msg`] = `Data Source from Redis Cache`; foreach ($redisData as $key => $val) { $resultData[`listData`][] = json_decode($val, true); } } else { $resultData[`redis_msg`] = `Redis is Expire`; $conditions = array(`status` => `:status`); $mysqlData = M(`User`)->where($conditions)->bind(`:status`, 1, PDO::PARAM_STR)->select(); if ($mysqlData) { $resultData[`status_code`] = 200; $resultData[`mysql_msg`] = `Data Source from Mysql is Success`; $redis->select(2); foreach ($mysqlData as $key => $val) { $resultData[`listData`][] = $val; //寫入Redis作為快取 $redis->rPush($listKey, json_encode($val)); } //同時設定一個過期時間 $redis->expire($listKey,30); } else { $resultData[`status_code`] = 500; $resultData[`mysql_msg`] = `Data Source from Mysql is Fail`; } } } else { $resultData[`redis_msg`] = `Redis server went away`; $resultData[`mysql_msg`] = `Mysql Data2`; $conditions = array(`status` => `:status`); $mysqlData = M(`User`)->where($conditions)->bind(`:status`, 1, PDO::PARAM_STR)->select(); foreach ($mysqlData as $key => $val) { $resultData[`listData`][] = $val; } } homePrint($resultData); }
四、離線天級統計和清理資料指令碼
<?php /** * static log :每天離線統計程式碼日誌和刪除五天前的日誌 * */ // 離線統計 $link_2004 = mysql_connect(`ip:port`, `user`, `pwd`); if (!$link_2004) { die("Could not connect:" . mysql_error()); } $crowd_db = mysql_select_db(`fb_log`, $link_2004); // 統計昨天的資料 $day_time = date("Y-m-d", time() - 60 * 60 * 24 * 1); $static_sql = "get sql"; $res = mysql_query($static_sql, $link_2004); // 獲取結果入庫略 // 清理15天之前的資料 $before_15_day = date("Y-m-d", time() - 60 * 60 * 24 * 15); $delete_sql = "delete from xxx where createtime < `" . $before_15_day . "`"; try { $res = mysql_query($delete_sql); }catch(Exception $e){ echo json_encode($e)." "; echo "delete result:".json_encode($res)." "; } mysql_close($link_2004); ?>
五:程式碼部署
主要是部署,批量入庫指令碼的呼叫和天級統計指令碼,crontab例行執行。
# 批量入庫指令碼 */2 * * * * /home/cuihuan/xxx/lamp/php5/bin/php /home/cuihuan/xxx/batchLog.php >>/home/cuihuan/xxx/batchlog.log # 天級統計指令碼 0 5 * * * /home/cuihuan/xxx/php5/bin/php /home/cuihuan/xxx/staticLog.php >>/home/cuihuan/xxx/staticLog.log
總結:相對於其他複雜的方式處理高併發,這個解決方案簡單有效:通過redis快取抗壓,mysql批量入庫解決資料庫瓶頸,離線計算解決統計資料,通過定期清理保證庫的大小。
相關文章
- Redis 快取穿透,擊穿解決方案,模擬高併發請求,附程式碼Redis快取穿透
- 高併發架構系列:Redis快取和MySQL資料一致性方案詳解架構Redis快取MySql
- Redis 快取穿透、快取雪崩原理及解決方案Redis快取穿透
- mysql 高併發 select update 併發更新問題解決方案MySql
- 【Redis】快取穿透,快取擊穿,快取雪崩及解決方案Redis快取穿透
- REDIS快取穿透,快取擊穿,快取雪崩原因+解決方案Redis快取穿透
- 圖解--佇列、併發佇列圖解佇列
- PHP高併發 商品秒殺 問題的 2大種(MySQL or Redis) 解決方案PHPMySqlRedis
- Redis快取穿透、快取雪崩、redis併發問題分析Redis快取穿透
- 快取問題(四) 快取穿透、快取雪崩、快取併發 解決案例快取穿透
- MySQL 與 Redis 快取的同步方案MySqlRedis快取
- Redis快取的主要異常及解決方案Redis快取
- redis快取相關問題及解決方案Redis快取
- 併發程式設計與高併發解決方案學習(CPU多級快取-亂序執行優化)程式設計快取優化
- redis訊息佇列簡單應用Redis佇列
- [分散式][高併發]訊息佇列的使用場景、概念、常見問題及解決方案分散式佇列
- 高併發解決方案詳解(9大常見解決方案)
- 高併發和大流量解決方案
- 高併發解決方案orleans實踐
- PHP利用Mysql鎖解決高併發PHPMySql
- Redis快取穿透解決方案--布隆過濾器Redis快取穿透過濾器
- Redis系列 - 快取雪崩、擊穿、穿透及解決方案Redis快取穿透
- Redis快取穿透/快取雪崩/快取擊穿(案例:產生的原因 解決方案利/弊)Redis快取穿透
- 高併發大容量NoSQL解決方案探索SQL
- 解讀 Java 併發佇列 BlockingQueueJava佇列BloC
- Redis 快取擊穿、穿透、雪崩的原因以及解決方案Redis快取穿透
- 優雅的快取解決方案--SpringCache和Redis整合(SpringBoot)快取GCRedisSpring Boot
- Java高併發快取架構,快取雪崩、快取穿透之謎Java快取架構穿透
- mysql查詢快取簡單使用MySql快取
- Spring Boot + Redis 快取方案深度解讀Spring BootRedis快取
- Redis快取資料庫-快速入門Redis快取資料庫
- 高併發下丟失更新的解決方案
- PHP高併發和大流量的解決方案PHP
- 海量資料和高併發的解決方案
- Redis 快取擊穿(失效)、快取穿透、快取雪崩怎麼解決?Redis快取穿透
- 快取穿透、快取擊穿、快取雪崩概念及解決方案快取穿透
- 微服務 - Redis快取 · 資料結構 · 持久化 · 分散式 · 高併發微服務Redis快取資料結構持久化分散式
- 『併發包入坑指北』之阻塞佇列佇列
- 快取穿透詳解及解決方案快取穿透