场景一:设备在线状态保持
需求
有若干台设备,设备具备定时向server发送heartbeat的功能,后台需要在设备列表实时监控设备的在线情况(允许有不大于5分钟的时间延迟)
实现
在openresty配置中加入用于处理心跳请求的location块,如下
location /device/heartbeat {
default_type text/html;
lua_need_request_body on;
content_by_lua_file src/heartbeat_detector.lua;
}
lua脚本
-- 获取请求方式,只允许POST请求
local request_method = ngx.var.request_method
-- ngx.log(ngx.ERR, "headers:"..ngx.var.http_user_agent)
if request_method ~= "POST" then
ngx.exit(ngx.HTTP_NOT_ALLOWED)
end
local function close_redis(red)
if not red then
return
end
--释放连接(连接池实现)
local pool_max_idle_time = 10000 --毫秒
local pool_size = 100 --连接池大小
local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
if not ok then
ngx.log(ngx.ERR, "set redis keepalive error : ", err)
end
end
-- 引入json处理库
local cjson = require "cjson"
-- 临时文件读取函数
function getFile(file_name)
local f = assert(io.open(file_name, 'r'))
local string = f:read("*all")
f:close()
return string
end
-- 获取请求体
ngx.req.read_body()
-- 获取请求body
local data = ngx.req.get_body_data()
-- 如果请求body为空,则从临时文件获取
if nil == data then
local file_name = ngx.req.get_body_file()
if file_name then
data = getFile(file_name)
end
end
-- 将data转换为object
local obj = cjson.decode(data)
-- 如果当前请求为心跳维持请求
if obj.Name == "KeepAlive" then
--[[
初始化redis
]]
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(1000)
local host = 'ip'
local port = port
local ok, err = red:connect(host,port)
if not ok then
return close_redis(red)
end
-- 请注意这里 auth 的调用过程
local count
ount, err = red:get_reused_times()
if 0 == count then
ok, err = red:auth("password")
if not ok then
ngx.say("failed to auth: ", err)
return
end
elseif err then
ngx.say("failed to get reused times: ", err)
return
end
-- redis前缀
local prefix = 'device_heartbeat:'
-- 缓存心跳
res, err = red:set(prefix..obj.DeviceId, 1, 'EX', 300)
end
需要保证在项目的已有路由中没有/device/heartbeat
,此时/device/heartbeat
的请求会交给lua脚本处理,相对于php性能消耗更少,php后台获取到数据后可直接在redis中查找该设备对应的心跳key是否存在来确认设备是否在线
场景二:小区访客单元开门密码
需求
当前项目的另外一个需求是:小区用户app具备访客密码生成/刷新功能,密码的过期时间是30分钟(使用被动过期实现),用户可将现有密码通过短信发送至访客手机,访客可通过密码进行小区单元门开门,同时对用户每天发送短信上限进行限制(5次),每次开门需要生成包含访客信息和被访人信息的记录
实现
使用两个key:value完成基本的实现,id_prefix_user_id:password,pwd_prefix_community_uuid_pwd:user_id
第一个key:用于app请求访客密码时判断是否需要重新生成,以及刷新密码时删除原有密码
第二个key:用于访客进行密码开锁时识别对应的用户,将访客和用户信息写入到访客记录
用户生成访客密码时拼接上小区的id(密码长度是4位,需要保证密码数量在10000以内,因此使用小区将用户分组减少数据量)
以下为laravel框架的代码实现(项目使用了jwt,二次封装了laravel原有的response()->json()
方法为beJson()
)
<?php
namespace App\Http\Controllers;
use App\Traits\JsonResponse;
use App\Traits\SendSMS;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
class UserController extends Controller
{
use JsonResponse, SendSMS;
private $auth;
//用户访客开门id前缀
const USER_VISITOR_ID_PREFIX = 'user_visitor_id:';
//用户访客开门密码前缀
const USER_VISITOR_PWD_PREFIX = 'user_visitor_pwd:';
//短信发送次数
const SMS_TIMES_PREFIX = 'user_sms_times:';
//设置用户ID,密码
const INIT_PWD = <<<EOF
-- 用户ID
redis.pcall('set', KEYS[1], ARGV[1], 'EX', ARGV[3]);
-- 访客密码
redis.pcall('set', KEYS[2], ARGV[2], 'EX', ARGV[3]);
EOF;
//延长过期时间
const EXTEND_EXPIRE = <<<EOF
-- 用户ID
redis.pcall('expire', KEYS[1], ARGV[1]);
-- 访客密码
redis.pcall('expire', KEYS[2], ARGV[1]);
EOF;
public function __construct()
{
$this->auth = auth()->userOrFail();
}
/*
* 访客密码管理
* */
public function visitorPwd()
{
//访客[开门,提交访客表单]密码
//判断密码是否存在,存在:读取 不存在:生成
$pwd = Redis::get(self::USER_VISITOR_ID_PREFIX . $this->auth->uuid);
if (!$pwd) {
//去重生成
do {
//生成密码
$pwd = mt_rand(1000, 9999);
} while (Redis::exists(self::USER_VISITOR_PWD_PREFIX . $this->auth->community_uuid . ':' . $pwd));
try {
Redis::eval(self::INIT_PWD, 2, self::USER_VISITOR_ID_PREFIX . $this->auth->uuid, self::USER_VISITOR_PWD_PREFIX . $this->auth->community_uuid . ':' .$pwd, $pwd, $this->auth->uuid, 1800);
//取值
$pwd = (string)$pwd;
} catch (\Exception $exception) {
return $this->beJson(-1, '刷新失败:' . $exception->getMessage());
}
}
return $this->beJson(0, 'success', compact('pwd'));
}
/*
* 访客密码刷新
* */
public function refreshVisitorPwd()
{
//删除当前可能存在的访客密码
$old_pwd = Redis::get(self::USER_VISITOR_ID_PREFIX . $this->auth->uuid);
Redis::del(self::USER_VISITOR_ID_PREFIX . $this->auth->uuid);
if ($old_pwd) Redis::del(self::USER_VISITOR_PWD_PREFIX . $this->auth->community_uuid . ':' .$old_pwd);
//去重生成
do {
//生成密码
$pwd = mt_rand(1000, 9999);
} while (Redis::exists(self::USER_VISITOR_PWD_PREFIX . $this->auth->community_uuid . ':' .$pwd));
try {
Redis::eval(self::INIT_PWD, 2, self::USER_VISITOR_ID_PREFIX . $this->auth->uuid, self::USER_VISITOR_PWD_PREFIX . $this->auth->community_uuid . ':' .$pwd, $pwd, $this->auth->uuid, 1800);
//取值
$pwd = (string)$pwd;
return $this->beJson(0, 'success', compact('pwd'));
} catch (\Exception $exception) {
return $this->beJson(-1, '刷新失败:' . $exception->getMessage());
}
}
/*
* 向访客发送临时密码短信
* */
public function sendPwdSms()
{
//获取用户当天已取消次数
$send_times = Redis::get(self::SMS_TIMES_PREFIX . $this->auth->uuid);
//每天允许最大次数
$max = 5;
if ($send_times && $send_times >= $max) {
return $this->beJson(-1, '发送失败:每天最多只能发送' . $max . '次访客密码');
}
//访客手机号
$mobile = request()->get('mobile');
//手机号校验
if (!is_numeric($mobile) || (strlen($mobile) != 11)) return $this->beJson(-1, '请填写11位手机号码');
//获取当前临时密码
$pwd = Redis::get(self::USER_VISITOR_ID_PREFIX . $this->auth->uuid);
if (!$pwd) {
return $this->beJson(-1, '发送失败:访客密码获取失败');
}
//短信通知内容
$content = sprintf('您的小区单元临时开锁密码为:%s', $pwd);
//短信发送
if (false !== $this->sendSMS($mobile, $content)) {
//成功,刷新当前临时密码过期时间,避免过快过期
Redis::eval(self::EXTEND_EXPIRE, 2, self::USER_VISITOR_ID_PREFIX . $this->auth->uuid, self::USER_VISITOR_PWD_PREFIX . $this->auth->community_uuid . ':' .$pwd, 1800);
//更新用户当天已取消次数
if ($send_times) {
//递增
Redis::incr(self::SMS_TIMES_PREFIX . $this->auth->uuid);
} else {
//新增
//获取当前时间-24点时间差
$current = time();
$next = strtotime(date('Y-m-d', $current) . ' 00:00:00' . "+1 days");
$expire = $next - $current;
//缓存当天取消次数
Redis::set(self::SMS_TIMES_PREFIX . $this->auth->uuid, 1, 'EX', $expire);
}
return $this->beJson(0, 'success');
} else {
//失败
return $this->beJson(-1, '短信发送失败,请稍后再试');
}
}
}
临时密码的实现完全脱离了mysql,减少性能损耗