Redis 实战之通讯录补全
前言
自动补全的例子在很多场景中都能看到,如浏览器输入框的常用网址补全,或是百度搜索框的自动补全
实现功能
第一阶段:需要保留最新的100个联系人,并且能够根据前缀自动弹出关联联系人名称 ,如:to 要出现 tom,toms
第二阶段:使用有序集合来存储联系人,且不限制100人,但是只允许向同一个群组的人员发送
// 公共代码部分,以便后面不在重复写
$redis = new Redis();
$redis->connect('127.0.0.1','6379','3');
$redis->select(9);
前提条件
联系人名称都是要26个字母组成无任何特殊符号.
第一阶段功能实现
思路:最新的100个在考虑到负载的情况下,使用list列表来存储,消耗最少的存储空间,自动补全的部分由后端语言来实现,如PHP(别的也不会)
// 添加/更新联系人
function add_update_contact($conn, $user, $contact) {
$ac_list = "recent:" . $user;
$pipe = $conn->multi(Redis::PIPLINE);
$pipe->lRem($ac_list, $contact, 0); // 移除所有相同的联系人
$pipe->lpush($ac_list, $contact);
$pipe->ltrim($ac_list, 0, 99); // 删除所有100个之后的联系人
$pipe->exec();
echo "联系人添加成功";
return true;
}
// 移除联系人
function remove_contact($conn, $user, $contact) {
$ac_list = "recent:" . $user;
return $conn->lRem($ac_list, $contact, 0);
}
// $prefix 搜索前缀
function fetch_autoComplete_list($conn, $user, $prefix) {
$ac_list = "recent:" . $user;
$candidates = $conn->lRange($ac_list, 0, -1);
$matches = [];
foreach ($candidates as $candidate) {
if (strpos(strtolower($candidate), strtolower($prefix)) === 0) {
$matches[] = strtolower($candidate);
}
}
return $matches;
}
// 测试代码
add_update_contact($redis, 'mowang', 'john');
add_update_contact($redis, 'mowang', 'jojo');
add_update_contact($redis, 'mowang', 'join');
add_update_contact($redis, 'mowang', 'johns');
var_dump(fetch_autoComplete_list($redis, 'mowang', 'jo'));
第二阶段功能实现
// 加入群组
function join_guild($conn, $guild, $user) {
$members_list = "members:" . $guild;
$conn->zAdd($members_list, 0, $user);
echo "加入群组成功";
return true;
}
// 离开群组
function leave_guild($conn, $guild, $user) {
$members_list = "members:" . $guild;
$conn->zRem($members_list, $user);
echo "离开群组成功";
return true;
}
function find_prefix_range($prefix) {
$valid_chars = "`abcdefghijklmnopqrstuvwxyz{";
// $prefix = 'dqc' // 输出dq
$position = strpos($valid_chars, substr($prefix, 0, -1));
$suffix = $valid_chars[$position > 0 ? $position - 1 : 0];
return [
substr($prefix, 0, -1) . $suffix . "{",
$prefix . "{"
];
}
function autocomplete_on_prefix($conn, $guild, $user) {
list($start, $end) = find_prefix_range($prefix);
$identifier = Uuid::uuid4()->toString();
$start .= $identifier;
$end .= $identifier;
$zset_name = 'members:'. $guild;
$conn->zAdd($zset_name, 0, $start);
$conn->zAdd($zset_name, 0, $end);
// zrange 是闭合区间,即0,0也是有值的
while (1) {
try {
$conn->watch($zset_name);
$sindex = $conn->zRank($zset_name, $start);
$eindex = $conn->zRank($zset_name, $end);
$erange = min($sindex + 9, $eindex - 2); // 最多取10条
$trans = $conn->multi(Redis::PIPELINE);
$trans->zrem($zset_name, $start);
$trans->zrem($zset_name, $end);
$trans->zRange($zset_name, $sindex, $erange);
$baidu = $trans->exec();
$items = end($baidu);
break;
} catch (Exception $e) {
continue;
}
}
// 过滤掉带有`{`符号的数据
return array_filter(
$items,
function ($item) {
return strpos($item, '{') === false;
}
)
}
结语
后续会补充支持中文等不同方式的自动补全,此博文只作为个人记录用