1.概述
看下 《Dubbo 用戶指南 —— Redis 註冊中心》 文檔,內容如下:
基於 Redis實現的註冊中心 :
使用 Redis 的 Key/Map 結構存儲數據結構:
- 主 Key 爲服務名和類型
- Map 中的 Key 爲 URL 地址
- Map 中的 Value 爲過期時間,用於判斷髒數據,髒數據由監控中心刪除
類圖如下:
2. RedisRegistry
2.1 屬性和構造方法
/**
* 默認端口
*/
private static final int DEFAULT_REDIS_PORT = 6379;
/**
* 默認 Redis 根節點
*/
private final static String DEFAULT_ROOT = "dubbo";
/**
* Redis Key 過期機制執行器
*/
private final ScheduledExecutorService expireExecutor = Executors.newScheduledThreadPool(1, new NamedThreadFactory("DubboRegistryExpireTimer", true));
/**
* Redis Key 過期機制 Future
*/
private final ScheduledFuture<?> expireFuture;
/**
* Redis 根節點
*/
private final String root;
/**
* JedisPool 集合
*
* key:ip:port
*/
private final Map<String, JedisPool> jedisPools = new ConcurrentHashMap<String, JedisPool>();
/**
* 通知器集合
*
* key:Root + Service ,例如 `/dubbo/com.alibaba.dubbo.demo.DemoService`
*/
private final ConcurrentMap<String, Notifier> notifiers = new ConcurrentHashMap<String, Notifier>();
/**
* 重連週期,單位:毫秒
*/
private final int reconnectPeriod;
/**
* 過期週期,單位:毫秒
*/
private final int expirePeriod;
/**
* 是否監控中心
*
* 用於判斷髒數據,髒數據由監控中心刪除 {@link #clean(Jedis)}
*/
private volatile boolean admin = false;
/**
* 是否複製模式
*/
private boolean replicate;
public RedisRegistry(URL url) {
super(url);
if (url.isAnyHost()) {
throw new IllegalStateException("registry address == null");
}
// 創建 GenericObjectPoolConfig 對象
GenericObjectPoolConfig config = new GenericObjectPoolConfig();
config.setTestOnBorrow(url.getParameter("test.on.borrow", true));
config.setTestOnReturn(url.getParameter("test.on.return", false));
config.setTestWhileIdle(url.getParameter("test.while.idle", false));
if (url.getParameter("max.idle", 0) > 0)
config.setMaxIdle(url.getParameter("max.idle", 0));
if (url.getParameter("min.idle", 0) > 0)
config.setMinIdle(url.getParameter("min.idle", 0));
if (url.getParameter("max.active", 0) > 0)
config.setMaxTotal(url.getParameter("max.active", 0));
if (url.getParameter("max.total", 0) > 0)
config.setMaxTotal(url.getParameter("max.total", 0));
if (url.getParameter("max.wait", url.getParameter("timeout", 0)) > 0)
config.setMaxWaitMillis(url.getParameter("max.wait", url.getParameter("timeout", 0)));
if (url.getParameter("num.tests.per.eviction.run", 0) > 0)
config.setNumTestsPerEvictionRun(url.getParameter("num.tests.per.eviction.run", 0));
if (url.getParameter("time.between.eviction.runs.millis", 0) > 0)
config.setTimeBetweenEvictionRunsMillis(url.getParameter("time.between.eviction.runs.millis", 0));
if (url.getParameter("min.evictable.idle.time.millis", 0) > 0)
config.setMinEvictableIdleTimeMillis(url.getParameter("min.evictable.idle.time.millis", 0));
// 是否複製模式
String cluster = url.getParameter("cluster", "failover");
if (!"failover".equals(cluster) && !"replicate".equals(cluster)) {
throw new IllegalArgumentException("Unsupported redis cluster: " + cluster + ". The redis cluster only supported failover or replicate.");
}
replicate = "replicate".equals(cluster);
// 解析
List<String> addresses = new ArrayList<String>();
addresses.add(url.getAddress());
String[] backups = url.getParameter(Constants.BACKUP_KEY, new String[0]);
if (backups != null && backups.length > 0) {
addresses.addAll(Arrays.asList(backups));
}
// 創建 JedisPool 對象
String password = url.getPassword();
for (String address : addresses) {
int i = address.indexOf(':');
String host;
int port;
if (i > 0) {
host = address.substring(0, i);
port = Integer.parseInt(address.substring(i + 1));
} else {
host = address;
port = DEFAULT_REDIS_PORT;
}
if (StringUtils.isEmpty(password)) { // 無密碼連接
this.jedisPools.put(address, new JedisPool(config, host, port,
url.getParameter(Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT)));
} else { // 有密碼連接
this.jedisPools.put(address, new JedisPool(config, host, port,
url.getParameter(Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT), password));
}
}
// 解析重連週期
this.reconnectPeriod = url.getParameter(Constants.REGISTRY_RECONNECT_PERIOD_KEY, Constants.DEFAULT_REGISTRY_RECONNECT_PERIOD);
// 獲得 Redis 根節點
String group = url.getParameter(Constants.GROUP_KEY, DEFAULT_ROOT);
if (!group.startsWith(Constants.PATH_SEPARATOR)) { // 頭 `/`
group = Constants.PATH_SEPARATOR + group;
}
if (!group.endsWith(Constants.PATH_SEPARATOR)) { // 尾 `/`
group = group + Constants.PATH_SEPARATOR;
}
this.root = group;
// 創建實現 Redis Key 過期機制的任務
this.expirePeriod = url.getParameter(Constants.SESSION_TIMEOUT_KEY, Constants.DEFAULT_SESSION_TIMEOUT);
this.expireFuture = expireExecutor.scheduleWithFixedDelay(new Runnable() {
public void run() {
try {
deferExpired(); // Extend the expiration time
} catch (Throwable t) { // Defensive fault tolerance
logger.error("Unexpected exception occur at defer expire time, cause: " + t.getMessage(), t);
}
}
}, expirePeriod / 2, expirePeriod / 2, TimeUnit.MILLISECONDS);
}
2.2 doRegister
註冊信息
public void doRegister(URL url) {
String key = toCategoryPath(url);
String value = url.toFullString();
// 計算過期時間
String expire = String.valueOf(System.currentTimeMillis() + expirePeriod);
boolean success = false;
RpcException exception = null;
// 向 Redis 註冊
for (Map.Entry<String, JedisPool> entry : jedisPools.entrySet()) {
JedisPool jedisPool = entry.getValue();
try {
Jedis jedis = jedisPool.getResource();
try {
// 寫入 Redis Map 鍵
jedis.hset(key, value, expire);
// 發佈 Redis 註冊事件
jedis.publish(key, Constants.REGISTER);
success = true;
// 如果服務器端已同步數據,只需寫入單臺機器
if (!replicate) {
break; // If the server side has synchronized data, just write a single machine
}
} finally {
jedisPool.returnResource(jedis);
}
} catch (Throwable t) {
exception = new RpcException("Failed to register service to redis registry. registry: " + entry.getKey() + ", service: " + url + ", cause: " + t.getMessage(), t);
}
}
// 處理異常
if (exception != null) {
if (success) { // 雖然發生異常,但是結果成功
logger.warn(exception.getMessage(), exception);
} else { // 最終未成功
throw exception;
}
}
}
注意一下這裏的處理異常的寫法
2.2.1 toCategoryPath
/**
* 獲得分類路徑
*
* Root + Service + Type
*
* @param url URL
* @return 分類路徑
*/
private String toCategoryPath(URL url) {
return toServicePath(url) + Constants.PATH_SEPARATOR + url.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY);
}
2.3 doUnregister
public void doUnregister(URL url) {
String key = toCategoryPath(url);
String value = url.toFullString();
RpcException exception = null;
boolean success = false;
// 向 Redis 註冊
for (Map.Entry<String, JedisPool> entry : jedisPools.entrySet()) {
JedisPool jedisPool = entry.getValue();
try {
Jedis jedis = jedisPool.getResource();
try {
// 刪除 Redis Map 鍵
jedis.hdel(key, value);
// 發佈 Redis 取消註冊事件
jedis.publish(key, Constants.UNREGISTER);
success = true;
// 如果服務器端已同步數據,只需寫入單臺機器
if (!replicate) {
break; // If the server side has synchronized data, just write a single machine
}
} finally {
jedisPool.returnResource(jedis);
}
} catch (Throwable t) {
exception = new RpcException("Failed to unregister service to redis registry. registry: " + entry.getKey() + ", service: " + url + ", cause: " + t.getMessage(), t);
}
}
// 處理異常
if (exception != null) {
if (success) { // 雖然發生異常,但是結果成功
logger.warn(exception.getMessage(), exception);
} else { // 最終未成功
throw exception;
}
}
}
2.4 doSubscribe
public void doSubscribe(final URL url, final NotifyListener listener) {
// 獲得服務路徑,例如:`/dubbo/com.alibaba.dubbo.demo.DemoService`
String service = toServicePath(url);
// 獲得通知器 Notifier 對象
Notifier notifier = notifiers.get(service);
// 不存在,則創建 Notifier 對象
if (notifier == null) {
Notifier newNotifier = new Notifier(service);
notifiers.putIfAbsent(service, newNotifier);
notifier = notifiers.get(service);
if (notifier == newNotifier) { // 保證併發的情況下,有且僅有一個啓動
notifier.start();
}
}
boolean success = false;
RpcException exception = null;
// 循環 `jedisPools` ,僅向一個 Redis 發起訂閱
for (Map.Entry<String, JedisPool> entry : jedisPools.entrySet()) {
JedisPool jedisPool = entry.getValue();
try {
Jedis jedis = jedisPool.getResource();
try {
// 處理所有 Service 層的發起訂閱,例如監控中心的訂閱
if (service.endsWith(Constants.ANY_VALUE)) {
admin = true;
// 獲得分類層集合,例如:`/dubbo/com.alibaba.dubbo.demo.DemoService/providers`
Set<String> keys = jedis.keys(service);
if (keys != null && !keys.isEmpty()) {
// 按照服務聚合 URL 集合
Map<String, Set<String>> serviceKeys = new HashMap<String, Set<String>>(); // Key:Root + Service ; Value:URL 。
for (String key : keys) {
String serviceKey = toServicePath(key);
Set<String> sk = serviceKeys.get(serviceKey);
if (sk == null) {
sk = new HashSet<String>();
serviceKeys.put(serviceKey, sk);
}
sk.add(key);
}
// 循環 serviceKeys ,按照每個 Service 層的發起通知
for (Set<String> sk : serviceKeys.values()) {
doNotify(jedis, sk, url, Collections.singletonList(listener));
}
}
// 處理指定 Service 層的發起通知
} else {
doNotify(jedis, jedis.keys(service + Constants.PATH_SEPARATOR + Constants.ANY_VALUE), url, Collections.singletonList(listener));
}
// 標記成功
success = true;
// 結束,僅僅從一臺服務器讀取數據
break; // Just read one server's data
} finally {
jedisPool.returnResource(jedis);
}
} catch (Throwable t) { // Try the next server
exception = new RpcException("Failed to subscribe service from redis registry. registry: " + entry.getKey() + ", service: " + url + ", cause: " + t.getMessage(), t);
}
}
// 處理異常
if (exception != null) {
if (success) { // 雖然發生異常,但是結果成功
logger.warn(exception.getMessage(), exception);
} else { // 最終未成功
throw exception;
}
}
}
2.4.1 toServicePath
獲得服務路徑
/**
* 獲得服務路徑,主要截掉多餘的部分
*
* Root + Type
*
* @param categoryPath 分類路徑
* @return 服務路徑
*/
private String toServicePath(String categoryPath) {
int i;
if (categoryPath.startsWith(root)) {
i = categoryPath.indexOf(Constants.PATH_SEPARATOR, root.length());
} else {
i = categoryPath.indexOf(Constants.PATH_SEPARATOR);
}
return i > 0 ? categoryPath.substring(0, i) : categoryPath;
}
2.4.2 toServicePath
獲得服務路徑
/**
* 獲得服務路徑
*
* Root + Type
*
* @param url URL
* @return 服務路徑
*/
private String toServicePath(URL url) {
return root + url.getServiceInterface();
}
2.5 doNotify
// @params key 分類數組,例如:`/dubbo/com.alibaba.dubbo.demo.DemoService/providers`
private void doNotify(Jedis jedis, String key) {
for (Map.Entry<URL, Set<NotifyListener>> entry : new HashMap<URL, Set<NotifyListener>>(getSubscribed()).entrySet()) {
doNotify(jedis, Collections.singletonList(key), entry.getKey(), new HashSet<NotifyListener>(entry.getValue()));
}
}
// @params keys 分類數組,元素例如:`/dubbo/com.alibaba.dubbo.demo.DemoService/providers`
private void doNotify(Jedis jedis, Collection<String> keys, URL url, Collection<NotifyListener> listeners) {
if (keys == null || keys.isEmpty() || listeners == null || listeners.isEmpty()) {
return;
}
long now = System.currentTimeMillis();
List<URL> result = new ArrayList<URL>();
List<String> categories = Arrays.asList(url.getParameter(Constants.CATEGORY_KEY, new String[0])); // 分類數組
String consumerService = url.getServiceInterface(); // 服務接口
// 循環分類層,例如:`/dubbo/com.alibaba.dubbo.demo.DemoService/providers`
for (String key : keys) {
// 若服務不匹配,返回
if (!Constants.ANY_VALUE.equals(consumerService)) {
String providerService = toServiceName(key);
if (!providerService.equals(consumerService)) {
continue;
}
}
// 若訂閱的不包含該分類,返回
String category = toCategoryName(key);
if (!categories.contains(Constants.ANY_VALUE) && !categories.contains(category)) {
continue;
}
// 獲得所有 URL 數組
List<URL> urls = new ArrayList<URL>();
Map<String, String> values = jedis.hgetAll(key);
if (values != null && values.size() > 0) {
for (Map.Entry<String, String> entry : values.entrySet()) {
URL u = URL.valueOf(entry.getKey());
if (!u.getParameter(Constants.DYNAMIC_KEY, true) // 非動態節點,因爲動態節點,不受過期的限制
|| Long.parseLong(entry.getValue()) >= now) { // 未過期
if (UrlUtils.isMatch(url, u)) {
urls.add(u);
}
}
}
}
// 若不存在匹配,則創建 `empty://` 的 URL返回,用於清空該服務的該分類。
if (urls.isEmpty()) {
urls.add(url.setProtocol(Constants.EMPTY_PROTOCOL)
.setAddress(Constants.ANYHOST_VALUE)
.setPath(toServiceName(key))
.addParameter(Constants.CATEGORY_KEY, category));
}
result.addAll(urls);
if (logger.isInfoEnabled()) {
logger.info("redis notify: " + key + " = " + urls);
}
}
if (result.isEmpty()) {
return;
}
// 全量數據獲取完成時,調用 `super#notify(...)` 方法,回調 NotifyListener
for (NotifyListener listener : listeners) {
super.notify(url, listener, result);
}
}
2.5.1 toServiceName
/**
* 獲得服務名,從服務路徑上
*
* Service
*
* @param categoryPath 服務路徑
* @return 服務名
*/
private String toServiceName(String categoryPath) {
String servicePath = toServicePath(categoryPath);
return servicePath.startsWith(root) ? servicePath.substring(root.length()) : servicePath;
}
2.5.2 toServiceName
/**
* 獲得分類名,從分類路徑上
*
* Type
*
* @param categoryPath 分類路徑
* @return 分類名
*/
private String toCategoryName(String categoryPath) {
int i = categoryPath.lastIndexOf(Constants.PATH_SEPARATOR);
return i > 0 ? categoryPath.substring(i + 1) : categoryPath;
}
2.6 deferExpired
private void deferExpired() {
for (Map.Entry<String, JedisPool> entry : jedisPools.entrySet()) {
JedisPool jedisPool = entry.getValue();
try {
Jedis jedis = jedisPool.getResource();
try {
// 循環已註冊的 URL 集合
for (URL url : new HashSet<URL>(getRegistered())) {
// 動態節點
if (url.getParameter(Constants.DYNAMIC_KEY, true)) {
// 獲得分類路徑
String key = toCategoryPath(url);
// 寫入 Redis Map 中
if (jedis.hset(key, url.toFullString(), String.valueOf(System.currentTimeMillis() + expirePeriod)) == 1) {
// 發佈 `register` 事件。
jedis.publish(key, Constants.REGISTER);
}
}
}
// 監控中心負責刪除過期髒數據
if (admin) {
clean(jedis);
}
// 如果服務器端已同步數據,只需寫入單臺機器
if (!replicate) {
break;// If the server side has synchronized data, just write a single machine
}
} finally {
jedisPool.returnResource(jedis);
}
} catch (Throwable t) {
logger.warn("Failed to write provider heartbeat to redis registry. registry: " + entry.getKey() + ", cause: " + t.getMessage(), t);
}
}
}
2.6.1 clean
監控中心負責刪除過期髒數據
private void clean(Jedis jedis) {
// 獲得所有服務
Set<String> keys = jedis.keys(root + Constants.ANY_VALUE);
if (keys != null && !keys.isEmpty()) {
for (String key : keys) {
// 獲得所有 URL
Map<String, String> values = jedis.hgetAll(key);
if (values != null && values.size() > 0) {
boolean delete = false;
long now = System.currentTimeMillis();
for (Map.Entry<String, String> entry : values.entrySet()) {
URL url = URL.valueOf(entry.getKey());
// 動態節點
if (url.getParameter(Constants.DYNAMIC_KEY, true)) {
long expire = Long.parseLong(entry.getValue());
// 已經過期
if (expire < now) {
//
jedis.hdel(key, entry.getKey());
delete = true;
if (logger.isWarnEnabled()) {
logger.warn("Delete expired key: " + key + " -> value: " + entry.getKey() + ", expire: " + new Date(expire) + ", now: " + new Date(now));
}
}
}
}
// 若刪除成功,發佈 `unregister` 事件
if (delete) {
jedis.publish(key, Constants.UNREGISTER);
}
}
}
}
}
2.6 isAvailable
判斷節點是否可用
public boolean isAvailable() {
for (JedisPool jedisPool : jedisPools.values()) {
try {
Jedis jedis = jedisPool.getResource();
try {
if (jedis.isConnected()) { // 至少一個 Redis 節點可用
return true; // At least one single machine is available.
}
} finally {
jedisPool.returnResource(jedis);
}
} catch (Throwable ignored) {
}
}
return false;
}
2.7 destroy
@Override
public void destroy() {
// 父類關閉
super.destroy();
// 關閉定時任務
try {
expireFuture.cancel(true);
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
}
// 關閉通知器
try {
for (Notifier notifier : notifiers.values()) {
notifier.shutdown();
}
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
}
// 關閉連接池
for (Map.Entry<String, JedisPool> entry : jedisPools.entrySet()) {
JedisPool jedisPool = entry.getValue();
try {
jedisPool.destroy();
} catch (Throwable t) {
logger.warn("Failed to destroy the redis registry client. registry: " + entry.getKey() + ", cause: " + t.getMessage(), t);
}
}
}
3 Notifier
3.1 構造方法和屬性
/**
* 服務名 Root + Service
*/
private final String service;
/**
* Jedis
*/
private volatile Jedis jedis;
/**
* 是否首次
*/
private volatile boolean first = true;
/**
* 是否運行中
*/
private volatile boolean running = true;
/**
* 連接次數隨機數
*/
private volatile int connectRandom;
/**
* 需要忽略連接的次數
*/
private final AtomicInteger connectSkip = new AtomicInteger();
/**
* 已經忽略連接的次數
*/
private final AtomicInteger connectSkiped = new AtomicInteger();
/**
* 隨機
*/
private final Random random = new Random();
public Notifier(String service) {
super.setDaemon(true);
super.setName("DubboRedisSubscribe");
this.service = service;
}
3.2 run
public void run() {
while (running) {
try {
// 是否跳過本次 Redis 連接
if (!isSkip()) {
try {
for (Map.Entry<String, JedisPool> entry : jedisPools.entrySet()) {
JedisPool jedisPool = entry.getValue();
try {
jedis = jedisPool.getResource();
try {
// 監控中心
if (service.endsWith(Constants.ANY_VALUE)) {
if (!first) {
first = false;
Set<String> keys = jedis.keys(service);
if (keys != null && !keys.isEmpty()) {
for (String s : keys) {
doNotify(jedis, s);
}
}
resetSkip();
}
// 批訂閱
jedis.psubscribe(new NotifySub(jedisPool), service); // blocking
// 服務提供者或消費者
} else {
if (!first) {
first = false;
doNotify(jedis, service);
resetSkip();
}
// 批訂閱
jedis.psubscribe(new NotifySub(jedisPool), service + Constants.PATH_SEPARATOR + Constants.ANY_VALUE); // blocking
}
break;
} finally {
jedisPool.returnBrokenResource(jedis);
}
} catch (Throwable t) { // Retry another server
logger.warn("Failed to subscribe service from redis registry. registry: " + entry.getKey() + ", cause: " + t.getMessage(), t);
// If you only have a single redis, you need to take a rest to avoid overtaking a lot of CPU resources
sleep(reconnectPeriod);
}
}
} catch (Throwable t) {
logger.error(t.getMessage(), t);
sleep(reconnectPeriod);
}
}
} catch (Throwable t) {
logger.error(t.getMessage(), t);
}
}
}
4 NotifySub
/**
* 通知訂閱實現類
*/
private class NotifySub extends JedisPubSub {
private final JedisPool jedisPool;
public NotifySub(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
@Override
public void onMessage(String key, String msg) {
if (logger.isInfoEnabled()) {
logger.info("redis event: " + key + " = " + msg);
}
if (msg.equals(Constants.REGISTER)
|| msg.equals(Constants.UNREGISTER)) {
try {
Jedis jedis = jedisPool.getResource();
try {
doNotify(jedis, key);
} finally {
jedisPool.returnResource(jedis);
}
} catch (Throwable t) { // TODO Notification failure does not restore mechanism guarantee
logger.error(t.getMessage(), t);
}
}
}
@Override
public void onPMessage(String pattern, String key, String msg) {
onMessage(key, msg);
}
@Override
public void onSubscribe(String key, int num) {
}
@Override
public void onPSubscribe(String pattern, int num) {
}
@Override
public void onUnsubscribe(String key, int num) {
}
@Override
public void onPUnsubscribe(String pattern, int num) {
}
}