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

thunguo pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-seata-go.git


The following commit(s) were added to refs/heads/master by this push:
     new e74ab61d [GSOC] Wire registry change snapshots into remoting address 
lifecycle (#1138)
e74ab61d is described below

commit e74ab61d7efc331efcda4da911a0ab8e4e326437
Author: CAICAII <[email protected]>
AuthorDate: Mon Aug 24 01:28:37 2026 +0800

    [GSOC] Wire registry change snapshots into remoting address lifecycle 
(#1138)
    
    * feat(discovery): add registry change subscriptions
    
    Signed-off-by: CAICAIIs <[email protected]>
    
    * fix(discovery): address subscription review feedback
    
    Signed-off-by: CAICAIIs <[email protected]>
    
    * feat(discovery): adapt namingserver registry subscriptions
    
    Signed-off-by: CAICAIIs <[email protected]>
    
    * feat(remoting): consume registry address subscriptions
    
    Signed-off-by: CAICAIIs <[email protected]>
    
    * feat(remoting): consume registry changes in grpc
    
    Signed-off-by: CAICAIIs <[email protected]>
    
    * feat(discovery): add raft registry subscriptions
    
    Signed-off-by: CAICAIIs <[email protected]>
    
    * fix(remoting): validate cached grpc channels
    
    Signed-off-by: CAICAIIs <[email protected]>
    
    * fix(registry): harden dynamic address lifecycle
    
    Signed-off-by: CAICAIIs <[email protected]>
    
    * test(remoting): avoid empty critical section in close test
    
    Signed-off-by: CAICAIIs <[email protected]>
    
    ---------
    
    Signed-off-by: CAICAIIs <[email protected]>
    Co-authored-by: ssshr-66 <[email protected]>
    Co-authored-by: Ethan <[email protected]>
    Co-authored-by: ThunGuo <[email protected]>
---
 pkg/discovery/base.go                      |  24 +++
 pkg/discovery/etcd3.go                     |  79 ++++++-
 pkg/discovery/etcd3_test.go                |  44 ++++
 pkg/discovery/file_test.go                 |  11 +
 pkg/discovery/metadata/metadata.go         |  31 ++-
 pkg/discovery/metadata/metadata_test.go    |  26 +++
 pkg/discovery/naming_server.go             | 216 +++++++++++++++++--
 pkg/discovery/naming_server_test.go        | 133 ++++++++++++
 pkg/discovery/raft.go                      | 147 ++++++++++++-
 pkg/discovery/raft_subscription_test.go    | 133 ++++++++++++
 pkg/discovery/store.go                     |  11 +-
 pkg/discovery/subscription.go              | 132 ++++++++++++
 pkg/discovery/subscription_test.go         | 136 ++++++++++++
 pkg/remoting/getty/getty_client.go         |  10 +-
 pkg/remoting/getty/getty_client_test.go    |  14 +-
 pkg/remoting/getty/session_manager.go      | 284 ++++++++++++++++++++++---
 pkg/remoting/getty/session_manager_test.go | 236 +++++++++++++++++++++
 pkg/remoting/grpc/channel.go               |  22 +-
 pkg/remoting/grpc/channel_manager.go       | 323 +++++++++++++++++++++++------
 pkg/remoting/grpc/channel_manager_test.go  | 205 ++++++++++++++++++
 pkg/remoting/grpc/listener.go              |  12 +-
 21 files changed, 2081 insertions(+), 148 deletions(-)

diff --git a/pkg/discovery/base.go b/pkg/discovery/base.go
index 9a30b04e..0c3745b2 100644
--- a/pkg/discovery/base.go
+++ b/pkg/discovery/base.go
@@ -45,3 +45,27 @@ type RegisterableRegistryService interface {
        Register(instance *ServiceInstance) error
        Unregister(instance *ServiceInstance) error
 }
+
+// RegistryChangeEvent is a backend-neutral snapshot of service instances for a
+// transaction service group.
+type RegistryChangeEvent struct {
+       Key       string
+       Instances []*ServiceInstance
+}
+
+// RegistryChangeListener receives backend-neutral registry change snapshots.
+type RegistryChangeListener func(event RegistryChangeEvent)
+
+// RegistrySubscription cancels a registry change subscription. Unsubscribe is
+// safe to call multiple times. A callback already in progress may still run.
+type RegistrySubscription interface {
+       Unsubscribe()
+}
+
+// RegistrySubscriber is an optional capability for registries that can report
+// address changes. Listeners receive the current snapshot first, followed by
+// backend-neutral snapshots. Calls for one subscription are serialized. Slow
+// listeners may skip intermediate snapshots.
+type RegistrySubscriber interface {
+       Subscribe(key string, listener RegistryChangeListener) 
(RegistrySubscription, error)
+}
diff --git a/pkg/discovery/etcd3.go b/pkg/discovery/etcd3.go
index 3997ab88..0f2547d8 100644
--- a/pkg/discovery/etcd3.go
+++ b/pkg/discovery/etcd3.go
@@ -39,13 +39,19 @@ const (
 type EtcdRegistryService struct {
        client        *etcd3.Client
        cfg           etcd3.Config
-       vgroupMapping map[string]string
+       vgroupMapping map[string]string // copied during construction; 
read-only afterwards
        store         *AddressStore
 
        stopCh    chan struct{}
        closeOnce sync.Once
+
+       subscriptionsMu sync.Mutex
+       subscriptions   map[*registryChangeSubscription]struct{}
+       closed          bool
 }
 
+var _ RegistrySubscriber = (*EtcdRegistryService)(nil)
+
 func newEtcdRegistryService(config *ServiceConfig, etcd3Config *Etcd3Config) 
(RegistryService, error) {
        if config == nil {
                return nil, fmt.Errorf("service config is nil")
@@ -62,7 +68,10 @@ func newEtcdRegistryService(config *ServiceConfig, 
etcd3Config *Etcd3Config) (Re
                return nil, fmt.Errorf("failed to create etcd3 client: %w", err)
        }
 
-       vgroupMapping := config.VgroupMapping
+       vgroupMapping := make(map[string]string, len(config.VgroupMapping))
+       for key, cluster := range config.VgroupMapping {
+               vgroupMapping[key] = cluster
+       }
 
        etcdRegistryService := &EtcdRegistryService{
                client:        cli,
@@ -104,8 +113,11 @@ func (s *EtcdRegistryService) watch(key string) {
                }
 
        }
-       // watch the changes of endpoints
-       watchCh := s.client.Watch(ctx, key, etcd3.WithPrefix())
+       watchOptions := []etcd3.OpOption{etcd3.WithPrefix()}
+       if resp != nil && resp.Header != nil {
+               watchOptions = append(watchOptions, 
etcd3.WithRev(resp.Header.Revision+1))
+       }
+       watchCh := s.client.Watch(ctx, key, watchOptions...)
 
        for {
                select {
@@ -198,14 +210,71 @@ func getClusterAndAddress(key []byte) (string, string, 
int, error) {
 func (s *EtcdRegistryService) Lookup(key string) ([]*ServiceInstance, error) {
        cluster := s.vgroupMapping[key]
        if cluster == "" {
-               return nil, fmt.Errorf("cluster doesnt exit")
+               return nil, fmt.Errorf("cluster doesn't exist")
        }
 
        return s.store.Snapshot(cluster), nil
 }
 
+func (s *EtcdRegistryService) Subscribe(key string, listener 
RegistryChangeListener) (RegistrySubscription, error) {
+       if listener == nil {
+               return nil, fmt.Errorf("registry change listener is nil")
+       }
+
+       cluster := s.vgroupMapping[key]
+       if cluster == "" {
+               return nil, fmt.Errorf("cluster doesn't exist")
+       }
+
+       subscription := newRegistryChangeSubscription(listener)
+       // Register before checking closed so taking the initial snapshot 
cannot miss
+       // an update. If Close wins the race, the check below removes the 
callback.
+       initial, unsubscribe := s.store.subscribeWithSnapshot(cluster, 
func(changedCluster string, instances []*ServiceInstance) {
+               if changedCluster == cluster {
+                       subscription.publish(RegistryChangeEvent{Key: key, 
Instances: instances})
+               }
+       })
+       subscription.initialize(RegistryChangeEvent{Key: key, Instances: 
initial}, func() {
+               unsubscribe()
+               s.removeSubscription(subscription)
+       })
+
+       s.subscriptionsMu.Lock()
+       if s.closed {
+               s.subscriptionsMu.Unlock()
+               subscription.Unsubscribe()
+               return nil, fmt.Errorf("registry service is closed")
+       }
+       if s.subscriptions == nil {
+               s.subscriptions = make(map[*registryChangeSubscription]struct{})
+       }
+       s.subscriptions[subscription] = struct{}{}
+       s.subscriptionsMu.Unlock()
+
+       subscription.start()
+       return subscription, nil
+}
+
+func (s *EtcdRegistryService) removeSubscription(subscription 
*registryChangeSubscription) {
+       s.subscriptionsMu.Lock()
+       delete(s.subscriptions, subscription)
+       s.subscriptionsMu.Unlock()
+}
+
 func (s *EtcdRegistryService) Close() {
        s.closeOnce.Do(func() {
+               s.subscriptionsMu.Lock()
+               s.closed = true
+               subscriptions := make([]*registryChangeSubscription, 0, 
len(s.subscriptions))
+               for subscription := range s.subscriptions {
+                       subscriptions = append(subscriptions, subscription)
+               }
+               s.subscriptions = nil
+               s.subscriptionsMu.Unlock()
+
+               for _, subscription := range subscriptions {
+                       subscription.Unsubscribe()
+               }
                if s.stopCh != nil {
                        close(s.stopCh)
                }
diff --git a/pkg/discovery/etcd3_test.go b/pkg/discovery/etcd3_test.go
index e8322688..346954d1 100644
--- a/pkg/discovery/etcd3_test.go
+++ b/pkg/discovery/etcd3_test.go
@@ -25,6 +25,7 @@ import (
 
        "github.com/golang/mock/gomock"
        "github.com/stretchr/testify/assert"
+       "go.etcd.io/etcd/api/v3/etcdserverpb"
        "go.etcd.io/etcd/api/v3/mvccpb"
        clientv3 "go.etcd.io/etcd/client/v3"
 
@@ -199,6 +200,39 @@ func TestEtcd3RegistryService_CloseIsRepeatable(t 
*testing.T) {
        }
 }
 
+func TestEtcd3RegistryService_WatchContinuesAfterSnapshotRevision(t 
*testing.T) {
+       ctrl := gomock.NewController(t)
+       mockEtcdClient := mock.NewMockEtcdClient(ctrl)
+       service := &EtcdRegistryService{
+               client: newTestEtcdClient(mockEtcdClient),
+               store:  NewAddressStore(),
+               stopCh: make(chan struct{}),
+       }
+
+       mockEtcdClient.EXPECT().Get(gomock.Any(), gomock.Any(), 
gomock.Any()).Return(&clientv3.GetResponse{
+               Header: &etcdserverpb.ResponseHeader{Revision: 41},
+       }, nil)
+       watchStarted := make(chan struct{})
+       watchCh := make(chan clientv3.WatchResponse)
+       mockEtcdClient.EXPECT().Watch(gomock.Any(), gomock.Any(), gomock.Any(), 
gomock.Any()).
+               DoAndReturn(func(_ context.Context, key string, options 
...clientv3.OpOption) clientv3.WatchChan {
+                       op := clientv3.OpGet(key, options...)
+                       assert.Equal(t, int64(42), op.Rev())
+                       close(watchStarted)
+                       return watchCh
+               })
+       mockEtcdClient.EXPECT().Close().Return(nil)
+
+       watchDone := make(chan struct{})
+       go func() {
+               service.watch(etcdClusterPrefix)
+               close(watchDone)
+       }()
+       waitForSignal(t, watchStarted, "etcd watch to start")
+       service.Close()
+       waitForSignal(t, watchDone, "etcd watch to stop")
+}
+
 func newTestEtcdClient(client mock.EtcdClient) *clientv3.Client {
        return clientv3.NewCtxClient(
                context.Background(),
@@ -208,3 +242,13 @@ func newTestEtcdClient(client mock.EtcdClient) 
*clientv3.Client {
                },
        )
 }
+
+func waitForSignal(t *testing.T, signal <-chan struct{}, name string) {
+       t.Helper()
+
+       select {
+       case <-signal:
+       case <-time.After(time.Second):
+               t.Fatalf("timed out waiting for %s", name)
+       }
+}
diff --git a/pkg/discovery/file_test.go b/pkg/discovery/file_test.go
index 9d653c13..55c27bd0 100644
--- a/pkg/discovery/file_test.go
+++ b/pkg/discovery/file_test.go
@@ -245,3 +245,14 @@ func TestFileRegistryService_Lookup(t *testing.T) {
                })
        }
 }
+
+func TestFileRegistryService_DoesNotImplementRegistrySubscriber(t *testing.T) {
+       service := newFileRegistryService(&ServiceConfig{
+               VgroupMapping: map[string]string{"default_tx_group": "default"},
+               Grouplist:     map[string]string{"default": "127.0.0.1:8091"},
+       })
+
+       if _, ok := service.(RegistrySubscriber); ok {
+               t.Fatal("file registry should keep lookup-only behavior")
+       }
+}
diff --git a/pkg/discovery/metadata/metadata.go 
b/pkg/discovery/metadata/metadata.go
index 3e9b4ed4..2134c94d 100644
--- a/pkg/discovery/metadata/metadata.go
+++ b/pkg/discovery/metadata/metadata.go
@@ -134,14 +134,24 @@ func (m *Metadata) GetClusterTerm(clusterName string) 
map[string]int64 {
 }
 
 func (m *Metadata) RefreshMetadata(clusterName string, response 
MetadataResponse) {
+       m.refreshMetadata(clusterName, "", response)
+}
+
+// RefreshGroupMetadata replaces the metadata for one queried group.
+func (m *Metadata) RefreshGroupMetadata(clusterName, group string, response 
MetadataResponse) {
+       m.refreshMetadata(clusterName, group, response)
+}
+
+func (m *Metadata) refreshMetadata(clusterName, queriedGroup string, response 
MetadataResponse) {
        nodesByGroup := make(map[string][]*Node)
        for _, node := range response.Nodes {
-               nodesByGroup[node.Group] = append(nodesByGroup[node.Group], 
node)
-               if node.Role == LEADER {
-                       groupMapAny, _ := m.leaders.LoadOrStore(clusterName, 
&sync.Map{})
-                       groupMap := groupMapAny.(*sync.Map)
-                       groupMap.Store(node.Group, node)
+               if node == nil {
+                       continue
                }
+               nodesByGroup[node.Group] = append(nodesByGroup[node.Group], 
node)
+       }
+       if len(nodesByGroup) == 0 && queriedGroup != "" {
+               nodesByGroup[queriedGroup] = nil
        }
 
        switch response.StoreMode {
@@ -152,9 +162,20 @@ func (m *Metadata) RefreshMetadata(clusterName string, 
response MetadataResponse
        }
 
        if len(nodesByGroup) > 0 {
+               leaderMapAny, _ := m.leaders.LoadOrStore(clusterName, 
&sync.Map{})
+               leaderMap := leaderMapAny.(*sync.Map)
                termMapAny, _ := m.clusterTerm.LoadOrStore(clusterName, 
&sync.Map{})
                termMap := termMapAny.(*sync.Map)
                for group, nodes := range nodesByGroup {
+                       leaderMap.Delete(group)
+                       clusterNodesAny, _ := 
m.clusterNodes.LoadOrStore(clusterName, &sync.Map{})
+                       clusterNodes := clusterNodesAny.(*sync.Map)
+                       clusterNodes.Delete(group)
+                       for _, node := range nodes {
+                               if node.Role == LEADER {
+                                       leaderMap.Store(group, node)
+                               }
+                       }
                        m.SetNodes(clusterName, group, nodes)
                        termMap.Store(group, response.Term)
                }
diff --git a/pkg/discovery/metadata/metadata_test.go 
b/pkg/discovery/metadata/metadata_test.go
index 01914634..ed584fcb 100644
--- a/pkg/discovery/metadata/metadata_test.go
+++ b/pkg/discovery/metadata/metadata_test.go
@@ -60,3 +60,29 @@ func TestRefreshMetadataGroupsNodesAndTermsByGroup(t 
*testing.T) {
        assert.Equal(t, int64(7), m.GetClusterTerm("test-cluster")["group-b"])
        assert.Equal(t, RAFT, m.storeMode)
 }
+
+func TestRefreshGroupMetadataReplacesRemovedNodes(t *testing.T) {
+       m := NewMetadata()
+       m.RefreshGroupMetadata("test-cluster", "group-a", MetadataResponse{
+               Term: 1,
+               Nodes: []*Node{
+                       {Transaction: &Endpoint{Host: "127.0.0.1", Port: 8001}, 
Group: "group-a", Role: LEADER},
+                       {Transaction: &Endpoint{Host: "127.0.0.1", Port: 8002}, 
Group: "group-a", Role: FOLLOWER},
+               },
+       })
+
+       m.RefreshGroupMetadata("test-cluster", "group-a", MetadataResponse{
+               Term: 2,
+               Nodes: []*Node{
+                       {Transaction: &Endpoint{Host: "127.0.0.1", Port: 8003}, 
Group: "group-a", Role: LEADER},
+               },
+       })
+
+       nodes := m.GetNodes("test-cluster", "group-a")
+       assert.Len(t, nodes, 1)
+       assert.Equal(t, 8003, m.GetLeader("test-cluster").Transaction.Port)
+
+       m.RefreshGroupMetadata("test-cluster", "group-a", 
MetadataResponse{Term: 3})
+       assert.Empty(t, m.GetNodes("test-cluster", "group-a"))
+       assert.Nil(t, m.GetLeader("test-cluster"))
+}
diff --git a/pkg/discovery/naming_server.go b/pkg/discovery/naming_server.go
index 711d7224..6d52f4ba 100644
--- a/pkg/discovery/naming_server.go
+++ b/pkg/discovery/naming_server.go
@@ -112,12 +112,14 @@ type NamingServerClient struct {
        vgroupAddressMap   sync.Map
        listenerServiceMap sync.Map
        subscribedVGroups  sync.Map
+       nextListenerID     uint64
 
        tokenMu  sync.RWMutex
        jwtToken string
 
        healthCheckTicker *time.Ticker
        closeChan         chan struct{}
+       closeOnce         sync.Once
        wg                sync.WaitGroup
 
        httpClient     *http.Client
@@ -128,11 +130,27 @@ type NamingListener interface {
        OnEvent(vGroup string) error
 }
 
+type namingListenerEntry struct {
+       id       uint64
+       listener NamingListener
+}
+
+type namingWatchState struct {
+       stopCh    chan struct{}
+       listeners int
+       permanent bool
+}
+
 type NamingServerRegistryService struct {
-       client *NamingServerClient
+       client          *NamingServerClient
+       closeOnce       sync.Once
+       subscriptionsMu sync.Mutex
+       subscriptions   map[*registryChangeSubscription]struct{}
+       closed          bool
 }
 
 var _ NamingServerRegistry = (*NamingServerRegistryService)(nil)
+var _ RegistrySubscriber = (*NamingServerRegistryService)(nil)
 
 func buildNamingServerURL(addr string, elem ...string) (string, error) {
        baseURL := strings.TrimSpace(addr)
@@ -149,8 +167,82 @@ func (n *NamingServerRegistryService) Lookup(key string) 
([]*ServiceInstance, er
        return n.client.Lookup(key)
 }
 
+// Subscribe reports NamingServer registry snapshots for a transaction service 
group.
+func (n *NamingServerRegistryService) Subscribe(key string, listener 
RegistryChangeListener) (RegistrySubscription, error) {
+       if listener == nil {
+               return nil, fmt.Errorf("registry change listener is nil")
+       }
+       if n == nil || n.client == nil {
+               return nil, fmt.Errorf("naming server client is nil")
+       }
+       if n.isClosed() {
+               return nil, fmt.Errorf("registry service is closed")
+       }
+
+       subscription := newRegistryChangeSubscription(listener)
+       unsubscribe, err := n.client.subscribe(key, 
&namingRegistryChangeListener{
+               key:          key,
+               client:       n.client,
+               subscription: subscription,
+       }, false)
+       if err != nil {
+               return nil, err
+       }
+
+       instances, err := n.client.lookupInstances(key)
+       if err != nil {
+               unsubscribe()
+               return nil, err
+       }
+       subscription.initialize(RegistryChangeEvent{Key: key, Instances: 
instances}, func() {
+               unsubscribe()
+               n.removeSubscription(subscription)
+       })
+
+       n.subscriptionsMu.Lock()
+       if n.closed {
+               n.subscriptionsMu.Unlock()
+               subscription.Unsubscribe()
+               return nil, fmt.Errorf("registry service is closed")
+       }
+       if n.subscriptions == nil {
+               n.subscriptions = make(map[*registryChangeSubscription]struct{})
+       }
+       n.subscriptions[subscription] = struct{}{}
+       n.subscriptionsMu.Unlock()
+
+       subscription.start()
+       return subscription, nil
+}
+
 func (n *NamingServerRegistryService) Close() {
-       n.client.Close()
+       n.closeOnce.Do(func() {
+               n.subscriptionsMu.Lock()
+               n.closed = true
+               subscriptions := make([]*registryChangeSubscription, 0, 
len(n.subscriptions))
+               for subscription := range n.subscriptions {
+                       subscriptions = append(subscriptions, subscription)
+               }
+               n.subscriptions = nil
+               n.subscriptionsMu.Unlock()
+
+               for _, subscription := range subscriptions {
+                       subscription.Unsubscribe()
+               }
+               n.client.Close()
+       })
+}
+
+func (n *NamingServerRegistryService) removeSubscription(subscription 
*registryChangeSubscription) {
+       n.subscriptionsMu.Lock()
+       delete(n.subscriptions, subscription)
+       n.subscriptionsMu.Unlock()
+}
+
+func (n *NamingServerRegistryService) isClosed() bool {
+       n.subscriptionsMu.Lock()
+       defer n.subscriptionsMu.Unlock()
+       return n.closed
 }
 
 func newNamingServerRegistryService(_ *ServiceConfig, cfg *NamingServerConfig) 
RegistryService {
@@ -286,6 +378,10 @@ func (c *NamingServerClient) Lookup(vGroup string) 
([]*ServiceInstance, error) {
                return nil, fmt.Errorf("subscribe failed: %w", err)
        }
 
+       return c.lookupInstances(vGroup)
+}
+
+func (c *NamingServerClient) lookupInstances(vGroup string) 
([]*ServiceInstance, error) {
        val, ok := c.vgroupAddressMap.Load(vGroup)
        if !ok {
                if err := c.RefreshGroup(vGroup); err != nil {
@@ -565,25 +661,88 @@ func (c *NamingServerClient) handleMetadata(metaResp 
*MetaResponse, vGroup strin
 }
 
 func (c *NamingServerClient) Subscribe(vGroup string, listener NamingListener) 
error {
+       _, err := c.subscribe(vGroup, listener, true)
+       return err
+}
+
+func (c *NamingServerClient) subscribe(vGroup string, listener NamingListener, 
permanent bool) (func(), error) {
+       var listenerID uint64
+       var state *namingWatchState
+       startWatch := false
+
        c.mu.Lock()
        if listener != nil {
-               var listeners []NamingListener
+               listenerID = atomic.AddUint64(&c.nextListenerID, 1)
+               var listeners []namingListenerEntry
                if val, ok := c.listenerServiceMap.Load(vGroup); ok {
-                       listeners = append(listeners, val.([]NamingListener)...)
+                       listeners = append(listeners, 
val.([]namingListenerEntry)...)
                }
-               listeners = append(listeners, listener)
+               listeners = append(listeners, namingListenerEntry{id: 
listenerID, listener: listener})
                c.listenerServiceMap.Store(vGroup, listeners)
        }
+       if val, ok := c.subscribedVGroups.Load(vGroup); ok {
+               state = val.(*namingWatchState)
+       } else {
+               state = &namingWatchState{stopCh: make(chan struct{})}
+               c.subscribedVGroups.Store(vGroup, state)
+               startWatch = true
+       }
+       if listenerID != 0 {
+               state.listeners++
+       }
+       if permanent {
+               state.permanent = true
+       }
        c.mu.Unlock()
 
-       if _, loaded := c.subscribedVGroups.LoadOrStore(vGroup, struct{}{}); 
!loaded {
+       if startWatch {
                c.wg.Add(1)
-               go c.watchLoop(vGroup)
+               go c.watchLoop(vGroup, state.stopCh)
+       }
+
+       return func() {
+               if listenerID != 0 {
+                       c.unsubscribe(vGroup, listenerID)
+               }
+       }, nil
+}
+
+func (c *NamingServerClient) unsubscribe(vGroup string, listenerID uint64) {
+       c.mu.Lock()
+       defer c.mu.Unlock()
+
+       val, ok := c.listenerServiceMap.Load(vGroup)
+       if !ok {
+               return
+       }
+       listeners := val.([]namingListenerEntry)
+       next := make([]namingListenerEntry, 0, len(listeners))
+       for _, entry := range listeners {
+               if entry.id != listenerID {
+                       next = append(next, entry)
+               }
+       }
+       if len(next) == 0 {
+               c.listenerServiceMap.Delete(vGroup)
+       } else {
+               c.listenerServiceMap.Store(vGroup, next)
+       }
+
+       val, ok = c.subscribedVGroups.Load(vGroup)
+       if !ok {
+               return
+       }
+       state := val.(*namingWatchState)
+       if state.listeners > 0 {
+               state.listeners--
+       }
+       if state.listeners == 0 && !state.permanent {
+               c.subscribedVGroups.Delete(vGroup)
+               close(state.stopCh)
        }
-       return nil
 }
 
-func (c *NamingServerClient) watchLoop(vGroup string) {
+func (c *NamingServerClient) watchLoop(vGroup string, stopCh <-chan struct{}) {
        defer c.wg.Done()
        var retryCount int
 
@@ -591,6 +750,8 @@ func (c *NamingServerClient) watchLoop(vGroup string) {
                select {
                case <-c.closeChan:
                        return
+               case <-stopCh:
+                       return
                default:
                }
 
@@ -604,6 +765,8 @@ func (c *NamingServerClient) watchLoop(vGroup string) {
                                case <-time.After(time.Duration(retryDelayMs) * 
time.Millisecond):
                                case <-c.closeChan:
                                        return
+                               case <-stopCh:
+                                       return
                                }
                        } else {
                                c.logger.Warn("watch error, will retry 
immediately", zap.Error(err))
@@ -622,8 +785,8 @@ func (c *NamingServerClient) watchLoop(vGroup string) {
                        if !ok {
                                continue
                        }
-                       for _, listener := range val.([]NamingListener) {
-                               if err := listener.OnEvent(vGroup); err != nil {
+                       for _, entry := range val.([]namingListenerEntry) {
+                               if err := entry.listener.OnEvent(vGroup); err 
!= nil {
                                        c.logger.Warn("listener callback 
failed", zap.Error(err))
                                }
                        }
@@ -692,10 +855,18 @@ func (c *NamingServerClient) Watch(vGroup string) (bool, 
error) {
 }
 
 func (c *NamingServerClient) Close() {
-       close(c.closeChan)
-       c.healthCheckTicker.Stop()
-       c.wg.Wait()
-       c.logger.Info("naming server client closed")
+       c.closeOnce.Do(func() {
+               if c.closeChan != nil {
+                       close(c.closeChan)
+               }
+               if c.healthCheckTicker != nil {
+                       c.healthCheckTicker.Stop()
+               }
+               c.wg.Wait()
+               if c.logger != nil {
+                       c.logger.Info("naming server client closed")
+               }
+       })
 }
 
 func (n *NamingServerRegistryService) Register(instance *ServiceInstance) 
error {
@@ -721,3 +892,18 @@ func (n *NamingServerRegistryService) RefreshGroup(vGroup 
string) error {
 func (n *NamingServerRegistryService) Watch(vGroup string) (bool, error) {
        return n.client.Watch(vGroup)
 }
+
+type namingRegistryChangeListener struct {
+       key          string
+       client       *NamingServerClient
+       subscription *registryChangeSubscription
+}
+
+func (l *namingRegistryChangeListener) OnEvent(vGroup string) error {
+       instances, err := l.client.lookupInstances(vGroup)
+       if err != nil {
+               return err
+       }
+       l.subscription.publish(RegistryChangeEvent{Key: l.key, Instances: 
instances})
+       return nil
+}
diff --git a/pkg/discovery/naming_server_test.go 
b/pkg/discovery/naming_server_test.go
index 8a2517c2..f8c0a96d 100644
--- a/pkg/discovery/naming_server_test.go
+++ b/pkg/discovery/naming_server_test.go
@@ -802,6 +802,97 @@ func TestSubscribeStartsWatchLoopPerVGroup(t *testing.T) {
        }, 2*time.Second, 20*time.Millisecond)
 }
 
+func TestNamingServerRegistryService_SubscribePublishesSnapshots(t *testing.T) 
{
+       client := newSubscriptionTestNamingClient(t)
+       client.vgroupAddressMap.Store("default_tx_group", []NamingServerNode{{
+               Healthy:     true,
+               Transaction: Endpoint{Host: "127.0.0.1", Port: 8091},
+       }})
+       service := &NamingServerRegistryService{client: client}
+
+       events := make(chan RegistryChangeEvent, 2)
+       subscription, err := service.Subscribe("default_tx_group", func(event 
RegistryChangeEvent) {
+               events <- event
+       })
+       if err != nil {
+               t.Fatalf("subscribe failed: %v", err)
+       }
+       defer subscription.Unsubscribe()
+
+       assert.Equal(t, RegistryChangeEvent{
+               Key:       "default_tx_group",
+               Instances: []*ServiceInstance{{Addr: "127.0.0.1", Port: 8091}},
+       }, nextRegistryChangeEvent(t, events))
+
+       client.vgroupAddressMap.Store("default_tx_group", []NamingServerNode{{
+               Healthy:     true,
+               Transaction: Endpoint{Host: "127.0.0.2", Port: 8092},
+       }})
+       fireNamingListeners(t, client, "default_tx_group")
+       assert.Equal(t, RegistryChangeEvent{
+               Key:       "default_tx_group",
+               Instances: []*ServiceInstance{{Addr: "127.0.0.2", Port: 8092}},
+       }, nextRegistryChangeEvent(t, events))
+
+       subscription.Unsubscribe()
+       if _, ok := client.listenerServiceMap.Load("default_tx_group"); ok {
+               t.Fatal("naming server listener was not removed after 
unsubscribe")
+       }
+       if _, ok := client.subscribedVGroups.Load("default_tx_group"); ok {
+               t.Fatal("naming server watch was not removed after unsubscribe")
+       }
+}
+
+func TestNamingServerRegistryService_CloseUnsubscribesListeners(t *testing.T) {
+       client := newSubscriptionTestNamingClient(t)
+       client.vgroupAddressMap.Store("default_tx_group", []NamingServerNode{{
+               Healthy:     true,
+               Transaction: Endpoint{Host: "127.0.0.1", Port: 8091},
+       }})
+       service := &NamingServerRegistryService{client: client}
+
+       events := make(chan RegistryChangeEvent, 1)
+       subscription, err := service.Subscribe("default_tx_group", func(event 
RegistryChangeEvent) {
+               events <- event
+       })
+       if err != nil {
+               t.Fatalf("subscribe failed: %v", err)
+       }
+       nextRegistryChangeEvent(t, events)
+
+       service.Close()
+       concreteSubscription := subscription.(*registryChangeSubscription)
+       waitForSignal(t, concreteSubscription.doneCh, "naming server registry 
subscription to close")
+       if _, ok := client.listenerServiceMap.Load("default_tx_group"); ok {
+               t.Fatal("naming server listener was not removed after close")
+       }
+       if _, ok := client.subscribedVGroups.Load("default_tx_group"); ok {
+               t.Fatal("naming server watch was not removed after close")
+       }
+       service.Close()
+}
+
+func TestNamingServerRegistryService_SubscribeAfterCloseDoesNotStartWatch(t 
*testing.T) {
+       client := newSubscriptionTestNamingClient(t)
+       client.vgroupAddressMap.Store("default_tx_group", []NamingServerNode{{
+               Healthy:     true,
+               Transaction: Endpoint{Host: "127.0.0.1", Port: 8091},
+       }})
+       service := &NamingServerRegistryService{client: client}
+
+       service.Close()
+       subscription, err := service.Subscribe("default_tx_group", 
func(RegistryChangeEvent) {})
+       if err == nil {
+               t.Fatal("expected subscribe after close to fail")
+       }
+       if subscription != nil {
+               t.Fatal("expected subscribe after close to return nil 
subscription")
+       }
+       if _, ok := client.subscribedVGroups.Load("default_tx_group"); ok {
+               t.Fatal("subscribe after close started a naming server watch")
+       }
+}
+
 func TestNamingServerRegistryService_RegisterDeregisterNotSupported(t 
*testing.T) {
        service := &NamingServerRegistryService{}
 
@@ -812,3 +903,45 @@ func 
TestNamingServerRegistryService_RegisterDeregisterNotSupported(t *testing.T
                t.Fatal("expected deregister to return an error")
        }
 }
+
+func newSubscriptionTestNamingClient(t *testing.T) *NamingServerClient {
+       t.Helper()
+
+       mockServer := httptest.NewServer(http.HandlerFunc(func(w 
http.ResponseWriter, r *http.Request) {
+               switch r.URL.Path {
+               case "/naming/v1/watch":
+                       w.WriteHeader(http.StatusNotModified)
+               default:
+                       w.WriteHeader(http.StatusNotFound)
+               }
+       }))
+       t.Cleanup(mockServer.Close)
+
+       mockAddr := mockServer.Listener.Addr().String()
+       client := &NamingServerClient{
+               config:            &NamingServerConfig{ServerAddr: mockAddr, 
Namespace: "public"},
+               logger:            zap.NewNop(),
+               closeChan:         make(chan struct{}),
+               healthCheckTicker: time.NewTicker(time.Hour),
+               httpClient:        &http.Client{Timeout: 100 * 
time.Millisecond},
+               longPollClient:    &http.Client{Timeout: 100 * 
time.Millisecond},
+       }
+       client.availableNamingMap.Store(mockAddr, int32(0))
+       client.namingAddrCache = mockAddr
+       t.Cleanup(client.Close)
+       return client
+}
+
+func fireNamingListeners(t *testing.T, client *NamingServerClient, vGroup 
string) {
+       t.Helper()
+
+       val, ok := client.listenerServiceMap.Load(vGroup)
+       if !ok {
+               t.Fatalf("no listener registered for vGroup %s", vGroup)
+       }
+       for _, entry := range val.([]namingListenerEntry) {
+               if err := entry.listener.OnEvent(vGroup); err != nil {
+                       t.Fatalf("listener callback failed: %v", err)
+               }
+       }
+}
diff --git a/pkg/discovery/raft.go b/pkg/discovery/raft.go
index f724ccbe..21448e5c 100644
--- a/pkg/discovery/raft.go
+++ b/pkg/discovery/raft.go
@@ -43,10 +43,11 @@ const (
 )
 
 type RaftRegistryService struct {
-       cfg                            *RaftConfig
-       metadata                       *metadata.Metadata
-       initAddresses                  sync.Map // clusterName -> 
[]*ServiceInstance
-       aliveNodes                     sync.Map // transactionServiceGroup -> 
[]*ServiceInstance
+       cfg           *RaftConfig
+       metadata      *metadata.Metadata
+       initAddresses sync.Map // clusterName -> []*ServiceInstance
+       aliveNodes    sync.Map // transactionServiceGroup -> []*ServiceInstance
+       // vgroupMapping is copied during construction and read-only afterward.
        vgroupMapping                  map[string]string
        namingserverAddress            string
        username                       string
@@ -56,14 +57,29 @@ type RaftRegistryService struct {
        currentTransactionServiceGroup string
        currentTransactionClusterName  string
        mu                             sync.RWMutex
+       stateMu                        sync.RWMutex
        stopCh                         chan struct{}
        refreshOnce                    sync.Once
+       closeOnce                      sync.Once
+       subscriptionsMu                sync.Mutex
+       subscriptions                  
map[*registryChangeSubscription]raftSubscription
+       closed                         bool
        httpClient                     *http.Client
        random                         *rand.Rand
 }
 
+type raftSubscription struct {
+       key     string
+       cluster string
+}
+
+var _ RegistrySubscriber = (*RaftRegistryService)(nil)
+
 func NewRaftRegistryService(config *ServiceConfig, raftConfig *RegistryConfig) 
*RaftRegistryService {
-       vgroupMapping := config.VgroupMapping
+       vgroupMapping := make(map[string]string, len(config.VgroupMapping))
+       for key, value := range config.VgroupMapping {
+               vgroupMapping[key] = value
+       }
 
        r := &RaftRegistryService{
                cfg:                 &raftConfig.Raft,
@@ -83,6 +99,9 @@ func NewRaftRegistryService(config *ServiceConfig, raftConfig 
*RegistryConfig) *
 }
 
 func (r *RaftRegistryService) Lookup(key string) ([]*ServiceInstance, error) {
+       if r.isClosed() {
+               return nil, fmt.Errorf("registry service is closed")
+       }
        clusterName := r.vgroupMapping[key]
        if clusterName == "" {
                return nil, fmt.Errorf("cluster doesn't exist for 
serviceGroup=%s", key)
@@ -125,6 +144,13 @@ func (r *RaftRegistryService) Lookup(key string) 
([]*ServiceInstance, error) {
                        r.startQueryMetadata()
                }
        }
+       r.stateMu.RLock()
+       instances, err := r.snapshotForClusterLocked(clusterName)
+       r.stateMu.RUnlock()
+       return instances, err
+}
+
+func (r *RaftRegistryService) snapshotForClusterLocked(clusterName string) 
([]*ServiceInstance, error) {
        leader := r.metadata.GetLeader(clusterName)
        if leader != nil {
                endpoint, err := r.selectEndpoint(transactionEndpoint, leader)
@@ -136,6 +162,63 @@ func (r *RaftRegistryService) Lookup(key string) 
([]*ServiceInstance, error) {
        return r.getServiceInstances(clusterName, "")
 }
 
+// Subscribe reports Raft registry snapshots for a transaction service group.
+func (r *RaftRegistryService) Subscribe(key string, listener 
RegistryChangeListener) (RegistrySubscription, error) {
+       if listener == nil {
+               return nil, fmt.Errorf("registry change listener is nil")
+       }
+       if r.isClosed() {
+               return nil, fmt.Errorf("registry service is closed")
+       }
+       clusterName := r.vgroupMapping[key]
+       if clusterName == "" {
+               return nil, fmt.Errorf("cluster doesn't exist for 
serviceGroup=%s", key)
+       }
+
+       if _, err := r.Lookup(key); err != nil {
+               return nil, err
+       }
+
+       subscription := newRegistryChangeSubscription(listener)
+       r.stateMu.Lock()
+       r.subscriptionsMu.Lock()
+       if r.closed {
+               r.subscriptionsMu.Unlock()
+               r.stateMu.Unlock()
+               return nil, fmt.Errorf("registry service is closed")
+       }
+       if r.subscriptions == nil {
+               r.subscriptions = 
make(map[*registryChangeSubscription]raftSubscription)
+       }
+       instances, err := r.snapshotForClusterLocked(clusterName)
+       if err != nil {
+               r.subscriptionsMu.Unlock()
+               r.stateMu.Unlock()
+               return nil, err
+       }
+       r.subscriptions[subscription] = raftSubscription{key: key, cluster: 
clusterName}
+       subscription.initialize(RegistryChangeEvent{Key: key, Instances: 
instances}, func() {
+               r.removeSubscription(subscription)
+       })
+       r.subscriptionsMu.Unlock()
+       r.stateMu.Unlock()
+
+       subscription.start()
+       return subscription, nil
+}
+
+func (r *RaftRegistryService) removeSubscription(subscription 
*registryChangeSubscription) {
+       r.subscriptionsMu.Lock()
+       delete(r.subscriptions, subscription)
+       r.subscriptionsMu.Unlock()
+}
+
+func (r *RaftRegistryService) isClosed() bool {
+       r.subscriptionsMu.Lock()
+       defer r.subscriptionsMu.Unlock()
+       return r.closed
+}
+
 func (r *RaftRegistryService) getServiceInstances(clusterName, group string) 
([]*ServiceInstance, error) {
        nodes := r.metadata.GetNodes(clusterName, group)
        if len(nodes) > 0 {
@@ -179,11 +262,23 @@ func (r *RaftRegistryService) 
RefreshAliveLookup(transactionServiceGroup string,
 }
 
 func (r *RaftRegistryService) Close() {
-       select {
-       case <-r.stopCh:
-       default:
-               close(r.stopCh)
-       }
+       r.closeOnce.Do(func() {
+               r.subscriptionsMu.Lock()
+               r.closed = true
+               subscriptions := make([]*registryChangeSubscription, 0, 
len(r.subscriptions))
+               for subscription := range r.subscriptions {
+                       subscriptions = append(subscriptions, subscription)
+               }
+               r.subscriptions = nil
+               r.subscriptionsMu.Unlock()
+
+               for _, subscription := range subscriptions {
+                       subscription.Unsubscribe()
+               }
+               if r.stopCh != nil {
+                       close(r.stopCh)
+               }
+       })
 }
 
 func (r *RaftRegistryService) selectEndpoint(t string, n *metadata.Node) 
(*ServiceInstance, error) {
@@ -368,7 +463,10 @@ func (r *RaftRegistryService) 
acquireClusterMetaData(clusterName, group string)
                if err = json.Unmarshal(body, &mr); err != nil {
                        return fmt.Errorf("unmarshal metadataResponse failed: 
%w", err)
                }
-               r.metadata.RefreshMetadata(clusterName, mr)
+               r.stateMu.Lock()
+               r.metadata.RefreshGroupMetadata(clusterName, group, mr)
+               r.publishClusterSnapshotLocked(clusterName)
+               r.stateMu.Unlock()
                return nil
        } else if resp.StatusCode == http.StatusUnauthorized {
                if err = r.refreshToken(); err != nil {
@@ -379,6 +477,33 @@ func (r *RaftRegistryService) 
acquireClusterMetaData(clusterName, group string)
        return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
 }
 
+func (r *RaftRegistryService) publishClusterSnapshotLocked(clusterName string) 
{
+       instances, err := r.snapshotForClusterLocked(clusterName)
+       if err != nil {
+               log.Warnf("build registry snapshot failed for cluster=%s: %v", 
clusterName, err)
+               return
+       }
+
+       r.subscriptionsMu.Lock()
+       subscriptions := make([]struct {
+               subscription *registryChangeSubscription
+               key          string
+       }, 0)
+       for subscription, state := range r.subscriptions {
+               if state.cluster == clusterName {
+                       subscriptions = append(subscriptions, struct {
+                               subscription *registryChangeSubscription
+                               key          string
+                       }{subscription: subscription, key: state.key})
+               }
+       }
+       r.subscriptionsMu.Unlock()
+
+       for _, item := range subscriptions {
+               item.subscription.publish(RegistryChangeEvent{Key: item.key, 
Instances: cloneServiceInstances(instances)})
+       }
+}
+
 /* -------------------- Token management -------------------- */
 
 func (r *RaftRegistryService) isTokenExpired() bool {
diff --git a/pkg/discovery/raft_subscription_test.go 
b/pkg/discovery/raft_subscription_test.go
new file mode 100644
index 00000000..cdcdadba
--- /dev/null
+++ b/pkg/discovery/raft_subscription_test.go
@@ -0,0 +1,133 @@
+/*
+ * 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 discovery
+
+import (
+       "math/rand"
+       "net/http"
+       "testing"
+       "time"
+
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+
+       "seata.apache.org/seata-go/v2/pkg/discovery/metadata"
+)
+
+func TestRaftRegistryServiceSubscribePublishesMetadataChanges(t *testing.T) {
+       service := newSubscriptionTestRaftRegistry()
+       service.metadata.RefreshGroupMetadata("test-cluster", "", 
metadata.MetadataResponse{
+               Term: 1,
+               Nodes: []*metadata.Node{{
+                       Transaction: &metadata.Endpoint{Host: "127.0.0.1", 
Port: 8001},
+                       Group:       "",
+                       Role:        metadata.LEADER,
+               }},
+       })
+
+       events := make(chan RegistryChangeEvent, 2)
+       subscription, err := service.Subscribe("default_tx_group", func(event 
RegistryChangeEvent) {
+               events <- event
+       })
+       require.NoError(t, err)
+
+       initial := nextRaftRegistryEvent(t, events)
+       assert.Equal(t, "default_tx_group", initial.Key)
+       assert.Equal(t, []*ServiceInstance{{Addr: "127.0.0.1", Port: 8001}}, 
initial.Instances)
+
+       service.metadata.RefreshGroupMetadata("test-cluster", "", 
metadata.MetadataResponse{
+               Term: 2,
+               Nodes: []*metadata.Node{{
+                       Transaction: &metadata.Endpoint{Host: "127.0.0.1", 
Port: 8002},
+                       Group:       "",
+                       Role:        metadata.LEADER,
+               }},
+       })
+       service.stateMu.Lock()
+       service.publishClusterSnapshotLocked("test-cluster")
+       service.stateMu.Unlock()
+
+       changed := nextRaftRegistryEvent(t, events)
+       assert.Equal(t, []*ServiceInstance{{Addr: "127.0.0.1", Port: 8002}}, 
changed.Instances)
+
+       subscription.Unsubscribe()
+       subscription.Unsubscribe()
+       service.Close()
+       service.Close()
+}
+
+func TestRaftRegistryServiceCloseStopsSubscription(t *testing.T) {
+       service := newSubscriptionTestRaftRegistry()
+       service.metadata.RefreshGroupMetadata("test-cluster", "", 
metadata.MetadataResponse{
+               Nodes: []*metadata.Node{{
+                       Transaction: &metadata.Endpoint{Host: "127.0.0.1", 
Port: 8001},
+                       Group:       "",
+                       Role:        metadata.LEADER,
+               }},
+       })
+
+       events := make(chan RegistryChangeEvent, 2)
+       _, err := service.Subscribe("default_tx_group", func(event 
RegistryChangeEvent) {
+               events <- event
+       })
+       require.NoError(t, err)
+       _ = nextRaftRegistryEvent(t, events)
+
+       service.Close()
+       service.stateMu.Lock()
+       service.publishClusterSnapshotLocked("test-cluster")
+       service.stateMu.Unlock()
+
+       assert.Equal(t, 0, len(events))
+}
+
+func TestRaftRegistryServiceSubscribeAfterCloseDoesNotLookup(t *testing.T) {
+       service := newSubscriptionTestRaftRegistry()
+       service.Close()
+
+       subscription, err := service.Subscribe("default_tx_group", 
func(RegistryChangeEvent) {})
+       assert.Nil(t, subscription)
+       assert.EqualError(t, err, "registry service is closed")
+
+       instances, err := service.Lookup("default_tx_group")
+       assert.Nil(t, instances)
+       assert.EqualError(t, err, "registry service is closed")
+}
+
+func newSubscriptionTestRaftRegistry() *RaftRegistryService {
+       return &RaftRegistryService{
+               cfg:           &RaftConfig{},
+               metadata:      metadata.NewMetadata(),
+               vgroupMapping: map[string]string{"default_tx_group": 
"test-cluster"},
+               stopCh:        make(chan struct{}),
+               httpClient:    &http.Client{},
+               random:        rand.New(rand.NewSource(1)),
+       }
+}
+
+func nextRaftRegistryEvent(t *testing.T, events <-chan RegistryChangeEvent) 
RegistryChangeEvent {
+       t.Helper()
+
+       select {
+       case event := <-events:
+               return event
+       case <-time.After(time.Second):
+               t.Fatal("timed out waiting for registry change event")
+       }
+       return RegistryChangeEvent{}
+}
diff --git a/pkg/discovery/store.go b/pkg/discovery/store.go
index fd53d194..928a197e 100644
--- a/pkg/discovery/store.go
+++ b/pkg/discovery/store.go
@@ -50,21 +50,28 @@ func (s *AddressStore) Update(cluster string, instances 
[]*ServiceInstance) {
 }
 
 func (s *AddressStore) Subscribe(subscriber AddressStoreSubscriber) func() {
+       _, unsubscribe := s.subscribeWithSnapshot("", subscriber)
+       return unsubscribe
+}
+
+func (s *AddressStore) subscribeWithSnapshot(cluster string, subscriber 
AddressStoreSubscriber) ([]*ServiceInstance, func()) {
        if subscriber == nil {
-               return func() {}
+               return s.Snapshot(cluster), func() {}
        }
 
        s.mu.Lock()
        id := s.nextID
        s.nextID++
        s.subscribers[id] = subscriber
+       snapshot := cloneServiceInstances(s.clusters[cluster])
        s.mu.Unlock()
 
-       return func() {
+       unsubscribe := func() {
                s.mu.Lock()
                delete(s.subscribers, id)
                s.mu.Unlock()
        }
+       return snapshot, unsubscribe
 }
 
 func (s *AddressStore) upsert(cluster string, instance *ServiceInstance) {
diff --git a/pkg/discovery/subscription.go b/pkg/discovery/subscription.go
new file mode 100644
index 00000000..c6bd5085
--- /dev/null
+++ b/pkg/discovery/subscription.go
@@ -0,0 +1,132 @@
+/*
+ * 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 discovery
+
+import "sync"
+
+type registryChangeSubscription struct {
+       once        sync.Once
+       listener    RegistryChangeListener
+       unsubscribe func()
+
+       mu      sync.Mutex
+       initial *RegistryChangeEvent
+       latest  *RegistryChangeEvent
+       closed  bool
+       wakeCh  chan struct{}
+       doneCh  chan struct{}
+}
+
+func newRegistryChangeSubscription(listener RegistryChangeListener) 
*registryChangeSubscription {
+       return &registryChangeSubscription{
+               listener: listener,
+               wakeCh:   make(chan struct{}, 1),
+               doneCh:   make(chan struct{}),
+       }
+}
+
+func (s *registryChangeSubscription) initialize(initial RegistryChangeEvent, 
unsubscribe func()) {
+       s.mu.Lock()
+       s.initial = &initial
+       s.unsubscribe = unsubscribe
+       s.mu.Unlock()
+}
+
+func (s *registryChangeSubscription) start() {
+       s.mu.Lock()
+       if s.closed {
+               s.mu.Unlock()
+               return
+       }
+       s.mu.Unlock()
+
+       go s.dispatch()
+       s.signal()
+}
+
+func (s *registryChangeSubscription) dispatch() {
+       for {
+               select {
+               case <-s.wakeCh:
+                       for {
+                               event, ok := s.nextEvent()
+                               if !ok {
+                                       break
+                               }
+                               s.listener(event)
+                       }
+               case <-s.doneCh:
+                       return
+               }
+       }
+}
+
+func (s *registryChangeSubscription) publish(event RegistryChangeEvent) {
+       s.mu.Lock()
+       if s.closed {
+               s.mu.Unlock()
+               return
+       }
+       s.latest = &event
+       s.mu.Unlock()
+
+       s.signal()
+}
+
+func (s *registryChangeSubscription) signal() {
+       select {
+       case s.wakeCh <- struct{}{}:
+       default:
+       }
+}
+
+func (s *registryChangeSubscription) nextEvent() (RegistryChangeEvent, bool) {
+       s.mu.Lock()
+       defer s.mu.Unlock()
+
+       if s.closed {
+               return RegistryChangeEvent{}, false
+       }
+       if s.initial != nil {
+               event := *s.initial
+               s.initial = nil
+               return event, true
+       }
+       if s.latest != nil {
+               event := *s.latest
+               s.latest = nil
+               return event, true
+       }
+       return RegistryChangeEvent{}, false
+}
+
+func (s *registryChangeSubscription) Unsubscribe() {
+       s.once.Do(func() {
+               s.mu.Lock()
+               s.closed = true
+               s.initial = nil
+               s.latest = nil
+               unsubscribe := s.unsubscribe
+               s.mu.Unlock()
+
+               if unsubscribe != nil {
+                       unsubscribe()
+               }
+               close(s.doneCh)
+       })
+}
diff --git a/pkg/discovery/subscription_test.go 
b/pkg/discovery/subscription_test.go
new file mode 100644
index 00000000..17a5e932
--- /dev/null
+++ b/pkg/discovery/subscription_test.go
@@ -0,0 +1,136 @@
+/*
+ * 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 discovery
+
+import (
+       "testing"
+       "time"
+
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+)
+
+func TestEtcd3RegistryService_Subscribe(t *testing.T) {
+       store := NewAddressStore()
+       store.Update("default", []*ServiceInstance{{Addr: "127.0.0.1", Port: 
8091}})
+       service := newTestEtcdRegistryService(store)
+
+       events := make(chan RegistryChangeEvent, 4)
+       subscription, err := service.Subscribe("default_tx_group", func(event 
RegistryChangeEvent) {
+               events <- event
+       })
+       require.NoError(t, err)
+       defer subscription.Unsubscribe()
+
+       assert.Equal(t, RegistryChangeEvent{
+               Key:       "default_tx_group",
+               Instances: []*ServiceInstance{{Addr: "127.0.0.1", Port: 8091}},
+       }, nextRegistryChangeEvent(t, events))
+
+       store.Update("other", []*ServiceInstance{{Addr: "127.0.0.2", Port: 
8092}})
+       store.Update("default", []*ServiceInstance{
+               {Addr: "127.0.0.1", Port: 8091},
+               {Addr: "127.0.0.3", Port: 8093},
+       })
+       assert.Equal(t, []*ServiceInstance{
+               {Addr: "127.0.0.1", Port: 8091},
+               {Addr: "127.0.0.3", Port: 8093},
+       }, nextRegistryChangeEvent(t, events).Instances)
+
+       subscription.Unsubscribe()
+       subscription.Unsubscribe()
+       store.mu.RLock()
+       assert.Empty(t, store.subscribers)
+       store.mu.RUnlock()
+}
+
+func TestEtcd3RegistryService_SubscribeValidatesInputs(t *testing.T) {
+       service := newTestEtcdRegistryService(NewAddressStore())
+
+       subscription, err := service.Subscribe("default_tx_group", nil)
+       assert.Nil(t, subscription)
+       assert.EqualError(t, err, "registry change listener is nil")
+
+       subscription, err = service.Subscribe("missing_tx_group", 
func(RegistryChangeEvent) {})
+       assert.Nil(t, subscription)
+       assert.EqualError(t, err, "cluster doesn't exist")
+}
+
+func TestEtcd3RegistryService_SubscribeCoalescesSlowListener(t *testing.T) {
+       store := NewAddressStore()
+       service := newTestEtcdRegistryService(store)
+
+       block := make(chan struct{})
+       events := make(chan RegistryChangeEvent, 4)
+       subscription, err := service.Subscribe("default_tx_group", func(event 
RegistryChangeEvent) {
+               events <- event
+               <-block
+       })
+       require.NoError(t, err)
+       defer subscription.Unsubscribe()
+
+       nextRegistryChangeEvent(t, events)
+       store.Update("default", []*ServiceInstance{{Addr: "127.0.0.1", Port: 
8091}})
+       store.Update("default", []*ServiceInstance{{Addr: "127.0.0.2", Port: 
8092}})
+       store.Update("default", []*ServiceInstance{{Addr: "127.0.0.3", Port: 
8093}})
+       close(block)
+
+       assert.Equal(t, []*ServiceInstance{{Addr: "127.0.0.3", Port: 8093}}, 
nextRegistryChangeEvent(t, events).Instances)
+}
+
+func TestEtcd3RegistryService_CloseUnsubscribesListeners(t *testing.T) {
+       store := NewAddressStore()
+       service := newTestEtcdRegistryService(store)
+
+       events := make(chan RegistryChangeEvent, 1)
+       subscription, err := service.Subscribe("default_tx_group", func(event 
RegistryChangeEvent) {
+               events <- event
+       })
+       require.NoError(t, err)
+       nextRegistryChangeEvent(t, events)
+
+       service.Close()
+       concreteSubscription := subscription.(*registryChangeSubscription)
+       waitForSignal(t, concreteSubscription.doneCh, "registry subscription to 
close")
+       store.mu.RLock()
+       assert.Empty(t, store.subscribers)
+       store.mu.RUnlock()
+
+       subscription, err = service.Subscribe("default_tx_group", 
func(RegistryChangeEvent) {})
+       assert.Nil(t, subscription)
+       assert.EqualError(t, err, "registry service is closed")
+}
+
+func newTestEtcdRegistryService(store *AddressStore) *EtcdRegistryService {
+       return &EtcdRegistryService{
+               vgroupMapping: map[string]string{"default_tx_group": "default"},
+               store:         store,
+       }
+}
+
+func nextRegistryChangeEvent(t *testing.T, events <-chan RegistryChangeEvent) 
RegistryChangeEvent {
+       t.Helper()
+
+       select {
+       case event := <-events:
+               return event
+       case <-time.After(time.Second):
+               t.Fatal("timed out waiting for registry change event")
+       }
+       return RegistryChangeEvent{}
+}
diff --git a/pkg/remoting/getty/getty_client.go 
b/pkg/remoting/getty/getty_client.go
index a945ac0a..4b60a9b5 100644
--- a/pkg/remoting/getty/getty_client.go
+++ b/pkg/remoting/getty/getty_client.go
@@ -52,20 +52,24 @@ func GetGettyRemotingClient() *GettyRemotingClient {
 }
 
 func (client *GettyRemotingClient) SendAsyncRequest(msg interface{}) error {
+       rpcMessage := newAsyncRequestMessage(int32(client.idGenerator.Inc()), 
msg)
+       return client.gettyRemoting.SendAsync(rpcMessage, nil, 
client.asyncCallback)
+}
+
+func newAsyncRequestMessage(id int32, msg interface{}) message.RpcMessage {
        var msgType message.RequestType
        if _, ok := msg.(message.HeartBeatMessage); ok {
                msgType = message.RequestTypeHeartbeatRequest
        } else {
                msgType = message.RequestTypeRequestOneway
        }
-       rpcMessage := message.RpcMessage{
-               ID:         int32(client.idGenerator.Inc()),
+       return message.RpcMessage{
+               ID:         id,
                Type:       msgType,
                Codec:      byte(codec.CodecTypeSeata),
                Compressor: 0,
                Body:       msg,
        }
-       return client.gettyRemoting.SendAsync(rpcMessage, nil, 
client.asyncCallback)
 }
 
 func (client *GettyRemotingClient) SendAsyncResponse(msgID int32, msg 
interface{}) error {
diff --git a/pkg/remoting/getty/getty_client_test.go 
b/pkg/remoting/getty/getty_client_test.go
index 432f31d5..a6ec8585 100644
--- a/pkg/remoting/getty/getty_client_test.go
+++ b/pkg/remoting/getty/getty_client_test.go
@@ -82,16 +82,10 @@ func TestGettyRemotingClient_SendAsyncRequest(t *testing.T) 
{
        }
        for _, test := range tests {
                t.Run(test.name, func(t *testing.T) {
-                       var capturedType message.RequestType
-                       patches := 
gomonkey.ApplyMethod(reflect.TypeOf(GetGettyRemotingClient().gettyRemoting), 
"SendAsync",
-                               func(_ *GettyRemoting, msg message.RpcMessage, 
s getty.Session, callback callbackMethod) error {
-                                       capturedType = msg.Type
-                                       return nil
-                               })
-                       defer patches.Reset()
-                       err := 
GetGettyRemotingClient().SendAsyncRequest(test.message)
-                       assert.Empty(t, err)
-                       assert.Equal(t, test.expectedType, capturedType)
+                       msg := newAsyncRequestMessage(1, test.message)
+                       assert.Equal(t, test.expectedType, msg.Type)
+                       assert.Equal(t, byte(codec.CodecTypeSeata), msg.Codec)
+                       assert.Equal(t, test.message, msg.Body)
                })
        }
 }
diff --git a/pkg/remoting/getty/session_manager.go 
b/pkg/remoting/getty/session_manager.go
index a467cd6a..8bb4bb16 100644
--- a/pkg/remoting/getty/session_manager.go
+++ b/pkg/remoting/getty/session_manager.go
@@ -51,11 +51,55 @@ var (
 
 type SessionManager struct {
        // serverAddress -> rpc_client.Session -> bool
-       serverSessions sync.Map
-       allSessions    sync.Map
-       sessionSize    int32
-       gettyConf      *config.Config
-       seataConfig    *config.SeataConfig
+       serverSessions        sync.Map
+       allSessions           sync.Map
+       sessionSize           int32
+       gettyConf             *config.Config
+       seataConfig           *config.SeataConfig
+       registrySubscription  discovery.RegistrySubscription
+       serverClients         sync.Map
+       serverAddressMu       sync.RWMutex
+       serverAddressSnapshot map[string]struct{}
+       serverAddressReady    bool
+       startClient           func(*discovery.ServiceInstance) closeableClient
+}
+
+type closeableClient interface {
+       Close()
+}
+
+type serverClientEntry struct {
+       mu     sync.Mutex
+       client closeableClient
+       closed bool
+}
+
+func (e *serverClientEntry) setClient(client closeableClient) bool {
+       e.mu.Lock()
+       if e.closed {
+               e.mu.Unlock()
+               client.Close()
+               return false
+       }
+       e.client = client
+       e.mu.Unlock()
+       return true
+}
+
+func (e *serverClientEntry) Close() {
+       e.mu.Lock()
+       if e.closed {
+               e.mu.Unlock()
+               return
+       }
+       e.closed = true
+       client := e.client
+       e.client = nil
+       e.mu.Unlock()
+
+       if client != nil {
+               client.Close()
+       }
 }
 
 func initSessionManager(gettyConfig *config.Config, seataConfig 
*config.SeataConfig) {
@@ -73,31 +117,158 @@ func initSessionManager(gettyConfig *config.Config, 
seataConfig *config.SeataCon
 }
 
 func (g *SessionManager) init() {
-       addressList := g.getAvailServerList()
+       g.initWithRegistry(discovery.GetRegistry())
+}
+
+func (g *SessionManager) initWithRegistry(registryService 
discovery.RegistryService) {
+       if registryService == nil {
+               log.Warn("registry service not initialized")
+               return
+       }
+       if g.subscribeRegistry(registryService) {
+               return
+       }
+
+       addressList := g.getAvailServerList(registryService)
        if len(addressList) == 0 {
                log.Warn("no have valid seata server list")
        }
-       for _, address := range addressList {
-               gettyClient := getty.NewTCPClient(
-                       getty.WithServerAddress(net.JoinHostPort(address.Addr, 
strconv.Itoa(address.Port))),
-                       // todo if read c.gettyConf.ConnectionNum, will cause 
the connect to fail
-                       getty.WithConnectionNumber(1),
-                       
getty.WithReconnectInterval(g.gettyConf.ReconnectInterval),
-                       getty.WithClientTaskPool(gxsync.NewTaskPoolSimple(0)),
-               )
-               go gettyClient.RunEventLoop(g.newSession)
+       g.refreshServerList(addressList)
+}
+
+func (g *SessionManager) subscribeRegistry(registryService 
discovery.RegistryService) bool {
+       subscriber, ok := registryService.(discovery.RegistrySubscriber)
+       if !ok {
+               return false
        }
+       subscription, err := subscriber.Subscribe(g.seataConfig.TxServiceGroup, 
func(event discovery.RegistryChangeEvent) {
+               if event.Key != g.seataConfig.TxServiceGroup {
+                       return
+               }
+               g.refreshServerList(event.Instances)
+       })
+       if err != nil {
+               log.Warnf("subscribe registry changes failed: %v", err)
+               return false
+       }
+       g.registrySubscription = subscription
+       return true
 }
 
-func (g *SessionManager) getAvailServerList() []*discovery.ServiceInstance {
-       registryService := discovery.GetRegistry()
+func (g *SessionManager) getAvailServerList(registryService 
discovery.RegistryService) []*discovery.ServiceInstance {
        instances, err := registryService.Lookup(g.seataConfig.TxServiceGroup)
        if err != nil {
+               log.Warnf("lookup seata server list failed: %v", err)
                return nil
        }
        return instances
 }
 
+func (g *SessionManager) refreshServerList(instances 
[]*discovery.ServiceInstance) {
+       servers := make(map[string]*discovery.ServiceInstance, len(instances))
+       for _, instance := range instances {
+               if instance == nil || instance.Addr == "" || instance.Port <= 0 
{
+                       continue
+               }
+               clone := &discovery.ServiceInstance{Addr: instance.Addr, Port: 
instance.Port}
+               servers[serverAddress(clone)] = clone
+       }
+
+       removedAddresses := g.replaceServerAddressSnapshot(servers)
+       for _, address := range removedAddresses {
+               g.releaseServerAddress(address)
+       }
+
+       for _, instance := range servers {
+               g.ensureServerClient(instance)
+       }
+}
+
+func (g *SessionManager) replaceServerAddressSnapshot(servers 
map[string]*discovery.ServiceInstance) []string {
+       g.serverAddressMu.Lock()
+       var removedAddresses []string
+       for address := range g.serverAddressSnapshot {
+               if _, ok := servers[address]; !ok {
+                       removedAddresses = append(removedAddresses, address)
+               }
+       }
+       g.serverAddressSnapshot = make(map[string]struct{}, len(servers))
+       for address := range servers {
+               g.serverAddressSnapshot[address] = struct{}{}
+       }
+       g.serverAddressReady = true
+       g.serverAddressMu.Unlock()
+       return removedAddresses
+}
+
+func (g *SessionManager) ensureServerClient(instance 
*discovery.ServiceInstance) {
+       address := serverAddress(instance)
+       entry := &serverClientEntry{}
+       if _, loaded := g.serverClients.LoadOrStore(address, entry); loaded {
+               return
+       }
+       var client closeableClient
+       if g.startClient != nil {
+               client = g.startClient(instance)
+       } else {
+               client = g.startGettyClient(instance)
+       }
+       if client == nil {
+               g.serverClients.CompareAndDelete(address, entry)
+               return
+       }
+       if !entry.setClient(client) {
+               return
+       }
+       if !g.isServerAddressAvailable(address) || 
!g.isServerClientEntryCurrent(address, entry) {
+               g.releaseServerClientEntry(address, entry)
+               return
+       }
+}
+
+func (g *SessionManager) isServerClientEntryCurrent(address string, entry 
*serverClientEntry) bool {
+       current, ok := g.serverClients.Load(address)
+       return ok && current == entry
+}
+
+func (g *SessionManager) releaseServerClientEntry(address string, entry 
*serverClientEntry) {
+       g.serverClients.CompareAndDelete(address, entry)
+       entry.Close()
+}
+
+func (g *SessionManager) startGettyClient(instance *discovery.ServiceInstance) 
getty.Client {
+       gettyClient := getty.NewTCPClient(
+               getty.WithServerAddress(serverAddress(instance)),
+               // todo if read c.gettyConf.ConnectionNum, will cause the 
connect to fail
+               getty.WithConnectionNumber(1),
+               getty.WithReconnectInterval(g.gettyConf.ReconnectInterval),
+               getty.WithClientTaskPool(gxsync.NewTaskPoolSimple(0)),
+       )
+       go gettyClient.RunEventLoop(g.newSession)
+       return gettyClient
+}
+
+func (g *SessionManager) releaseServerAddress(address string) {
+       if clientAny, loaded := g.serverClients.LoadAndDelete(address); loaded {
+               if client, ok := clientAny.(closeableClient); ok && client != 
nil {
+                       client.Close()
+               }
+       }
+       if sessionsAny, ok := g.serverSessions.LoadAndDelete(address); ok {
+               sessions := sessionsAny.(*sync.Map)
+               sessions.Range(func(key, _ interface{}) bool {
+                       if session, ok := key.(getty.Session); ok {
+                               g.releaseSession(session)
+                       }
+                       return true
+               })
+       }
+}
+
+func serverAddress(instance *discovery.ServiceInstance) string {
+       return net.JoinHostPort(instance.Addr, strconv.Itoa(instance.Port))
+}
+
 func (g *SessionManager) setSessionConfig(session getty.Session) {
        session.SetName(g.gettyConf.SessionConfig.SessionName)
        session.SetMaxMsgLen(g.gettyConf.SessionConfig.MaxMsgLen)
@@ -160,27 +331,22 @@ func (g *SessionManager) newSession(session 
getty.Session) error {
 }
 
 func (g *SessionManager) selectSession(msg interface{}) getty.Session {
-       selected := loadbalance.Select(loadbalance.GetLoadBalanceConfig().Type, 
&g.allSessions, g.getXid(msg))
+       sessions := g.selectableSessions()
+       selected := loadbalance.Select(loadbalance.GetLoadBalanceConfig().Type, 
sessions, g.getXid(msg))
        session, ok := selected.(getty.Session)
        if ok && session != nil {
                return session
        }
 
-       if g.sessionSize == 0 {
+       if selectableConnectionCount(sessions) == 0 {
                ticker := time.NewTicker(time.Duration(checkAliveInternal) * 
time.Millisecond)
                defer ticker.Stop()
                for i := 0; i < maxCheckAliveRetry; i++ {
                        <-ticker.C
-                       g.allSessions.Range(func(key, value interface{}) bool {
-                               session = key.(getty.Session)
-                               if session.IsClosed() {
-                                       g.releaseSession(session)
-                               } else {
-                                       return false
-                               }
-                               return true
-                       })
-                       if session != nil {
+                       sessions = g.selectableSessions()
+                       selected = 
loadbalance.Select(loadbalance.GetLoadBalanceConfig().Type, sessions, 
g.getXid(msg))
+                       session, ok = selected.(getty.Session)
+                       if ok && session != nil {
                                return session
                        }
                }
@@ -188,6 +354,36 @@ func (g *SessionManager) selectSession(msg interface{}) 
getty.Session {
        return nil
 }
 
+func (g *SessionManager) selectableSessions() *sync.Map {
+       sessions := &sync.Map{}
+       g.allSessions.Range(func(key, value interface{}) bool {
+               session, ok := key.(getty.Session)
+               if !ok {
+                       return true
+               }
+               if session.IsClosed() {
+                       g.releaseSession(session)
+                       return true
+               }
+               if g.isServerAddressAvailable(session.RemoteAddr()) {
+                       sessions.Store(session, value)
+               }
+               return true
+       })
+       return sessions
+}
+
+func (g *SessionManager) isServerAddressAvailable(address string) bool {
+       g.serverAddressMu.RLock()
+       defer g.serverAddressMu.RUnlock()
+
+       if !g.serverAddressReady {
+               return true
+       }
+       _, ok := g.serverAddressSnapshot[address]
+       return ok
+}
+
 func (g *SessionManager) getXid(msg interface{}) string {
        var xid string
        if tmpMsg, ok := msg.(message.AbstractGlobalEndRequest); ok {
@@ -210,20 +406,42 @@ func (g *SessionManager) getXid(msg interface{}) string {
 }
 
 func (g *SessionManager) releaseSession(session getty.Session) {
-       g.allSessions.Delete(session)
-       if !session.IsClosed() {
-               m, _ := g.serverSessions.LoadOrStore(session.RemoteAddr(), 
&sync.Map{})
+       if session == nil {
+               return
+       }
+       if _, loaded := g.allSessions.LoadAndDelete(session); !loaded {
+               return
+       }
+       if m, ok := g.serverSessions.Load(session.RemoteAddr()); ok {
                sMap := m.(*sync.Map)
                sMap.Delete(session)
+       }
+       if !session.IsClosed() {
                session.Close()
        }
        atomic.AddInt32(&g.sessionSize, -1)
 }
 
 func (g *SessionManager) registerSession(session getty.Session) {
-       g.allSessions.Store(session, true)
+       if !g.isServerAddressAvailable(session.RemoteAddr()) {
+               log.Warnf("skip session for removed server address: %s", 
session.RemoteAddr())
+               session.Close()
+               return
+       }
+       if _, loaded := g.allSessions.LoadOrStore(session, true); loaded {
+               return
+       }
        m, _ := g.serverSessions.LoadOrStore(session.RemoteAddr(), &sync.Map{})
        sMap := m.(*sync.Map)
        sMap.Store(session, true)
        atomic.AddInt32(&g.sessionSize, 1)
 }
+
+func selectableConnectionCount(connections *sync.Map) int {
+       count := 0
+       connections.Range(func(_, _ interface{}) bool {
+               count++
+               return true
+       })
+       return count
+}
diff --git a/pkg/remoting/getty/session_manager_test.go 
b/pkg/remoting/getty/session_manager_test.go
new file mode 100644
index 00000000..963e9829
--- /dev/null
+++ b/pkg/remoting/getty/session_manager_test.go
@@ -0,0 +1,236 @@
+/*
+ * 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 getty
+
+import (
+       "sync"
+       "sync/atomic"
+       "testing"
+       "time"
+
+       getty "github.com/apache/dubbo-getty"
+       "github.com/golang/mock/gomock"
+       "github.com/stretchr/testify/assert"
+
+       "seata.apache.org/seata-go/v2/pkg/discovery"
+       "seata.apache.org/seata-go/v2/pkg/protocol/message"
+       "seata.apache.org/seata-go/v2/pkg/remoting/config"
+       "seata.apache.org/seata-go/v2/pkg/remoting/loadbalance"
+       "seata.apache.org/seata-go/v2/pkg/remoting/mock"
+)
+
+func TestSessionManagerRefreshesServerListFromRegistrySubscription(t 
*testing.T) {
+       registry := &fakeSubscriberRegistry{
+               fakeLookupRegistry: fakeLookupRegistry{
+                       instances: []*discovery.ServiceInstance{{Addr: 
"127.0.0.1", Port: 8091}},
+               },
+       }
+       started := make(chan string, 2)
+       closed := make(chan string, 1)
+       manager := newTestSessionManager(func(instance 
*discovery.ServiceInstance) closeableClient {
+               address := serverAddress(instance)
+               started <- address
+               return &fakeServerClient{address: address, closed: closed}
+       })
+
+       manager.initWithRegistry(registry)
+       assert.Equal(t, "default_tx_group", registry.subscribeKey)
+       assert.Equal(t, "127.0.0.1:8091", nextStartedServer(t, started))
+
+       registry.publish([]*discovery.ServiceInstance{{Addr: "127.0.0.2", Port: 
8092}})
+       assert.Equal(t, "127.0.0.1:8091", nextClosedServer(t, closed))
+       assert.Equal(t, "127.0.0.2:8092", nextStartedServer(t, started))
+       assert.False(t, manager.isServerAddressAvailable("127.0.0.1:8091"))
+       assert.True(t, manager.isServerAddressAvailable("127.0.0.2:8092"))
+}
+
+func TestSessionManagerFallsBackToLookupWhenRegistryDoesNotSubscribe(t 
*testing.T) {
+       registry := &fakeLookupRegistry{
+               instances: []*discovery.ServiceInstance{{Addr: "127.0.0.1", 
Port: 8091}},
+       }
+       started := make(chan string, 1)
+       manager := newTestSessionManager(func(instance 
*discovery.ServiceInstance) closeableClient {
+               address := serverAddress(instance)
+               started <- address
+               return &fakeServerClient{address: address}
+       })
+
+       manager.initWithRegistry(registry)
+       assert.Equal(t, "default_tx_group", registry.lookupKey)
+       assert.Equal(t, "127.0.0.1:8091", nextStartedServer(t, started))
+       assert.True(t, manager.isServerAddressAvailable("127.0.0.1:8091"))
+}
+
+func TestSessionManagerSelectSessionSkipsRemovedServerAddress(t *testing.T) {
+       previousLoadBalance := loadbalance.GetLoadBalanceConfig()
+       loadbalance.InitLoadBalanceConfig(loadbalance.Config{Type: 
"RoundRobinLoadBalance"})
+       t.Cleanup(func() {
+               loadbalance.InitLoadBalanceConfig(previousLoadBalance)
+       })
+
+       ctrl := gomock.NewController(t)
+       manager := newTestSessionManager(nil)
+       manager.refreshServerList([]*discovery.ServiceInstance{
+               {Addr: "127.0.0.1", Port: 8091},
+               {Addr: "127.0.0.2", Port: 8092},
+       })
+
+       removed := mock.NewMockTestSession(ctrl)
+       removed.EXPECT().IsClosed().Return(false).AnyTimes()
+       removed.EXPECT().RemoteAddr().Return("127.0.0.1:8091").AnyTimes()
+       removed.EXPECT().Close().Times(1)
+       kept := mock.NewMockTestSession(ctrl)
+       kept.EXPECT().IsClosed().Return(false).AnyTimes()
+       kept.EXPECT().RemoteAddr().Return("127.0.0.2:8092").AnyTimes()
+
+       manager.registerSession(removed)
+       manager.registerSession(kept)
+       manager.refreshServerList([]*discovery.ServiceInstance{{Addr: 
"127.0.0.2", Port: 8092}})
+
+       selected := 
manager.selectSession(message.GlobalBeginRequest{TransactionName: "tx"})
+       assert.Equal(t, getty.Session(kept), selected)
+       assert.Equal(t, int32(1), atomic.LoadInt32(&manager.sessionSize))
+       if _, ok := manager.serverClients.Load("127.0.0.1:8091"); ok {
+               t.Fatal("removed server client is still tracked")
+       }
+}
+
+func TestSessionManagerClosesStaleClientWhenAddressRemovedDuringStart(t 
*testing.T) {
+       started := make(chan string, 1)
+       resumeStart := make(chan struct{})
+       closed := make(chan string, 1)
+       manager := newTestSessionManager(func(instance 
*discovery.ServiceInstance) closeableClient {
+               address := serverAddress(instance)
+               started <- address
+               <-resumeStart
+               return &fakeServerClient{address: address, closed: closed}
+       })
+
+       refreshDone := make(chan struct{})
+       go func() {
+               manager.refreshServerList([]*discovery.ServiceInstance{{Addr: 
"127.0.0.1", Port: 8091}})
+               close(refreshDone)
+       }()
+       assert.Equal(t, "127.0.0.1:8091", nextStartedServer(t, started))
+
+       manager.refreshServerList(nil)
+       close(resumeStart)
+
+       select {
+       case <-refreshDone:
+       case <-time.After(time.Second):
+               t.Fatal("timed out waiting for stale server client start to 
finish")
+       }
+       assert.Equal(t, "127.0.0.1:8091", nextClosedServer(t, closed))
+       if _, ok := manager.serverClients.Load("127.0.0.1:8091"); ok {
+               t.Fatal("stale server client is still tracked")
+       }
+}
+
+func newTestSessionManager(startClient func(*discovery.ServiceInstance) 
closeableClient) *SessionManager {
+       if startClient == nil {
+               startClient = func(*discovery.ServiceInstance) closeableClient {
+                       return &fakeServerClient{}
+               }
+       }
+       return &SessionManager{
+               gettyConf:   &config.Config{},
+               seataConfig: &config.SeataConfig{TxServiceGroup: 
"default_tx_group"},
+               startClient: startClient,
+       }
+}
+
+func nextStartedServer(t *testing.T, started <-chan string) string {
+       t.Helper()
+
+       select {
+       case address := <-started:
+               return address
+       case <-time.After(time.Second):
+               t.Fatal("timed out waiting for server client start")
+       }
+       return ""
+}
+
+func nextClosedServer(t *testing.T, closed <-chan string) string {
+       t.Helper()
+
+       select {
+       case address := <-closed:
+               return address
+       case <-time.After(time.Second):
+               t.Fatal("timed out waiting for server client close")
+       }
+       return ""
+}
+
+type fakeLookupRegistry struct {
+       lookupKey string
+       instances []*discovery.ServiceInstance
+}
+
+func (r *fakeLookupRegistry) Lookup(key string) ([]*discovery.ServiceInstance, 
error) {
+       r.lookupKey = key
+       return cloneTestServiceInstances(r.instances), nil
+}
+
+func (r *fakeLookupRegistry) Close() {}
+
+type fakeSubscriberRegistry struct {
+       fakeLookupRegistry
+       subscribeKey string
+       listener     discovery.RegistryChangeListener
+}
+
+func (r *fakeSubscriberRegistry) Subscribe(key string, listener 
discovery.RegistryChangeListener) (discovery.RegistrySubscription, error) {
+       r.subscribeKey = key
+       r.listener = listener
+       listener(discovery.RegistryChangeEvent{Key: key, Instances: 
cloneTestServiceInstances(r.instances)})
+       return fakeRegistrySubscription{}, nil
+}
+
+func (r *fakeSubscriberRegistry) publish(instances 
[]*discovery.ServiceInstance) {
+       r.listener(discovery.RegistryChangeEvent{Key: r.subscribeKey, 
Instances: cloneTestServiceInstances(instances)})
+}
+
+type fakeRegistrySubscription struct{}
+
+func (fakeRegistrySubscription) Unsubscribe() {}
+
+func cloneTestServiceInstances(instances []*discovery.ServiceInstance) 
[]*discovery.ServiceInstance {
+       clones := make([]*discovery.ServiceInstance, 0, len(instances))
+       for _, instance := range instances {
+               clone := *instance
+               clones = append(clones, &clone)
+       }
+       return clones
+}
+
+type fakeServerClient struct {
+       address string
+       closed  chan<- string
+       once    sync.Once
+}
+
+func (c *fakeServerClient) Close() {
+       c.once.Do(func() {
+               if c.closed != nil {
+                       c.closed <- c.address
+               }
+       })
+}
diff --git a/pkg/remoting/grpc/channel.go b/pkg/remoting/grpc/channel.go
index b480a066..c6093829 100644
--- a/pkg/remoting/grpc/channel.go
+++ b/pkg/remoting/grpc/channel.go
@@ -62,14 +62,26 @@ func (c *Channel) IsClosed() bool {
 
 func (c *Channel) close() {
        c.mu.Lock()
-       defer c.mu.Unlock()
-       if !c.IsClosed() {
-               close(c.closeCh)
-               if err := c.stream.CloseSend(); err != nil {
+       if c.IsClosed() {
+               c.mu.Unlock()
+               return
+       }
+       close(c.closeCh)
+       stream := c.stream
+       conn := c.conn
+       c.mu.Unlock()
+
+       if stream != nil {
+               if err := stream.CloseSend(); err != nil {
                        log.Debugf("CloseSend error: %v", err)
                }
-               c.wg.Wait()
        }
+       if conn != nil {
+               if err := conn.Close(); err != nil {
+                       log.Debugf("ClientConn close error: %v", err)
+               }
+       }
+       c.wg.Wait()
 }
 
 func (c *Channel) softReconnect() error {
diff --git a/pkg/remoting/grpc/channel_manager.go 
b/pkg/remoting/grpc/channel_manager.go
index a1859a67..085352bb 100644
--- a/pkg/remoting/grpc/channel_manager.go
+++ b/pkg/remoting/grpc/channel_manager.go
@@ -58,16 +58,26 @@ type ChannelManager struct {
        //addr:map[Channel]bool
        serverChannels sync.Map
        //stream:bool
-       allChannels sync.Map
-       clientSize  int32
-       config      *config.Config
+       allChannels           sync.Map
+       clientSize            int32
+       config                *config.Config
+       seataConfig           *config.SeataConfig
+       registrySubscription  discovery.RegistrySubscription
+       startedAddresses      sync.Map
+       serverAddressMu       sync.RWMutex
+       serverAddressSnapshot map[string]struct{}
+       serverAddressReady    bool
+       startChannel          func(*discovery.ServiceInstance)
 }
 
-func initChannelManager(config *config.Config) {
+type channelStartEntry struct{}
+
+func initChannelManager(grpcConfig *config.Config) {
        if channelManager == nil {
                onceChannelManager.Do(func() {
                        channelManager = &ChannelManager{
-                               config:         config,
+                               config:         grpcConfig,
+                               seataConfig:    config.GetSeataConfig(),
                                allChannels:    sync.Map{},
                                serverChannels: sync.Map{},
                        }
@@ -77,50 +87,162 @@ func initChannelManager(config *config.Config) {
 }
 
 func (g *ChannelManager) init() {
-       addressList := g.getAvailServerList()
+       g.initWithRegistry(discovery.GetRegistry())
+}
+
+func (g *ChannelManager) initWithRegistry(registryService 
discovery.RegistryService) {
+       if registryService == nil {
+               log.Warn("registry service not initialized")
+               return
+       }
+       if g.seataConfig == nil || g.seataConfig.TxServiceGroup == "" {
+               log.Warn("transaction service group not initialized")
+               return
+       }
+       if g.subscribeRegistry(registryService) {
+               return
+       }
+
+       addressList := g.getAvailServerList(registryService)
        if len(addressList) == 0 {
                log.Warn("no have valid seata server list")
        }
-       for _, address := range addressList {
-               addr := net.JoinHostPort(address.Addr, 
strconv.Itoa(address.Port))
-               if conn, err := g.newConn(addr); err != nil {
-                       log.Errorf("failed to dial gRPC addr %s: %v", addr, err)
+       g.refreshServerList(addressList)
+}
+
+func (g *ChannelManager) subscribeRegistry(registryService 
discovery.RegistryService) bool {
+       subscriber, ok := registryService.(discovery.RegistrySubscriber)
+       if !ok {
+               return false
+       }
+       subscription, err := subscriber.Subscribe(g.seataConfig.TxServiceGroup, 
func(event discovery.RegistryChangeEvent) {
+               if event.Key != g.seataConfig.TxServiceGroup {
+                       return
+               }
+               g.refreshServerList(event.Instances)
+       })
+       if err != nil {
+               log.Warnf("subscribe registry changes failed: %v", err)
+               return false
+       }
+       g.registrySubscription = subscription
+       return true
+}
+
+func (g *ChannelManager) refreshServerList(instances 
[]*discovery.ServiceInstance) {
+       servers := make(map[string]*discovery.ServiceInstance, len(instances))
+       for _, instance := range instances {
+               if instance == nil || instance.Addr == "" || instance.Port <= 0 
{
                        continue
-               } else {
-                       regLock := sync.Mutex{}
-                       registered := atomic.Bool{}
-                       // todo if read g.config.ConnectionNum, will cause the 
connect to fail
-                       for i := 1; i <= 1; i++ {
-                               channel := &Channel{
-                                       addr:       addr,
-                                       conn:       conn,
-                                       sendCh:     make(chan 
*pb.GrpcMessageProto, defaultSendChBuffer),
-                                       closeCh:    make(chan struct{}),
-                                       wg:         sync.WaitGroup{},
-                                       mu:         sync.Mutex{},
-                                       regLock:    &regLock,
-                                       registered: &registered,
-                               }
-
-                               channel, err = g.initChannel(channel)
-                               if err != nil {
-                                       log.Errorf("failed to create gRPC 
stream error: %v", err)
-                                       continue
-                               }
-                               g.registerChannel(channel)
-                       }
-                       if err = g.registerTm(addr); err != nil {
-                               log.Errorf("%v", err)
-                               g.releaseChannelByAddr(addr)
-                       }
                }
+               clone := &discovery.ServiceInstance{Addr: instance.Addr, Port: 
instance.Port}
+               servers[serverAddress(clone)] = clone
+       }
+
+       removedAddresses := g.replaceServerAddressSnapshot(servers)
+       for _, address := range removedAddresses {
+               g.releaseChannelByAddr(address)
+       }
+
+       for _, instance := range servers {
+               g.ensureChannel(instance)
        }
 }
 
-func (g *ChannelManager) getAvailServerList() []*discovery.ServiceInstance {
-       registryService := discovery.GetRegistry()
-       instances, err := 
registryService.Lookup(config.GetSeataConfig().TxServiceGroup)
+func (g *ChannelManager) replaceServerAddressSnapshot(servers 
map[string]*discovery.ServiceInstance) []string {
+       g.serverAddressMu.Lock()
+       var removedAddresses []string
+       for address := range g.serverAddressSnapshot {
+               if _, ok := servers[address]; !ok {
+                       removedAddresses = append(removedAddresses, address)
+               }
+       }
+       g.serverAddressSnapshot = make(map[string]struct{}, len(servers))
+       for address := range servers {
+               g.serverAddressSnapshot[address] = struct{}{}
+       }
+       g.serverAddressReady = true
+       g.serverAddressMu.Unlock()
+       return removedAddresses
+}
+
+func (g *ChannelManager) ensureChannel(instance *discovery.ServiceInstance) {
+       address := serverAddress(instance)
+       entry := &channelStartEntry{}
+       if _, loaded := g.startedAddresses.LoadOrStore(address, entry); loaded {
+               return
+       }
+       if g.startChannel != nil {
+               g.startChannel(instance)
+               return
+       }
+       go g.startGrpcChannel(instance, entry)
+}
+
+func (g *ChannelManager) startGrpcChannel(instance *discovery.ServiceInstance, 
entry *channelStartEntry) {
+       addr := serverAddress(instance)
+       if !g.isServerAddressAvailable(addr) || !g.isChannelStartCurrent(addr, 
entry) {
+               g.deleteChannelStart(addr, entry)
+               return
+       }
+
+       conn, err := g.newConn(addr)
        if err != nil {
+               log.Errorf("failed to dial gRPC addr %s: %v", addr, err)
+               g.deleteChannelStart(addr, entry)
+               return
+       }
+
+       regLock := sync.Mutex{}
+       registered := atomic.Bool{}
+       // todo if read g.config.ConnectionNum, will cause the connect to fail
+       channel := &Channel{
+               addr:       addr,
+               conn:       conn,
+               sendCh:     make(chan *pb.GrpcMessageProto, 
defaultSendChBuffer),
+               closeCh:    make(chan struct{}),
+               wg:         sync.WaitGroup{},
+               mu:         sync.Mutex{},
+               regLock:    &regLock,
+               registered: &registered,
+       }
+
+       channel, err = g.initChannel(channel)
+       if err != nil {
+               log.Errorf("failed to create gRPC stream error: %v", err)
+               _ = conn.Close()
+               g.deleteChannelStart(addr, entry)
+               return
+       }
+       if !g.isServerAddressAvailable(addr) || !g.isChannelStartCurrent(addr, 
entry) {
+               channel.close()
+               g.deleteChannelStart(addr, entry)
+               return
+       }
+       if !g.registerChannel(channel) {
+               _ = conn.Close()
+               g.deleteChannelStart(addr, entry)
+               return
+       }
+       if err = g.registerTm(addr); err != nil {
+               log.Errorf("%v", err)
+               g.releaseChannelByAddr(addr)
+       }
+}
+
+func (g *ChannelManager) isChannelStartCurrent(address string, entry 
*channelStartEntry) bool {
+       current, ok := g.startedAddresses.Load(address)
+       return ok && current == entry
+}
+
+func (g *ChannelManager) deleteChannelStart(address string, entry 
*channelStartEntry) {
+       g.startedAddresses.CompareAndDelete(address, entry)
+}
+
+func (g *ChannelManager) getAvailServerList(registryService 
discovery.RegistryService) []*discovery.ServiceInstance {
+       instances, err := registryService.Lookup(g.seataConfig.TxServiceGroup)
+       if err != nil {
+               log.Warnf("lookup seata server list failed: %v", err)
                return nil
        }
        return instances
@@ -190,26 +312,19 @@ func (g *ChannelManager) registerTm(addr string) error {
 }
 
 func (g *ChannelManager) selectChannel(msg interface{}) *Channel {
-       selected := loadbalance.Select(loadbalance.GetLoadBalanceConfig().Type, 
&g.allChannels, g.getXid(msg))
-       channel, ok := selected.(*Channel)
-       if ok && channel != nil {
+       channels := g.selectableChannels()
+       channel := g.selectAvailableChannel(channels, msg)
+       if channel != nil {
                return channel
        }
 
-       if g.clientSize == 0 {
+       if selectableConnectionCount(channels) == 0 {
                ticker := time.NewTicker(time.Duration(checkAliveInternal) * 
time.Millisecond)
                defer ticker.Stop()
                for i := 0; i < maxCheckAliveRetry; i++ {
                        <-ticker.C
-                       g.allChannels.Range(func(key, value interface{}) bool {
-                               channel = key.(*Channel)
-                               if channel.IsClosed() {
-                                       g.releaseChannel(channel)
-                               } else {
-                                       return false
-                               }
-                               return true
-                       })
+                       channels = g.selectableChannels()
+                       channel = g.selectAvailableChannel(channels, msg)
                        if channel != nil {
                                return channel
                        }
@@ -218,6 +333,31 @@ func (g *ChannelManager) selectChannel(msg interface{}) 
*Channel {
        return nil
 }
 
+func (g *ChannelManager) selectAvailableChannel(channels *sync.Map, msg 
interface{}) *Channel {
+       selected := loadbalance.Select(loadbalance.GetLoadBalanceConfig().Type, 
channels, g.getXid(msg))
+       channel, ok := selected.(*Channel)
+       if ok && g.isChannelSelectable(channels, channel) {
+               return channel
+       }
+
+       // Some load balancers cache their result. Re-select from the current
+       // snapshot if a cached result points to a removed server address.
+       selected = loadbalance.Select("RandomLoadBalance", channels, 
g.getXid(msg))
+       channel, ok = selected.(*Channel)
+       if ok && g.isChannelSelectable(channels, channel) {
+               return channel
+       }
+       return nil
+}
+
+func (g *ChannelManager) isChannelSelectable(channels *sync.Map, channel 
*Channel) bool {
+       if channel == nil || channel.IsClosed() || 
!g.isServerAddressAvailable(channel.addr) {
+               return false
+       }
+       _, ok := channels.Load(channel)
+       return ok
+}
+
 func (g *ChannelManager) getXid(msg interface{}) string {
        switch tmpMsg := msg.(type) {
        case *pb.AbstractGlobalEndRequestProto:
@@ -253,22 +393,43 @@ func (g *ChannelManager) getXid(msg interface{}) string {
 }
 
 func (g *ChannelManager) releaseChannel(channel *Channel) {
-       g.allChannels.Delete(channel)
-       if !channel.IsClosed() {
-               m, _ := g.serverChannels.LoadOrStore(channel.addr, &sync.Map{})
+       if _, loaded := g.allChannels.LoadAndDelete(channel); !loaded {
+               return
+       }
+       if m, ok := g.serverChannels.Load(channel.addr); ok {
                sMap := m.(*sync.Map)
                sMap.Delete(channel)
+       }
+       if !channel.IsClosed() {
                channel.close()
        }
        atomic.AddInt32(&g.clientSize, -1)
+       if g.getAllChannelIsClosedByAddr(channel.addr) {
+               g.startedAddresses.Delete(channel.addr)
+       }
 }
 
-func (g *ChannelManager) registerChannel(channel *Channel) {
-       g.allChannels.LoadOrStore(channel, true)
+func (g *ChannelManager) registerChannel(channel *Channel) bool {
+       if !g.isServerAddressAvailable(channel.addr) {
+               log.Warnf("skip channel for removed server address: %s", 
channel.addr)
+               if _, loaded := g.allChannels.Load(channel); loaded {
+                       g.releaseChannel(channel)
+               } else if !channel.IsClosed() {
+                       channel.close()
+               }
+               return false
+       }
+       if _, loaded := g.allChannels.LoadOrStore(channel, true); loaded {
+               return true
+       }
        m, _ := g.serverChannels.LoadOrStore(channel.addr, &sync.Map{})
        sMap := m.(*sync.Map)
        sMap.Store(channel, true)
+       if _, loaded := g.startedAddresses.Load(channel.addr); !loaded {
+               g.startedAddresses.Store(channel.addr, &channelStartEntry{})
+       }
        atomic.AddInt32(&g.clientSize, 1)
+       return true
 }
 
 func (g *ChannelManager) releaseChannelByAddr(addr string) {
@@ -279,7 +440,10 @@ func (g *ChannelManager) releaseChannelByAddr(addr string) 
{
                }
                return true
        })
+       g.serverChannels.Delete(addr)
+       g.startedAddresses.Delete(addr)
 }
+
 func (g *ChannelManager) getAllChannelIsClosedByAddr(addr string) bool {
        flag := true
        g.allChannels.Range(func(key, value any) bool {
@@ -294,3 +458,46 @@ func (g *ChannelManager) getAllChannelIsClosedByAddr(addr 
string) bool {
        })
        return flag
 }
+
+func (g *ChannelManager) selectableChannels() *sync.Map {
+       channels := &sync.Map{}
+       g.allChannels.Range(func(key, value interface{}) bool {
+               channel, ok := key.(*Channel)
+               if !ok {
+                       return true
+               }
+               if channel.IsClosed() {
+                       g.releaseChannel(channel)
+                       return true
+               }
+               if g.isServerAddressAvailable(channel.addr) {
+                       channels.Store(channel, value)
+               }
+               return true
+       })
+       return channels
+}
+
+func selectableConnectionCount(connections *sync.Map) int {
+       count := 0
+       connections.Range(func(_, _ interface{}) bool {
+               count++
+               return true
+       })
+       return count
+}
+
+func (g *ChannelManager) isServerAddressAvailable(address string) bool {
+       g.serverAddressMu.RLock()
+       defer g.serverAddressMu.RUnlock()
+
+       if !g.serverAddressReady {
+               return true
+       }
+       _, ok := g.serverAddressSnapshot[address]
+       return ok
+}
+
+func serverAddress(instance *discovery.ServiceInstance) string {
+       return net.JoinHostPort(instance.Addr, strconv.Itoa(instance.Port))
+}
diff --git a/pkg/remoting/grpc/channel_manager_test.go 
b/pkg/remoting/grpc/channel_manager_test.go
new file mode 100644
index 00000000..fb23f193
--- /dev/null
+++ b/pkg/remoting/grpc/channel_manager_test.go
@@ -0,0 +1,205 @@
+/*
+ * 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 grpc
+
+import (
+       "sync"
+       "testing"
+       "time"
+
+       "github.com/stretchr/testify/assert"
+
+       "seata.apache.org/seata-go/v2/pkg/discovery"
+       "seata.apache.org/seata-go/v2/pkg/remoting/config"
+       "seata.apache.org/seata-go/v2/pkg/remoting/loadbalance"
+)
+
+func TestChannelManagerRefreshesServerListFromRegistrySubscription(t 
*testing.T) {
+       registry := &fakeSubscriberRegistry{
+               fakeLookupRegistry: fakeLookupRegistry{
+                       instances: []*discovery.ServiceInstance{{Addr: 
"127.0.0.1", Port: 8091}},
+               },
+       }
+       started := make(chan string, 2)
+       startCount := make(map[string]int)
+       var startMu sync.Mutex
+       manager := newTestChannelManager(func(instance 
*discovery.ServiceInstance) {
+               address := serverAddress(instance)
+               startMu.Lock()
+               startCount[address]++
+               startMu.Unlock()
+               started <- address
+       })
+
+       manager.initWithRegistry(registry)
+       assert.Equal(t, "default_tx_group", registry.subscribeKey)
+       assert.Equal(t, "127.0.0.1:8091", nextStartedChannel(t, started))
+
+       registry.publish([]*discovery.ServiceInstance{{Addr: "127.0.0.2", Port: 
8092}})
+       assert.Equal(t, "127.0.0.2:8092", nextStartedChannel(t, started))
+       assert.False(t, manager.isServerAddressAvailable("127.0.0.1:8091"))
+       assert.True(t, manager.isServerAddressAvailable("127.0.0.2:8092"))
+
+       registry.publish([]*discovery.ServiceInstance{{Addr: "127.0.0.2", Port: 
8092}})
+       startMu.Lock()
+       assert.Equal(t, 1, startCount["127.0.0.2:8092"])
+       startMu.Unlock()
+
+       registry.publish([]*discovery.ServiceInstance{{Addr: "127.0.0.1", Port: 
8091}})
+       assert.Equal(t, "127.0.0.1:8091", nextStartedChannel(t, started))
+       startMu.Lock()
+       assert.Equal(t, 2, startCount["127.0.0.1:8091"])
+       startMu.Unlock()
+}
+
+func TestChannelManagerFallsBackToLookupWhenRegistryDoesNotSubscribe(t 
*testing.T) {
+       registry := &fakeLookupRegistry{
+               instances: []*discovery.ServiceInstance{{Addr: "127.0.0.1", 
Port: 8091}},
+       }
+       started := make(chan string, 1)
+       manager := newTestChannelManager(func(instance 
*discovery.ServiceInstance) {
+               started <- serverAddress(instance)
+       })
+
+       manager.initWithRegistry(registry)
+       assert.Equal(t, "default_tx_group", registry.lookupKey)
+       assert.Equal(t, "127.0.0.1:8091", nextStartedChannel(t, started))
+       assert.True(t, manager.isServerAddressAvailable("127.0.0.1:8091"))
+}
+
+func TestChannelManagerSelectChannelSkipsRemovedServerAddress(t *testing.T) {
+       previousLoadBalance := loadbalance.GetLoadBalanceConfig()
+       loadbalance.InitLoadBalanceConfig(loadbalance.Config{Type: 
"RoundRobinLoadBalance"})
+       t.Cleanup(func() {
+               loadbalance.InitLoadBalanceConfig(previousLoadBalance)
+       })
+
+       manager := newTestChannelManager(nil)
+       manager.refreshServerList([]*discovery.ServiceInstance{
+               {Addr: "127.0.0.1", Port: 8091},
+               {Addr: "127.0.0.2", Port: 8092},
+       })
+
+       removed := newSelectableTestChannel("127.0.0.1:8091")
+       kept := newSelectableTestChannel("127.0.0.2:8092")
+       assert.True(t, manager.registerChannel(removed))
+       assert.True(t, manager.registerChannel(kept))
+       manager.refreshServerList([]*discovery.ServiceInstance{{Addr: 
"127.0.0.2", Port: 8092}})
+
+       assert.True(t, removed.IsClosed())
+       assert.Equal(t, kept, manager.selectChannel(&struct{ TransactionName 
string }{TransactionName: "tx"}))
+       if _, ok := manager.startedAddresses.Load("127.0.0.1:8091"); ok {
+               t.Fatal("removed channel address is still tracked")
+       }
+}
+
+func TestChannelCloseDoesNotWaitWithMutexHeld(t *testing.T) {
+       channel := &Channel{closeCh: make(chan struct{})}
+       channel.wg.Add(1)
+       observedClosed := make(chan bool, 1)
+
+       go func() {
+               defer channel.wg.Done()
+               <-channel.closeCh
+               channel.mu.Lock()
+               observedClosed <- channel.IsClosed()
+               channel.mu.Unlock()
+       }()
+
+       closed := make(chan struct{})
+       go func() {
+               channel.close()
+               close(closed)
+       }()
+
+       select {
+       case <-closed:
+       case <-time.After(time.Second):
+               t.Fatal("timed out waiting for channel close")
+       }
+       assert.True(t, <-observedClosed)
+}
+
+func newTestChannelManager(startChannel func(*discovery.ServiceInstance)) 
*ChannelManager {
+       if startChannel == nil {
+               startChannel = func(*discovery.ServiceInstance) {}
+       }
+       return &ChannelManager{
+               config:       &config.Config{},
+               seataConfig:  &config.SeataConfig{TxServiceGroup: 
"default_tx_group"},
+               startChannel: startChannel,
+       }
+}
+
+func newSelectableTestChannel(addr string) *Channel {
+       return &Channel{addr: addr, closeCh: make(chan struct{})}
+}
+
+func nextStartedChannel(t *testing.T, started <-chan string) string {
+       t.Helper()
+
+       select {
+       case address := <-started:
+               return address
+       case <-time.After(time.Second):
+               t.Fatal("timed out waiting for gRPC channel start")
+       }
+       return ""
+}
+
+type fakeLookupRegistry struct {
+       lookupKey string
+       instances []*discovery.ServiceInstance
+}
+
+func (r *fakeLookupRegistry) Lookup(key string) ([]*discovery.ServiceInstance, 
error) {
+       r.lookupKey = key
+       return cloneTestServiceInstances(r.instances), nil
+}
+
+func (r *fakeLookupRegistry) Close() {}
+
+type fakeSubscriberRegistry struct {
+       fakeLookupRegistry
+       subscribeKey string
+       listener     discovery.RegistryChangeListener
+}
+
+func (r *fakeSubscriberRegistry) Subscribe(key string, listener 
discovery.RegistryChangeListener) (discovery.RegistrySubscription, error) {
+       r.subscribeKey = key
+       r.listener = listener
+       listener(discovery.RegistryChangeEvent{Key: key, Instances: 
cloneTestServiceInstances(r.instances)})
+       return fakeRegistrySubscription{}, nil
+}
+
+func (r *fakeSubscriberRegistry) publish(instances 
[]*discovery.ServiceInstance) {
+       r.listener(discovery.RegistryChangeEvent{Key: r.subscribeKey, 
Instances: cloneTestServiceInstances(instances)})
+}
+
+type fakeRegistrySubscription struct{}
+
+func (fakeRegistrySubscription) Unsubscribe() {}
+
+func cloneTestServiceInstances(instances []*discovery.ServiceInstance) 
[]*discovery.ServiceInstance {
+       clones := make([]*discovery.ServiceInstance, 0, len(instances))
+       for _, instance := range instances {
+               clone := *instance
+               clones = append(clones, &clone)
+       }
+       return clones
+}
diff --git a/pkg/remoting/grpc/listener.go b/pkg/remoting/grpc/listener.go
index f4d2915f..500ad1dd 100644
--- a/pkg/remoting/grpc/listener.go
+++ b/pkg/remoting/grpc/listener.go
@@ -66,6 +66,10 @@ func (g *grpcClientHandler) monitorStreamHealth(ctx 
context.Context, channel *Ch
        for {
                select {
                case <-ticker.C:
+                       if 
!channelManager.isServerAddressAvailable(channel.addr) {
+                               channelManager.releaseChannel(channel)
+                               return
+                       }
                        err := g.transferHeartBeat(channel, 
&pb.HeartbeatMessageProto{Ping: true})
                        if err != nil {
                                heartBeatRetryTimes++
@@ -80,6 +84,10 @@ func (g *grpcClientHandler) monitorStreamHealth(ctx 
context.Context, channel *Ch
                                        flag := false
                                        var reconnectErr error
                                        for !flag {
+                                               if 
!channelManager.isServerAddressAvailable(channel.addr) {
+                                                       
channelManager.releaseChannel(channel)
+                                                       return
+                                               }
                                                state := channel.conn.GetState()
                                                switch state {
                                                case connectivity.Shutdown:
@@ -98,7 +106,9 @@ func (g *grpcClientHandler) monitorStreamHealth(ctx 
context.Context, channel *Ch
                                                        
channel.conn.WaitForStateChange(ctx, state)
                                                        continue
                                                }
-                                               
channelManager.registerChannel(channel)
+                                               if 
!channelManager.registerChannel(channel) {
+                                                       return
+                                               }
 
                                                flag = true
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to