Redis键和值的设计原则/key 键格式

复杂业务场景下的Redis Key设计

上面讲了简单的key存储,如 xdd的存储,此时普通的需求可以满足;然而在实际业务中,往往key键的存储会非常的复杂,比如我们现在有一个需求:

需求:根据基础数据系统中的数据字典类型查询对应的字典集合

这时,我们需要关注的业务就变得复杂了,就不能使用常规的key键存储方式,上面的需求大致可以拆分为:

系统:基础数据系统 模块:数据字典 方法:根据数据字典类型查询 参数:字典类型

系统:基础数据系统
模块:数据字典
方法:根据数据字典类型查询
参数:字典类型

为什么要这样拆分呢?为了可读性;也为了抽象出key存储规则;因为业务复杂情况下,我们定义的key键太多时就不便于管理,也不便于查找,以 系统-模块-方法-参数 这样的规则定义,我们可以很清晰的了解redis key存储的值是做了什么事情

common:sys:sex:1 男 common:sys:sex:0 女 common:page:title 欢迎使用XX管理系统 user:1 {id:1.name:小明} user:2 {id:2.name:小李}

common:sys:sex:1 男
common:sys:sex:0 女
common:page:title 欢迎使用XX管理系统
user:1  {id:1.name:小明}
user:2  {id:2.name:小李}

这个在使用工具去查看的时候就可以看出层级关系啦

