yii2-redis 的 Connection 源码分析 [ 2.0 版本 ]
文章已经同步到 GitHub 仓库 https://github.com/Zhucola/yii_core_debug ,欢迎 star!
yii2安装后不会有yii-redis组件,需要手动安装该组件
composer require yiisoft/yii2-redis
然后在组件配置中添加redis配件
'redis' => [
'class' => 'yii\redis\Connection',
'hostname' => '192.168.124.10',
'port' => 6379,
'database' => 0,
],
然后就可以在控制器里面使用redis了
public function actionRedis(){
$redis = Yii::$app->get("redis");
$redis->set("g",1);
return $redis->get("g");
}
yii/redis/Connection是redis的基础组件,底层的会话、Mutex、Cache都是依赖这个组件
没有redis扩展也可以使用,因为底层使用的是stream_socket_client连接redis客户端
public function open()
{
if ($this->_socket !== false) {
return;
}
$connection = ($this->unixSocket ?: $this->hostname . ':' . $this->port) . ', database=' . $this->database;
//记录日志
\Yii::trace('Opening redis DB connection: ' . $connection, __METHOD__);
//连接客户端
$this->_socket = @stream_socket_client(
$this->unixSocket ? 'unix://' . $this->unixSocket : 'tcp://' . $this->hostname . ':' . $this->port,
//连接失败的错误号
$errorNumber,
//连接失败的错误信息
$errorDescription,
//连接超时时间
$this->connectionTimeout ? $this->connectionTimeout : ini_get('default_socket_timeout'),
$this->socketClientFlags
);
if ($this->_socket) {
if ($this->dataTimeout !== null) {
//读、写操作的超时时间
stream_set_timeout($this->_socket, $timeout = (int) $this->dataTimeout, (int) (($this->dataTimeout - $timeout) * 1000000));
}
if ($this->password !== null) {
//密码
$this->executeCommand('AUTH', [$this->password]);
}
if ($this->database !== null) {
//redis数据库
$this->executeCommand('SELECT', [$this->database]);
}
//连接成功的事件
$this->initConnection();
} else {
\Yii::error("Failed to open redis DB connection ($connection): $errorNumber - $errorDescription", __CLASS__);
$message = YII_DEBUG ? "Failed to open redis DB connection ($connection): $errorNumber - $errorDescription" : 'Failed to open DB connection.';
throw new Exception($message, $errorDescription, $errorNumber);
}
}
protected function initConnection()
{
//连接redis成功的事件
$this->trigger(self::EVENT_AFTER_OPEN);
}
如果要使用命令,那么会被__call魔术方法执行
public function __call($name, $params)
{
//格式化命令,可以理解为mb_ucwords
$redisCommand = strtoupper(Inflector::camel2words($name, false));
//判断命令是否可用
if (in_array($redisCommand, $this->redisCommands)) {
//命令可用,执行
return $this->executeCommand($redisCommand, $params);
} else {
//命令不可用,调用父类的魔术方法
return parent::__call($name, $params);
}
}
给redis服务发命令的代码十分有意思
public function executeCommand($name, $params = [])
{
$this->open();
//这块是非常有意思的代码
$params = array_merge(explode(' ', $name), $params);
$command = '';
$paramsCount = 0;
foreach ($params as $arg) {
if ($arg === null) {
continue;
}
$paramsCount++;
$command .= '$' . mb_strlen($arg, '8bit') . "\r\n" . $arg . "\r\n";
}
$command = '*' . $paramsCount . "\r\n" . $command;
\Yii::trace("Executing Redis Command: {$name}", __METHOD__);
//重发机制
if ($this->retries > 0) {
$tries = $this->retries;
while ($tries-- > 0) {
try {
//成功了直接return
return $this->sendCommandInternal($command, $params);
} catch (SocketException $e) {
\Yii::error($e, __METHOD__);
// backup retries, fail on commands that fail inside here
$retries = $this->retries;
$this->retries = 0;
//关闭连接
$this->close();
//重新建立连接
$this->open();
$this->retries = $retries;
}
}
}
//非重发机制
return $this->sendCommandInternal($command, $params);
}
核心就是executeCommand方法的那个foreach,如果给redis发一个hmset a a 1 b 2 c 3,php-redis是这样的
$redis=new Redis();
$redis->connect(...);
$redis->hmset("a",["a"=>1,"b"=>2,"c"=>3]);
但是yii的redis是
$redis = Yii::$app->get("redis");
$redis->hmset("a","a",1,"b",2,"c",3);
给redis发过去的是这样的格式
*8 //代表有8个数据组组成,就是HMSET a a 1 b 2 c 3,一共8个
$5 //代表HMSET一共5个字节
HMSET
$1
a
$1
a
$1
1
$1
b
$1
2
$1
c
$1
3
最后将数据发给redis服务
private function sendCommandInternal($command, $params)
{
$written = @fwrite($this->_socket, $command);
if ($written === false) {
throw new SocketException("Failed to write to socket.\nRedis command was: " . $command);
}
if ($written !== ($len = mb_strlen($command, '8bit'))) {
throw new SocketException("Failed to write to socket. $written of $len bytes written.\nRedis command was: " . $command);
}
return $this->parseResponse(implode(' ', $params));
}
然后需要得到redis服务的响应
private function parseResponse($command)
{
if (($line = fgets($this->_socket)) === false) {
throw new SocketException("Failed to read from socket.\nRedis command was: " . $command);
}
$type = $line[0];
$line = mb_substr($line, 1, -2, '8bit');
switch ($type) {
case '+': // Status reply
if ($line === 'OK' || $line === 'PONG') {
return true;
} else {
return $line;
}
case '-': // Error reply
throw new Exception("Redis error: " . $line . "\nRedis command was: " . $command);
case ':': // Integer reply
// no cast to int as it is in the range of a signed 64 bit integer
return $line;
case '$': // Bulk replies
if ($line == '-1') {
return null;
}
$length = (int)$line + 2;
$data = '';
while ($length > 0) {
if (($block = fread($this->_socket, $length)) === false) {
throw new SocketException("Failed to read from socket.\nRedis command was: " . $command);
}
$data .= $block;
$length -= mb_strlen($block, '8bit');
}
return mb_substr($data, 0, -2, '8bit');
case '*': // Multi-bulk replies
$count = (int) $line;
$data = [];
for ($i = 0; $i < $count; $i++) {
$data[] = $this->parseResponse($command);
}
return $data;
default:
throw new Exception('Received illegal data from redis: ' . $line . "\nRedis command was: " . $command);
}
}
简单来说,给redis发一个hmset命令,redis会返回
+OK
然后在判断返回的信息是否正确
Zhucola China
注册时间:2019-06-17
最后登录:2019-10-12
在线时长:11小时36分
最后登录:2019-10-12
在线时长:11小时36分
- 粉丝11
- 金钱800
- 威望100
- 积分1910
热门源码
- 基于 Yii 2 + Bootstrap 3 搭建一套后台管理系统 CMF
- 整合完 yii2-rbac+yii2-admin+adminlte 等库的基础开发后台源码
- 适合初学者学习的一款通用的管理后台
- yii-goaop - 将 goaop 集成到 Yii,在 Yii 中优雅的面向切面编程
- yii-log-target - 监控系统异常且多渠道发送异常信息通知
- 店滴云1.3.0
- 面向对象的一小步:添加 ActiveRecord 的 Scope 功能
- Yii2 开源商城 FecShop
- 基于 Yii2 开发的多店铺商城系统,免费开源 + 适合二开
- leadshop - 基于 Yii2 开发的一款免费开源且支持商业使用的商城管理系统
共 1 条评论
这其实是在网络协议层面和redis服务交互。redis通讯协议应该属于应用层协议,和http协议都在同一层,下面传输层用tcp协议。
所以它是命令行redis-cli,php-redis扩展等各种形式redis客户端的底层。
换句话说,yii/redis/Connection也是一个redis客户端,只不过还能让我们看到如何往tcp层发送数据流的代码,有意思。
了解redis通讯协议,你当然可以写个客户端了。