class RateLimiter extends Predis\Command\ScriptCommand
{
public function getKeysCount()
{
return 1;
}
public function getScript()
{
return <<<LUA
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = redis.call('INCR', key)
if current == 1 then
redis.call('EXPIRE', key, window)
end
if current > limit then
return 0
else
return 1
end
LUA;
}
}
// 注入命令
parameters, [
'commands' => [
'ratelimit' => 'RateLimiter',
],
]);
// 使用
limit = 10; // 每分钟10次请求
client->ratelimit("rate:limit,
第一章 意外的相遇
炎炎夏日,阳光炙烤着大地。米小饭拖着疲惫的身躯走进了一家网吧。作为一名刚毕业的程序员,他正为找工作而发愁。
“呼~总算找到避暑的地方了。”米小饭长舒一口气,坐到了一台电脑前。
正当他准备打开招聘网站时,突然听到旁边传来一声惊呼:”哎呀,怎么又出错了!”
米小饭好奇地转头一看,只见邻座的一位戴着眼镜的中年男子正皱眉盯着屏幕。
“这位大叔,您遇到什么问题了吗?”米小饭忍不住问道。
中年男子转过头来,略显惊讶地看了米小饭一眼,然后苦笑着说:”哦,没什么大事。只是我们公司的网站最近总是响应很慢,我正在尝试优化数据库,可是效果不太理想。”
“原来如此。”米小饭点点头,”不知道您用的是什么数据库呢?”
“MySQL。”中年男子回答道,”不过我在考虑引入Redis来做缓存,听说可以大幅提升性能。可惜我对Redis不太熟悉,正在研究怎么用PHP连接Redis呢。”
“Redis?这个我倒是有些了解!”米小饭眼前一亮,”我在学校的时候做过相关的项目。如果您不介意的话,也许我可以帮您看看?”
中年男子露出了惊喜的表情:”真的吗?那太好了!我叫步子哥,是一家小型科技公司的技术总监。你叫什么名字?”
“我叫米小饭,是刚毕业的应届生。”米小饭笑着做了个自我介绍。
“米小饭?有趣的名字。”步子哥笑道,”既然这么有缘,不如我们找个安静的地方好好聊聊?正好我知道附近有家不错的咖啡馆。”
“好啊!”米小饭欣然同意。
就这样,两人来到了咖啡馆,找了个安静的角落坐下。
步子哥打开笔记本电脑,指着屏幕说:”我刚才在网上找到一个叫Predis的PHP Redis客户端库,看起来挺不错的。你对这个有了解吗?”
米小饭凑近看了看,然后兴奋地说:”Predis!我在学校的项目里就是用的这个库。它确实很棒,功能齐全而且使用简单。”
步子哥眼睛一亮:”那太好了!你能给我详细讲讲它的特性和用法吗?”
米小饭点点头:”当然可以。不过可能需要一点时间,内容比较多。”
步子哥笑道:”没关系,我们有的是时间。来,我给你点杯咖啡?”
“那就麻烦您了。”米小饭不好意思地说。
等服务员送来咖啡,米小饭喝了一口,整理了下思路,开始娓娓道来:
“Predis是一个功能强大且灵活的PHP Redis客户端库。它支持Redis从3.0到7.2版本的所有功能,包括集群、主从复制、哨兵等高级特性。最重要的是,它使用纯PHP实现,不需要安装任何PHP扩展,这让它的安装和使用变得非常简单。”
步子哥若有所思地点点头:”听起来很不错。那它具体有哪些主要特性呢?”
米小饭掰着指头数道:”首先,它支持客户端分片的集群模式,可以灵活地分配键空间。其次,它支持Redis原生的集群模式。第三,支持主从复制和哨兵模式。第四,可以透明地给所有键加前缀。第五,支持命令流水线处理。第六,支持Redis事务和CAS操作。第七,支持Lua脚本,会自动在EVAL和EVALSHA之间切换。第八,支持SCAN族命令的迭代器抽象。最后,它的连接是惰性建立的,可以持久化连接,还支持TCP和Unix套接字连接。”
步子哥听得连连点头:”哇,功能真丰富!那它的安装和基本使用方法是怎样的呢?”
米小饭笑道:”安装很简单,如果你使用Composer的话,只需要运行一条命令:”
“然后在PHP代码中,你可以这样创建一个客户端实例并使用:”
步子哥惊讶地说:”这么简单?不需要其他配置吗?”
米小饭解释道:”是的,如果你不指定任何参数,Predis会默认连接到127.0.0.1:6379。当然,你也可以自定义连接参数:”
步子哥若有所思地说:”我明白了。那如果Redis服务器需要密码认证呢?”
米小饭回答:”很简单,只需要在参数中加上password就可以了。如果是Redis 6.0以上版本启用了ACL,还需要提供username:”
步子哥眼前一亮:”太棒了!看来Predis真的很全面。不过我还有个疑问,如果我们想使用Redis集群,该怎么配置Predis呢?”
米小饭喝了口咖啡,说道:”这个问题问得好。Predis支持两种集群模式:客户端分片和Redis原生集群。对于Redis原生集群,你可以这样配置:”
“Predis会自动将读操作发送到从节点,写操作发送到主节点。”
步子哥惊叹道:”真是太方便了!Predis似乎考虑到了各种场景。那么,它的性能如何呢?”
米小饭笑道:”Predis的性能是很不错的。不过如果你追求极致性能,它还提供了一个叫Relay的集成方案。Relay是一个PHP扩展,可以在PHP的共享运行时内存中缓存部分Redis数据集,从而大幅提升性能。使用起来也很简单:”
“而事务则可以确保一组命令要么全部执行成功,要么全部失败。Predis的事务接口非常友好:”
步子哥看完,惊叹道:”哇,这真是太酷了!我们可以像使用普通Redis命令一样使用这个自定义的脚本命令。”
米小饭点头说:”没错!这种方式不仅让我们可以封装复杂的操作,还能充分利用Redis的原子性和性能优势。”
步子哥若有所思地说:”我明白了。那如果我们有一些通用的Lua脚本,不想每次都定义一个新的命令类,有什么简单的方法吗?”
米小饭笑道:”当然有!Predis提供了一个
EVAL
命令的包装,让我们可以直接执行Lua脚本。比如:”步子哥点点头:”这确实很方便。不过,如果我们需要多次执行同一个脚本,每次都传输完整的脚本内容是不是有点浪费?”
米小饭赞许地说:”好问题!实际上,Predis会自动处理这个问题。它内部会缓存脚本的SHA1哈希值,并优先使用
EVALSHA
命令。如果服务器上没有对应的脚本,它会自动回退到使用EVAL
。这个过程对我们来说是完全透明的。”步子哥恍然大悟:”原来如此!这设计真是太巧妙了。”
米小饭继续说:”还有一点值得一提的是,Predis的脚本命令支持是可扩展的。如果我们需要更复杂的脚本逻辑或者特殊的参数处理,我们可以继承
Predis\Command\ScriptCommand
类,并覆盖相应的方法。”步子哥听得连连点头,突然想到了什么:”对了,你之前提到Predis支持事务。Lua脚本和事务有什么关系吗?”
米小饭笑道:”好问题!实际上,Lua脚本本身就是原子的,相当于一个微型事务。所有在脚本中的操作要么全部执行,要么全部不执行。这意味着在大多数情况下,我们可以使用Lua脚本来替代事务,而且通常会有更好的性能。”
步子哥若有所思地说:”我明白了。那在什么情况下我们应该选择使用Lua脚本,而不是普通的Redis命令或事务呢?”
米小饭思考了一下,回答道:”这是个很好的问题。通常在以下几种情况下,使用Lua脚本会更有优势:
比如,假设我们需要实现一个简单的限流器,限制每个用户每分钟的请求次数。使用Lua脚本,我们可以这样实现:”
步子哥看完,惊讶地说:”哇,这看起来很简洁高效!我们可以很容易地将这种模式应用到其他查询中。”
米小饭点头说:”没错。这种模式可以大大减轻数据库的压力。现在,让我们来看第二个问题:分布式锁。Predis提供了一些基本的命令,我们可以基于这些命令实现一个简单但有效的分布式锁。”
他继续coding:
步子哥看完,赞叹道:”这个实现很巧妙!使用Lua脚本来确保释放锁的原子性,真是高明。”