spring boot 集成 RedisSearch 和 RedisJSON

1. 准备工作

  1. 环境说明

    • java 8;redis7.2.2,redis集成RedisSearch、redisJson 模块;spring boot 2.5
    • 在执行 redis 命令, 或者监控 程序执行的redis 指令时,可以采用 redisinsight查看,下载地址
  2. 背景说明

    • 需要对在线的用户进行搜索,之前是存储成 string, 每次搜索需要先全部遍历,然后加载到内存,然后进行筛选。十分消耗性能并且速度极慢。使用 redisJson + redisSearch 可以极大的优化查询性能
    • 项目后期需要支持全文搜索。
  3. 实现思路:采用 RedisTemplate, 执行 lua 脚本。

2. 实现

2.1 配置

引入依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

配置 redisTermplate, 配置 lua 脚本,便于 redisTemplate 执行[^2]

@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 序列化配置


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);
    }
}

数据实体类

@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


@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]

# 连接redis, 如果使用 redisinsight 则不需要这步
redis-cli -a "password"
# 创建索引
FT.CREATE login_tokens_idx 
  on JSON prefix 1 "login_tokens1:"
  SCHEMA $.ipaddr tag $.username text

3. 测试


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"));
    }
}
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值