• 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 配置

    引入依赖

            
                org.springframework.boot
                spring-boot-starter-data-redis
            
    
    • 1
    • 2
    • 3
    • 4

    配置 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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    数据实体类

    @Data
    public class LoginUser {
        private String ipaddr;
        private String username;
    
        public LoginUser(String ipaddr, String username) {
            this.ipaddr = ipaddr;
            this.username = username;
        }
    }
     
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    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:";
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    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"));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
  • 相关阅读:
    Docker:本地目录挂载
    学习笔记2--自动驾驶汽车关键技术
    【DevPress】V2.4.1版本发布,增加抽奖组件
    关于亚马逊 CodeWhisperer 的使用体验
    Django框架基本语法(二)
    『Android』Toolbar+DrawerLayout+NavigationView实现类似QQ侧边栏效果
    东南电子开启申购:客户集中度较高,预计上市时市值约18亿元
    DTO 模式(数据传输对象)
    关于序列化协议,你需要知道的一些内容(3)
    网站打不开的九个因素
  • 原文地址:https://blog.csdn.net/qq_22783587/article/details/134446850