基于SpringBoot + RedisJSON + RedisSearch:用 Redis 替代部分 MySQL,实现高性能文档查询

今天咱们聊聊一个在高并发场景下很有意思的话题:用Redis做文档查询。

传统关系型数据库的局限

在我们的日常开发工作中,经常会遇到这样的场景:

  • 用户表有几百万条数据,复杂的联合查询响应时间过长
  • 电商商品信息查询需要全文搜索功能,MySQL性能不佳
  • 配置信息、缓存数据需要结构化存储和快速查询
  • 频繁的分页查询导致数据库压力过大

传统的MySQL等关系型数据库在处理半结构化数据查询时,性能往往不尽如人意。今天我们就来聊聊如何用RedisJSON + RedisSearch来解决这些问题。

为什么选择RedisJSON + RedisSearch

相比传统的数据库方案,RedisJSON + RedisSearch有以下优势:

  • 文档存储:原生支持JSON文档存储和查询
  • 全文搜索:内置强大的全文搜索引擎
  • 高性能:内存存储,查询速度快
  • 灵活Schema:支持动态字段,无需预定义表结构
  • 丰富索引:支持文本、数值、地理等多种索引类型

解决方案思路

今天我们要解决的,就是如何用SpringBoot + RedisJSON + RedisSearch构建一个高性能的文档查询系统。

核心思路是:

  1. JSON文档存储:使用RedisJSON存储结构化数据
  2. 索引构建:利用RedisSearch创建高效索引
  3. 复合查询:支持多条件组合查询
  4. 性能优化:合理设计文档结构和索引策略

Redis环境准备

1. 安装Redis模块

# 需要安装RedisJSON和RediSearch模块
docker run -p 6379:6379 \
  -v redis-data:/data \
  redislabs/redisearch:latest

2. 基础配置

spring:
  redis:
    host: localhost
    port: 6379
    timeout: 2000ms
    jedis:
      pool:
        max-active: 20
        max-idle: 10
        min-idle: 5

SpringBoot集成实现

1. 依赖配置

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>4.3.1</version>
</dependency>
<dependency>
    <groupId>com.redis</groupId>
    <artifactId>redis-om-spring</artifactId>
    <version>0.9.0</version>
</dependency>

2. 文档实体定义

@Document(value = "users")
@Data
public class UserDoc {
    @Id
    private String id;
    
    @Indexed
    private String userId;
    
    @Indexed
    private String name;
    
    @Indexed
    private String email;
    
    @Indexed
    private Integer age;
    
    @Indexed
    private String department;
    
    @Indexed
    private String position;
    
    @Indexed
    private LocalDateTime createTime;
    
    @Indexed
    private LocalDateTime updateTime;
    
    // 动态字段,支持灵活扩展
    @Indexed
    private Map<String, Object> extraInfo;
}

3. Repository接口

@Repository
public interface UserDocRepository extends DocumentRepository<UserDoc, String> {
    
    // 基础查询
    List<UserDoc> findByNameContaining(String name);
    
    // 复合查询
    List<UserDoc> findByDepartmentAndAgeGreaterThan(String department, Integer minAge);
    
    // 范围查询
    List<UserDoc> findByCreateTimeBetween(LocalDateTime start, LocalDateTime end);
    
    // 排序查询
    @Query(sortBy = @SortBy(field = "createTime", direction = Sort.Direction.DESC))
    Page<UserDoc> findByDepartment(String department, Pageable pageable);
    
    // 聚合查询
    @Aggregation(pipeline = {
        @Aggregation.Group(value = "department", 
                          fields = {@Aggregation.Field(name = "count", expression = "COUNT(*)")})
    })
    List<Map<String, Object>> countByDepartment();
}

高级查询功能

1. 全文搜索

@Service
public class FullTextSearchService {
    
    @Autowired
    private RedisModulesCommands commands;
    
    public List<UserDoc> searchUsers(String query) {
        // 使用RediSearch进行全文搜索
        SearchQuery searchQuery = new SearchQuery(query)
            .limit(0, 100)
            .sortBy("createTime", Order.DESC);
        
        return commands.ftSearch("idx:users", searchQuery)
            .getDocuments()
            .stream()
            .map(this::convertToUserDoc)
            .collect(Collectors.toList());
    }
    
