模拟场景:
假如要搭建一个公司内部的交流系统,其中有一个功能就是用户可以通过搜索查找公司内部所有员工,为了方便用户快速方便查找,需要提供快速自动补全姓名的查询,比如用户输入“王”,然后提示以“王”开头的所有用户;
原理:
我们使用redis的有序集合数据结构,有序集合有个特性就是当所有成员的分值都相等的时候,有序集合将根据成员的名字来进行排序;当所有成员的分值都为0时,成员将按照字符串的二进制顺利徐进行排序。
假如:用户的名字都由小写字母组成,那么当输入"abc"的时候,那么以"abc"开头的所有字符将集中在从"abb{"(前驱字符串)到"abc{"(后继字符串)之间,因为在ASCII编码中,排在字母"z"之后的字符是"{"。
由于redis中不能保存中文字符,那么我们需要提供一个中文和字符互转的方法,方便我们操作redis。
public static String codeUnicode(String gbString) {
if (gbString == null) {
return "";
}
char[] utfChar = gbString.toCharArray();
String unicodeString = "";
for (int i=0; i<utfChar.length; i++) {
String hexB = Integer.toHexString(utfChar[i]);
if (hexB.length() <= 2) {
hexB = "00" + hexB;
}
unicodeString = unicodeString + "-" + hexB;
}
return unicodeString;
}
public static String decodeUnicode(String unicodeString) {
if (unicodeString == null) {
return "";
}
String[] unicodeArray = unicodeString.split("-");
StringBuffer stringBuffer = new StringBuffer();
if (unicodeArray != null && unicodeArray.length > 0) {
for (int i=0; i<unicodeArray.length; i++) {
String arrayString = unicodeArray[i].trim();
if (arrayString != null && !"".equals(arrayString)) {
char c = (char) Integer.parseInt(arrayString, 16);
stringBuffer.append(new Character(c).toString());
}
}
}
return stringBuffer.toString();
}
从代码中我们可以看到,我们把每一个字符转换成了16进制字符串,然后每个字符串之间用"-"隔开(方便后期我们把16进制字符串转换回来),所以转换后的字符串由字符"-"、数字和字母"a-f"组成,那么我们可以确定一个字符序列“,-0123456789abcdefg”,这个字符序列方便我们找到一个字符的前驱字符和后继字符。
/**
* 根据给出的前缀字符串计算出查找发范围
* 原理:
* 在redis的有序集合里面,当所有成员的分值都相等时,有序集合将按照成员的名字进行排序;而当所有成员的分值都是0时,成员按照字符串的二进制顺序排序;
* 因为假系人的姓名先转换为了16进制字符串,那么比-小的字符是',',比'f'大的字符是'g';
* 所以假如prefix='-00ff',那么所有已-00ff开头的字符串都在'-00feg'到'-00ffg'之间
* @param prefix
* @return
*/
private static final String VALID_CHARACTERS = ",-0123456789abcdefg";
private String[] findPrefixRange(String prefix) {
int posn = VALID_CHARACTERS.indexOf(prefix.charAt(prefix.length() - 1)); //查找出前缀字符串最后一个字符在列表中的位置
char suffix = VALID_CHARACTERS.charAt(posn > 0 ? posn - 1 : 0); //找出前驱字符
String start = prefix.substring(0, prefix.length() - 1) + suffix + 'g'; //生成前缀字符串的前驱字符串
String end = prefix + 'g'; //生成前缀字符串的后继字符串
return new String[]{start, end};
}
到此为止,我们前期的工作已经完成。
/**
* 根据输入的姓名匹配全名
* @param conn
* @param guild
* @param prefix
* @return
*/
private static final String guildName = "AUTO_COMPLETE";
public List<String> autocomplete(Jedis conn, String prefix) {
List<String> list = new ArrayList<>();
prefix = codeUnicode(prefix); //把输入的字符转换成16进制字符串,因为redis里面存的是每个字符对应的16进制字符串
String[] range = findPrefixRange(prefix);
String start = range[0];
String end = range[1];
String identifier = UUID.randomUUID().toString();
start += identifier;
end += identifier; // 防止多个群成员可以同时操作有序集合,将相同的前驱字符串和后继字符串插入有序集合
conn.zadd(guildName, 0, start);
conn.zadd(guildName, 0, end);
while (true) {
conn.watch(guildName);
int sindex = conn.zrank(guildName, start).intValue();
int eindex = conn.zrank(guildName, end).intValue(); //找出两个插入元素的位置
int erange = Math.min(sindex + 9, eindex-2); //因为最多展示10个,所以计算出结束为止
Transaction transaction = conn.multi();
transaction.zrem(guildName, start);
transaction.zrem(guildName, end);
transaction.zrange(guildName, sindex, erange);
List<Object> results = transaction.exec();
if (results != null) {
Set<String> set = (Set<String>) results.get(results.size() - 1);
list.addAll(set);
break;
}
}
ListIterator<String> iterator = list.listIterator();
// 这里过滤多个成员添加前驱字符串和后继字符串引起的不符合的数据
while (iterator.hasNext()) {
String string = iterator.next();
if (string.indexOf("g") != -1) {
iterator.remove();
} else {
iterator.set(decodeUnicode(string)); //把16进制字符串转换回来
}
}
return list;
}
/**
* 添加姓名到redis中
*/
public void joinGuild(Jedis conn, String member) {
conn.zadd(codeUnicode(guildName), 0, member);
}