class RedisLuaScript {
protected $_redis;
/**
* 是否支持lua脚本
* @var bool
*/
protected $_support_lua_script = true;
/**
* 脚本列表
* @var array
*/
protected $_func_table = [
// 先获取榜首信息再加积分
'zincrby_get_top' => [
'sha1' => '66448d0c16571ae1e5039d243174382377435aa5',
'script' => "local top1 = redis.call('zRevRange',KEYS[1],0,0,'WITHSCORES'); local score = redis.call('zincrby',KEYS[1],ARGV[1],ARGV[2]); return {top1, score}"
],
];
/**
* redis geo连接
* @date 2021/9/29
*/
public function connect()
{
try {
$this->_redis = new \Redis();
$this->_redis->pconnect('127.0.0.1', 6379);
if ($this->_redis->ping() != '+PONG') {
throw new \Exception(__METHOD__ . ' PONG 失败');
}
}catch (Exception $e){
echo "redis链接失败 ". $e->getMessage();
exit();
}
}
/**
* 拓展zincrby_get_top指令,可以原子性的加积分前先获取榜首信息
*
* @date 2021/9/27
* @param $key
* @param $increment
* @param $member
* @return array|bool
* @throws \Exception
*/
public function zincrby_get_top($key, $increment, $member)
{
$this->connect(); //链接redis
$redis_cmd = __FUNCTION__;
try {
if ($this->_support_lua_script) {
$result = $this->eval_script($redis_cmd, $key, [$increment,$member]);
/**
* 因为zRevRange 返回的是 [key=>value]
* lua返回的zRevRange是原始的redis结构 [0=>key,1=>value]
* 为了跟php的zRevRange返回值保持一致所以在此重新组装top1
*/
// $top1 = $result[0];
// $len = count($top1);
// if($len >1){
// for ($i=0;$i<$len;$i+=2){
// $val[$top1[$i]] = $top1[$i+1];
// }
// $result[0] = $val;
// }
//简化写法
if(!empty($result[0])){
$result[0] = [$result[0][0] => $result[0][1]];
}
}
else {
$top1= $this->_redis->zRevRange($key, 0, 0, true);
$score = $this->_redis->zincrby($key, $increment, $member);
$result = [$top1,$score];
}
} catch (\Exception $e) {
$result = false;
}
return $result;
}
/**
* @param string $script_Key
* @param array|string $keys 键名
* @param array|string $args 参数
*
* @return bool|mixed
* @throws \Exception
*
* @example
* obj->eval_script('incr', 'test', 100); // incr, key, 过期时间
* 增量计数器,超过最大值就重置为0
* obj->eval_script('incr_reset', 'test', [max_counter, 100]); // incr_reset, key, [最大值, 过期时间]
* 增量计数器,如果当前值没有大于限定值,才可以加一并返回[1, 累加后的值],否则返回[0, 当前值]
* obj->eval_script('incr_max', 'test', [max_counter, 100]); // incr_max, key, [最大值, 过期时间]
*
* @warning TODO 不能在multi中使用,否则脚本无法执行load,导致命令无效
* @warning TODO 只在单机节点和客户端实现集群调用策略上验证过,云redis暂未测试过
*/
public function eval_script($script_Key, $keys, $args=[]) {
if(!isset($this->_func_table[$script_Key]) || empty($this->_func_table[$script_Key]['script'])) {
// 开发中解决错误
throw new \Exception(__METHOD__ . ' 请先配置'.$script_Key.'脚本');
}
if(empty($this->_func_table[$script_Key]['sha1'])) {
$this->_func_table[$script_Key]['sha1'] = sha1($this->_func_table[$script_Key]['script']);
}
$this->connect();
$sha1 = $this->_func_table[$script_Key]['sha1'];
try {
if(!is_array($keys)) {
$keys = [$keys];
}
else {
// 不需要键名索引,用数字重新建立索引
$keys = array_values($keys);
}
if(!is_array($args)) {
$args = [$args];
}
else {
// 不需要键名索引,用数字重新建立索引
$args = array_values($args);
}
$key_count = count($keys);
$args = array_merge($keys, $args);
for($i =0; $i < 2; $i++) {
$result = $this->_redis->evalSha($sha1, $args, $key_count);
if($result === false && $i === 0) {
$errorMsg = $this->_redis->getLastError();
$this->_redis->clearLastError();
// 该脚本不存在该节点上,需要执行load
if(stripos($errorMsg, 'NOSCRIPT') !== false) {
$loadParams = [
'load',
$this->_func_table[$script_Key]['script']
];
// load脚本
$serverSha1 = $this->_redis->script(...$loadParams);
// 在开发阶段解决这个错误
if($serverSha1 !== $sha1) {
throw new \Exception($script_Key.'脚本的sha1与服务端返回的sha1不一致'.$sha1.'=='.$serverSha1, 999);
}
continue;
}
}
break;
}
} catch (\Exception $e) {
// 如果是开发阶段能解决的错误,就抛出去
if($e->getCode() === 999) {
throw new \Exception($e->getMessage(), $e->getCode());
}
$result = false;
}
return $result;
}
}
$redis = new RedisLuaScript();
$res = $redis->zincrby_get_top('ranking_girl_list',10,123);
var_dump($res);
08-31
661
07-05
875