    public List<UserDoc> advancedSearch(UserSearchCriteria criteria) {
        StringBuilder queryBuilder = new StringBuilder("*");
        
        if (StringUtils.hasText(criteria.getName())) {
            queryBuilder.append(" @name:")
                .append(criteria.getName());
        }
        
        if (StringUtils.hasText(criteria.getEmail())) {
            queryBuilder.append(" @email:")
                .append(criteria.getEmail());
        }
        
        if (criteria.getMinAge() != null) {
            queryBuilder.append(" @age:[")
                .append(criteria.getMinAge())
                .append(" ")
                .append(criteria.getMaxAge() != null ? criteria.getMaxAge() : "+inf")
                .append("]");
        }
        
        SearchQuery searchQuery = new SearchQuery(queryBuilder.toString())
            .limit(0, criteria.getPageSize())
            .sortBy(criteria.getSortField(), 
                   "desc".equalsIgnoreCase(criteria.getSortOrder()) ? Order.DESC : Order.ASC);
        
        return commands.ftSearch("idx:users", searchQuery)
            .getDocuments()
            .stream()
            .map(this::convertToUserDoc)
            .collect(Collectors.toList());
    }
}

2. 地理位置查询

@Data
public class LocationDoc {
    @Id
    private String id;
    
    @Indexed
    private String name;
    
    @Indexed
    private Point location; // Redis支持的地理坐标
    
    @Indexed
    private String city;
}

@Service
public class LocationSearchService {
    
    public List<LocationDoc> searchNearby(Point center, double radiusInKm) {
        // 搜索指定范围内的地点
        SearchQuery searchQuery = new SearchQuery("*")
            .geFilter("location", center.getX(), center.getY(), radiusInKm, GeoUnit.KILOMETERS)
            .limit(0, 50);
        
        return commands.ftSearch("idx:locations", searchQuery)
            .getDocuments()
            .stream()
            .map(this::convertToLocationDoc)
            .collect(Collectors.toList());
    }
}

性能优化策略

1. 索引设计

@Component
public class IndexCreationService {
    
    @PostConstruct
    public void createIndexes() {
        // 创建复合索引
        Schema schema = new Schema()
            .addField(new TextField("name", 1.0))
            .addField(new TextField("email", 1.0))
            .addField(new NumericField("age"))
            .addField(new TextField("department", 1.0))
            .addField(new NumericField("createTime"));
        
        commands.ftCreate("idx:users", FTCreateParams.createParams().on(IndexDataType.HASH), schema);
    }
}

2. 批量操作

@Service
public class BatchOperationService {
    
    public void batchUpsertUsers(List<UserDoc> users) {
        // 批量插入/更新
        users.forEach(user -> {
            String key = "user:" + user.getUserId();
            commands.jsonSet(key, Path.ROOT_PATH, user);
        });
    }
    
    public List<UserDoc> batchGetUsers(List<String> userIds) {
        // 批量获取
        List<String> keys = userIds.stream()
            .map(id -> "user:" + id)
            .collect(Collectors.toList());
        
        return commands.jsonMGet(keys, Path.ROOT_PATH)
            .stream()
            .map(json -> JSON.parseObject((String) json[0], UserDoc.class))
            .collect(Collectors.toList());
    }
}

数据同步策略

1. 实时同步

@Service
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public class DataSyncService {
    
    public void handleUserUpdated(UserUpdatedEvent event) {
        User user = event.getUser();
        
        // 同步到Redis
        UserDoc userDoc = convertToDoc(user);
        String key = "user:" + user.getId();
        
        commands.jsonSet(key, Path.ROOT_PATH, userDoc);
    }
    
    public void handleUserDeleted(UserDeletedEvent event) {
        String key = "user:" + event.getUserId();
        commands.del(key);
    }
}

2. 增量同步

@Component
public class IncrementalSyncService {
    
    @Scheduled(fixedRate = 300000) // 每5分钟执行一次
    public void syncRecentChanges() {
        LocalDateTime syncTime = getLastSyncTime();
        LocalDateTime currentTime = LocalDateTime.now();
        
        // 获取最近变更的数据
        List<User> recentUsers = userService.findUpdatedAfter(syncTime, currentTime);
        
        // 批量同步到Redis
        batchUpsertUsers(recentUsers.stream()
            .map(this::convertToDoc)
            .collect(Collectors.toList()));
        
        updateLastSyncTime(currentTime);
    }
}

