1. 准备工作
-
环境说明
- java 8;redis7.2.2,redis集成RedisSearch、redisJson 模块;spring boot 2.5
- 在执行 redis 命令, 或者监控 程序执行的redis 指令时,可以采用 redisinsight查看,下载地址。
-
背景说明
- 需要对在线的用户进行搜索,之前是存储成 string, 每次搜索需要先全部遍历,然后加载到内存,然后进行筛选。十分消耗性能并且速度极慢。使用 redisJson + redisSearch 可以极大的优化查询性能
- 项目后期需要支持全文搜索。
-
实现思路:采用 RedisTemplate, 执行 lua 脚本。
2. 实现
2.1 配置
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置 redisTermplate, 配置 lua 脚本,便于 redisTemplate 执行[^2]
java
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport
{
@Bean
@SuppressWarnings(value = { "unchecked", "rawtypes" })
public RedisTemplate<String, Object> redisTemplate1(RedisConnectionFactory connectionFactory)
{
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
RedisSearchSerializer serializer = new RedisSearchSerializer(Object.class);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
// Hash的key也采用StringRedisSerializer的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
// lua 脚本配置
@Bean
public DefaultRedisScript<String> jsonSetScript()
{
DefaultRedisScript<String> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText("return redis.call('JSON.SET', KEYS[1], '$', ARGV[1]);");
redisScript.setResultType(String.class);
return redisScript;
}
@Bean
public DefaultRedisScript<Object> jsonGetScript()
{
DefaultRedisScript<Object> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText("return redis.call('JSON.GET', KEYS[1]);");
redisScript.setResultType(Object.class);
return redisScript;
}
@Bean
public DefaultRedisScript<List> jsonSearchScript()
{
DefaultRedisScript<List> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(
"local offset = tonumber(ARGV[2])\n" +
"local count = tonumber(ARGV[3])\n" +
"return redis.call('FT.SEARCH', KEYS[1], ARGV[1], 'return', 0, 'limit', offset, count);");
redisScript.setResultType(List.class);
return redisScript;
}
}
RedisSearchSerializer 序列化配置
java
import com.alibaba.fastjson2.JSON;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import java.nio.charset.Charset;
/**
* Redis使用FastJson序列化
*
* @author ruoyi
*/
public class RedisSearchSerializer<T> implements RedisSerializer<T> {
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private Class<T> clazz;
public RedisSearchSerializer(Class<T> clazz) {
super();
this.clazz = clazz;
}
@Override
public byte[] serialize(T t) throws SerializationException {
if (t == null) {
return new byte[0];
}
if (t instanceof String) {
return ((String)t).getBytes(DEFAULT_CHARSET);
}
return JSON.toJSONString(t).getBytes(DEFAULT_CHARSET);
}
@Override
public T deserialize(byte[] bytes) throws SerializationException {
if (bytes == null || bytes.length <= 0) {
return null;
}
String str = new String(bytes, DEFAULT_CHARSET);
// 不是 json 也不是 序列化的字符串,那就只能是数字,如果不是数字直接返回
if (!str.startsWith("{") && !str.startsWith("[") && !str.startsWith("\"") && !str.matches("^\\d*$")) {
return clazz.cast(str);
}
return JSON.parseObject(str, clazz);
}
}
数据实体类
java
@Data
public class LoginUser {
private String ipaddr;
private String username;
public LoginUser(String ipaddr, String username) {
this.ipaddr = ipaddr;
this.username = username;
}
}
2.2 封装 对 json的 操作
redisService
java
@Service
public class RedisService {
@Autowired
private RedisScript<String> jsonSetScript;
@Autowired
private RedisScript<Object> jsonGetScript;
@Autowired
private RedisScript<List> jsonSearchScript;
@Autowired
private RedisTemplate<String, Object> redisTemplate1;
public LoginUser getLoginUser(String uuid) {
String key = RedisKeys.LOGIN_TOKEN_KEY + uuid;
JSONObject obj = (JSONObject) redisTemplate1.execute(this.jsonGetScript, Collections.singletonList(key));
if (obj == null) {
return null;
}
return obj.toJavaObject(LoginUser.class);
}
public void setLoginUser(String uuid, LoginUser loginUser, int expireTime, TimeUnit unit) {
String key = RedisKeys.LOGIN_TOKEN_KEY + uuid;
redisTemplate1.execute(this.jsonSetScript, Collections.singletonList(key), loginUser);
redisCache.expire(key, expireTime, unit);
}
public Page<String> searchLoginUser(String query, Pageable page) {
List list = redisTemplate1.execute(
this.jsonSearchScript,
Collections.singletonList("login_tokens_idx"),
query, page.getOffset(), page.getPageSize());
Long total = (Long) list.get(0);
List<String> ids = new ArrayList<>();
for (int i = 1; i < list.size(); i++) {
ids.add(((String) list.get(i)).replaceAll(RedisKeys.LOGIN_TOKEN_KEY, ""));
}
return new PageImpl<>(ids, page, total);
}
public interface RedisKeys {
String LOGIN_TOKEN_KEY = "login_tokens1:";
}
}
2.3 在 redis 中创建索引
redis 创建索引[^1], 其中 ipaddr 是 IP 字段,包含 "." 等特殊字符,所以需要将 索引中的 ipaddr 设置成 tag 类型,才能搜索到[^3]
bash
# 连接redis, 如果使用 redisinsight 则不需要这步
redis-cli -a "password"
# 创建索引
FT.CREATE login_tokens_idx
on JSON prefix 1 "login_tokens1:"
SCHEMA $.ipaddr tag $.username text
3. 测试
java
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.concurrent.TimeUnit;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = YourApplication.class)
@ActiveProfiles("prod-local")
public class RedisServiceTest {
@Autowired
private RedisService redisService;
@Test
public void testSetAndGetLoginUser() {
LoginUser user = new LoginUser("192.168.1.1", "testUser");
redisService.setLoginUser("123456", user, 60, TimeUnit.SECONDS);
LoginUser getUser = redisService.getLoginUser("123456");
Assert.assertEquals(user.getIpaddr(), getUser.getIpaddr());
Assert.assertEquals(user.getUsername(), getUser.getUsername());
}
@Test
public void testDeleteLoginUser() {
LoginUser user = new LoginUser("192.168.1.1", "testUser");
redisService.setLoginUser("123456", user, 60, TimeUnit.SECONDS);
redisService.deleteLoginUser("123456");
LoginUser getUser = redisService.getLoginUser("123456");
Assert.assertNull(getUser);
}
@Test
public void testSearchLoginUser() {
// 添加测试数据
LoginUser user1 = new LoginUser("192.168.1.1", "user1");
LoginUser user2 = new LoginUser("192.168.1.2", "user2");
redisService.setLoginUser("123456", user1, 60, TimeUnit.SECONDS);
redisService.setLoginUser("789012", user2, 60, TimeUnit.SECONDS);
// 搜索测试
Page<String> page = redisService.searchLoginUser("user*", PageRequest.of(0, 10));
Assert.assertEquals(page.getTotalElements(), 2);
Assert.assertEquals(page.getContent().size(), 2);
Assert.assertTrue(page.getContent().contains("123456"));
Assert.assertTrue(page.getContent().contains("789012"));
}
}