1. Redis 中使用 Lua 脚本
Redis 本身已经提供了丰富的命令,但是直接用来处理一些复杂业务时可能还不够方便,会有一定的局限性。因此,在 Redis2.6 版本开始提供了对 Lua 脚本的支持,Lua 脚本的使用还是比较广泛的,比如商品秒杀、分布式锁等,使用 Lua 脚本可以带来以下的好处:
为了让例子更加的贴近实际应用,这里实现一个简单版的分布式锁。这里先用 Jedis 操作。
上边详细的介绍了分布式锁的实现过程,以及可能出现的问题,最终,我们决定删除锁的操作使用 Lua 脚本实现,对应的脚本如下:
Lua 脚本中执行具体的 Redis 命令,需要使用 redis.call() 方法, KEYS 表示客户端发起脚本执行命令时携带的 Redis key 的一个集合, ARGV 则是其它参数的一个集合,主意下标从1开始。结合我们的业务,这里的 KEYS[1] 则表示 lock , ARGV[1] 则是一个随机字符串。整个脚本的含义就是,如果客户端传递的 lock 的 value 和 Redis 中存储的一致,就删除 lock 。
Lua 脚本的语法还是比较简单的,具体内容可以自行学习。
前边的准备工作基本结束了,文章开始说过执行脚本有两种途径,下边我们具体来看:
这里使用 jedis.eval() 发送脚本到 Redis 服务器执行,后两个参数分别是 key 的集合,以及 value 参数的集合。
先将脚本以文件形式放到 Redis 里,例如这样:
然后通过如下命令让 Redis 服务器缓存脚本:
script load 命令会在 Redis 服务器缓存 Lua 脚本,并且脚本内容经过 SHA-1 签名算法处理后,会返回脚本内容的 SHA1 校验和的编码,然后在端调用时,传入编码字符串作为参数,这样 Redis 服务器就会执行对应缓存的脚本了,就不用了每次发送具体的脚本内容了。
还有两个比较有用的命令:
除了使用上边的命令缓存脚本、生成脚本的 SHA1 校验和的编码,还可以使用 Jedis 实现,但最终的 SHA1 编码内容是不同的:
实际的项目中,可能更多的会在 SpringBoot 项目中整合 Redis,此时执行 Lua 脚本的基本流程如下:
核心的类就是 DefaultRedisScript ,它实现了 RedisScript 接口。 execute() 方法最后一个参数是可变类型的,用来传递多个 value 参数。初次执行 execute() 方法时,其内部会自动缓存 Lua 脚本到 Redis 服务器;同时每次执行脚本时会根据脚本内容自动计算出对应的 SHA1 校验和的编码,去匹配、执行缓存的脚本。
具体的 SHA1 校验和的编码,可以在 execute() 方法执行后,使用 redisScript.getSha1() 查看。使用 SpringBoot 方式 执行 Lua 脚本生成的 SHA1 校验和的编码和前边直接使用 Jedis 生成的一致。
无论用那种方式在 Redis 中使用 Lua 脚本,其中的原理都是类似的。
2. 何时部署要启动lua nginx
Lua是一个可以嵌入到Nginx配置文件中的动态脚本语言,从而可以在Nginx请求处理的任何阶段执行各种Lua代码。刚开始我们只是用Lua 把请求路由到后端服务器,但是它对我们架构的作用超出了我们的预期。下面就讲讲我们所做的工作。
强制搜索引擎只索引mixlr.com
Google把子域名当作完全独立的网站,我们不希望爬虫抓取子域名的页面,降低我们的Page rank
location /robots.txt {
rewrite_by_lua '
if ngx.var.http_host ~= "mixlr.com" then
return ngx.exec("/robots_disallow.txt");
end
';
}
如果对robots.txt的请求不是mixlr.com域名的话,则内部重写到robots_diallow.txt,虽然标准的重写指令也可以实现这个需求,但是 Lua的实现更容易理解和维护。
根据程序逻辑设置响应头
Lua提供了比Nginx默认配置规则更加灵活的设置方式。 在下面的例子中,我们要保证正确设置响应头,这样浏览器如果发送了指定请求头后,就可以 无限期缓存静态文件,是的用户只需下载一次即可。
这个重写规则使得任何静态文件,如果请求参数中包含时间戳值,那么就设置相应的Expires和Cache-Control响应头。
location / {
header_filter_by_lua '
if ngx.var.query_string and ngx.re.match( ngx.var.query_string, "^([0-9]{10})$" ) then
ngx.header["Expires"] = ngx.http_time( ngx.time() + 31536000 );
ngx.header["Cache-Control"] = "max-age=31536000";
end
';
try_files $uri @dynamic;}
删除jQuery JSONP请求的时间戳参数
很多外部客户端请求JSONP接口时,都会包含一个时间戳类似的参数,从而导致Nginx proxy缓存无法命中(因为无法忽略指定的HTTP参数)。下面的 规则删除了时间戳参数,使得Nginx可以缓存upstream server的响应内容,减轻后端服务器的负载。
location / {
rewrite_by_lua '
if ngx.var.args ~= nil then
-- /some_request?_=1346491660 becomes /some_request
local fixed_args, count = ngx.re.sub( ngx.var.args, "&?_=[0-9]+", "" );
if count > 0 then
return ngx.exec(ngx.var.uri, fixed_args);
end
end
';}
把后端的慢请求日志记录到Nginx的错误日志
如果后端请求响应很慢,可以把它记录到Nginx的错误日志,以备后续追查。
location / {
log_by_lua '
if tonumber(ngx.var.upstream_response_time) >= 1 then
ngx.log(ngx.WARN, "[SLOW] Ngx upstream response time: " .. ngx.var.upstream_response_time .. "s from " .. ngx.var.upstream_addr);
end
';}
基于Redis的实时IP封禁
某些情况下,需要阻止流氓爬虫的抓取,这可以通过专门的封禁设备去做,但是通过Lua,也可以实现简单版本的封禁。
lua_shared_dict banned_ips 1m;
location / {
access_by_lua '
local banned_ips = ngx.shared.banned_ips;
local updated_at = banned_ips:get("updated_at");
-- only update banned_ips from Redis once every ten seconds:
if updated_at == nil or updated_at < ( ngx.now() - 10 ) then
local redis = require "resty.redis";
local red = redis:new();
red:set_timeout(200);
local ok, err = red:connect("your-redis-hostname", 6379);
if not ok then
ngx.log(ngx.WARN, "Redis connection error retrieving banned_ips: " .. err);
else
local updated_banned_ips, err = red:smembers("banned_ips");
if err then
ngx.log(ngx.WARN, "Redis read error retrieving banned_ips: " .. err);
else
-- replace the locally stored banned_ips with the updated values:
banned_ips:flush_all();
for index, banned_ip in ipairs(updated_banned_ips) do
banned_ips:set(banned_ip, true);
end
banned_ips:set("updated_at", ngx.now());
end
end
end
if banned_ips:get(ngx.var.remote_addr) then
ngx.log(ngx.WARN, "Banned IP detected and refused access: " .. ngx.var.remote_addr);
return ngx.exit(ngx.HTTP_FORBIDDEN);
end
';}
现在就可以阻止特定IP的访问:
1
ruby> $redis.sadd("banned_ips", "200.1.35.4")
Nginx进程每隔10秒从Redis获取一次最新的禁止IP名单。需要注意的是,如果架构中使用了Haproxy这样类似的负载均衡服务器时, 需要把$remote_addr设置为正确的远端IP地址。
这个方法还可以用于HTTP User-Agent字段的检查,要求满足指定条件。
使用Nginx输出CSRF(form_authenticity_token)
Mixlr大量使用页面缓存,由此引入的一个问题是如何给每个页面输出会话级别的CSRF token。我们通过Nginx的子请求,从upstream web server 获取token,然后利用Nginx的SSI(server-side include)功能输出到页面中。这样既解决了CSRF攻击问题,也保证了cache能被正常利用。
location /csrf_token_endpoint {
internal;
include /opt/nginx/conf/proxy.conf;
proxy_pass "http://upstream";}
location @dynamic {
ssi on;
set $csrf_token '';
rewrite_by_lua '
-- Using a subrequest, we our upstream servers for the CSRF token for this session:
local csrf_capture = ngx.location.capture("/csrf_token_endpoint");
if csrf_capture.status == 200 then
ngx.var.csrf_token = csrf_capture.body;
-- if this is a new session, ensure it sticks by passing through the new session_id
-- to both the subsequent upstream request, and the response:
if not ngx.var.cookie_session then
local match = ngx.re.match(csrf_capture.header["Set-Cookie"], "session=([a-zA-Z0-9_+=/+]+);");
if match then
ngx.req.set_header("Cookie", "session=" .. match[1]);
ngx.header["Set-Cookie"] = csrf_capture.header["Set-Cookie"];
end
end
else
ngx.log(ngx.WARN, "No CSRF token returned from upstream, ignoring.");
end
';
try_files /maintenance.html /rails_cache$uri @thin;}
CSRF token生成 app/metal/csrf_token_endpoint.rb:
class CsrfTokenEndpoint
def self.call(env)
if env["PATH_INFO"] =~ /^\/csrf_token_endpoint/
session = env["rack.session"] || {}
token = session[:_csrf_token]
if token.nil?
token = SecureRandom.base64(32)
session[:_csrf_token] = token
end
[ 200, { "Content-Type" => "text/plain" }, [ token ] ]
else
[404, {"Content-Type" => "text/html"}, ["Not Found"]]
end
endend
我们的模版文件示例:
<meta name="csrf-param" value="authenticity_token"/>
<meta name="csrf-token" value="<!--# echo var="csrf_token" default="" encoding="none" -->"/>
Again you could make use of lua_shared_dict to store in memory the CSRF token for a particular session. This minimises the number of trips made to /csrf_token_endpoint.
3. 如何将分页数据 放入redis
普通分页
一般分页做缓存都是直接查找出来,按页放到缓存里,但是这种缓存方式有很多缺点。
如缓存不能及时更新,一旦数据有变化,所有的之前的分页缓存都失效了。
比如像微博这样的场景,微博下面现在有一个顶次数的排序。这个用传统的分页方式很难应对。
一种思路
最近想到了另一种思路。
数据以ID为key缓存到Redis里;
把数据ID和排序打分存到Redis的skip list,即zset里;
当查找数据时,先从Redis里的skip list取出对应的分页数据,得到ID列表。
用multi get从redis上一次性把ID列表里的所有数据都取出来。如果有缺少某些ID的数据,再从数据库里查找,再一块返回给用户,并把查出来的数据按ID缓存到Redis里。
在最后一步,可以有一些小技巧:
比如在缺少一些ID数据的情况下,先直接返回给用户,然后前端再用ajax请求缺少的ID的数据,再动态刷新。
还有一些可能用Lua脚本合并操作的优化,不过考虑到Lua脚本比较慢,可能要仔细测试。
如果是利用Lua脚本的话,可以在一个请求里完成下面的操作:
查找某页的所有文章,返回已缓存的文章的ID及内容,还有不在缓存里的文章的ID列表。
其它的一些东东:
Lua是支持LRU模式的,即像Memcached一样工作。但是貌似没有见到有人这样用,很是奇怪。
可能是用redis早就准备好把redis做存储了,也不担心内存的容量问题。