<think>我们之前已经讨论过Redis没有直接通过查找的命令,但提供了多种解决方案。现在,我将从Java开发者的角度,详细说明如何通过Java代码实现根据查找的需求,并分析各种方法的适用场景。 ### 解决方案总览 #### 方法1:使用SCAN命令迭代(适用于开发环境或小数据集) **原理**:通过分批次扫描所有,避免阻塞Redis服务,然后逐一比对。 ```java import redis.clients.jedis.Jedis; import redis.clients.jedis.ScanParams; import redis.clients.jedis.ScanResult; import java.util.ArrayList; import java.util.List; public class RedisValueSearch { public static List<String> findKeysByValue(Jedis jedis, String targetValue) { List<String> matchingKeys = new ArrayList<>(); String cursor = ScanParams.SCAN_POINTER_START; ScanParams scanParams = new ScanParams().count(100); // 每批扫描100个 do { ScanResult<String> scanResult = jedis.scan(cursor, scanParams); List<String> keys = scanResult.getResult(); cursor = scanResult.getCursor(); for (String key : keys) { // 注意:这里假所有都是字符串类型,如果是其他类型需要调整 String value = jedis.get(key); if (targetValue.equals(value)) { matchingKeys.add(key); } } } while (!cursor.equals(ScanParams.SCAN_POINTER_START)); return matchingKeys; } } ``` **缺点**: - 性能随数据量线性下降 - 可能返回重复(需自行去重) - 不适用于集群环境(需分节点扫描) #### 方法2:维护反向索引(推荐生产环境使用) **原理**:在写入数据时,同步维护一个哈希表作为反向索引(->)。 ```java public class RedisIndexedStorage { private Jedis jedis; public void set(String key, String value) { // 保存原始对 jedis.set(key, value); // 维护反向索引:使用作为哈希表的字段,作为 jedis.hset("reverse_index", value, key); } public String getKeyByValue(String targetValue) { // 直接从哈希表获取对应的 return jedis.hget("reverse_index", targetValue); } // 处理重复的情况:使用集合存储多个 public void setWithMultiKeys(String key, String value) { jedis.set(key, value); jedis.sadd("idx:" + value, key); } public Set<String> getKeysByValue(String targetValue) { return jedis.smembers("idx:" + targetValue); } } ``` #### 方法3:使用RediSearch模块(企业级解决方案) **原理**:利用Redis的全文搜索模块建立二级索引。 ```java // 前置条件:已安装RediSearch模块 // 创建索引(通常在应用初始化时执行) public void createIndex(Jedis jedis) { String schema = "value TEXT"; jedis.sendCommand(ModuleCommand.FT_CREATE, "myIdx", "ON", "HASH", "PREFIX", "1", "user:", "SCHEMA", schema); } // 查询示例 public Set<String> searchKeysByValue(Jedis jedis, String targetValue) { String query = "@value:" + targetValue; SearchResult result = jedis.ftSearch("myIdx", query); Set<String> keys = new HashSet<>(); for (Document doc : result.getDocuments()) { keys.add(doc.getId()); } return keys; } ``` ### 关问题分析 #### 为什么Redis不原生支持通过? 1. **计哲学**:Redis存储,优化方向是查询(O(1)复杂度) 2. **性能考量**:反向搜索需要遍历所有数据(O(n)复杂度) 3. **存储结构**:不同数据类型(哈希、列表等)无法统一反向查找 #### 重复处理策略 | 场景 | 解决方案 | 代码示例 | |---------------------|------------------------------|------------------------------------------| | 唯一(如ID) | 哈希表存储(->) | `jedis.hset("index", value, key)` | | 可能重复(如状态)| 集合存储(->集合) | `jedis.sadd("index:"+value, key)` | | 需要排序 | 有序集合存储(带权重) | `jedis.zadd("index:"+value, score, key)` | ### 生产环境注意事项 1. **事务保证**: ```java // 使用事务确保索引数据一致性 Transaction tx = jedis.multi(); tx.set(key, value); tx.hset("reverse_index", value, key); tx.exec(); ``` 2. **过期同步**: ```java // 过期时同步清理索引 String luaScript = "local key = KEYS[1] " + "local value = redis.call('GET', key) " + "redis.call('EXPIRE', key, ARGV[1]) " + "redis.call('EXPIRE', 'reverse_index:'..value, ARGV[1])"; jedis.eval(luaScript, 1, key, "3600"); ``` 3. **集群支持**: ```java // 在Redis集群中,反向索引必须与数据存储在同一个槽位 public void setInCluster(JedisCluster jedis, String key, String value) { // 强制索引使用相同哈希槽 int slot = JedisClusterCRC16.getSlot(key); String indexedKey = "{tag}" + value; // 使用相同哈希标签 jedis.set(key, value); jedis.hset(indexedKey, value, key); // 确保indexedKeykey在同一个槽位 } ``` ### 性能优化技巧 - **批量扫描优化**:调整SCAN的COUNT参数(建议100-1000) - **管道加速**:使用管道批量获取 ```java Pipeline p = jedis.pipelined(); for (String key : keysBatch) { p.get(key); } List<Object> values = p.syncAndReturnAll(); ``` - **异步索引**:对于非关数据,使用异步更新索引 ```java executor.submit(() -> { jedis.hset("async_index", value, key); }); ``` --- §§相关问题§§: 1. Redis集群模式下如何保证反向索引与原始数据在同一个槽位? 2. 使用RediSearch模块建立索引时,如何避免脏数据? 3. 如何实现Redis对的模糊查询(非精确匹配)? 4. 当反向索引的数据量极大时,如何优化内存使用?</think>### Redis 根据查找的 Java 实现方案 在 Redis 中,**没有直接通过查找的原生命令**,因为 Redis 是高性能存储,优化了的查询(O(1) 复杂度),但的反向查询需要额外处理。以下是完整的 Java 解决方案: #### 方案 1:使用 SCAN 命令遍历(适合开发环境) ```java import redis.clients.jedis.Jedis; import redis.clients.jedis.ScanParams; import redis.clients.jedis.ScanResult; import java.util.ArrayList; import java.util.List; public class RedisValueFinder { public static List<String> findKeysByValue(Jedis jedis, String targetValue) { List<String> matchedKeys = new ArrayList<>(); String cursor = "0"; ScanParams scanParams = new ScanParams().count(100); // 每批扫描100个 do { ScanResult<String> scanResult = jedis.scan(cursor, scanParams); cursor = scanResult.getCursor(); for (String key : scanResult.getResult()) { // 仅处理字符串类型(其他类型需调整) if (jedis.type(key).equals("string")) { String value = jedis.get(key); if (targetValue.equals(value)) { matchedKeys.add(key); } } } } while (!cursor.equals("0")); return matchedKeys; } public static void main(String[] args) { try (Jedis jedis = new Jedis("localhost", 6379)) { List<String> keys = findKeysByValue(jedis, "user@example.com"); System.out.println("匹配的: " + keys); } } } ``` **特点**: - 使用 `SCAN` 代替 `KEYS *` 避免阻塞 - 分批处理降低内存压力 - 时间复杂度 O(n),仅适合小数据集 #### 方案 2:维护反向索引(生产推荐) ```java import redis.clients.jedis.Jedis; import redis.clients.jedis.Transaction; public class ReverseIndexManager { // 写入数据时创建双向索引 public static void setWithIndex(Jedis jedis, String key, String value) { Transaction tx = jedis.multi(); // 1. 存储原始数据 tx.set(key, value); // 2. 创建反向索引: -> 集合 tx.sadd("reverse_index:" + value, key); // 3. 记录的映射关系 tx.hset("value_to_keys", value, key); tx.exec(); } // 通过查找 public static List<String> getKeysByValue(Jedis jedis, String targetValue) { // 方案A: 返回集合中所有 return new ArrayList<>(jedis.smembers("reverse_index:" + targetValue)); // 方案B: 返回最新关联 (需维护) // return Arrays.asList(jedis.hget("value_to_keys", targetValue)); } // 删除数据时清理索引 public static void delWithIndex(Jedis jedis, String key) { String value = jedis.get(key); Transaction tx = jedis.multi(); tx.del(key); tx.srem("reverse_index:" + value, key); tx.hdel("value_to_keys", value); tx.exec(); } } ``` #### 方案 3:使用 RediSearch 模块(企业级方案) ```java import io.redisearch.client.Client; import io.redisearch.Document; import io.redisearch.SearchResult; import io.redisearch.Schema; public class RedisearchSearcher { public static void setupIndex(Client client) { // 创建索引 Schema schema = new Schema() .addTextField("value", 1.0); client.createIndex(schema, Client.IndexOptions.Default()); } public static List<String> searchKeysByValue(Client client, String targetValue) { SearchResult result = client.search("@value:" + targetValue); List<String> keys = new ArrayList<>(); for (Document doc : result.docs) { keys.add(doc.getId()); } return keys; } public static void main(String[] args) { Client client = new Client("user_index", "localhost", 6379); System.out.println("匹配的: " + searchKeysByValue(client, "admin")); } } ``` ### 关问题解析 #### 为什么 Redis 不直接支持? 1. **架构设计原则**: ```mermaid graph LR A[] -->|O(1) 直接访问| B[] C[] -->|需要遍历| D[] ``` 2. **性能考量**: - 查询:哈希表实现 O(1) 复杂度 - 查询:需要全扫描 O(n) 复杂度 3. **数据类型限制**: - 不同数据类型(String/Hash/List)无法统一处理 #### 生产环境最佳实践 1. **索引维护策略**: ```java // 自动过期同步 public static void setWithExpire(Jedis jedis, String key, String value, int ttl) { Transaction tx = jedis.multi(); tx.setex(key, ttl, value); tx.sadd("reverse_index:" + value, key); tx.expire("reverse_index:" + value, ttl); // 索引同步过期 tx.exec(); } ``` 2. **集群环境处理**: ```java // 使用哈希标签确保索引在同一槽位 public static String getClusterSafeKey(String value) { int slot = JedisClusterCRC16.getSlot(value) % 16384; return "{reverse_index_" + slot + "}:" + value; } ``` 3. **性能优化技巧**: ```java // 管道批处理加速索引创建 public static void batchIndexCreate(Jedis jedis, Map<String, String> data) { Pipeline p = jedis.pipelined(); for (Map.Entry<String, String> entry : data.entrySet()) { p.set(entry.getKey(), entry.getValue()); p.sadd("reverse_index:" + entry.getValue(), entry.getKey()); } p.sync(); } ``` ### 方案对比表 | 方法 | 时间复杂度 | 实时性 | 内存开销 | 集群支持 | |--------------|------------|----------|----------|----------| | SCAN 遍历 | O(n) | 实时 | 低 | 部分 | | 反向索引 | O(1) | 实时 | 中 | 需优化 | | RediSearch | O(log n) | 近实时 | 高 | 完整 | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值