Redis Cluster 提供高可用、自動分片的分散式快取方案。雖然 Spring Data Redis 1.7+ 已原生支援 Cluster,但許多專案仍處於舊版本(如 1.6.x),需直接使用原生 Jedis 客戶端操作 Cluster。本文提供完整的Spring XML 整合配置、JedisCluster 封裝工具類,並演示String、Set、List、Hash、ZSet五大資料結構的寫入、讀取、遍歷操作,附帶Key 管理策略避免 `KEYS *` 性能災難。
核心思路:配置 `
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://www.springframework.org/schema/beans"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="jedisCluster" class="redis.clients.jedis.JedisCluster">
<constructor-arg index="0">
<set>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg name="host" value="127.0.0.1"/>
<constructor-arg name="port" value="7000"/>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg name="host" value="127.0.0.1"/>
<constructor-arg name="port" value="7001"/>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg name="host" value="127.0.0.1"/>
<constructor-arg name="port" value="7002"/>
</bean>
<!-- 建議配置 3-6 個種子節點,確保至少一個可連通 -->
</set>
</constructor-arg>
</bean>
</beans>
關鍵說明:
package com.cnblogs.yjmyzz.redis;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPool;
import java.util.*;
public class AppDemo {
private static final Logger logger = LoggerFactory.getLogger(AppDemo.class);
private static JedisCluster jc = null;
// Key 分類常數——避免使用 KEYS * 掃描全庫
private static final String KEYS_STRING = "SYS:KEYS:STRING";
private static final String KEYS_SET = "SYS:KEYS:SET";
private static final String KEYS_LIST = "SYS:KEYS:LIST";
private static final String KEYS_HASH = "SYS:KEYS:HASH";
private static final String KEYS_ZSET = "SYS:KEYS:ZSET";
// 統一註冊 Key 到分類 Set
private static void addKey(String category, String key) {
if (!jc.exists(category)) {
jc.sadd(category, key);
} else if (!jc.smembers(category).contains(key)) {
jc.sadd(category, key);
}
}
// ========== String ==========
private static String set(String key, String value) {
String result = jc.set(key, value);
addKey(KEYS_STRING, key);
return result;
}
// ========== Set ==========
private static Long sadd(String key, String... members) {
Long result = jc.sadd(key, members);
addKey(KEYS_SET, key);
return result;
}
// ========== List ==========
private static Long lpush(String key, String... values) {
Long result = jc.lpush(key, values);
addKey(KEYS_LIST, key);
return result;
}
// ========== Hash ==========
private static Long hset(String key, String field, String value) {
Long result = jc.hset(key, field, value);
addKey(KEYS_HASH, key);
return result;
}
// ========== ZSet ==========
private static Long zadd(String key, double score, String member) {
Long result = jc.zadd(key, score, member);
addKey(KEYS_ZSET, key);
return result;
}
private static Long zadd(String key, String member) { // 預設 score=0
return zadd(key, 0d, member);
}
// ========== 主程式演示 ==========
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-redis.xml");
jc = ctx.getBean(JedisCluster.class);
// 1. 列印叢集節點資訊並清空測試資料
Map nodes = jc.getClusterNodes();
for (Map.Entry entry : nodes.entrySet()) {
logger.info("節點: {} => {}", entry.getKey(), entry.getValue());
try { entry.getValue().getResource().flushDB(); }
catch (Exception e) { logger.info("Slave 節點 flushDB 失敗: {}", e.getMessage()); }
}
// 2. String 測試
logger.info("String 寫入: {}", set("a", "hello world!"));
logger.info("String 讀取: {}", jc.get("a"));
// 3. Set 測試
logger.info("Set 寫入: {}", sadd("set1", "a", "b", "c"));
logger.info("Set 類型: {}", jc.type("set1"));
jc.smembers("set1").forEach(s -> logger.info("Set 成員: {}", s));
// 4. List 測試
logger.info("List 寫入: {}", lpush("list1", "1", "2", "3"));
jc.lrange("list1", 0, 999).forEach(s -> logger.info("List 元素: {}", s));
// 5. Hash 測試
logger.info("Hash 寫入: {}", hset("hash1", "jimmy", "楊俊明"));
logger.info("Hash 寫入: {}", hset("hash1", "CN", "中國"));
jc.hgetAll("hash1").forEach((k,v) -> logger.info("Hash: {}={}", k, v));
// 6. ZSet 測試
logger.info("ZSet 寫入: {}", zadd("zset1", "3"));
logger.info("ZSet 寫入: {}", zadd("zset1", 1.5, "2")); // 帶分數
jc.zrange("zset1", 0, 999).forEach(s -> logger.info("ZSet 有序: {}", s));
// 7. 遍歷所有分類 Key(替代危險的 KEYS *)
logger.info("所有 String Key: {}", jc.smembers(KEYS_STRING));
logger.info("所有 Hash Key: {}", jc.smembers(KEYS_HASH));
logger.info("所有 Set Key: {}", jc.smembers(KEYS_SET));
logger.info("所有 List Key: {}", jc.smembers(KEYS_LIST));
logger.info("所有 ZSet Key: {}", jc.smembers(KEYS_ZSET));
}
}
| 實踐 | 說明 | Cluster 模式下 `KEYS` 需掃描所有分片,極易導致阻塞。改用應用層維護 Key 分類 Set(如本例 `SYS:KEYS:*`)。 | 啟動時只需一個可達節點,但為容錯建議配 3-6 個不同 Master。
| 生產必須配置 `JedisPoolConfig`:`maxTotal=200`、`maxIdle=50`、`minIdle=10`、`maxWaitMillis=3000`、`testOnBorrow=true`。
| 批量操作時使用 `jc.pipelined()` 減少網路往返(Cluster 模式下需注意槽位分佈)。
| 配置 `JedisCluster` 時可設 `readFrom=SLAVE_PREFERRED` 分散讀壓力(需 Redis 2.8+)。
| 捕獲 `JedisConnectionException`、`JedisMovedDataException`、`JedisClusterMaxRedirectionsException`,實施重試/降級。 |
|---|
Jedis 方法命名遵循規律,無官方文檔時可按前綴推斷:
在 Spring Data Redis 未支援 Cluster 的版本窗口期,原生 Jedis 是操作 Redis Cluster 的最佳選擇。透過Spring XML 整合、統一 Key 管理策略、五大資料結構封裝,能構建出穩定、高效、可維護的分散式快取存取層。升級至 Spring Data Redis 1.7+ 後,可平滑遷移至 `RedisTemplate` + `EnableRedisCluster` 註解配置,業務代碼幾乎無需變動。