You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@dubbo.apache.org by al...@apache.org on 2024/07/29 02:23:49 UTC

(dubbo-spi-extensions) branch 3.3 updated: Import metadata-report-redis implementation from Dubbo 3.3 (#457)

This is an automated email from the ASF dual-hosted git repository.

albumenj pushed a commit to branch 3.3
in repository https://gitbox.apache.org/repos/asf/dubbo-spi-extensions.git


The following commit(s) were added to refs/heads/3.3 by this push:
     new 13244130 Import metadata-report-redis implementation from Dubbo 3.3 (#457)
13244130 is described below

commit 1324413013e5621b122f332dcc8a85b333539efb
Author: aofall <10...@users.noreply.github.com>
AuthorDate: Mon Jul 29 10:23:44 2024 +0800

    Import metadata-report-redis implementation from Dubbo 3.3 (#457)
    
    * Import metadata-report-redis implementation from Dubbo 3.3
    
    * Import metadata-report-redis implementation from Dubbo 3.3
    
    * rename password template
---
 .../dubbo-metadata-report-redis/pom.xml            |  58 +++
 .../metadata/store/redis/RedisMetadataReport.java  | 555 +++++++++++++++++++++
 .../store/redis/RedisMetadataReportFactory.java    |  32 ++
 ...che.dubbo.metadata.report.MetadataReportFactory |   1 +
 .../store/redis/RedisMetadata4TstService.java      |  24 +
 .../store/redis/RedisMetadataReportTest.java       | 365 ++++++++++++++
 .../src/test/resources/log4j2-test.xml             |  28 ++
 dubbo-metadata-report-extensions/pom.xml           |   1 +
 8 files changed, 1064 insertions(+)

diff --git a/dubbo-metadata-report-extensions/dubbo-metadata-report-redis/pom.xml b/dubbo-metadata-report-extensions/dubbo-metadata-report-redis/pom.xml
new file mode 100644
index 00000000..b2cd88cf
--- /dev/null
+++ b/dubbo-metadata-report-extensions/dubbo-metadata-report-redis/pom.xml
@@ -0,0 +1,58 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.apache.dubbo.extensions</groupId>
+    <artifactId>dubbo-metadata-report-extensions</artifactId>
+    <version>${revision}</version>
+  </parent>
+
+  <artifactId>dubbo-metadata-report-redis</artifactId>
+
+  <properties>
+      <jedis.version>5.1.0</jedis.version>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.dubbo</groupId>
+      <artifactId>dubbo-metadata-api</artifactId>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>redis.clients</groupId>
+      <artifactId>jedis</artifactId>
+      <version>${jedis.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.github.codemonstur</groupId>
+      <artifactId>embedded-redis</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-lang3</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-slf4j-impl</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/dubbo-metadata-report-extensions/dubbo-metadata-report-redis/src/main/java/org/apache/dubbo/metadata/store/redis/RedisMetadataReport.java b/dubbo-metadata-report-extensions/dubbo-metadata-report-redis/src/main/java/org/apache/dubbo/metadata/store/redis/RedisMetadataReport.java
new file mode 100644
index 00000000..1c010f07
--- /dev/null
+++ b/dubbo-metadata-report-extensions/dubbo-metadata-report-redis/src/main/java/org/apache/dubbo/metadata/store/redis/RedisMetadataReport.java
@@ -0,0 +1,555 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.metadata.store.redis;
+
+import org.apache.dubbo.common.URL;
+import org.apache.dubbo.common.config.configcenter.ConfigItem;
+import org.apache.dubbo.common.logger.ErrorTypeAwareLogger;
+import org.apache.dubbo.common.logger.LoggerFactory;
+import org.apache.dubbo.common.utils.ConcurrentHashMapUtils;
+import org.apache.dubbo.common.utils.ConcurrentHashSet;
+import org.apache.dubbo.common.utils.JsonUtils;
+import org.apache.dubbo.common.utils.StringUtils;
+import org.apache.dubbo.metadata.MappingChangedEvent;
+import org.apache.dubbo.metadata.MappingListener;
+import org.apache.dubbo.metadata.MetadataInfo;
+import org.apache.dubbo.metadata.ServiceNameMapping;
+import org.apache.dubbo.metadata.report.identifier.BaseMetadataIdentifier;
+import org.apache.dubbo.metadata.report.identifier.KeyTypeEnum;
+import org.apache.dubbo.metadata.report.identifier.MetadataIdentifier;
+import org.apache.dubbo.metadata.report.identifier.ServiceMetadataIdentifier;
+import org.apache.dubbo.metadata.report.identifier.SubscriberMetadataIdentifier;
+import org.apache.dubbo.metadata.report.support.AbstractMetadataReport;
+import org.apache.dubbo.rpc.RpcException;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.JedisCluster;
+import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.JedisPoolConfig;
+import redis.clients.jedis.JedisPubSub;
+import redis.clients.jedis.Transaction;
+import redis.clients.jedis.params.SetParams;
+import redis.clients.jedis.util.JedisClusterCRC16;
+
+import static org.apache.dubbo.common.constants.CommonConstants.CLUSTER_KEY;
+import static org.apache.dubbo.common.constants.CommonConstants.CYCLE_REPORT_KEY;
+import static org.apache.dubbo.common.constants.CommonConstants.DEFAULT_TIMEOUT;
+import static org.apache.dubbo.common.constants.CommonConstants.GROUP_CHAR_SEPARATOR;
+import static org.apache.dubbo.common.constants.CommonConstants.QUEUES_KEY;
+import static org.apache.dubbo.common.constants.CommonConstants.TIMEOUT_KEY;
+import static org.apache.dubbo.common.constants.LoggerCodeConstants.TRANSPORT_FAILED_RESPONSE;
+import static org.apache.dubbo.metadata.MetadataConstants.META_DATA_STORE_TAG;
+import static org.apache.dubbo.metadata.ServiceNameMapping.DEFAULT_MAPPING_GROUP;
+import static org.apache.dubbo.metadata.ServiceNameMapping.getAppNames;
+import static org.apache.dubbo.metadata.report.support.Constants.DEFAULT_METADATA_REPORT_CYCLE_REPORT;
+
+/**
+ * RedisMetadataReport
+ */
+public class RedisMetadataReport extends AbstractMetadataReport {
+
+    private static final int ONE_DAY_IN_MILLISECONDS = 86400000;
+
+    private static final String REDIS_DATABASE_KEY = "database";
+    private static final ErrorTypeAwareLogger logger = LoggerFactory.getErrorTypeAwareLogger(RedisMetadataReport.class);
+
+    // protected , for test
+    protected JedisPool pool;
+    private Set<HostAndPort> jedisClusterNodes;
+    private int timeout;
+    private String password;
+    private final String root;
+    private final ConcurrentHashMap<String, MappingDataListener> mappingDataListenerMap = new ConcurrentHashMap<>();
+    private SetParams jedisParams = SetParams.setParams();
+
+    public RedisMetadataReport(URL url) {
+        super(url);
+        timeout = url.getParameter(TIMEOUT_KEY, DEFAULT_TIMEOUT);
+        password = url.getPassword();
+        this.root = url.getGroup(DEFAULT_ROOT);
+        if (url.getParameter(CYCLE_REPORT_KEY, DEFAULT_METADATA_REPORT_CYCLE_REPORT)) {
+            // ttl default is twice the cycle-report time
+            jedisParams.ex(ONE_DAY_IN_MILLISECONDS * 2);
+        }
+        if (url.getParameter(CLUSTER_KEY, false)) {
+            jedisClusterNodes = new HashSet<>();
+            List<URL> urls = url.getBackupUrls();
+            for (URL tmpUrl : urls) {
+                jedisClusterNodes.add(new HostAndPort(tmpUrl.getHost(), tmpUrl.getPort()));
+            }
+        } else {
+            int database = url.getParameter(REDIS_DATABASE_KEY, 0);
+            pool = new JedisPool(new JedisPoolConfig(), url.getHost(), url.getPort(), timeout, password, database);
+        }
+    }
+
+    @Override
+    protected void doStoreProviderMetadata(MetadataIdentifier providerMetadataIdentifier, String serviceDefinitions) {
+        this.storeMetadata(providerMetadataIdentifier, serviceDefinitions);
+    }
+
+    @Override
+    protected void doStoreConsumerMetadata(MetadataIdentifier consumerMetadataIdentifier, String value) {
+        this.storeMetadata(consumerMetadataIdentifier, value);
+    }
+
+    @Override
+    protected void doSaveMetadata(ServiceMetadataIdentifier serviceMetadataIdentifier, URL url) {
+        this.storeMetadata(serviceMetadataIdentifier, URL.encode(url.toFullString()));
+    }
+
+    @Override
+    protected void doRemoveMetadata(ServiceMetadataIdentifier serviceMetadataIdentifier) {
+        this.deleteMetadata(serviceMetadataIdentifier);
+    }
+
+    @Override
+    protected List<String> doGetExportedURLs(ServiceMetadataIdentifier metadataIdentifier) {
+        String content = getMetadata(metadataIdentifier);
+        if (StringUtils.isEmpty(content)) {
+            return Collections.emptyList();
+        }
+        return new ArrayList<>(Arrays.asList(URL.decode(content)));
+    }
+
+    @Override
+    protected void doSaveSubscriberData(SubscriberMetadataIdentifier subscriberMetadataIdentifier, String urlListStr) {
+        this.storeMetadata(subscriberMetadataIdentifier, urlListStr);
+    }
+
+    @Override
+    protected String doGetSubscribedURLs(SubscriberMetadataIdentifier subscriberMetadataIdentifier) {
+        return this.getMetadata(subscriberMetadataIdentifier);
+    }
+
+    @Override
+    public String getServiceDefinition(MetadataIdentifier metadataIdentifier) {
+        return this.getMetadata(metadataIdentifier);
+    }
+
+    private void storeMetadata(BaseMetadataIdentifier metadataIdentifier, String v) {
+        if (pool != null) {
+            storeMetadataStandalone(metadataIdentifier, v);
+        } else {
+            storeMetadataInCluster(metadataIdentifier, v);
+        }
+    }
+
+    private void storeMetadataInCluster(BaseMetadataIdentifier metadataIdentifier, String v) {
+        try (JedisCluster jedisCluster =
+                new JedisCluster(jedisClusterNodes, timeout, timeout, 2, password, new GenericObjectPoolConfig<>())) {
+            jedisCluster.set(metadataIdentifier.getIdentifierKey() + META_DATA_STORE_TAG, v, jedisParams);
+        } catch (Throwable e) {
+            String msg =
+                    "Failed to put " + metadataIdentifier + " to redis cluster " + v + ", cause: " + e.getMessage();
+            logger.error(TRANSPORT_FAILED_RESPONSE, "", "", msg, e);
+            throw new RpcException(msg, e);
+        }
+    }
+
+    private void storeMetadataStandalone(BaseMetadataIdentifier metadataIdentifier, String v) {
+        try (Jedis jedis = pool.getResource()) {
+            jedis.set(metadataIdentifier.getUniqueKey(KeyTypeEnum.UNIQUE_KEY), v, jedisParams);
+        } catch (Throwable e) {
+            String msg = "Failed to put " + metadataIdentifier + " to redis " + v + ", cause: " + e.getMessage();
+            logger.error(TRANSPORT_FAILED_RESPONSE, "", "", msg, e);
+            throw new RpcException(msg, e);
+        }
+    }
+
+    private void deleteMetadata(BaseMetadataIdentifier metadataIdentifier) {
+        if (pool != null) {
+            deleteMetadataStandalone(metadataIdentifier);
+        } else {
+            deleteMetadataInCluster(metadataIdentifier);
+        }
+    }
+
+    private void deleteMetadataInCluster(BaseMetadataIdentifier metadataIdentifier) {
+        try (JedisCluster jedisCluster =
+                new JedisCluster(jedisClusterNodes, timeout, timeout, 2, password, new GenericObjectPoolConfig<>())) {
+            jedisCluster.del(metadataIdentifier.getIdentifierKey() + META_DATA_STORE_TAG);
+        } catch (Throwable e) {
+            String msg = "Failed to delete " + metadataIdentifier + " from redis cluster , cause: " + e.getMessage();
+            logger.error(TRANSPORT_FAILED_RESPONSE, "", "", msg, e);
+            throw new RpcException(msg, e);
+        }
+    }
+
+    private void deleteMetadataStandalone(BaseMetadataIdentifier metadataIdentifier) {
+        try (Jedis jedis = pool.getResource()) {
+            jedis.del(metadataIdentifier.getUniqueKey(KeyTypeEnum.UNIQUE_KEY));
+        } catch (Throwable e) {
+            String msg = "Failed to delete " + metadataIdentifier + " from redis , cause: " + e.getMessage();
+            logger.error(TRANSPORT_FAILED_RESPONSE, "", "", msg, e);
+            throw new RpcException(msg, e);
+        }
+    }
+
+    private String getMetadata(BaseMetadataIdentifier metadataIdentifier) {
+        if (pool != null) {
+            return getMetadataStandalone(metadataIdentifier);
+        } else {
+            return getMetadataInCluster(metadataIdentifier);
+        }
+    }
+
+    private String getMetadataInCluster(BaseMetadataIdentifier metadataIdentifier) {
+        try (JedisCluster jedisCluster =
+                new JedisCluster(jedisClusterNodes, timeout, timeout, 2, password, new GenericObjectPoolConfig<>())) {
+            return jedisCluster.get(metadataIdentifier.getIdentifierKey() + META_DATA_STORE_TAG);
+        } catch (Throwable e) {
+            String msg = "Failed to get " + metadataIdentifier + " from redis cluster , cause: " + e.getMessage();
+            logger.error(TRANSPORT_FAILED_RESPONSE, "", "", msg, e);
+            throw new RpcException(msg, e);
+        }
+    }
+
+    private String getMetadataStandalone(BaseMetadataIdentifier metadataIdentifier) {
+        try (Jedis jedis = pool.getResource()) {
+            return jedis.get(metadataIdentifier.getUniqueKey(KeyTypeEnum.UNIQUE_KEY));
+        } catch (Throwable e) {
+            String msg = "Failed to get " + metadataIdentifier + " from redis , cause: " + e.getMessage();
+            logger.error(TRANSPORT_FAILED_RESPONSE, "", "", msg, e);
+            throw new RpcException(msg, e);
+        }
+    }
+
+    /**
+     * Store class and application names using Redis hashes
+     * key: default 'dubbo:mapping'
+     * field: class (serviceInterface)
+     * value: application_names
+     * @param serviceInterface field(class)
+     * @param defaultMappingGroup  {@link ServiceNameMapping#DEFAULT_MAPPING_GROUP}
+     * @param newConfigContent new application_names
+     * @param ticket previous application_names
+     * @return
+     */
+    @Override
+    public boolean registerServiceAppMapping(
+            String serviceInterface, String defaultMappingGroup, String newConfigContent, Object ticket) {
+        try {
+            if (null != ticket && !(ticket instanceof String)) {
+                throw new IllegalArgumentException("redis publishConfigCas requires stat type ticket");
+            }
+            String pathKey = buildMappingKey(defaultMappingGroup);
+
+            return storeMapping(pathKey, serviceInterface, newConfigContent, (String) ticket);
+        } catch (Exception e) {
+            logger.warn(TRANSPORT_FAILED_RESPONSE, "", "", "redis publishConfigCas failed.", e);
+            return false;
+        }
+    }
+
+    private boolean storeMapping(String key, String field, String value, String ticket) {
+        if (pool != null) {
+            return storeMappingStandalone(key, field, value, ticket);
+        } else {
+            return storeMappingInCluster(key, field, value, ticket);
+        }
+    }
+
+    /**
+     * use 'watch' to implement cas.
+     * Find information about slot distribution by key.
+     */
+    private boolean storeMappingInCluster(String key, String field, String value, String ticket) {
+        try (JedisCluster jedisCluster =
+                new JedisCluster(jedisClusterNodes, timeout, timeout, 2, password, new GenericObjectPoolConfig<>())) {
+            Jedis jedis = new Jedis(jedisCluster.getConnectionFromSlot(JedisClusterCRC16.getSlot(key)));
+            jedis.watch(key);
+            String oldValue = jedis.hget(key, field);
+            if (null == oldValue || null == ticket || oldValue.equals(ticket)) {
+                Transaction transaction = jedis.multi();
+                transaction.hset(key, field, value);
+                List<Object> result = transaction.exec();
+                if (null != result) {
+                    jedisCluster.publish(buildPubSubKey(), field);
+                    return true;
+                }
+            } else {
+                jedis.unwatch();
+            }
+            jedis.close();
+        } catch (Throwable e) {
+            String msg = "Failed to put " + key + ":" + field + " to redis " + value + ", cause: " + e.getMessage();
+            logger.error(TRANSPORT_FAILED_RESPONSE, "", "", msg, e);
+            throw new RpcException(msg, e);
+        }
+        return false;
+    }
+
+    /**
+     * use 'watch' to implement cas.
+     * Find information about slot distribution by key.
+     */
+    private boolean storeMappingStandalone(String key, String field, String value, String ticket) {
+        try (Jedis jedis = pool.getResource()) {
+            jedis.watch(key);
+            String oldValue = jedis.hget(key, field);
+            if (null == oldValue || null == ticket || oldValue.equals(ticket)) {
+                Transaction transaction = jedis.multi();
+                transaction.hset(key, field, value);
+                List<Object> result = transaction.exec();
+                if (null != result) {
+                    jedis.publish(buildPubSubKey(), field);
+                    return true;
+                }
+            }
+            jedis.unwatch();
+        } catch (Throwable e) {
+            String msg = "Failed to put " + key + ":" + field + " to redis " + value + ", cause: " + e.getMessage();
+            logger.error(TRANSPORT_FAILED_RESPONSE, "", "", msg, e);
+            throw new RpcException(msg, e);
+        }
+        return false;
+    }
+
+    /**
+     * build mapping key
+     * @param defaultMappingGroup {@link ServiceNameMapping#DEFAULT_MAPPING_GROUP}
+     * @return
+     */
+    private String buildMappingKey(String defaultMappingGroup) {
+        return this.root + GROUP_CHAR_SEPARATOR + defaultMappingGroup;
+    }
+
+    /**
+     * build pub/sub key
+     */
+    private String buildPubSubKey() {
+        return buildMappingKey(DEFAULT_MAPPING_GROUP) + GROUP_CHAR_SEPARATOR + QUEUES_KEY;
+    }
+
+    /**
+     * get content and use content to complete cas
+     * @param serviceKey class
+     * @param group {@link ServiceNameMapping#DEFAULT_MAPPING_GROUP}
+     */
+    @Override
+    public ConfigItem getConfigItem(String serviceKey, String group) {
+        String key = buildMappingKey(group);
+        String content = getMappingData(key, serviceKey);
+
+        return new ConfigItem(content, content);
+    }
+
+    /**
+     * get current application_names
+     */
+    private String getMappingData(String key, String field) {
+        if (pool != null) {
+            return getMappingDataStandalone(key, field);
+        } else {
+            return getMappingDataInCluster(key, field);
+        }
+    }
+
+    private String getMappingDataInCluster(String key, String field) {
+        try (JedisCluster jedisCluster =
+                new JedisCluster(jedisClusterNodes, timeout, timeout, 2, password, new GenericObjectPoolConfig<>())) {
+            return jedisCluster.hget(key, field);
+        } catch (Throwable e) {
+            String msg = "Failed to get " + key + ":" + field + " from redis cluster , cause: " + e.getMessage();
+            logger.error(TRANSPORT_FAILED_RESPONSE, "", "", msg, e);
+            throw new RpcException(msg, e);
+        }
+    }
+
+    private String getMappingDataStandalone(String key, String field) {
+        try (Jedis jedis = pool.getResource()) {
+            return jedis.hget(key, field);
+        } catch (Throwable e) {
+            String msg = "Failed to get " + key + ":" + field + " from redis , cause: " + e.getMessage();
+            logger.error(TRANSPORT_FAILED_RESPONSE, "", "", msg, e);
+            throw new RpcException(msg, e);
+        }
+    }
+
+    /**
+     * remove listener. If have no listener,thread will dead
+     */
+    @Override
+    public void removeServiceAppMappingListener(String serviceKey, MappingListener listener) {
+        MappingDataListener mappingDataListener = mappingDataListenerMap.get(buildPubSubKey());
+        if (null != mappingDataListener) {
+            NotifySub notifySub = mappingDataListener.getNotifySub();
+            notifySub.removeListener(serviceKey, listener);
+            if (notifySub.isEmpty()) {
+                mappingDataListener.shutdown();
+            }
+        }
+    }
+
+    /**
+     * Start a thread and subscribe to {@link this#buildPubSubKey()}.
+     * Notify {@link MappingListener} if there is a change in the 'application_names' message.
+     */
+    @Override
+    public Set<String> getServiceAppMapping(String serviceKey, MappingListener listener, URL url) {
+        MappingDataListener mappingDataListener =
+                ConcurrentHashMapUtils.computeIfAbsent(mappingDataListenerMap, buildPubSubKey(), k -> {
+                    MappingDataListener dataListener = new MappingDataListener(buildPubSubKey());
+                    dataListener.start();
+                    return dataListener;
+                });
+        mappingDataListener.getNotifySub().addListener(serviceKey, listener);
+        return this.getServiceAppMapping(serviceKey, url);
+    }
+
+    @Override
+    public Set<String> getServiceAppMapping(String serviceKey, URL url) {
+        String key = buildMappingKey(DEFAULT_MAPPING_GROUP);
+        return getAppNames(getMappingData(key, serviceKey));
+    }
+
+    @Override
+    public MetadataInfo getAppMetadata(SubscriberMetadataIdentifier identifier, Map<String, String> instanceMetadata) {
+        String content = this.getMetadata(identifier);
+        return JsonUtils.toJavaObject(content, MetadataInfo.class);
+    }
+
+    @Override
+    public void publishAppMetadata(SubscriberMetadataIdentifier identifier, MetadataInfo metadataInfo) {
+        this.storeMetadata(identifier, metadataInfo.getContent());
+    }
+
+    @Override
+    public void unPublishAppMetadata(SubscriberMetadataIdentifier identifier, MetadataInfo metadataInfo) {
+        this.deleteMetadata(identifier);
+    }
+
+    // for test
+    public MappingDataListener getMappingDataListener() {
+        return mappingDataListenerMap.get(buildPubSubKey());
+    }
+
+    /**
+     * Listen for changes in the 'application_names' message and notify the listener.
+     */
+    class NotifySub extends JedisPubSub {
+
+        private final Map<String, Set<MappingListener>> listeners = new ConcurrentHashMap<>();
+
+        public void addListener(String key, MappingListener listener) {
+            Set<MappingListener> listenerSet = listeners.computeIfAbsent(key, k -> new ConcurrentHashSet<>());
+            listenerSet.add(listener);
+        }
+
+        public void removeListener(String serviceKey, MappingListener listener) {
+            Set<MappingListener> listenerSet = this.listeners.get(serviceKey);
+            if (listenerSet != null) {
+                listenerSet.remove(listener);
+                if (listenerSet.isEmpty()) {
+                    this.listeners.remove(serviceKey);
+                }
+            }
+        }
+
+        public Boolean isEmpty() {
+            return this.listeners.isEmpty();
+        }
+
+        @Override
+        public void onMessage(String key, String msg) {
+            logger.info("sub from redis:" + key + " message:" + msg);
+            String applicationNames = getMappingData(buildMappingKey(DEFAULT_MAPPING_GROUP), msg);
+            MappingChangedEvent mappingChangedEvent = new MappingChangedEvent(msg, getAppNames(applicationNames));
+            if (!listeners.get(msg).isEmpty()) {
+                for (MappingListener mappingListener : listeners.get(msg)) {
+                    mappingListener.onEvent(mappingChangedEvent);
+                }
+            }
+        }
+
+        @Override
+        public void onPMessage(String pattern, String key, String msg) {
+            onMessage(key, msg);
+        }
+
+        @Override
+        public void onPSubscribe(String pattern, int subscribedChannels) {
+            super.onPSubscribe(pattern, subscribedChannels);
+        }
+    }
+
+    /**
+     * Subscribe application names change message.
+     */
+    class MappingDataListener extends Thread {
+
+        private String path;
+
+        private final NotifySub notifySub = new NotifySub();
+        // for test
+        protected volatile boolean running = true;
+
+        public MappingDataListener(String path) {
+            this.path = path;
+        }
+
+        public NotifySub getNotifySub() {
+            return notifySub;
+        }
+
+        @Override
+        public void run() {
+            while (running) {
+                if (pool != null) {
+                    try (Jedis jedis = pool.getResource()) {
+                        jedis.subscribe(notifySub, path);
+                    } catch (Throwable e) {
+                        String msg = "Failed to subscribe " + path + ", cause: " + e.getMessage();
+                        logger.error(TRANSPORT_FAILED_RESPONSE, "", "", msg, e);
+                        throw new RpcException(msg, e);
+                    }
+                } else {
+                    try (JedisCluster jedisCluster = new JedisCluster(
+                            jedisClusterNodes, timeout, timeout, 2, password, new GenericObjectPoolConfig<>())) {
+                        jedisCluster.subscribe(notifySub, path);
+                    } catch (Throwable e) {
+                        String msg = "Failed to subscribe " + path + ", cause: " + e.getMessage();
+                        logger.error(TRANSPORT_FAILED_RESPONSE, "", "", msg, e);
+                        throw new RpcException(msg, e);
+                    }
+                }
+            }
+        }
+
+        public void shutdown() {
+            try {
+                running = false;
+                notifySub.unsubscribe(path);
+            } catch (Throwable e) {
+                String msg = "Failed to unsubscribe " + path + ", cause: " + e.getMessage();
+                logger.error(TRANSPORT_FAILED_RESPONSE, "", "", msg, e);
+            }
+        }
+    }
+}
diff --git a/dubbo-metadata-report-extensions/dubbo-metadata-report-redis/src/main/java/org/apache/dubbo/metadata/store/redis/RedisMetadataReportFactory.java b/dubbo-metadata-report-extensions/dubbo-metadata-report-redis/src/main/java/org/apache/dubbo/metadata/store/redis/RedisMetadataReportFactory.java
new file mode 100644
index 00000000..d871877f
--- /dev/null
+++ b/dubbo-metadata-report-extensions/dubbo-metadata-report-redis/src/main/java/org/apache/dubbo/metadata/store/redis/RedisMetadataReportFactory.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.metadata.store.redis;
+
+import org.apache.dubbo.common.URL;
+import org.apache.dubbo.metadata.report.MetadataReport;
+import org.apache.dubbo.metadata.report.support.AbstractMetadataReportFactory;
+
+/**
+ * RedisMetadataReportFactory.
+ */
+public class RedisMetadataReportFactory extends AbstractMetadataReportFactory {
+
+    @Override
+    public MetadataReport createMetadataReport(URL url) {
+        return new RedisMetadataReport(url);
+    }
+}
diff --git a/dubbo-metadata-report-extensions/dubbo-metadata-report-redis/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.metadata.report.MetadataReportFactory b/dubbo-metadata-report-extensions/dubbo-metadata-report-redis/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.metadata.report.MetadataReportFactory
new file mode 100644
index 00000000..2e6effa1
--- /dev/null
+++ b/dubbo-metadata-report-extensions/dubbo-metadata-report-redis/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.metadata.report.MetadataReportFactory
@@ -0,0 +1 @@
+redis=org.apache.dubbo.metadata.store.redis.RedisMetadataReportFactory
diff --git a/dubbo-metadata-report-extensions/dubbo-metadata-report-redis/src/test/java/org/apache/dubbo/metadata/store/redis/RedisMetadata4TstService.java b/dubbo-metadata-report-extensions/dubbo-metadata-report-redis/src/test/java/org/apache/dubbo/metadata/store/redis/RedisMetadata4TstService.java
new file mode 100644
index 00000000..c9662c31
--- /dev/null
+++ b/dubbo-metadata-report-extensions/dubbo-metadata-report-redis/src/test/java/org/apache/dubbo/metadata/store/redis/RedisMetadata4TstService.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.metadata.store.redis;
+
+public interface RedisMetadata4TstService {
+
+    int getCounter();
+
+    void printResult(String var);
+}
diff --git a/dubbo-metadata-report-extensions/dubbo-metadata-report-redis/src/test/java/org/apache/dubbo/metadata/store/redis/RedisMetadataReportTest.java b/dubbo-metadata-report-extensions/dubbo-metadata-report-redis/src/test/java/org/apache/dubbo/metadata/store/redis/RedisMetadataReportTest.java
new file mode 100644
index 00000000..5a922875
--- /dev/null
+++ b/dubbo-metadata-report-extensions/dubbo-metadata-report-redis/src/test/java/org/apache/dubbo/metadata/store/redis/RedisMetadataReportTest.java
@@ -0,0 +1,365 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.metadata.store.redis;
+
+import org.apache.commons.lang3.SystemUtils;
+import org.apache.dubbo.common.URL;
+import org.apache.dubbo.common.config.configcenter.ConfigItem;
+import org.apache.dubbo.common.utils.JsonUtils;
+import org.apache.dubbo.common.utils.NetUtils;
+import org.apache.dubbo.metadata.MappingChangedEvent;
+import org.apache.dubbo.metadata.MappingListener;
+import org.apache.dubbo.metadata.MetadataInfo;
+import org.apache.dubbo.metadata.definition.ServiceDefinitionBuilder;
+import org.apache.dubbo.metadata.definition.model.FullServiceDefinition;
+import org.apache.dubbo.metadata.report.identifier.KeyTypeEnum;
+import org.apache.dubbo.metadata.report.identifier.MetadataIdentifier;
+import org.apache.dubbo.metadata.report.identifier.SubscriberMetadataIdentifier;
+import org.apache.dubbo.rpc.RpcException;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.exceptions.JedisConnectionException;
+import redis.clients.jedis.exceptions.JedisDataException;
+import redis.embedded.RedisServer;
+
+import static org.apache.dubbo.common.constants.CommonConstants.CONSUMER_SIDE;
+import static org.apache.dubbo.common.constants.CommonConstants.PROVIDER_SIDE;
+import static org.apache.dubbo.common.constants.CommonConstants.SYNC_REPORT_KEY;
+import static org.apache.dubbo.metadata.ServiceNameMapping.DEFAULT_MAPPING_GROUP;
+import static redis.embedded.RedisServer.newRedisServer;
+
+@DisabledOnOs(OS.WINDOWS)
+class RedisMetadataReportTest {
+
+    private static final String REDIS_URL_TEMPLATE = "redis://%slocalhost:%d",
+            REDIS_PASSWORD = "password",
+            REDIS_URL_AUTH_SECTION = "username:" + REDIS_PASSWORD + "@";
+
+    RedisMetadataReport redisMetadataReport;
+    RedisMetadataReport syncRedisMetadataReport;
+    RedisServer redisServer;
+    URL registryUrl;
+
+    @BeforeEach
+    public void constructor(final TestInfo testInfo) {
+        final boolean usesAuthentication = usesAuthentication(testInfo);
+        int redisPort = 0;
+        IOException exception = null;
+
+        for (int i = 0; i < 10; i++) {
+            try {
+                redisPort = NetUtils.getAvailablePort(30000 + new Random().nextInt(10000));
+                redisServer = newRedisServer()
+                        .port(redisPort)
+                        // set maxheap to fix Windows error 0x70 while starting redis
+                        .settingIf(SystemUtils.IS_OS_WINDOWS, "maxheap 128mb")
+                        .settingIf(usesAuthentication, "requirepass " + REDIS_PASSWORD)
+                        .build();
+                this.redisServer.start();
+                exception = null;
+            } catch (IOException e) {
+                e.printStackTrace();
+                exception = e;
+            }
+            if (exception == null) {
+                break;
+            }
+        }
+
+        Assertions.assertNull(exception);
+        registryUrl = newRedisUrl(usesAuthentication, redisPort);
+        redisMetadataReport = (RedisMetadataReport) new RedisMetadataReportFactory().createMetadataReport(registryUrl);
+        URL syncRegistryUrl = registryUrl.addParameter(SYNC_REPORT_KEY, "true");
+        syncRedisMetadataReport =
+                (RedisMetadataReport) new RedisMetadataReportFactory().createMetadataReport(syncRegistryUrl);
+    }
+
+    private static boolean usesAuthentication(final TestInfo testInfo) {
+        final String methodName = testInfo.getTestMethod().get().getName();
+        return "testAuthRedisMetadata".equals(methodName) || "testWrongAuthRedisMetadata".equals(methodName);
+    }
+
+    private static URL newRedisUrl(final boolean usesAuthentication, final int redisPort) {
+        final String urlAuthSection = usesAuthentication ? REDIS_URL_AUTH_SECTION : "";
+        return URL.valueOf(String.format(REDIS_URL_TEMPLATE, urlAuthSection, redisPort));
+    }
+
+    @AfterEach
+    public void tearDown() throws Exception {
+        this.redisServer.stop();
+    }
+
+    @Test
+    void testAsyncStoreProvider() throws ClassNotFoundException {
+        testStoreProvider(redisMetadataReport, "1.0.0.redis.md.p1", 3000);
+    }
+
+    @Test
+    void testSyncStoreProvider() throws ClassNotFoundException {
+        testStoreProvider(syncRedisMetadataReport, "1.0.0.redis.md.p2", 3);
+    }
+
+    private void testStoreProvider(RedisMetadataReport redisMetadataReport, String version, long moreTime)
+            throws ClassNotFoundException {
+        String interfaceName = "org.apache.dubbo.metadata.store.redis.RedisMetadata4TstService";
+        String group = null;
+        String application = "vic.redis.md";
+        MetadataIdentifier providerMetadataIdentifier =
+                storePrivider(redisMetadataReport, interfaceName, version, group, application);
+        Jedis jedis = null;
+        try {
+            jedis = redisMetadataReport.pool.getResource();
+            String keyTmp = providerMetadataIdentifier.getUniqueKey(KeyTypeEnum.UNIQUE_KEY);
+            String value = jedis.get(keyTmp);
+            if (value == null) {
+                Thread.sleep(moreTime);
+                value = jedis.get(keyTmp);
+            }
+
+            Assertions.assertNotNull(value);
+
+            FullServiceDefinition fullServiceDefinition = JsonUtils.toJavaObject(value, FullServiceDefinition.class);
+            Assertions.assertEquals(fullServiceDefinition.getParameters().get("paramTest"), "redisTest");
+        } catch (Throwable e) {
+            throw new RpcException("Failed to put to redis . cause: " + e.getMessage(), e);
+        } finally {
+            if (jedis != null) {
+                jedis.del(providerMetadataIdentifier.getUniqueKey(KeyTypeEnum.UNIQUE_KEY));
+            }
+            redisMetadataReport.pool.close();
+        }
+    }
+
+    @Test
+    void testAsyncStoreConsumer() throws ClassNotFoundException {
+        testStoreConsumer(redisMetadataReport, "1.0.0.redis.md.c1", 3000);
+    }
+
+    @Test
+    void testSyncStoreConsumer() throws ClassNotFoundException {
+        testStoreConsumer(syncRedisMetadataReport, "1.0.0.redis.md.c2", 3);
+    }
+
+    private void testStoreConsumer(RedisMetadataReport redisMetadataReport, String version, long moreTime)
+            throws ClassNotFoundException {
+        String interfaceName = "org.apache.dubbo.metadata.store.redis.RedisMetadata4TstService";
+        String group = null;
+        String application = "vic.redis.md";
+        MetadataIdentifier consumerMetadataIdentifier =
+                storeConsumer(redisMetadataReport, interfaceName, version, group, application);
+        Jedis jedis = null;
+        try {
+            jedis = redisMetadataReport.pool.getResource();
+            String keyTmp = consumerMetadataIdentifier.getUniqueKey(KeyTypeEnum.UNIQUE_KEY);
+            String value = jedis.get(keyTmp);
+            if (value == null) {
+                Thread.sleep(moreTime);
+                value = jedis.get(keyTmp);
+            }
+            Assertions.assertEquals(value, "{\"paramConsumerTest\":\"redisCm\"}");
+        } catch (Throwable e) {
+            throw new RpcException("Failed to put to redis . cause: " + e.getMessage(), e);
+        } finally {
+            if (jedis != null) {
+                jedis.del(consumerMetadataIdentifier.getUniqueKey(KeyTypeEnum.UNIQUE_KEY));
+            }
+            redisMetadataReport.pool.close();
+        }
+    }
+
+    private MetadataIdentifier storePrivider(
+            RedisMetadataReport redisMetadataReport,
+            String interfaceName,
+            String version,
+            String group,
+            String application)
+            throws ClassNotFoundException {
+        URL url = URL.valueOf("xxx://" + NetUtils.getLocalAddress().getHostName() + ":4444/" + interfaceName
+                + "?paramTest=redisTest&version=" + version + "&application=" + application
+                + (group == null ? "" : "&group=" + group));
+
+        MetadataIdentifier providerMetadataIdentifier =
+                new MetadataIdentifier(interfaceName, version, group, PROVIDER_SIDE, application);
+        Class interfaceClass = Class.forName(interfaceName);
+        FullServiceDefinition fullServiceDefinition =
+                ServiceDefinitionBuilder.buildFullDefinition(interfaceClass, url.getParameters());
+
+        redisMetadataReport.storeProviderMetadata(providerMetadataIdentifier, fullServiceDefinition);
+        try {
+            Thread.sleep(300);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+        return providerMetadataIdentifier;
+    }
+
+    private MetadataIdentifier storeConsumer(
+            RedisMetadataReport redisMetadataReport,
+            String interfaceName,
+            String version,
+            String group,
+            String application)
+            throws ClassNotFoundException {
+        URL url = URL.valueOf("xxx://" + NetUtils.getLocalAddress().getHostName() + ":4444/" + interfaceName
+                + "?version=" + version + "&application=" + application + (group == null ? "" : "&group=" + group));
+
+        MetadataIdentifier consumerMetadataIdentifier =
+                new MetadataIdentifier(interfaceName, version, group, CONSUMER_SIDE, application);
+        Class interfaceClass = Class.forName(interfaceName);
+
+        Map<String, String> tmp = new HashMap<>();
+        tmp.put("paramConsumerTest", "redisCm");
+        redisMetadataReport.storeConsumerMetadata(consumerMetadataIdentifier, tmp);
+        try {
+            Thread.sleep(300);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+        return consumerMetadataIdentifier;
+    }
+
+    @Test
+    void testAuthRedisMetadata() throws ClassNotFoundException {
+        testStoreProvider(redisMetadataReport, "1.0.0.redis.md.p1", 3000);
+    }
+
+    @Test
+    void testWrongAuthRedisMetadata() throws ClassNotFoundException {
+        redisMetadataReport = (RedisMetadataReport) new RedisMetadataReportFactory().createMetadataReport(registryUrl);
+        try {
+            testStoreProvider(redisMetadataReport, "1.0.0.redis.md.p1", 3000);
+        } catch (RpcException e) {
+            if (e.getCause() instanceof JedisConnectionException
+                    && e.getCause().getCause() instanceof JedisDataException) {
+                Assertions.assertEquals(
+                        "WRONGPASS invalid username-password pair or user is disabled.",
+                        e.getCause().getCause().getMessage());
+            } else {
+                Assertions.fail("no invalid password exception!");
+            }
+        }
+    }
+
+    @Test
+    void testRegisterServiceAppMapping() throws InterruptedException {
+        String serviceKey1 = "org.apache.dubbo.metadata.store.redis.RedisMetadata4TstService";
+        String serviceKey2 = "org.apache.dubbo.metadata.store.redis.RedisMetadata4TstService2";
+
+        String appNames1 = "test1";
+        String appNames2 = "test1,test2";
+        CountDownLatch latch = new CountDownLatch(2);
+        CountDownLatch latch2 = new CountDownLatch(2);
+
+        MappingListener mappingListener = new MappingListener() {
+            @Override
+            public void onEvent(MappingChangedEvent event) {
+                Set<String> apps = event.getApps();
+                if (apps.size() == 1) {
+                    Assertions.assertTrue(apps.contains("test1"));
+                } else {
+                    Assertions.assertTrue(apps.contains("test1"));
+                    Assertions.assertTrue(apps.contains("test2"));
+                }
+                if (serviceKey1.equals(event.getServiceKey())) {
+                    latch.countDown();
+                } else if (serviceKey2.equals(event.getServiceKey())) {
+                    latch2.countDown();
+                }
+            }
+
+            @Override
+            public void stop() {}
+        };
+
+        Set<String> serviceAppMapping =
+                redisMetadataReport.getServiceAppMapping(serviceKey1, mappingListener, registryUrl);
+
+        Assertions.assertTrue(serviceAppMapping.isEmpty());
+
+        ConfigItem configItem = redisMetadataReport.getConfigItem(serviceKey1, DEFAULT_MAPPING_GROUP);
+
+        redisMetadataReport.registerServiceAppMapping(
+                serviceKey1, DEFAULT_MAPPING_GROUP, appNames1, configItem.getTicket());
+        configItem = redisMetadataReport.getConfigItem(serviceKey1, DEFAULT_MAPPING_GROUP);
+
+        redisMetadataReport.registerServiceAppMapping(
+                serviceKey1, DEFAULT_MAPPING_GROUP, appNames2, configItem.getTicket());
+
+        latch.await();
+
+        serviceAppMapping = redisMetadataReport.getServiceAppMapping(serviceKey2, mappingListener, registryUrl);
+
+        Assertions.assertTrue(serviceAppMapping.isEmpty());
+
+        configItem = redisMetadataReport.getConfigItem(serviceKey2, DEFAULT_MAPPING_GROUP);
+
+        redisMetadataReport.registerServiceAppMapping(
+                serviceKey2, DEFAULT_MAPPING_GROUP, appNames1, configItem.getTicket());
+        configItem = redisMetadataReport.getConfigItem(serviceKey2, DEFAULT_MAPPING_GROUP);
+        redisMetadataReport.registerServiceAppMapping(
+                serviceKey2, DEFAULT_MAPPING_GROUP, appNames2, configItem.getTicket());
+
+        latch2.await();
+        RedisMetadataReport.MappingDataListener mappingDataListener = redisMetadataReport.getMappingDataListener();
+        Assertions.assertTrue(mappingDataListener.running);
+        Assertions.assertTrue(!mappingDataListener.getNotifySub().isEmpty());
+
+        redisMetadataReport.removeServiceAppMappingListener(serviceKey1, mappingListener);
+        Assertions.assertTrue(mappingDataListener.running);
+        Assertions.assertTrue(!mappingDataListener.getNotifySub().isEmpty());
+        redisMetadataReport.removeServiceAppMappingListener(serviceKey2, mappingListener);
+        Assertions.assertTrue(!mappingDataListener.running);
+        Assertions.assertTrue(mappingDataListener.getNotifySub().isEmpty());
+    }
+
+    @Test
+    void testAppMetadata() {
+        String serviceKey = "org.apache.dubbo.metadata.store.redis.RedisMetadata4TstService";
+        String appName = "demo";
+        URL url = URL.valueOf("test://127.0.0.1:8888/" + serviceKey);
+
+        MetadataInfo metadataInfo = new MetadataInfo(appName);
+        metadataInfo.addService(url);
+        SubscriberMetadataIdentifier identifier =
+                new SubscriberMetadataIdentifier(appName, metadataInfo.calAndGetRevision());
+        MetadataInfo appMetadata = redisMetadataReport.getAppMetadata(identifier, Collections.emptyMap());
+        Assertions.assertNull(appMetadata);
+
+        redisMetadataReport.publishAppMetadata(identifier, metadataInfo);
+        appMetadata = redisMetadataReport.getAppMetadata(identifier, Collections.emptyMap());
+        Assertions.assertNotNull(appMetadata);
+        Assertions.assertEquals(appMetadata.toFullString(), metadataInfo.toFullString());
+        redisMetadataReport.unPublishAppMetadata(identifier, metadataInfo);
+        appMetadata = redisMetadataReport.getAppMetadata(identifier, Collections.emptyMap());
+        Assertions.assertNull(appMetadata);
+    }
+}
diff --git a/dubbo-metadata-report-extensions/dubbo-metadata-report-redis/src/test/resources/log4j2-test.xml b/dubbo-metadata-report-extensions/dubbo-metadata-report-redis/src/test/resources/log4j2-test.xml
new file mode 100644
index 00000000..14fdc8c5
--- /dev/null
+++ b/dubbo-metadata-report-extensions/dubbo-metadata-report-redis/src/test/resources/log4j2-test.xml
@@ -0,0 +1,28 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You under the Apache License, Version 2.0
+  ~ (the "License"); you may not use this file except in compliance with
+  ~ the License.  You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<Configuration status="WARN">
+    <Appenders>
+        <Console name="Console" target="SYSTEM_OUT" follow="true">
+            <PatternLayout pattern="%d{HH:mm:ss.SSS} |-%highlight{%-5p} [%t] %40.40c:%-3L -| %m%n%rEx{filters(jdk.internal.reflect,java.lang.reflect,sun.reflect,org.junit,org.mockito)}" charset="UTF-8"/>
+        </Console>
+    </Appenders>
+    <Loggers>
+        <Root level="info">
+            <AppenderRef ref="Console"/>
+        </Root>
+    </Loggers>
+</Configuration>
diff --git a/dubbo-metadata-report-extensions/pom.xml b/dubbo-metadata-report-extensions/pom.xml
index 2414b426..1c458b26 100644
--- a/dubbo-metadata-report-extensions/pom.xml
+++ b/dubbo-metadata-report-extensions/pom.xml
@@ -34,5 +34,6 @@
         <module>dubbo-metadata-report-consul</module>
         <module>dubbo-metadata-report-etcd</module>
         <module>dubbo-metadata-rest</module>
+        <module>dubbo-metadata-report-redis</module>
     </modules>
 </project>