查询优化技巧

1. 分页查询优化

@Service
public class OptimizedQueryService {
    
    public Page<UserDoc> paginatedSearch(UserSearchCriteria criteria, Pageable pageable) {
        // 构建查询条件
        String query = buildQuery(criteria);
        
        SearchQuery searchQuery = new SearchQuery(query)
            .limit((int) pageable.getOffset(), pageable.getPageSize())
            .sortBy(pageable.getSort().get().findFirst().get().getProperty(), 
                   pageable.getSort().get().findFirst().get().isDescending() ? Order.DESC : Order.ASC);
        
        // 执行查询
        SearchResults<Document> results = commands.ftSearch("idx:users", searchQuery);
        
        List<UserDoc> content = results.getDocuments()
            .stream()
            .map(this::convertToUserDoc)
            .collect(Collectors.toList());
        
        // 获取总数(这里可以考虑缓存总数以提高性能)
        long total = getTotalCount(query);
        
        return new PageImpl<>(content, pageable, total);
    }
    
    private long getTotalCount(String query) {
        SearchQuery countQuery = new SearchQuery(query).limit(0, 0);
        SearchResults<Document> results = commands.ftSearch("idx:users", countQuery);
        return results.getTotalResults();
    }
}

2. 缓存策略

@Service
public class CachedSearchService {
    
    @Cacheable(value = "userSearch", key = "#criteria.hashCode()")
    public List<UserDoc> searchWithCache(UserSearchCriteria criteria) {
        return advancedSearch(criteria);
    }
    
    @CacheEvict(value = "userSearch", allEntries = true)
    @EventListener
    public void handleUserChanged(AbstractUserEvent event) {
        // 用户数据变更时清除相关缓存
    }
}

实际应用场景

1. 用户信息查询

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @Autowired
    private UserDocRepository userRepository;
    
    @GetMapping("/search")
    public Page<UserDoc> searchUsers(
            @RequestParam(required = false) String name,
            @RequestParam(required = false) String department,
            @RequestParam(required = false) Integer minAge,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {
        
        Pageable pageable = PageRequest.of(page, size);
        
        if (StringUtils.hasText(name)) {
            return userRepository.findByNameContaining(name, pageable);
        } else if (StringUtils.hasText(department)) {
            return userRepository.findByDepartment(department, pageable);
        } else {
            return userRepository.findAll(pageable);
        }
    }
}

2. 配置信息管理

@Document(value = "configs")
@Data
public class ConfigDoc {
    @Id
    private String id;
    
    @Indexed
    private String configKey;
    
    @Indexed
    private String configType;
    
    @Indexed
    private String tenantId;
    
    @Indexed
    private Boolean enabled = true;
    
    @Indexed
    private LocalDateTime expireTime;
    
    private Object configValue; // 支持任意类型配置值
}

注意事项

在使用RedisJSON + RedisSearch时,需要注意以下几点:

  1. 数据持久化:合理配置Redis持久化策略,避免数据丢失
  2. 内存管理:监控内存使用情况,设置合理的过期时间
  3. 索引维护:定期清理不必要的索引,避免性能下降
  4. 数据一致性:确保Redis与主数据库的数据同步
  5. 安全配置:生产环境要配置密码认证和网络隔离

总结

通过SpringBoot + RedisJSON + RedisSearch的组合,我们可以构建一个高性能的文档查询系统。这种方案特别适合半结构化数据的快速查询场景,能够显著提升查询性能,减轻关系型数据库的压力。

当然,Redis并不是银弹,需要根据具体业务场景选择合适的存储方案。在某些对事务一致性要求极高的场景,仍然需要依赖传统的关系型数据库。

希望这篇文章对你有所帮助!如果你觉得有用,欢迎关注【服务端技术精选】公众号,获取更多后端技术干货。


原文首发于 www.jiangyi.space

转载请注明出处


标题:基于SpringBoot + RedisJSON + RedisSearch:用 Redis 替代部分 MySQL,实现高性能文档查询
作者:jiangyi
地址:http://jiangyi.space/articles/2026/01/21/1768974100295.html

    0 评论
avatar