Redis.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. <?php
  2. /**
  3. * Created by PhpStorm.
  4. * User: wangfanchang
  5. * Date: 18/1/10
  6. * Time: 下午2:13
  7. */
  8. namespace app\common\library;
  9. use think\Config;
  10. use Predis;
  11. use think\Exception;
  12. use think\exception\HttpException;
  13. use think\Log;
  14. class Redis
  15. {
  16. /**
  17. * 单例对象
  18. */
  19. protected static $instance;
  20. /**
  21. * redis 连接池
  22. * @var array
  23. */
  24. protected static $connections = [];
  25. /**
  26. * redis 区分链接的规则
  27. * @var array
  28. */
  29. protected static $rules = null;
  30. /**
  31. * redis 取模方式区分链接的规则
  32. * @var array
  33. */
  34. protected static $modKeyRules = null;
  35. /**
  36. * redis auto自增id连接
  37. * @var null|\Redis
  38. */
  39. protected static $auto = null;
  40. /**
  41. * 梦嘉redis连接_glory书库
  42. */
  43. protected static $glory = null;
  44. /**
  45. * redis $book 书籍信息
  46. * @var null|\Redis
  47. */
  48. protected static $book = null;
  49. /**
  50. * redis 书籍按160切片切分后生产的key=>val组
  51. * @var null
  52. */
  53. protected static $clientarr = null;
  54. /*
  55. * redis connect参数
  56. * @var null
  57. */
  58. protected static $connectParams = null;
  59. /**
  60. * 链接map 编号对应配置$connectParams
  61. * @var null
  62. */
  63. protected static $connectMap = null;
  64. /**
  65. * 初始化 连接池
  66. * @access public
  67. * @return \Redis
  68. */
  69. public static function instance()
  70. {
  71. if (is_null(self::$instance)) {
  72. self::$instance = new static();
  73. }
  74. return self::$instance;
  75. }
  76. /**
  77. * 初始化book链接redis地址的数组
  78. */
  79. public static function buildArr(){
  80. if (is_null(self::$clientarr)) {
  81. $bookArr = [];
  82. $config = Config::get('redis');
  83. $clientArr = explode(';', $config['changebook']);
  84. if ($config['changebooknew']) {
  85. $clientArrNew = explode(';', $config['changebooknew']);
  86. } else {
  87. $clientArrNew = null;
  88. }
  89. //Log::write(json_encode($clientArr),'cctest');
  90. for ($i = 0; $i < count($clientArr); $i++) {
  91. $cluster = $clientArr[$i];
  92. try {
  93. $numReps = 160;
  94. for ($m = 0; $m < $numReps / 4; $m++) {
  95. $digest = md5($cluster . '-' . $m, true);
  96. for ($h = 0; $h < 4; $h++) {
  97. $k = (ord($digest[3 + $h * 4]) & 0xFF) << 24
  98. | (ord($digest[2 + $h * 4]) & 0xFF) << 16
  99. | (ord($digest[1 + $h * 4]) & 0xFF) << 8
  100. | ord($digest[$h * 4]) & 0xFF;
  101. if ($clientArrNew) {
  102. $bookArr[$k] = $clientArrNew[$i];
  103. } else {
  104. $bookArr[$k] = $cluster;
  105. }
  106. }
  107. }
  108. } catch (Exception $e) {
  109. Log::error('生成redis连接对应关系数组失败');
  110. }
  111. }
  112. //Log::write(json_encode($bookArr),'cctest');
  113. ksort($bookArr);
  114. self::$clientarr = $bookArr;
  115. }
  116. return self::$clientarr;
  117. }
  118. /**
  119. * 初始化 redis book 书籍信息
  120. *
  121. * @access public
  122. * @return \Redis
  123. */
  124. public static function instanceBookChange($ppval)
  125. {
  126. $buildArr = self::buildArr();
  127. $conInfo = '';
  128. foreach($buildArr as $key=>$val){
  129. if($key>$ppval){
  130. $conInfo = $val;
  131. break;
  132. }
  133. }
  134. //如果取不到,取第一条
  135. if(empty($conInfo)){
  136. $conInfo = reset($buildArr);
  137. }
  138. //Log::write('书籍链接的reids地址:'.$conInfo,'cctest');
  139. $con = explode(':',$conInfo);
  140. self::$glory = new \Redis();
  141. $config = Config::get('redis');
  142. //Log::info('RedisChange连接 ' . $con[0] . ':' . $con[1]);
  143. if ($config['pconnect']) {
  144. self::$glory->pconnect($con[0], $con[1], $config['timeout']);
  145. } else {
  146. self::$glory->connect($con[0], $con[1], $config['timeout']);
  147. }
  148. self::$glory->auth($config['password']);
  149. return self::$glory;
  150. }
  151. /**
  152. * 初始化 redis auto 用户id自增
  153. *
  154. * @access public
  155. * @return \Redis
  156. */
  157. public static function instanceAuto()
  158. {
  159. if(isset(self::$connectMap['auto'])){
  160. return self::getRedisConnect('auto');
  161. }else{
  162. $config = Config::get('redis');
  163. $con = explode(':', $config['auto']);
  164. if (count($con) < 3) {
  165. Log::error('用户自增id redis配置错误!');
  166. throw new HttpException(500, '请刷新页面重新访问!');
  167. }
  168. self::loadRedisConfig('auto',$con,$config['pconnect'],$config['timeout']);
  169. return self::getRedisConnect('auto');
  170. }
  171. }
  172. /**
  173. * 初始化 redis book 书籍信息
  174. *
  175. * @access public
  176. * @return \Redis
  177. */
  178. public static function instanceBook()
  179. {
  180. if (is_null(self::$book)) {
  181. $config = Config::get('redis');
  182. $list = explode(';', $config['book']);
  183. $arr = [];
  184. foreach ($list as $key => $con) {
  185. if ($con) {
  186. $c = explode(':', $con);
  187. if (count($c) >= 3) {
  188. $arr[] = $c;
  189. }
  190. }
  191. }
  192. unset($con);
  193. if (empty($arr)) {
  194. Log::error('书籍信息 redis配置错误!');
  195. throw new HttpException(500, '请刷新页面重新访问!');
  196. }
  197. shuffle($arr);
  198. $con = array_pop($arr);
  199. self::$book = new \Redis();
  200. Log::info('Redis连接 ' . $con[0] . ':' . $con[1]);
  201. if ($config['pconnect']) {
  202. self::$book->pconnect($con[0], $con[1], $config['timeout']);
  203. } else {
  204. self::$book->connect($con[0], $con[1], $config['timeout']);
  205. }
  206. if (count($con) >= 3 && $con[2]) {
  207. self::$book->auth($con[2]);
  208. }
  209. if (count($con) >= 4 && is_numeric($con[3])) {
  210. self::$book->select($con[3]);
  211. }
  212. }
  213. return self::$book;
  214. }
  215. /**
  216. * 初始化 连接池 并 返回第一个连接,用于 easywechat&redis lock 使用
  217. * @access public
  218. * @return \Redis
  219. */
  220. public static function instanceCache()
  221. {
  222. if (is_null(self::$instance)) {
  223. self::$instance = new static();
  224. }
  225. return self::getRedisConnect(0);
  226. }
  227. /**
  228. * 构造函数 redis连接池 长连接方式
  229. * @throws
  230. */
  231. public function __construct()
  232. {
  233. $config = Config::get('redis');
  234. $list = explode(';', $config['list']); //共n个redis
  235. foreach ($list as $item) {
  236. $con = explode('=', $item); // 0 编号 1 配置
  237. if (count($con) >= 2) {
  238. $c = explode(':', $con[1]); //配置 0IP 1端口 2密码 3库编号
  239. self::loadRedisConfig($con[0],$c,$config['pconnect'],$config['timeout']);
  240. }
  241. }
  242. $modKeyList = explode(';', $config['modkeylist']);
  243. foreach ($modKeyList as $item) {
  244. $con = explode('=', $item); // 0 编号 1 配置
  245. if (count($con) >= 2) {
  246. $c = explode(':', $con[1]); //配置 0IP 1端口 2密码 3库编号
  247. self::loadRedisConfig($con[0], $c, $config['pconnect'], $config['timeout']);
  248. }
  249. }
  250. if(count(self::$connectParams) == 0){
  251. throw new \Exception('redis config error!');
  252. }
  253. }
  254. /**
  255. * 按规则拆分将key归属到响应的redis编号上
  256. * @param array $keys
  257. * @return array 返回数组的key为redis的编号,返回数组的value为需要在当前编号redis上操作的key的数组
  258. */
  259. public static function splitKeysByRule(array $keys)
  260. {
  261. $redisIndex = [];
  262. foreach ($keys as $key) {
  263. $redisIdx = self::getRedisIndex($key);
  264. $redisIndex[$redisIdx][] = $key;
  265. }
  266. return $redisIndex;
  267. }
  268. private static function buildRedisRules()
  269. {
  270. if (is_null(self::$rules)) {
  271. $rules = Config::get('redis.rules');
  272. $list = explode(';', $rules);
  273. foreach ($list as $item) {
  274. $con = explode('=', $item); // 0 编号 1 配置
  275. if (count($con) >= 2) {
  276. $c = explode(',', $con[1]); //规则
  277. foreach ($c as $it) {
  278. if ($it) {
  279. self::$rules[$con[0]][] = $it;
  280. }
  281. }
  282. }
  283. }
  284. }
  285. }
  286. /**
  287. * 根据key获取redis的编号
  288. * @param $key
  289. * @return int|mixed|string
  290. */
  291. public static function getRedisIndex($key)
  292. {
  293. self::buildRedisRules();
  294. #region 从rule中匹配对应的redis编号
  295. if ($key && count(self::$rules)) {
  296. foreach (self::$rules as $idx => $item) {
  297. foreach ($item as $v) {
  298. if (strpos($key, $v) === 0) { //查找到,且必须为第一个0
  299. return $idx;
  300. }
  301. }
  302. }
  303. }
  304. #endregion
  305. #region 构造modkey规则
  306. if (is_null(self::$modKeyRules)) {
  307. $modKeyRules = Config::get('redis.modkeyrules');
  308. self::$modKeyRules = explode(',', $modKeyRules);
  309. }
  310. #endregion
  311. #region 从modkeyrule中匹配对应的redis编号
  312. if ($key && count(self::$modKeyRules)) {
  313. foreach (self::$modKeyRules as $modKeyRule) {
  314. if (strlen($modKeyRule) > 0 && strpos($key, $modKeyRule) === 0) {
  315. $modKeyListStr = Config::get('redis.modkeylist');
  316. $modKeyList = explode(';', $modKeyListStr);
  317. $modKeyCount = count($modKeyList);
  318. $modNumber = hash_code($key) % $modKeyCount;
  319. $modNumber = abs($modNumber);
  320. $modKeyServer = $modKeyList[$modNumber];
  321. $aModKeyServer = explode('=', $modKeyServer);// 0 编号 1 配置
  322. return $aModKeyServer[0];
  323. }
  324. }
  325. }
  326. #endregion
  327. #region 没有找到符合的规则,使用默认redis服务器
  328. $default = Config::get('redis.default');
  329. if (isset(self::$connectMap[$default])) {
  330. return $default;
  331. } else {
  332. return 0;
  333. }
  334. #endregion
  335. }
  336. /**
  337. * 根据key区分redis连接
  338. * @param string $key
  339. * @return [int,\Redis]
  340. */
  341. private static function getRedis($key = '')
  342. {
  343. $idx = self::getRedisIndex($key);
  344. return [$idx, self::getRedisConnect($idx)];
  345. }
  346. /**
  347. * 载入Redis配置
  348. * @param $index
  349. * @param $config
  350. * @param $isPconnect
  351. * @param $timeout
  352. */
  353. private static function loadRedisConfig($index,$config,$isPconnect = false,$timeout = 2){
  354. if (count($config) >= 2) {
  355. //设置链接Map
  356. $val = $config[0].':'.$config[1];
  357. if (count($config) >= 4 && is_numeric($config[3])) {
  358. $val = $val . ':' . $config[3];
  359. }
  360. self::$connectMap[$index] = $val;
  361. //是否是长连接
  362. self::$connectParams[$val]['isLongConnection'] = $isPconnect;
  363. //IP地址
  364. self::$connectParams[$val]['ip'] = $config[0];
  365. //端口
  366. self::$connectParams[$val]['port'] = $config[1];
  367. //超时时间
  368. self::$connectParams[$val]['timeout'] = $timeout;
  369. //是否进行权限认证
  370. if (count($config) >= 3 && $config[2]) {
  371. self::$connectParams[$val]['auth'] = $config[2];
  372. }else{
  373. self::$connectParams[$val]['auth'] = false;
  374. }
  375. //选择库
  376. if (count($config) >= 4 && is_numeric($config[3])) {
  377. self::$connectParams[$val]['db'] = $config[3];
  378. }else{
  379. self::$connectParams[$val]['db'] = false;
  380. }
  381. }
  382. }
  383. /**
  384. * 获取Redis链接
  385. * @param $index
  386. * @param bool $isNo 是否是根据编号获取
  387. * @return mixed|\Redis
  388. */
  389. public static function getRedisConnect($index, $isNo = true)
  390. {
  391. //根据编号获取对应的IP和端口
  392. $mapKey = $index;
  393. if($isNo){
  394. if(!isset(self::$connectMap[$index])){
  395. Log::info("Redis Connect Error: Get Redis No {$index} Map Fail");
  396. return null;
  397. }else{
  398. $mapKey = self::$connectMap[$index];
  399. }
  400. }
  401. //检查链接是否已存在
  402. if(isset(self::$connections[$mapKey])){
  403. return self::$connections[$mapKey];
  404. }
  405. //不存在时创建链接
  406. if(!$redisConfig = self::$connectParams[$mapKey]){
  407. Log::info("Redis Connect Error: Get Redis MapKey {$mapKey} Params Fail");
  408. return null;
  409. }
  410. try{
  411. $redis = new \Redis();
  412. //判断长连短连
  413. if($redisConfig['isLongConnection']){
  414. $redis->pconnect($redisConfig['ip'],$redisConfig['port'],$redisConfig['timeout']);
  415. }else{
  416. $redis->connect($redisConfig['ip'],$redisConfig['port'],$redisConfig['timeout']);
  417. }
  418. //校验
  419. if($redisConfig['auth']){
  420. $redis->auth($redisConfig['auth']);
  421. }
  422. //库
  423. if($redisConfig['db']){
  424. $redis->select($redisConfig['db']);
  425. }
  426. self::$connections[$mapKey] = $redis;
  427. Log::info("Redis Connect Info: Ip:{$redisConfig['ip']} Port:{$redisConfig['port']} No:{$index}");
  428. $clients = $redis->info();
  429. if (!is_array($clients) || $clients['connected_clients'] > 3000 || $clients['instantaneous_ops_per_sec'] > 100000) {
  430. Log::error("redis 连接数超出最大限制!client:{$clients['connected_clients']} ops:{$clients['instantaneous_ops_per_sec']}");
  431. $file = sys_get_temp_dir() . "/cps_redis_error";
  432. @file_put_contents($file, time() + 2);
  433. http_response_code(502);
  434. header("Content-Type: text/html;charset=utf-8");
  435. echo '<meta charset="utf-8">';
  436. echo '<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">';
  437. echo '<title>502 Sorry, HTTP ERROR~</title>';
  438. echo '502 Sorry, HTTP ERROR~';
  439. echo "<br><br><br>";
  440. echo '<a href="javascript:window.location.reload();">点我重试一次</a>';
  441. die();
  442. }
  443. return self::$connections[$mapKey];
  444. }catch (\Exception $e){
  445. Log::error("Redis Connect Error: Ip:{$redisConfig['ip']} Port:{$redisConfig['port']} No:{$index} Message:{$e->getMessage()}");
  446. http_response_code(502);
  447. header("Content-Type: text/html;charset=utf-8");
  448. echo '<meta charset="utf-8">';
  449. echo '<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">';
  450. echo '<title>502 Sorry, HTTP ERROR~</title>';
  451. echo '502 Sorry, HTTP ERROR~';
  452. echo "<br><br><br>";
  453. echo '<a href="javascript:window.location.reload();">点我重试一次</a>';
  454. die();
  455. }
  456. }
  457. /**
  458. * 批量删除正则keys
  459. *
  460. * @param string $pattern
  461. * @throws \Exception
  462. */
  463. public static function delScan($pattern)
  464. {
  465. set_time_limit(0);
  466. self::instance();
  467. $key = substr($pattern, 0, strpos($pattern, ':') + 1);
  468. self::buildRedisRules();
  469. $matchIdxs = [];
  470. #region 从rule规则中匹配
  471. if ($key && count(self::$rules)) {
  472. foreach (self::$rules as $idx => $item) {
  473. foreach ($item as $v) {
  474. if (strpos($v, $key) === 0) {
  475. $matchIdxs[] = $idx;
  476. break;
  477. }
  478. }
  479. }
  480. }
  481. #endregion
  482. #region 构造modkey规则
  483. if (is_null(self::$modKeyRules)) {
  484. $modKeyRules = Config::get('redis.modkeyrules');
  485. self::$modKeyRules = explode(',', $modKeyRules);
  486. }
  487. #endregion
  488. #region 从modkey中匹配规则,如果匹配成功,那么所有modlist服务器做一次删除
  489. if ($key && count(self::$modKeyRules)) {
  490. foreach (self::$modKeyRules as $modKeyRule) {
  491. if (strlen($modKeyRule) > 0 && strpos($modKeyRule, $key) === 0) {
  492. $modKeyListStr = Config::get('redis.modkeylist');
  493. $modKeyList = explode(';', $modKeyListStr);
  494. foreach ($modKeyList as $modKeyServerParam) {
  495. $aModKeyServer = explode('=', $modKeyServerParam);
  496. $matchIdxs[] = $aModKeyServer[0];
  497. }
  498. break;
  499. }
  500. }
  501. }
  502. #endregion
  503. //默认redis服务器也需要做一次删除
  504. $matchIdxs[] = Config::get('redis.default');
  505. #region 匹配的服务器做删除操作
  506. foreach ($matchIdxs as $matchIdx) {
  507. $redis = self::getRedisConnect($matchIdx);
  508. $cursor = null;
  509. while ($cursor !== 0) {
  510. $keys = $redis->scan($cursor, $pattern, 500);
  511. if (is_array($keys) && count($keys)) {
  512. $redis->del($keys); //批量删除key
  513. }
  514. }
  515. }
  516. #endregion
  517. }
  518. // public function pfAdd($name,$val){
  519. // try{
  520. // if(!is_array($val)){
  521. // $val = [$val];
  522. // }
  523. // $redis = self::getRedis($name);
  524. // return $redis->pfAdd($name,$val);
  525. // }catch (\Exception $e){
  526. // Log::error("Redis Call Function Error: Name:pfAdd Key:{$name} Args:".var_export($val,true));
  527. // }
  528. // }
  529. function __call($name, $arguments)
  530. {
  531. try{
  532. if (count($arguments)) {
  533. list($idx,$redis) = self::getRedis($arguments[0]); //参数1不一定就是key
  534. } else {
  535. list($idx,$redis) = self::getRedis();
  536. }
  537. if (strtolower($name) == 'pfadd' && !is_array($arguments[1])) {
  538. $arguments[1] = [$arguments[1]];
  539. }
  540. if (strtolower($name) == 'delete') {
  541. $name = 'del';
  542. }
  543. // $t = microtime(true);
  544. $ret = call_user_func_array([$redis, $name], $arguments);
  545. // Log::info("Redis Call Function Idx:{$idx} Name:{$name} Args:" . json_encode($arguments,
  546. // JSON_UNESCAPED_UNICODE) . " Time:" . number_format(microtime(true) - $t, 6));
  547. return $ret;
  548. }catch (\Exception $e){
  549. Log::error("Redis Call Function Error: Name:{$name} Args:" . json_encode($arguments,
  550. true) . " Msg:" . $e->getMessage());
  551. }
  552. }
  553. }