注意通过 Redis 在 CakePHP 中反序列化错误
Notice unserialize error in CakePHP via Redis
更新:
根据以下已接受答案的建议,我测试了读取负数:
$negativeInt = -1;
Cache::write('my-test-count', $negativeInt, 'short');
$readVal = Cache::read('my-test-count', 'short');
debug($readVal);
exit;
尝试读取任何负数时,反序列化错误始终重现。它现在是一个公认的错误,我认为它会在 2.8.1
中得到解决
原题:
我一直收到此错误,但不知道为什么,甚至不知道如何进一步解决问题。
抛出错误的行只有在 Cache::read()
returns false 时才会被命中。但是,如果我没有在它前面加上 @,那行本身会抛出一个反序列化错误。
问题:
如何在不定期收到反序列化通知的情况下可靠地使用 Redis 进行计数?如果密钥中的数据是 "bad",我怎么能在不通过 ::read
得到 Notice 的情况下知道这一点。我已尝试确保我的数据是 (int)(见下文),但这似乎无济于事。
Notice (8): unserialize(): Error at offset 0 of 2 bytes
[APP/Vendor/pear-pear.cakephp.org/CakePHP/Cake/Cache/Engine/RedisEngine.php,
line 136]
经检查发现错误:
> unserialize - [internal], line ??
> RedisEngine::read() - APP/Vendor/pear-pear.cakephp.org/CakePHP/Cake/Cache/Engine/RedisEngine.php, line 136
> Cache::read() - APP/Vendor/pear-pear.cakephp.org/CakePHP/Cake/Cache/Cache.php, line 358
> Cache::remember() - APP/Vendor/pear-pear.cakephp.org/CakePHP/Cake/Cache/Cache.php, line 567
> Item::getCount() - APP/Model/Item.php, line 812
它似乎来自这个函数:
public function getCount($id) {
$model = $this;
// here is where the Cache::read() and debug are in the update below
return Cache::remember('item' . $id. '_count', function() use ($model, $id) {
$count = $model->find('count', array(
'conditions' => array(
$model->alias . '.status' => 1,
$model->alias . '.id' => $id
)
));
return ($count === false) ? 0 : (int)$count;
}, 'my_counts'); // THIS IS LINE 812
}
public function decrementCount($id, $offset = 1) {
if(empty($id)) return false;
$count = @Cache::read('item' . $id . '_count', 'my_counts');
if($count === false) {
$this->getCount($id);
} else {
Cache::decrement('item' . $id . '_count', $offset, 'my_counts');
}
}
public function incrementCount($id, $offset = 1) {
if(empty($id)) return false;
$count = @Cache::read('item' . $id. '_count', 'my_counts');
if($count === false) {
$this->getCount($id);
} else {
Cache::increment('item' . $id. '_count', $offset, 'my_counts');
}
}
更新:
这个函数在一个循环中得到 运行(通过 1-20 个项目)。当我在 Cache::remember(...
之前添加以下内容时:
$toReturn = Cache::read('item' . $id. '_count', 'my_counts');
debug($toReturn);
它给出了这个:
- 调试:(整数)0
- 调试:(整数)0
- 通知 (8): unserialize(): Error at...(来自 Cache::read)
- 调试:错误
- 通知 (8): unserialize(): Error at...(来自 Cache::remember
- 调试:(整数)0
- 通知 (8): unserialize(): Error at...(来自 Cache::read)
- 调试:错误
- 通知 (8): unserialize(): Error at...(来自 Cache::remember
- 0
- 3
- 1
- 1
...
解决方案:
return ($count === false) ? 0 : (int)$count;
长版:
您不应该 increment/decrement 序列化值。
Cache::remember 做这样的事情:
$key = 'key';
$value = 10;
$serialized = serialize($value); // $serialized = 'i:10;'
$redis->set($key, $serialized);
Cache::read 做这样的事情:
$key = 'key';
$serialized = $redis->get($key); // $serialized = 'i:10;'
$value = unserialize($serialized); // $value = (int) 10
return $value;
但是 Cache::decrement 做了这样的事情:
$key = 'key';
$count = 1;
$redis->decrby($key, $count); // Error, decrby expects plain value, "10", not "i:10;" ( http://redis.io/commands/decrby )
此外,如果你递减成功,那么你在redis中将有普通值,所以结果将是:
$key = 'key';
$serialized = $redis->get($key); // $serialized = '10'
$value = unserialize($serialized); // error, cannot unserialize '10'
编辑:
检查来源后:
https://github.com/cakephp/cakephp/blob/master/src/Cache/Engine/RedisEngine.php
我看到 Cake 会检查:
if (!is_int($value)) {
$value = serialize($value);
}
因此,要避免序列化,您需要做的就是确保存储的是 (int)。
这似乎是一个核心错误。我也许可以回答为什么会出现此问题。
我猜测这个问题是由 int(-1)
引起的。
写入数据时,RedisEngine
不会序列化数据,如果它是一个整数。
因此,int(-1)
将被保存而不调用serialize()
。
public function write($key, $value, $duration) {
if (!is_int($value)) {
$value = serialize($value);
}
...
}
但是在读取数据时,实现似乎是不对称的。我不太了解 Redis
,但我猜测 Redis
将 int(-1)
存储为 string(-1)
。由于 ctype_digit("-1")
returns false
,string(-1)
将被错误地反序列化。
public function read($key) {
$value = $this->_Redis->get($key);
if (ctype_digit($value)) { // ctype_digit("-1") === false
$value = (int)$value;
}
if ($value !== false && is_string($value)) { // is_string("-1") === true
$value = unserialize($value);
}
...
}
因此,您将看到 "Notice (8): unserialize(): Error at offset 0 of 2 bytes"。 string(-1)
的大小是 2 个字节。
在 github 上开一个问题怎么样,如果你能重现那篇文章 int(-1)
并且阅读它会引发一个通知。
除此之外,解决方法是停止使用 Cache::decrement()
。在竞争条件下,该方法将存储 int(-1)
。
如果它不是重要数据,您可以使用 Cache::write()
代替。例如:
$count = Cache::read('item' . $id. '_count', 'my_counts');
if ($count === false || 0 > $count - $offset) {
$this->getCount($id);
} else {
Cache::write('item' . $id. '_count', $count - $offset, 'my_counts');
}
但是如果是重要数据,那么可能需要实现独占lock/unlock.
感谢您阅读本文,如有错误请见谅。
去掉第三个参数,应该可以了
return Cache::remember('item' . $id. '_count', function() use ($model, $id) {
$count = $model->find('count', array(
'conditions' => array(
$model->alias . '.status' => 1,
$model->alias . '.id' => $id
)
));
return ($count === false) ? 0 : (int)$count;
});
更新:
根据以下已接受答案的建议,我测试了读取负数:
$negativeInt = -1;
Cache::write('my-test-count', $negativeInt, 'short');
$readVal = Cache::read('my-test-count', 'short');
debug($readVal);
exit;
尝试读取任何负数时,反序列化错误始终重现。它现在是一个公认的错误,我认为它会在 2.8.1
中得到解决原题:
我一直收到此错误,但不知道为什么,甚至不知道如何进一步解决问题。
抛出错误的行只有在 Cache::read()
returns false 时才会被命中。但是,如果我没有在它前面加上 @,那行本身会抛出一个反序列化错误。
问题:
如何在不定期收到反序列化通知的情况下可靠地使用 Redis 进行计数?如果密钥中的数据是 "bad",我怎么能在不通过 ::read
得到 Notice 的情况下知道这一点。我已尝试确保我的数据是 (int)(见下文),但这似乎无济于事。
Notice (8): unserialize(): Error at offset 0 of 2 bytes [APP/Vendor/pear-pear.cakephp.org/CakePHP/Cake/Cache/Engine/RedisEngine.php, line 136]
经检查发现错误:
> unserialize - [internal], line ??
> RedisEngine::read() - APP/Vendor/pear-pear.cakephp.org/CakePHP/Cake/Cache/Engine/RedisEngine.php, line 136
> Cache::read() - APP/Vendor/pear-pear.cakephp.org/CakePHP/Cake/Cache/Cache.php, line 358
> Cache::remember() - APP/Vendor/pear-pear.cakephp.org/CakePHP/Cake/Cache/Cache.php, line 567
> Item::getCount() - APP/Model/Item.php, line 812
它似乎来自这个函数:
public function getCount($id) {
$model = $this;
// here is where the Cache::read() and debug are in the update below
return Cache::remember('item' . $id. '_count', function() use ($model, $id) {
$count = $model->find('count', array(
'conditions' => array(
$model->alias . '.status' => 1,
$model->alias . '.id' => $id
)
));
return ($count === false) ? 0 : (int)$count;
}, 'my_counts'); // THIS IS LINE 812
}
public function decrementCount($id, $offset = 1) {
if(empty($id)) return false;
$count = @Cache::read('item' . $id . '_count', 'my_counts');
if($count === false) {
$this->getCount($id);
} else {
Cache::decrement('item' . $id . '_count', $offset, 'my_counts');
}
}
public function incrementCount($id, $offset = 1) {
if(empty($id)) return false;
$count = @Cache::read('item' . $id. '_count', 'my_counts');
if($count === false) {
$this->getCount($id);
} else {
Cache::increment('item' . $id. '_count', $offset, 'my_counts');
}
}
更新:
这个函数在一个循环中得到 运行(通过 1-20 个项目)。当我在 Cache::remember(...
之前添加以下内容时:
$toReturn = Cache::read('item' . $id. '_count', 'my_counts');
debug($toReturn);
它给出了这个:
- 调试:(整数)0
- 调试:(整数)0
- 通知 (8): unserialize(): Error at...(来自 Cache::read)
- 调试:错误
- 通知 (8): unserialize(): Error at...(来自 Cache::remember
- 调试:(整数)0
- 通知 (8): unserialize(): Error at...(来自 Cache::read)
- 调试:错误
- 通知 (8): unserialize(): Error at...(来自 Cache::remember
- 0
- 3
- 1
- 1 ...
解决方案:
return ($count === false) ? 0 : (int)$count;
长版:
您不应该 increment/decrement 序列化值。
Cache::remember 做这样的事情:
$key = 'key';
$value = 10;
$serialized = serialize($value); // $serialized = 'i:10;'
$redis->set($key, $serialized);
Cache::read 做这样的事情:
$key = 'key';
$serialized = $redis->get($key); // $serialized = 'i:10;'
$value = unserialize($serialized); // $value = (int) 10
return $value;
但是 Cache::decrement 做了这样的事情:
$key = 'key';
$count = 1;
$redis->decrby($key, $count); // Error, decrby expects plain value, "10", not "i:10;" ( http://redis.io/commands/decrby )
此外,如果你递减成功,那么你在redis中将有普通值,所以结果将是:
$key = 'key';
$serialized = $redis->get($key); // $serialized = '10'
$value = unserialize($serialized); // error, cannot unserialize '10'
编辑: 检查来源后: https://github.com/cakephp/cakephp/blob/master/src/Cache/Engine/RedisEngine.php
我看到 Cake 会检查:
if (!is_int($value)) {
$value = serialize($value);
}
因此,要避免序列化,您需要做的就是确保存储的是 (int)。
这似乎是一个核心错误。我也许可以回答为什么会出现此问题。
我猜测这个问题是由 int(-1)
引起的。
写入数据时,RedisEngine
不会序列化数据,如果它是一个整数。
因此,int(-1)
将被保存而不调用serialize()
。
public function write($key, $value, $duration) {
if (!is_int($value)) {
$value = serialize($value);
}
...
}
但是在读取数据时,实现似乎是不对称的。我不太了解 Redis
,但我猜测 Redis
将 int(-1)
存储为 string(-1)
。由于 ctype_digit("-1")
returns false
,string(-1)
将被错误地反序列化。
public function read($key) {
$value = $this->_Redis->get($key);
if (ctype_digit($value)) { // ctype_digit("-1") === false
$value = (int)$value;
}
if ($value !== false && is_string($value)) { // is_string("-1") === true
$value = unserialize($value);
}
...
}
因此,您将看到 "Notice (8): unserialize(): Error at offset 0 of 2 bytes"。 string(-1)
的大小是 2 个字节。
在 github 上开一个问题怎么样,如果你能重现那篇文章 int(-1)
并且阅读它会引发一个通知。
除此之外,解决方法是停止使用 Cache::decrement()
。在竞争条件下,该方法将存储 int(-1)
。
如果它不是重要数据,您可以使用 Cache::write()
代替。例如:
$count = Cache::read('item' . $id. '_count', 'my_counts');
if ($count === false || 0 > $count - $offset) {
$this->getCount($id);
} else {
Cache::write('item' . $id. '_count', $count - $offset, 'my_counts');
}
但是如果是重要数据,那么可能需要实现独占lock/unlock.
感谢您阅读本文,如有错误请见谅。
去掉第三个参数,应该可以了
return Cache::remember('item' . $id. '_count', function() use ($model, $id) {
$count = $model->find('count', array(
'conditions' => array(
$model->alias . '.status' => 1,
$model->alias . '.id' => $id
)
));
return ($count === false) ? 0 : (int)$count;
});