123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611 |
- <?php
- /**
- * Created by PhpStorm.
- * User: wangfanchang
- * Date: 18/1/10
- * Time: 下午2:13
- */
- namespace app\common\library;
- use think\Config;
- use Predis;
- use think\Exception;
- use think\exception\HttpException;
- use think\Log;
- class Redis
- {
- /**
- * 单例对象
- */
- protected static $instance;
- /**
- * redis 连接池
- * @var array
- */
- protected static $connections = [];
- /**
- * redis 区分链接的规则
- * @var array
- */
- protected static $rules = null;
- /**
- * redis 取模方式区分链接的规则
- * @var array
- */
- protected static $modKeyRules = null;
- /**
- * redis auto自增id连接
- * @var null|\Redis
- */
- protected static $auto = null;
- /**
- * 梦嘉redis连接_glory书库
- */
- protected static $glory = null;
- /**
- * redis $book 书籍信息
- * @var null|\Redis
- */
- protected static $book = null;
- /**
- * redis 书籍按160切片切分后生产的key=>val组
- * @var null
- */
- protected static $clientarr = null;
- /*
- * redis connect参数
- * @var null
- */
- protected static $connectParams = null;
- /**
- * 链接map 编号对应配置$connectParams
- * @var null
- */
- protected static $connectMap = null;
- /**
- * 初始化 连接池
- * @access public
- * @return \Redis
- */
- public static function instance()
- {
- if (is_null(self::$instance)) {
- self::$instance = new static();
- }
- return self::$instance;
- }
- /**
- * 初始化book链接redis地址的数组
- */
- public static function buildArr(){
- if (is_null(self::$clientarr)) {
- $bookArr = [];
- $config = Config::get('redis');
- $clientArr = explode(';', $config['changebook']);
- if ($config['changebooknew']) {
- $clientArrNew = explode(';', $config['changebooknew']);
- } else {
- $clientArrNew = null;
- }
- //Log::write(json_encode($clientArr),'cctest');
- for ($i = 0; $i < count($clientArr); $i++) {
- $cluster = $clientArr[$i];
- try {
- $numReps = 160;
- for ($m = 0; $m < $numReps / 4; $m++) {
- $digest = md5($cluster . '-' . $m, true);
- for ($h = 0; $h < 4; $h++) {
- $k = (ord($digest[3 + $h * 4]) & 0xFF) << 24
- | (ord($digest[2 + $h * 4]) & 0xFF) << 16
- | (ord($digest[1 + $h * 4]) & 0xFF) << 8
- | ord($digest[$h * 4]) & 0xFF;
- if ($clientArrNew) {
- $bookArr[$k] = $clientArrNew[$i];
- } else {
- $bookArr[$k] = $cluster;
- }
- }
- }
- } catch (Exception $e) {
- Log::error('生成redis连接对应关系数组失败');
- }
- }
- //Log::write(json_encode($bookArr),'cctest');
- ksort($bookArr);
- self::$clientarr = $bookArr;
- }
- return self::$clientarr;
- }
- /**
- * 初始化 redis book 书籍信息
- *
- * @access public
- * @return \Redis
- */
- public static function instanceBookChange($ppval)
- {
- $buildArr = self::buildArr();
- $conInfo = '';
- foreach($buildArr as $key=>$val){
- if($key>$ppval){
- $conInfo = $val;
- break;
- }
- }
- //如果取不到,取第一条
- if(empty($conInfo)){
- $conInfo = reset($buildArr);
- }
- //Log::write('书籍链接的reids地址:'.$conInfo,'cctest');
- $con = explode(':',$conInfo);
- self::$glory = new \Redis();
- $config = Config::get('redis');
- //Log::info('RedisChange连接 ' . $con[0] . ':' . $con[1]);
- if ($config['pconnect']) {
- self::$glory->pconnect($con[0], $con[1], $config['timeout']);
- } else {
- self::$glory->connect($con[0], $con[1], $config['timeout']);
- }
- self::$glory->auth($config['password']);
- return self::$glory;
- }
- /**
- * 初始化 redis auto 用户id自增
- *
- * @access public
- * @return \Redis
- */
- public static function instanceAuto()
- {
- if(isset(self::$connectMap['auto'])){
- return self::getRedisConnect('auto');
- }else{
- $config = Config::get('redis');
- $con = explode(':', $config['auto']);
- if (count($con) < 3) {
- Log::error('用户自增id redis配置错误!');
- throw new HttpException(500, '请刷新页面重新访问!');
- }
- self::loadRedisConfig('auto',$con,$config['pconnect'],$config['timeout']);
- return self::getRedisConnect('auto');
- }
- }
- /**
- * 初始化 redis book 书籍信息
- *
- * @access public
- * @return \Redis
- */
- public static function instanceBook()
- {
- if (is_null(self::$book)) {
- $config = Config::get('redis');
- $list = explode(';', $config['book']);
- $arr = [];
- foreach ($list as $key => $con) {
- if ($con) {
- $c = explode(':', $con);
- if (count($c) >= 3) {
- $arr[] = $c;
- }
- }
- }
- unset($con);
- if (empty($arr)) {
- Log::error('书籍信息 redis配置错误!');
- throw new HttpException(500, '请刷新页面重新访问!');
- }
- shuffle($arr);
- $con = array_pop($arr);
- self::$book = new \Redis();
- Log::info('Redis连接 ' . $con[0] . ':' . $con[1]);
- if ($config['pconnect']) {
- self::$book->pconnect($con[0], $con[1], $config['timeout']);
- } else {
- self::$book->connect($con[0], $con[1], $config['timeout']);
- }
- if (count($con) >= 3 && $con[2]) {
- self::$book->auth($con[2]);
- }
- if (count($con) >= 4 && is_numeric($con[3])) {
- self::$book->select($con[3]);
- }
- }
- return self::$book;
- }
- /**
- * 初始化 连接池 并 返回第一个连接,用于 easywechat&redis lock 使用
- * @access public
- * @return \Redis
- */
- public static function instanceCache()
- {
- if (is_null(self::$instance)) {
- self::$instance = new static();
- }
- return self::getRedisConnect(0);
- }
- /**
- * 构造函数 redis连接池 长连接方式
- * @throws
- */
- public function __construct()
- {
- $config = Config::get('redis');
- $list = explode(';', $config['list']); //共n个redis
- foreach ($list as $item) {
- $con = explode('=', $item); // 0 编号 1 配置
- if (count($con) >= 2) {
- $c = explode(':', $con[1]); //配置 0IP 1端口 2密码 3库编号
- self::loadRedisConfig($con[0],$c,$config['pconnect'],$config['timeout']);
- }
- }
- $modKeyList = explode(';', $config['modkeylist']);
- foreach ($modKeyList as $item) {
- $con = explode('=', $item); // 0 编号 1 配置
- if (count($con) >= 2) {
- $c = explode(':', $con[1]); //配置 0IP 1端口 2密码 3库编号
- self::loadRedisConfig($con[0], $c, $config['pconnect'], $config['timeout']);
- }
- }
- if(count(self::$connectParams) == 0){
- throw new \Exception('redis config error!');
- }
- }
- /**
- * 按规则拆分将key归属到响应的redis编号上
- * @param array $keys
- * @return array 返回数组的key为redis的编号,返回数组的value为需要在当前编号redis上操作的key的数组
- */
- public static function splitKeysByRule(array $keys)
- {
- $redisIndex = [];
- foreach ($keys as $key) {
- $redisIdx = self::getRedisIndex($key);
- $redisIndex[$redisIdx][] = $key;
- }
- return $redisIndex;
- }
- private static function buildRedisRules()
- {
- if (is_null(self::$rules)) {
- $rules = Config::get('redis.rules');
- $list = explode(';', $rules);
- foreach ($list as $item) {
- $con = explode('=', $item); // 0 编号 1 配置
- if (count($con) >= 2) {
- $c = explode(',', $con[1]); //规则
- foreach ($c as $it) {
- if ($it) {
- self::$rules[$con[0]][] = $it;
- }
- }
- }
- }
- }
- }
- /**
- * 根据key获取redis的编号
- * @param $key
- * @return int|mixed|string
- */
- public static function getRedisIndex($key)
- {
- self::buildRedisRules();
- #region 从rule中匹配对应的redis编号
- if ($key && count(self::$rules)) {
- foreach (self::$rules as $idx => $item) {
- foreach ($item as $v) {
- if (strpos($key, $v) === 0) { //查找到,且必须为第一个0
- return $idx;
- }
- }
- }
- }
- #endregion
- #region 构造modkey规则
- if (is_null(self::$modKeyRules)) {
- $modKeyRules = Config::get('redis.modkeyrules');
- self::$modKeyRules = explode(',', $modKeyRules);
- }
- #endregion
- #region 从modkeyrule中匹配对应的redis编号
- if ($key && count(self::$modKeyRules)) {
- foreach (self::$modKeyRules as $modKeyRule) {
- if (strlen($modKeyRule) > 0 && strpos($key, $modKeyRule) === 0) {
- $modKeyListStr = Config::get('redis.modkeylist');
- $modKeyList = explode(';', $modKeyListStr);
- $modKeyCount = count($modKeyList);
- $modNumber = hash_code($key) % $modKeyCount;
- $modNumber = abs($modNumber);
- $modKeyServer = $modKeyList[$modNumber];
- $aModKeyServer = explode('=', $modKeyServer);// 0 编号 1 配置
- return $aModKeyServer[0];
- }
- }
- }
- #endregion
- #region 没有找到符合的规则,使用默认redis服务器
- $default = Config::get('redis.default');
- if (isset(self::$connectMap[$default])) {
- return $default;
- } else {
- return 0;
- }
- #endregion
- }
- /**
- * 根据key区分redis连接
- * @param string $key
- * @return [int,\Redis]
- */
- private static function getRedis($key = '')
- {
- $idx = self::getRedisIndex($key);
- return [$idx, self::getRedisConnect($idx)];
- }
- /**
- * 载入Redis配置
- * @param $index
- * @param $config
- * @param $isPconnect
- * @param $timeout
- */
- private static function loadRedisConfig($index,$config,$isPconnect = false,$timeout = 2){
- if (count($config) >= 2) {
- //设置链接Map
- $val = $config[0].':'.$config[1];
- if (count($config) >= 4 && is_numeric($config[3])) {
- $val = $val . ':' . $config[3];
- }
- self::$connectMap[$index] = $val;
- //是否是长连接
- self::$connectParams[$val]['isLongConnection'] = $isPconnect;
- //IP地址
- self::$connectParams[$val]['ip'] = $config[0];
- //端口
- self::$connectParams[$val]['port'] = $config[1];
- //超时时间
- self::$connectParams[$val]['timeout'] = $timeout;
- //是否进行权限认证
- if (count($config) >= 3 && $config[2]) {
- self::$connectParams[$val]['auth'] = $config[2];
- }else{
- self::$connectParams[$val]['auth'] = false;
- }
- //选择库
- if (count($config) >= 4 && is_numeric($config[3])) {
- self::$connectParams[$val]['db'] = $config[3];
- }else{
- self::$connectParams[$val]['db'] = false;
- }
- }
- }
- /**
- * 获取Redis链接
- * @param $index
- * @param bool $isNo 是否是根据编号获取
- * @return mixed|\Redis
- */
- public static function getRedisConnect($index, $isNo = true)
- {
- //根据编号获取对应的IP和端口
- $mapKey = $index;
- if($isNo){
- if(!isset(self::$connectMap[$index])){
- Log::info("Redis Connect Error: Get Redis No {$index} Map Fail");
- return null;
- }else{
- $mapKey = self::$connectMap[$index];
- }
- }
- //检查链接是否已存在
- if(isset(self::$connections[$mapKey])){
- return self::$connections[$mapKey];
- }
- //不存在时创建链接
- if(!$redisConfig = self::$connectParams[$mapKey]){
- Log::info("Redis Connect Error: Get Redis MapKey {$mapKey} Params Fail");
- return null;
- }
- try{
- $redis = new \Redis();
- //判断长连短连
- if($redisConfig['isLongConnection']){
- $redis->pconnect($redisConfig['ip'],$redisConfig['port'],$redisConfig['timeout']);
- }else{
- $redis->connect($redisConfig['ip'],$redisConfig['port'],$redisConfig['timeout']);
- }
- //校验
- if($redisConfig['auth']){
- $redis->auth($redisConfig['auth']);
- }
- //库
- if($redisConfig['db']){
- $redis->select($redisConfig['db']);
- }
- self::$connections[$mapKey] = $redis;
- Log::info("Redis Connect Info: Ip:{$redisConfig['ip']} Port:{$redisConfig['port']} No:{$index}");
- $clients = $redis->info();
- if (!is_array($clients) || $clients['connected_clients'] > 3000 || $clients['instantaneous_ops_per_sec'] > 100000) {
- Log::error("redis 连接数超出最大限制!client:{$clients['connected_clients']} ops:{$clients['instantaneous_ops_per_sec']}");
- $file = sys_get_temp_dir() . "/cps_redis_error";
- @file_put_contents($file, time() + 2);
- http_response_code(502);
- header("Content-Type: text/html;charset=utf-8");
- echo '<meta charset="utf-8">';
- echo '<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">';
- echo '<title>502 Sorry, HTTP ERROR~</title>';
- echo '502 Sorry, HTTP ERROR~';
- echo "<br><br><br>";
- echo '<a href="javascript:window.location.reload();">点我重试一次</a>';
- die();
- }
- return self::$connections[$mapKey];
- }catch (\Exception $e){
- Log::error("Redis Connect Error: Ip:{$redisConfig['ip']} Port:{$redisConfig['port']} No:{$index} Message:{$e->getMessage()}");
- http_response_code(502);
- header("Content-Type: text/html;charset=utf-8");
- echo '<meta charset="utf-8">';
- echo '<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">';
- echo '<title>502 Sorry, HTTP ERROR~</title>';
- echo '502 Sorry, HTTP ERROR~';
- echo "<br><br><br>";
- echo '<a href="javascript:window.location.reload();">点我重试一次</a>';
- die();
- }
- }
- /**
- * 批量删除正则keys
- *
- * @param string $pattern
- * @throws \Exception
- */
- public static function delScan($pattern)
- {
- set_time_limit(0);
- self::instance();
- $key = substr($pattern, 0, strpos($pattern, ':') + 1);
- self::buildRedisRules();
- $matchIdxs = [];
- #region 从rule规则中匹配
- if ($key && count(self::$rules)) {
- foreach (self::$rules as $idx => $item) {
- foreach ($item as $v) {
- if (strpos($v, $key) === 0) {
- $matchIdxs[] = $idx;
- break;
- }
- }
- }
- }
- #endregion
- #region 构造modkey规则
- if (is_null(self::$modKeyRules)) {
- $modKeyRules = Config::get('redis.modkeyrules');
- self::$modKeyRules = explode(',', $modKeyRules);
- }
- #endregion
- #region 从modkey中匹配规则,如果匹配成功,那么所有modlist服务器做一次删除
- if ($key && count(self::$modKeyRules)) {
- foreach (self::$modKeyRules as $modKeyRule) {
- if (strlen($modKeyRule) > 0 && strpos($modKeyRule, $key) === 0) {
- $modKeyListStr = Config::get('redis.modkeylist');
- $modKeyList = explode(';', $modKeyListStr);
- foreach ($modKeyList as $modKeyServerParam) {
- $aModKeyServer = explode('=', $modKeyServerParam);
- $matchIdxs[] = $aModKeyServer[0];
- }
- break;
- }
- }
- }
- #endregion
- //默认redis服务器也需要做一次删除
- $matchIdxs[] = Config::get('redis.default');
- #region 匹配的服务器做删除操作
- foreach ($matchIdxs as $matchIdx) {
- $redis = self::getRedisConnect($matchIdx);
- $cursor = null;
- while ($cursor !== 0) {
- $keys = $redis->scan($cursor, $pattern, 500);
- if (is_array($keys) && count($keys)) {
- $redis->del($keys); //批量删除key
- }
- }
- }
- #endregion
- }
- // public function pfAdd($name,$val){
- // try{
- // if(!is_array($val)){
- // $val = [$val];
- // }
- // $redis = self::getRedis($name);
- // return $redis->pfAdd($name,$val);
- // }catch (\Exception $e){
- // Log::error("Redis Call Function Error: Name:pfAdd Key:{$name} Args:".var_export($val,true));
- // }
- // }
- function __call($name, $arguments)
- {
- try{
- if (count($arguments)) {
- list($idx,$redis) = self::getRedis($arguments[0]); //参数1不一定就是key
- } else {
- list($idx,$redis) = self::getRedis();
- }
- if (strtolower($name) == 'pfadd' && !is_array($arguments[1])) {
- $arguments[1] = [$arguments[1]];
- }
- if (strtolower($name) == 'delete') {
- $name = 'del';
- }
- // $t = microtime(true);
- $ret = call_user_func_array([$redis, $name], $arguments);
- // Log::info("Redis Call Function Idx:{$idx} Name:{$name} Args:" . json_encode($arguments,
- // JSON_UNESCAPED_UNICODE) . " Time:" . number_format(microtime(true) - $t, 6));
- return $ret;
- }catch (\Exception $e){
- Log::error("Redis Call Function Error: Name:{$name} Args:" . json_encode($arguments,
- true) . " Msg:" . $e->getMessage());
- }
- }
- }
|