记录一次在我的项目当中利用 Nginx + Lua + Redis 解决访问量的问题。
我的项目背景
我的项目背景是这样的。我的项目是文章散发平台,就是在一个平台上发送一篇带有图文的文章,发送文章时会抉择要把文章发送到哪些媒体资讯平台,比方:聚焦财经、太原新闻网...等这类平台。然而呢文章公布到了媒体资讯平台,那么文章在各媒体资讯平台的浏览量就获取不到了。
首次计划-放弃
最后想到了一种形式:依据文章在媒体资讯平台上的 URL 链接,通过程序抓取文章的浏览量,然而呢每家媒体资讯平台浏览量的展示形式都不一样,这样解决起来很是麻烦,所以就放弃了。
调整计划
文章资讯尽管是发送到了其余媒体资讯平台,然而呢这个图片的资源应用的还是咱们本人的服务,就想到了在图片进行动手的想法。
在每篇文章内容里都增加上一张固定的图片 http://www.example.com/code.jpg
,with 和 height 都设置为0,不占用页面上的空间,在加载文章时还能够加载图片。
<p>测试测试</p><p><img src="http://www.example.com/code.jpg?u=21&o=6&m=12-20" with="0" height="0" /></p>
图片上携带对于文章信息的参数,比方:u 示意用户,o 示意文章ID,m 示意公布的媒体资讯平台的ID,用“-”连贯。
在加载这张固定图片的时候,获取 URL 上的参数而后进行解决。
lua_nginx_module 模块
列一下须要用到的模块:
- lua_nginx_module (Nginx 的 Lua 模块)
- neturl(Lua 中解析 URL 的包)
- lua-resty-redis(Lua 中应用 Redis 的包)
在 Nginx 中应用 lua 的形式曾经被广泛应用了,所以采纳这种形式。上面演示整个配置流程:
引入 lua-resty-redis 和 neturl
咱们的服务器环境是应用 宝塔Linux
面板搭建的,外面曾经集成了 lua_nginx_module
模块,想要理解 Nginx 如何装置第三方模块的自行百度吧。
在 Nginx 的 http 中引入 lua 包:
http { lua_package_path "/www/server/nginx_module/lua-resty-redis/lib/?.lua;/www/server/nginx_module/neturl/lib/?.lua;/www/server/nginx_module/local_media_mapp/lib/?.lua;;";}
这里还引入了一个本人写的包,内容是我的项目须要的媒体资讯平台 ID 和域名映射关系,内容大抵如下:
local site_map = {["1"]="www.baidu.com",["2"]="www.sina.com",["20"]="www.ngxtest.vip",...}return site_map
图片参数解决
在 http 中引入了 lua 包之后,接下来在 nginx server 块中解决图片携带的参数:
这里配置两个网址来演示:www.nginxtest.vip 图片寄存的域名,www.ngxtest.vip 媒体资讯平台域名。
server{ listen 80; server_name www.nginxtest.vip; index index.php index.html index.htm default.php default.htm default.html; root /www/wwwroot/www.nginxtest.vip; location ~ /code.jpg$ { valid_referers www.nginxtest.vip www.ngxtest.vip; if ($invalid_referer) { rewrite ^/ http://www.nginxtest.vip/403.jpeg permanent; } access_by_lua_block { -- 获取 URL 参数和 header 头 local args = ngx.req.get_uri_args(); local receive_headers = ngx.req.get_headers(); function string:split(sep) local sep, fields = sep or "\t", {} local pattern = string.format("([^%s]+)", sep) self:gsub(pattern, function(c) fields[#fields+1] = c end) return fields end if args.u and args.m and args.o and receive_headers.referer then local url = require "net.url"; local uinfo = url.parse(receive_headers.referer); local referer = uinfo.host; --local site_map = {["12"]="www.baidu.com",["13"]="www.sina.com",["14"]="www.ngxtest.vip"}; local site_map = require "media.sitemap"; local mids = args.m; local midsarr = mids:split("-"); for k,v in pairs(midsarr) do if site_map[v] == referer then local redis = require "resty.redis"; local instance = redis:new(); local ok,err = instance:connect("127.0.0.1", 6379); if ok then local hashkey="media_visit"; local key = "media:"..args.u..":"..args.o..":"..v; local hexists_ok,hexists_err = instance:hexists(hashkey,key); if hexists_ok then local hincr_ok,hincr_err = instance:hincrby(hashkey,key,1); else local hset_ok,hset_err = instance:hset(hashkey,key,1); end end local close_ok, close_err = instance:close(); end end end } }}
这里应用的是 access_by_lua_block
块,没用 content_by_lua_block
块是因为 content 块间接返回了。
这是图片的防盗链解决,能够依据理论抉择是否增加:
valid_referers www.nginxtest.vip www.ngxtest.vip;if ($invalid_referer) { rewrite ^/ http://www.nginxtest.vip/403.jpeg permanent;}
这是自定义的字符串宰割为数组的函数:
-- 字符串宰割为数组的函数function string:split(sep) local sep, fields = sep or "\t", {} local pattern = string.format("([^%s]+)", sep) self:gsub(pattern, function(c) fields[#fields+1] = c end) return fieldsend
这一块内容是解决图片携带的参数程序,解决的思路简略叙述下:
- 先判断图片是否携带了指定的参数:u、o、m,和 header 头中是否存在 referer 信息。如果存在则进行下一步解决,不存在则寻找图片资源。
- 应用 URL 解析包解析 header 头的 referer。
- 引入自定义的媒体资讯平台ID 和域名映射包。
- 将参数 m 的值宰割为数组,而后循环数组。
- 判断以后的媒体资讯 ID 映射的域名是否和 header 中的 referer 相等。
- 引入 Redis 包并实例化。
- 这里应用的 Redis 的 Hash 类型,对于 Hash 类型就不多说了。
- 先判断指定的 field 是否在 hashkey 中,如果存在则值 + 1,不存在则执行 set 初始值为 1。
-- 判断图片的申请中是否存在字段:u、o、m,header 头中是否存在 referer 信息if args.u and args.m and args.o and receive_headers.referer then -- 引入 URL 解析包 local url = require "net.url"; -- 解析 header 头的 referer local uinfo = url.parse(receive_headers.referer); local referer = uinfo.host; -- 引入自定义的媒体资讯平台ID 和域名映射包 local site_map = require "media.sitemap"; -- 解析参数 m 为数组 local mids = args.m; local midsarr = mids:split("-"); -- 循环数组 for k,v in pairs(midsarr) do -- 判断以后的媒体资讯 ID 映射的域名是否和 header 中的 referer 相等 if site_map[v] == referer then -- 引入 Redis 包 local redis = require "resty.redis"; local instance = redis:new(); local ok,err = instance:connect("127.0.0.1", 6379); if ok then -- 这里采纳的将数据存入 Redis Hash 类型中 -- 定义 hashkey local hashkey="media_visit"; -- 组装 hash field local key = "media:"..args.u..":"..args.o..":"..v; -- 判断是否在 hashkey 中存在 local hexists_ok,hexists_err = instance:hexists(hashkey,key); if hexists_ok then -- 存在则值 + 1 local hincr_ok,hincr_err = instance:hincrby(hashkey,key,1); else -- 不存在则 set 到 hashkey 中,初始值为 1 local hset_ok,hset_err = instance:hset(hashkey,key,1); end end end endend
www.ngxtest.vip 援用图片的 HTML 代码
<!doctype html><html><head> <meta charset="utf-8"> <title>祝贺,站点创立胜利!</title> <style> .container { width: 60%; margin: 10% auto 0; background-color: #f0f0f0; padding: 2% 5%; border-radius: 10px } ul { padding-left: 20px; } ul li { line-height: 2.3 } a { color: #20a53a } </style></head><body> <div class="container"> <h1>祝贺, 站点创立胜利!</h1> <h3>这是默认index.html,本页面由零碎主动生成</h3> <ul> <li>本页面在FTP根目录下的index.html</li> <li>您能够批改、删除或笼罩本页面</li> <li>FTP相干信息,请到“面板零碎后盾 > FTP” 查看</li> </ul> </div> <img src="http://www.nginxtest.vip/img/map.png?u=21&m=12-20&o=6" width="0" height="0" /></body></html>
Redis hashkey 详情
统计入库
能够每天定时统计 Redis 中的数据更新入库,而后再清空 hashkey 的数据,避免我的项目运行工夫长了数据过大。
小结
自己主写的语言不是 Lua,以上的 Lua 代码是现学现卖写的,如果有语法上的谬误请指出。这个计划呢在施行上可能会存在肯定的风险性,比如说:伪造 header 中的 referer;更改图片申请参数值发动歹意申请(能够通过限流形式解决)等,如有想到的能够提出来,应用需谨慎,目前在我的项目中已应用也是十分审慎。通过这种计划还能够延申出统计用户 IP等。