开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

高频算法面试题(三十八)- 判断一棵树是否为二叉搜索树

发表于 2021-11-23

「这是我参与11月更文挑战的第 23 天,活动详情查看:2021最后一次更文挑战」

刷算法题,从来不是为了记题,而是练习把实际的问题抽象成具体的数据结构或算法模型,然后利用对应的数据结构或算法模型来进行解题。个人觉得,带着这种思维刷题,不仅能解决面试问题,也能更多的学会在日常工作中思考,如何将实际的场景抽象成相应的算法模型,从而提高代码的质量和性能

判断一棵树是否为二叉搜索树

题目来源:LeetCode-98. 验证二叉搜索树

题目描述

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

有效 二叉搜索树定义如下:

  • 节点的左子树只包含 小于 当前节点的数
  • 节点的右子树只包含 大于 当前节点的数
  • 所有左子树和右子树自身必须也是二叉搜索树

示例

示例 1

1.png

1
2
ini复制代码输入:root = [2,1,3]
输出:true

示例 2

2.png

1
2
3
csharp复制代码输入:root = [5,1,4,null,null,3,6]
输出:false
解释:根节点的值是 5 ,但是右子节点的值是 4

提示:

  • 树中节点数目范围在[1, 10^4] 内
  • 2^31 <= Node.val <= 2^31 - 1

解题

解法一:递归

思路

如果一个结点的左右子树是二插搜索树,那么它本身也是二插搜索树。根据题目中给的说明,一个二插搜索树满足这样的条件:

  • 节点的左子树只包含 小于 当前节点的数
  • 节点的右子树只包含 大于 当前节点的数
  • 所有左子树和右子树自身必须也是二叉搜索树

这里有一个误区,代码容易写成仅判断了根节点和它左右节点组成的子树是不是平衡二叉树。这样其实是不对,比如这样写就是不对的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
go复制代码func isValidBST(root *TreeNode) bool {
if root == nil {
return true
}

return isValid(root)
}

func isValid(root *TreeNode) bool {
if root == nil {
return true
}

if root.Left == nil && root.Right == nil {
return true
}

if root.Left != nil && root.Left.Val >= root.Val {
return false
}

if root.Right != nil && root.Right.Val <= root.Val {
return false
}

return isValid(root.Left) && isValid(root.Right)
}

比如这种情况,上边的代码就会不通过

3.png

代码只判断出来了5、4、6和6、3、7都是二插搜索树,但是整体并不是一个二插搜索树

因此,需要换种思路。很容易想到使用递归,假设root为根的子树,应该判断子树中的所有结点都在(min,max)这个区间内(开区间),如果root的值不在(min,max)这个区间内,说明不满足二插搜索树的条件,直接返回即可。否则就继续遍历左右子树

在递归左子树的时候,上边界的max,就应该替换成root.Val(也就是当前节点的值)。同理,在递归右子树的时候,下边界的min,就应该替换成root.Val

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
arduino复制代码func isValidBST(root *TreeNode) bool {
return isValid(root, math.MinInt64, math.MaxInt64)
}

func isValid(root *TreeNode, min, max int) bool {
if root == nil {
return true
}

if root.Val <= min || root.Val >= max {
return false
}

// isValid(root.Left, min, root.Val) 左子树的值,都应该比当前这个节点小,因此它的下边界的值应该是当前结点的值
//isValid(root.Right, root.Val, max) 右子树的值,都应该比当前这个节点大,因此它的上边界的值应该是当前结点的值
return isValid(root.Left, min, root.Val) && isValid(root.Right, root.Val, max)
}

解法二:中序遍历

思路

因为我们知道,对于一颗二插搜索树,它的中序遍历,可以升序的打印出所有节点的值。利用这一特性来进行解题,因为在遍历的过程中,需要比较当前结点的值,是否大于前一个结点的值,因此递归实现中序遍历,不好加这层判断,所以,需要使用中序遍历的非递归遍历

我前边有篇文章,详细的分享了树的前、中、后序遍历的递归和非递归遍历,以及层序遍历,如果你不是很了解,可以移步这里。二叉树的变量,是所有树的算法题的基础,你会发现,所有树的题,都是遍历树的变形题,只是在遍历过程中增加了一些操作

二叉树的中序非递归遍历,需要借助栈,因此用栈来实现,具体代码如下

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
go复制代码//中序遍历实现
func isValidBST2(root *TreeNode) bool {
nodeStack := []*TreeNode{}
preNodeVal := math.MinInt64
for len(nodeStack) != 0 || root != nil {
for root != nil {
nodeStack = append(nodeStack, root)
root = root.Left
}
root = nodeStack[len(nodeStack)-1]
nodeStack = nodeStack[:len(nodeStack)-1]
if root.Val <= preNodeVal {
return false
}
preNodeVal = root.Val
root = root.Right
}

return true
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

curator对Zookeeper节点监听总结 zookee

发表于 2021-11-23

这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战

zookeeper简介

ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。

ZooKeeper的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。

ZooKeeper包含一个简单的原语集,提供Java和C的接口。

ZooKeeper代码版本中,提供了分布式独享锁、选举、队列的接口,代码在zookeeper-3.4.3\src\recipes。其中分布锁和队列有Java和C两个版本,选举只有Java版本。

以上内容摘自【百度百科】,详细了解可以去官网https:/zookeeper.apache.org/

zookeeper集群搭建

关于单节点,单服务器多节点的伪集群,集群,网上相关资料很多,不再赘述。

curator简介

zookeeper提供的原生API操作过于烦琐,curator框架是对zookeeper提供的原生API进行了封装,提供了更高级的API接口,使客户端程序员使用zookeeper更加容易及高效。

关于curaotr的详细介绍可参看官网curator.apache.org/

zookeeper监听的原生API

zookeeper原生的JAVA API提供监听接口是Watcher接口,使用的时候实现该接口重写process(WatchedEvent watchedEvent)方法即可。

缺点:该监听只能监听一次,触发一次之后即失效,如果需要重复使用,需要每使用一次,即注册一次。

测试代码及说明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
java复制代码public class ZkWatcherTest {

  private static final String ADDR = "192.168.100.1:2181";
  private static final String PATH = "/test1";
  private static ZooKeeper zk = null;
  static {
      try {
          zk = new ZooKeeper(ADDR, 3000, new WatcherTest());
      } catch (IOException e) {
          e.printStackTrace();
      }
  }

  public static void main(String[] args) throws InterruptedException {
      test1();
      Thread.sleep(Integer.MAX_VALUE);
  }

  private static void test1(){
      try {
          /**
            * set 事件触发(修改节点值)
            * get 不触发
            * 对一级子节点操作不触发
            * 删除当前节点触发,如果是同步会抛异常(KeeperErrorCode = NoNode),再创建再删除不再触发
            * 创建节点不触发
            * 同步连接时,exists事件未触发,异步未测试
            */
          zk.getData(PATH, true, null);
          /**
            * set 事件触发(修改节点值)
            * get 不触发
            * 对一级子节点操作(CRUD)不触发
            * 删除当前节点触发,未抛异常
            * 同步连接时,exists事件未触发,异步未测试
            * 创建节点触发
            */
          zk.exists(PATH, true);
          /**
            * 删除节点触发,抛出异常
            * set 不触发(修改节点值)
            * get 不触发
            * 删除节点再创建之后 ,操作一级节点不触发
            * 创建一级节点触发
            * 删除一级节点触发
            * 修改一级节点的值不触发
            * 同步连接时,exists事件未触发,异步未测试
            */
          zk.getChildren(PATH, true);
      } catch (KeeperException e) {
          e.printStackTrace();
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
  }

  private static class WatcherTest implements Watcher{
      public void process(WatchedEvent watchedEvent) {
          System.out.println("wathceEvent: " + watchedEvent);
          try {
            //watcher当回调方法后已失效,如下面第二个参数为true,注册默认Watcher
              zk.getChildren(PATH, true);
          } catch (KeeperException e) {
              e.printStackTrace();
          } catch (InterruptedException e) {
              e.printStackTrace();
          }

      }
  }
}
public class ZkWatcherTest {

  private static final String ADDR = "192.168.100.1:2181";
  private static final String PATH = "/test1";
  private static ZooKeeper zk = null;
  static {
      try {
          zk = new ZooKeeper(ADDR, 3000, new WatcherTest());
      } catch (IOException e) {
          e.printStackTrace();
      }
  }

  public static void main(String[] args) throws InterruptedException {
      test1();
      Thread.sleep(Integer.MAX_VALUE);
  }

  private static void test1(){
      try {
          /**
            * set 事件触发(修改节点值)
            * get 不触发
            * 对一级子节点操作不触发
            * 删除当前节点触发,如果是同步会抛异常(KeeperErrorCode = NoNode),再创建再删除不再触发
            * 创建节点不触发
            * 同步连接时,exists事件未触发,异步未测试
            */
          zk.getData(PATH, true, null);
          /**
            * set 事件触发(修改节点值)
            * get 不触发
            * 对一级子节点操作(CRUD)不触发
            * 删除当前节点触发,未抛异常
            * 同步连接时,exists事件未触发,异步未测试
            * 创建节点触发
            */
          zk.exists(PATH, true);
          /**
            * 删除节点触发,抛出异常
            * set 不触发(修改节点值)
            * get 不触发
            * 删除节点再创建之后 ,操作一级节点不触发
            * 创建一级节点触发
            * 删除一级节点触发
            * 修改一级节点的值不触发
            * 同步连接时,exists事件未触发,异步未测试
            */
          zk.getChildren(PATH, true);
      } catch (KeeperException e) {
          e.printStackTrace();
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
  }

  private static class WatcherTest implements Watcher{
      public void process(WatchedEvent watchedEvent) {
          System.out.println("wathceEvent: " + watchedEvent);
          try {
            //watcher当回调方法后已失效,如下面第二个参数为true,注册默认Watcher
              zk.getChildren(PATH, true);
          } catch (KeeperException e) {
              e.printStackTrace();
          } catch (InterruptedException e) {
              e.printStackTrace();
          }

      }
  }
}

对于Zookeeper提供的原生JAVA API来说,初始化客户端实例的时候需要传入一个Watcher参数,该值可以为空,这是注册一个默认的Watcher,该Watcher在第一次调用之后便会失效,getData, exists, getChildren三个接口的第二个参数设置为true即可再次注册watcher(默认Watcher,即初始化Zookeeper客户端传入的Watcher),对于每个接口注册watcher能够监听的事件状态和触发Watcher的事件类型,参看注释说明。

curator官方推荐的高级监听API

curator官方推荐的API是对zookeeper原生的JAVA API进行了封装,将重复注册,事件信息等很好的处理了。而且监听事件返回了详细的信息,包括变动的节点路径,节点值等等,这是原生API所没有的。

这个对事件的监听类似于一个本地缓存视图和远程Zookeeper视图的对比过程。

注:curator的方法调用采用的是流式API,此种风格的优点及使用注意事项可自行查阅资料了解。

官方推荐的API提供了三个接口,分别如下:

  • NodeCache

对一个节点进行监听,监听事件包括指定路径的增删改操作

  • PathChildrenCache

对指定路径节点的一级子目录监听,不对该节点的操作监听,对其子目录的增删改操作监听

  • TreeCache

综合NodeCache和PathChildrenCahce的特性,是对整个目录进行监听,可以设置监听深度。

这三个API的用法及注意事项和部分参数说明如下测试代码所示:内容比较琐碎不再抽出进行详细总结。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
java复制代码public class ZKCurator {
 
  private static final String ADDR = "192.168.100.1:2181";
  private static final String PATH = "/zktest1";

  public static void main(String[] args) throws InterruptedException {
      final CuratorFramework zkClient = CuratorFrameworkFactory.newClient(ADDR, new RetryNTimes(10, 5000));
      zkClient.start();
      System.out.println("start zkclient...");
      Thread thread = null;

      try {
          registerWatcher(zkClient);
          //registerNodeCache(zkClient);
      } catch (Exception e) {
          e.printStackTrace();

      }

      System.out.println("register wathcer end...");
      Thread.sleep(Integer.MAX_VALUE);
      zkClient.close();
  }

  private static void registerWatcher(CuratorFramework zkClient) throws Exception {
      /**
        * 注册监听器,当前节点不存在,创建该节点:未抛出异常及错误日志
        * 注册子节点触发type=[CHILD_ADDED]
        * 更新触发type=[CHILD_UPDATED]
        *
        * zk挂掉type=CONNECTION_SUSPENDED,,一段时间后type=CONNECTION_LOST
        * 重启zk:type=CONNECTION_RECONNECTED, data=null
        * 更新子节点:type=CHILD_UPDATED, data=ChildData{path='/zktest111/tt1', stat=4294979983,4294979993,1501037475236,1501037733805,2,0,0,0,6,0,4294979983
        , data=[55, 55, 55, 55, 55, 55]}

        * 删除子节点type=CHILD_REMOVED
        * 更新根节点:不触发
        * 删除根节点:不触发 无异常
        * 创建根节点:不触发
        * 再创建及更新子节点不触发
        *
        * 重启时,与zk连接失败
        */
      ExecutorService service = Executors.newFixedThreadPool(3);
      PathChildrenCache watcher = new PathChildrenCache(zkClient, PATH, true/*,false, service*/);
      watcher.getListenable().addListener(new PathChildrenCacheListener() {
          public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent pathChildrenCacheEvent) throws Exception {
              System.out.println(pathChildrenCacheEvent);
          }
      });
      /*PathChildrenCache.StartMode说明如下
      *POST_INITIALIZED_EVENT
      *1、在监听器启动的时候即,会枚举当前路径所有子节点,触发CHILD_ADDED类型的事件
      * 2、同时会监听一个INITIALIZED类型事件
      * NORMAL异步初始化cache
      * POST_INITIALIZED_EVENT异步初始化,初始化完成触发事件PathChildrenCacheEvent.Type.INITIALIZED
      /*NORMAL只和POST_INITIALIZED_EVENT的1情况一样,不会ALIZED类型事件触发
   
      /*BUILD_INITIAL_CACHE 不会触发上面两者事件,同步初始化客户端的cache,及创建cache后,就从服务器端拉入对应的数据       */
      watcher.start(PathChildrenCache.StartMode.BUILD_INITIAL_CACHE);
      System.out.println("注册watcher成功...");
  }

  public static void registerNodeCache(CuratorFramework client) throws Exception{
      /*
      * 节点路径不存在时,set不触发监听
      * 节点路径不存在,,,创建事件触发监听
      * 节点路径存在,set触发监听
      * 节点路径存在,delete触发监听
      *
      *
      * 节点挂掉,未触发任何监听
      * 节点重连,未触发任何监听
      * 节点重连 ,恢复监听
      * */
      final NodeCache nodeCache = new NodeCache(client, PATH, false);
      nodeCache.getListenable().addListener(new NodeCacheListener() {
          public void nodeChanged() throws Exception {
              System.out.println("当前节点:"+nodeCache.getCurrentData());
          }
      });
      //如果为true则首次不会缓存节点内容到cache中,默认为false,设置为true首次不会触发监听事件
      nodeCache.start(true);
  }

public static void registTreeCache(CuratorFramework client) throws Exception {
      /**
        * TreeCache.nodeState == LIVE的时候,才能执行getCurrentChildren非空,默认为PENDING
        * 初始化完成之后,监听节点操作时 TreeCache.nodeState == LIVE
        *
        * maxDepth值设置说明,比如当前监听节点/t1,目录最深为/t1/t2/t3/t4,则maxDepth=3,说明下面3级子目录全
        * 监听,即监听到t4,如果为2,则监听到t3,对t3的子节点操作不再触发
        * maxDepth最大值2147483647
        *
        * 初次开启监听器会把当前节点及所有子目录节点,触发[type=NODE_ADDED]事件添加所有节点(小等于maxDepth目录)
        * 默认监听深度至最低层
        * 初始化以[type=INITIALIZED]结束
        *
        * [type=NODE_UPDATED],set更新节点值操作,范围[当前节点,maxDepth目录节点](闭区间)
        *
        *
        * [type=NODE_ADDED] 增加节点 范围[当前节点,maxDepth目录节点](左闭右闭区间)
        *
        * [type=NODE_REMOVED] 删除节点, 范围[当前节点, maxDepth目录节点](闭区间),删除当前节点无异常
        *
        * 事件信息
        * TreeCacheEvent{type=NODE_ADDED, data=ChildData{path='/zktest1', stat=4294979373,4294979373,1499850881635,1499850881635,0,0,0,0,2,0,4294979373
        , data=[116, 49]}}
        *
        */
      final TreeCache treeCache = TreeCache.newBuilder(client, PATH).setCacheData(true).setMaxDepth(2).build();
      treeCache.getListenable().addListener(new TreeCacheListener() {
          public void childEvent(CuratorFramework curatorFramework, TreeCacheEvent treeCacheEvent) throws Exception {
              System.out.println("treeCacheEvent: "+treeCacheEvent);
          }
      });
      //没有开启模式作为入参的方法
      treeCache.start();
  }
} class ZKCurator {
 
  private static final String ADDR = "192.168.100.1:2181";
  private static final String PATH = "/zktest1";

  public static void main(String[] args) throws InterruptedException {
      final CuratorFramework zkClient = CuratorFrameworkFactory.newClient(ADDR, new RetryNTimes(10, 5000));
      zkClient.start();
      System.out.println("start zkclient...");
      Thread thread = null;

      try {
          registerWatcher(zkClient);
          //registerNodeCache(zkClient);
      } catch (Exception e) {
          e.printStackTrace();

      }

      System.out.println("register wathcer end...");
      Thread.sleep(Integer.MAX_VALUE);
      zkClient.close();
  }

  private static void registerWatcher(CuratorFramework zkClient) throws Exception {
      /**
        * 注册监听器,当前节点不存在,创建该节点:未抛出异常及错误日志
        * 注册子节点触发type=[CHILD_ADDED]
        * 更新触发type=[CHILD_UPDATED]
        *
        * zk挂掉type=CONNECTION_SUSPENDED,,一段时间后type=CONNECTION_LOST
        * 重启zk:type=CONNECTION_RECONNECTED, data=null
        * 更新子节点:type=CHILD_UPDATED, data=ChildData{path='/zktest111/tt1', stat=4294979983,4294979993,1501037475236,1501037733805,2,0,0,0,6,0,4294979983
        , data=[55, 55, 55, 55, 55, 55]}

        * 删除子节点type=CHILD_REMOVED
        * 更新根节点:不触发
        * 删除根节点:不触发 无异常
        * 创建根节点:不触发
        * 再创建及更新子节点不触发
        *
        * 重启时,与zk连接失败
        */
      ExecutorService service = Executors.newFixedThreadPool(3);
      PathChildrenCache watcher = new PathChildrenCache(zkClient, PATH, true/*,false, service*/);
      watcher.getListenable().addListener(new PathChildrenCacheListener() {
          public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent pathChildrenCacheEvent) throws Exception {
              System.out.println(pathChildrenCacheEvent);
          }
      });
      /*PathChildrenCache.StartMode说明如下
      *POST_INITIALIZED_EVENT
      *1、在监听器启动的时候即,会枚举当前路径所有子节点,触发CHILD_ADDED类型的事件
      * 2、同时会监听一个INITIALIZED类型事件
      * NORMAL异步初始化cache
      * POST_INITIALIZED_EVENT异步初始化,初始化完成触发事件PathChildrenCacheEvent.Type.INITIALIZED
      /*NORMAL只和POST_INITIALIZED_EVENT的1情况一样,不会ALIZED类型事件触发
   
      /*BUILD_INITIAL_CACHE 不会触发上面两者事件,同步初始化客户端的cache,及创建cache后,就从服务器端拉入对应的数据       */
      watcher.start(PathChildrenCache.StartMode.BUILD_INITIAL_CACHE);
      System.out.println("注册watcher成功...");
  }

  public static void registerNodeCache(CuratorFramework client) throws Exception{
      /*
      * 节点路径不存在时,set不触发监听
      * 节点路径不存在,,,创建事件触发监听
      * 节点路径存在,set触发监听
      * 节点路径存在,delete触发监听
      *
      *
      * 节点挂掉,未触发任何监听
      * 节点重连,未触发任何监听
      * 节点重连 ,恢复监听
      * */
      final NodeCache nodeCache = new NodeCache(client, PATH, false);
      nodeCache.getListenable().addListener(new NodeCacheListener() {
          public void nodeChanged() throws Exception {
              System.out.println("当前节点:"+nodeCache.getCurrentData());
          }
      });
      //如果为true则首次不会缓存节点内容到cache中,默认为false,设置为true首次不会触发监听事件
      nodeCache.start(true);
  }

public static void registTreeCache(CuratorFramework client) throws Exception {
      /**
        * TreeCache.nodeState == LIVE的时候,才能执行getCurrentChildren非空,默认为PENDING
        * 初始化完成之后,监听节点操作时 TreeCache.nodeState == LIVE
        *
        * maxDepth值设置说明,比如当前监听节点/t1,目录最深为/t1/t2/t3/t4,则maxDepth=3,说明下面3级子目录全
        * 监听,即监听到t4,如果为2,则监听到t3,对t3的子节点操作不再触发
        * maxDepth最大值2147483647
        *
        * 初次开启监听器会把当前节点及所有子目录节点,触发[type=NODE_ADDED]事件添加所有节点(小等于maxDepth目录)
        * 默认监听深度至最低层
        * 初始化以[type=INITIALIZED]结束
        *
        * [type=NODE_UPDATED],set更新节点值操作,范围[当前节点,maxDepth目录节点](闭区间)
        *
        *
        * [type=NODE_ADDED] 增加节点 范围[当前节点,maxDepth目录节点](左闭右闭区间)
        *
        * [type=NODE_REMOVED] 删除节点, 范围[当前节点, maxDepth目录节点](闭区间),删除当前节点无异常
        *
        * 事件信息
        * TreeCacheEvent{type=NODE_ADDED, data=ChildData{path='/zktest1', stat=4294979373,4294979373,1499850881635,1499850881635,0,0,0,0,2,0,4294979373
        , data=[116, 49]}}
        *
        */
      final TreeCache treeCache = TreeCache.newBuilder(client, PATH).setCacheData(true).setMaxDepth(2).build();
      treeCache.getListenable().addListener(new TreeCacheListener() {
          public void childEvent(CuratorFramework curatorFramework, TreeCacheEvent treeCacheEvent) throws Exception {
              System.out.println("treeCacheEvent: "+treeCacheEvent);
          }
      });
      //没有开启模式作为入参的方法
      treeCache.start();
  }
}

注意事项:

1、Curator只是封装了原生Zookeeper的监听事件,使客户端程序员无序重复注册Watcher,但是Wathcer的一次性还是存在的,只是由curator完成。因此对于某些场景使用依然需要慎重。因为curator需要重复注册,因此,第一次触发Wathcer与再次注册Watcher即使是异常操作,但是中间还是存在时延,假使对于Zookeeper瞬时触发几个事件,则该监听器并不能保证监听到所有状态的改变,至于可以监听到多少取决于服务器的处理速度。

2、只要curator的cache启动成功,监听器注册成功,理论上只要没有1的情况下,监听器是可以很完美的处理需要监听到的事件。但是如果在cache.start()的时候,与Zookeeper的连接是中断的,则后续连接恢复,也无法让客户端感知到需要监听的变动。我当时想到的一个解决方案是在Zookeeper启动的时候设置一个连接状态的监听器(连接状态监听器看第7节),如果Zookeeper客户端连接状态是连接失败,则添加这个监听器,恢复连接的时候,调用cache.clearAndRefresh(),然后移除连接状态监听器即可。

但是,这个接口只针对PathChildrenCache,因为该监听器监听节点删除的时候,再次创建也不会再有重新监听的效果,调用该接口即可恢复。另外两种监听器可以不用考虑这种情况,原因取决于监听器的内部实现。

curator使用zookeeper原生监听器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码final CuratorFramework client = CuratorFrameworkFactory.builder()
              .retryPolicy(new RetryNTimes(0, 1000))
              .connectionTimeoutMs(4000)
              .sessionTimeoutMs(40000)
              .connectString(ADDR)
              .defaultData(null)
              .build();

client.checkExists().usingWatcher(new Watcher() {
  public void process(WatchedEvent event) {

  }
}); CuratorFramework client = CuratorFrameworkFactory.builder()
              .retryPolicy(new RetryNTimes(0, 1000))
              .connectionTimeoutMs(4000)
              .sessionTimeoutMs(40000)
              .connectString(ADDR)
              .defaultData(null)
              .build();

client.checkExists().usingWatcher(new Watcher() {
  public void process(WatchedEvent event) {

  }
});

如代码所示,和原生API的使用方法差不多,只不过curator的接口调用风格,监听器的用法、特性及触发事件和原生监听器一样,因为这里传入的参数便是Zookeeper原生监听器,当然也可以是CuratorWathcer参数。

这样用的话,监听器便需要自己实现重复注册了。

使用curator但是却不使用它提供的高级监听器的API是应对于某些特殊的业务场景。

其它监听器

比如连接状态监听器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码final CuratorFramework client = CuratorFrameworkFactory.builder()
              .retryPolicy(new RetryNTimes(0, 1000))
              .connectionTimeoutMs(4000)
              .sessionTimeoutMs(40000)
              .connectString(ADDR)
              .defaultData(null)
              .build();
client.getConnectionStateListenable().addListener(new ConnectionStateListener() {
  public void stateChanged(CuratorFramework client, ConnectionState newState) {
     
  }
}); CuratorFramework client = CuratorFrameworkFactory.builder()
              .retryPolicy(new RetryNTimes(0, 1000))
              .connectionTimeoutMs(4000)
              .sessionTimeoutMs(40000)
              .connectString(ADDR)
              .defaultData(null)
              .build();
client.getConnectionStateListenable().addListener(new ConnectionStateListener() {
  public void stateChanged(CuratorFramework client, ConnectionState newState) {
     
  }
});

ConnectionState参数说明:

ConnectionState 说明
CONNECTED 第一次成功连接时
SUSPENDED 连接丢失但是连接尚未超时的时候
RECONECTED SUSPENDED、LOST、READ_ONLY三种状态之后并重新建立连接的时候
LOST 连接确认丢失的时候
READ_ONLY 只读模式。该模式会在客户端初始化的时候设置

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

HTTP/2 都还没上用,HTTP/3 又是什么鬼?

发表于 2021-11-23

作者:IT影子

链接:www.jianshu.com/p/b0b3c6df1…

HTTP/3是超文本传输协议(HTTP)的第三个正式版本,将改善网络性能和稳定性,解决各种安全隐私问题,但尽管如此,仍存在一些安全挑战。

HTTP/3不再使用传输控制协议(TCP),相反,将使用2012年谷歌提出的QUIC传输协议。实际上,HTTP/3前身是HTTP-over-QUIC。

2018年10月,互联网工程任务组(IETF) HTTP和QUIC工作组主席Mark Nottingham提出了将HTTP-over-QUIC更名为HTTP/3

QUIC是基于用户数据包协议(UDP)连接的复用版本的传输层协议。与TCP不同,UDP不遵循TCP三向交握,而是使用单个UDP往返。因此,在用户代理和Web服务器之间的每个连接都使用UDP,QUIC协议极大地改善了任何web组件的网络性能。

同样,QUIC依靠多路复用来在单个连接上无缝地管理用户代理与服务器之间的多个交互,而没有一个阻塞另一个,因此与以前的版本相比,有助于提高性能。从性能和稳定性的角度考虑,HTTP/3似乎都有很大的优势。从安全性来说,HTTP/3有其先进性也有其局限性。

安全优势

1.端到端加密

TCP协议旨在确保在传输过程中进行有效负载加密,但是对于特定传输的信息仍未加密,所以这会引发许多安全和隐私问题。预防攻击的对策不是在TCP堆栈上,而是在处理协议和网络的网络设备和中间盒上。此外,解析器可以克服负载均衡器和其他网络设备中的这些问题,但它们也还存在严重的性能问题,并且可能会限制网络发展速度和可靠性。

使用QUIC协议时,只有网段中的必填字段未加密,而其余信息默认情况下是加密的。通过查看TCP和QUIC的网络段,我们发现包括数据包标志(数据包NR和ACK NR),窗口和选项的字段在QUIC中已加密,但在TCP中未加密。QUIC中建议加密有助于防止普遍存在的监视攻击(在HTTP / 3的前身中很普遍)以及协议工件和元数据、应用程序数据的侵入式信息收集。

下面的图1显示了QUIC协议在网络分析器工具Wireshark中的呈现方式。根据QUIC的网段,互联网协议(IP)层保存源IP地址和目标IP地址信息。UDP保留源端口和目标端口,而QUIC包含公共标志,数据包编号,连接ID和加密的有效负载。

Wireshark代码段显示QUIC协议的网段

2.TLS安全连接

为了在连接期间支持端到端加密,QUIC主要依赖于加密和传输层握手。由于QUIC直接与TLS 1.3 交互,因此它可用于所有原始连接的授权加密,并且没有禁用TLS。QUIC还负责确保建立安全连接,同时考虑到所有原始连接的机密性和完整性保护。与HTTP / 2 + TLS实现不同,QUIC在其传输上下文中处理TLS握手和警报机制,这反过来又帮助QUIC利用从握手交换的密钥来建立密码保护。

如果我们从整体上考虑该协议,则TLS和QUIC之间存在两个主要通信:

QUIC为TLS提供了稳定的流抽象,通过QUIC发送和接收消息。

TLS使用以下内容更新QUIC组件。

1.秘密的、经过身份验证的加密算法和密钥派生功能(KDF)

2.数据包保护密钥

3.协议状态更改(例如握手状态、服务器证书)

与使用TLS的“ application_data”记录的HTTP/2不同,QUIC使用STREAM帧,通过QUIC数据包形式展现。TLS握手以CRYPTO帧的形式形成,主要由连续流中的握手数据组成。QUIC旨在并行发送数据包,有时会将不同的消息捆绑成一个消息并加密,因为这些消息具有相同的加密级别。此功能为网络性能提供了极大的优势,同时确保在传输过程中应用正确的加密模式。

3.完全正向保密性

当在用户代理和服务器之间交换临时私钥时,可以实现协议中的完全前向保密性(PFS)。用户代理启动的每个会话都使用新的唯一会话密钥,并且它与先前的会话密钥没有任何关系。通过为每次传输使用单独的会话密钥,即使任何会话密钥被泄露,来自较早或将来会话的任何信息也不会受到破坏。从加密角度来看,没有密钥交换可以提供完美前向保密性。但是,完全正向保密性,一个新术语对PFS的实现提供了可能。

QUIC使用TLS 1.3,该协议支持椭圆曲线(EC)DHE密钥交换或有限字段上的预共享密钥(PSK)和Diffie-Hellman(DH)。0-RTT密钥交换提供了完全的正向保密性,因为加密规范仅接受通过0-RTT握手的前向安全连接。尽管TLS 1.2还支持前向保密性,但从技术上讲,当用户代理发送由只有服务器已知的对称密钥保护的机密资料副本时,正向保密性在会话恢复期间会丢失。该协议甚至为用户代理和服务器之间的初始消息提供了完全的正向保密。此外,由于QUIC协议不支持长期密钥,因此QUIC借助TLS 1.3可以使用其协议层为应用程序提供完全正向保密功能。

4.重放攻击防护

除了随机数,QUIC实现还用于存储密钥派生的客户端值。服务器会识别并拒绝具有相同密钥派生值和随机数的任何重复请求。考虑到用户代理和服务器之间的协议通信开销,这种设计被称为性能噩梦。从理论上讲,该解决方案看似适用,但是在实践中,该协议可能会变得很占内存并导致性能问题。当前的设计不是最好的,但是从协议层面来说,这会防止任何服务器多次接受同一密钥。同样,QUIC在初始步骤中不提供重放保护,而是在服务器初始回复后立即开始保护。QUIC是让初始交易能得到应用程序保护并减少协议所占内存。考虑到Web组件可能会使用从会话密钥派生的密钥,因此在此阶段可能会发生重放攻击。但是,可以在应用程序层面使用预防措施来减轻这种情况。

5.IP欺骗保护

QUIC在握手期间支持地址验证,并且需要签名的地址证明,从而消除了任何IP欺骗攻击。IP地址欺骗问题主要在QUIC中通过广泛利用“源地址令牌”来解决,“源地址令牌”是服务器的经过身份验证的加密块,其中包含用户代理的IP地址和服务器的时间戳。用户代理可以重复使用服务器生成的源地址令牌,除非连接更改、IP地址不在变化。由于源地址令牌用作承载令牌,因此它们可以反复使用,并且可以绕过服务器设置的任何IP地址限制。由于服务器仅响应令牌中的IP地址,因此即使是被盗的cookie或令牌也不会成功进行IP欺骗。

6.防止SSL降级

TLS 1.3可以防止TLS降级攻击,因为该协议规定了所有握手通信的密钥哈希,并且要求握手接收方验证发送的密钥哈希。在握手过程中,任何检测到的对客户端功能的篡改尝试都将导致握手终止并出现错误。此外,检测还涉及用户代理与服务器之间的证书验证消息,包括有关特定连接的所有先前消息的PKCS RSA哈希签名。QUIC中的校验和实现将成功防止TLS降级攻击。

安全挑战

1.0-RTT恢复漏洞

HTTP / 3的最大优势之一是0-RTT恢复,它可以极大地提高连接速度并减少延迟。但是,仅当成功建立了先前的连接,并且当前交易使用在上一次连接期间建立了预共享机密时,这一优势才发挥作用。

0-RTT恢复功能存在一些安全方面的缺点。最常见的攻击媒介之一是重放攻击,当对手重新发送初始数据包时可能会造成这种攻击。在特定的情况下,这可能会迫使服务器认为该请求来自先前已知的客户端。恢复0-RTT的另一个安全缺点是完全前向保密的部分失效。如果对手破坏了令牌,那么他们就可以解密用户代理发送的0-RTT通信内容。

2.连接ID操纵攻击

连接ID操纵攻击要求将攻击者处在用户代理与服务器之间。他们可以在交换客户端和服务器问候消息的初始握手期间操纵连接ID。握手将照常进行,服务器假定已建立连接,但是用户代理将无法解密,因为连接ID需要加密密钥派生过程的输入步骤,并且用户代理和服务器将计算不同的加密键。用户代理最终将超时,并向服务器发送错误消息,告知连接已终止。由于客户端使用原始的加密密钥将错误消息加密到服务器,因此服务器将无法解密,并且将保持连接状态,直到空闲连接超时(通常在10分钟内)到期为止。

当大规模执行时,相同的攻击可能会对服务器造成拒绝服务攻击,并保留多个连接,直到连接状态过期。保持连接有效的另一种攻击方法是更改其他参数,例如源地址令牌,从而防止客户端建立任何连接。

2.UDP放大攻击

为了成功进行放大攻击,攻击者必须欺骗受害者的IP地址,并将UDP请求发送到服务器。如果服务器发回更重要的UDP响应,则攻击者可以大规模利用此服务器行为并创建DDOS攻击情形。

具体来说,在QUIC中,当对手从目标接受地址验证令牌并释放最初用于生成令牌的IP地址时,就会发生UDP放大攻击。攻击者可以使用相同的IP地址将0-RTT连接发送回服务器,该IP地址可能已被改为指向不同的端点。通过执行此设置,攻击者可以潜在地指示服务器向受害服务器发送大量流量。为了防止这种攻击,HTTP / 3具有速率限制功能和短暂的验证令牌,可以充当DDOS攻击的补偿控制,同时部分缓解攻击情形。

3.流量耗尽型攻击

当对手有意启动多个连接流时,就会发生流耗尽攻击,这可能导致端点耗尽。攻击者可以通过反复提交大量请求来利用穷尽序列。尽管特定的传输参数可能会限制并发活动流的数量,但是在某些情况下,可能会故意将服务器配置设置为更高数值。由于服务器的协议配置增加了协议性能,因此受害服务器可能成为此类攻击的目标。

4.连接重置攻击

连接重置攻击主要是向受害者发送无状态重置,从而可能产生类似于TCP重置注入攻击的拒绝服务攻击。如果攻击者可以获得具有特定连接ID的连接生成的重置令牌,则可能存在潜在的攻击媒介。最后,攻击者可以使用生成的令牌重置具有相同连接ID的活动连接,从而使服务器等待连接,直到发生超时为止。如果大规模进行此攻击,则服务器必须大量消耗其资源,以等待连接完成。

5.QUIC版本降级攻击

QUIC数据包保护为通信中的所有数据包(版本协商数据包除外)提供身份验证和加密。版本协商数据包旨在协商用户代理和服务器之间QUIC的版本。该功能可能允许攻击者将版本降级到QUIC的不安全版本。该攻击目前暂时不会发生,因为只有QUIC的一个版本,但是将来需要注意。

6.缺少监视支持

尽管一些用户代理,服务器和信誉良好的网站支持HTTP3 / QUIC,但是许多网络设备(例如反向/正向代理,负载均衡器,Web应用程序防火墙和安全事件监视工具)并不完全支持HTTP / 3。与TCP不同,QUIC连接中不需要套接字,这使得检测主机和恶意连接变得更加困难。恶意攻击者可能能够通过QUIC中继恶意有效载荷并执行数据泄露攻击,并且保持隐身状态,因为大多数检测工具无法检测到QUIC流量。

QUIC的历史

2016年,互联网工程任务组(IETF)开始标准化Google的QUIC,并宣布IETF QUIC成为新HTTP / 3版本的基础。但是,出于性能和安全方面的考虑,IETF QUIC与原始QUIC设计大相径庭。

TCP上的传统Web流量需要三向握手。QUIC使用UDP,由于往返次数减少和发送的数据包减少,因此延迟减少,从而加快了网络流量传输。UDP除了速度更快之外,还具有其他优点,包括连接迁移、改进延迟、拥塞控制和内置加密。根据Google的说法, “与TCP + TLS的1-3次往返相比, QUIC握手通常需要零往返来发送有效负载。” 第一个连接需要一个往返,而随后的连接则不需要任何往返。同样,由于QUIC用于多路复用操作,因此与TCP相比,它在数据包丢失方面做得更好,并且握手速度更快。

Google的QUIC版本现在是gQUIC。从gQUIC进化的HTTP / 3,具备了重大的改进,并得到IETF工作组的贡献和增强。尽管从技术上讲HTTP / 3是完整的应用程序协议,但QUIC指的是基础传输协议,它不限于服务Web流量。UDP是无连接的,不是很可靠。QUIC通过在UDP上添加类似于TCP的堆栈,来添加可靠的连接,并在其之上重新发送具有流控制功能的方式来克服这些限制,同时解决了TCP的行头阻塞问题。

HTTP / 3使用UDP,类似于HTTP / 2使用TCP的方式。每个连接都有几个并行流,这些并行流用于通过单个连接同时传输数据,而不会影响其他流。因此,与TCP不同,为特定的单个流承载数据的丢失数据包只会影响该特定的流。然后,每个流帧都可以在到达时立即分配给该流,因此可以在不丢失任何流的情况下继续在应用程序中重新组合。QUIC的这种连接建立策略是通过加密和传输握手的组合来实现的。

和HTTP/2的比较分析

QUIC旨在通过减轻HTTP/2的数据包丢失和延迟问题来提高性能。虽然HTTP/2对每个数据来源使用单个TCP连接,但这会导致行头阻塞问题。例如,一个请求的对象可能会停滞在另一个遭受丢失的对象之后,直到该对象恢复为止。QUIC通过将HTTP/2的流层向下推送到传输层来解决此问题,从而避免了应用程序层和传输层的问题。HTTP/3还支持多路复用,在与TLS直接集成的同时,提供独立于其他连接请求的请求。尽管HTTP/2和HTTP/3的工作方式相似,但以下是HTTP/2和HTTP/3的一些重要区别。

从网络堆栈的角度来看,HTTP/2广泛使用了符合HTTP标准的TLS 1.2+,底层的TCP充当了传输协议。但是,在HTTP/3中,默认情况下,除了QUIC以外,还使用TLS 1.3,而UDP是传输协议。下图说明了QUIC在网络协议堆栈中的位置。相比之下,以前的版本使用TLS 1.2,并使用TCP的拥塞控制丢失恢复功能,而HTTP/2处理多流功能。

QUIC在网络协议堆栈中的位置

连接ID的优势

TCP连接即利用数据源和目标网络实体(主要是地址和端口)来标识特定连接。但是,QUIC连接使用连接ID,它是64位随机生成的客户端标识符。这项更改对于当前的Web技术非常有利,主要是因为要求它们支持用户的移动性。如果用户从Wi-Fi网络移动到蜂窝网络,则HTTP/2 TCP协议将需要基于当前地址建立新的连接。但是,由于HTTP/3 QUIC协议使用随机连接ID,因此当从蜂窝网络转移到Wi-Fi连接时,HTTP/3上的客户端更改IP地址将继续使用现有的连接ID而不会中断。

从协议的角度来看,连接ID提供了其他好处。服务器和用户代理可以使用连接ID识别原始连接和重传连接,并避免TCP中普遍存在的重传歧义问题。

结论

QUIC已获得多数浏览器的支持。YouTube和Facebook等重要网站已启用该功能,可以更快地加载页面。在撰写本文时,目前只有4%的顶级网站支持QUIC。微软已经宣布,他们将在内核中交付带有通用QUIC库MsQuic的Windows,以支持各种收件箱功能。

QUIC和HTTP/3旨在满足当今互联网网络性能、可靠性和安全性的目标。强制性支持TLS 1.3的安全性得到了显着改善,从而解决了HTTP/2和早期版本的HTTP的弱点。在HTTP/3传输过程中使用端到端加密有助于抵御攻击者和数据聚合者的一些隐私问题。尽管存在一些弱点,但从性能和安全性角度来看,HTTP/3仍将继续发展,不管怎么说都是对HTTP/2的重大改进。

近期热文推荐:

1.1,000+ 道 Java面试题及答案整理(2021最新版)

2.别在再满屏的 if/ else 了,试试策略模式,真香!!

3.卧槽!Java 中的 xx ≠ null 是什么新语法?

4.Spring Boot 2.6 正式发布,一大波新特性。。

5.《Java开发手册(嵩山版)》最新发布,速速下载!

觉得不错,别忘了随手点赞+转发哦!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Synchronized锁升级过程图解

发表于 2021-11-23

这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战

在多线程并发编程中synchronized一直是元老级角色,称呼它为重量级锁。对synchronized进行各种优化之后,为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程。

一、使用synchronized实现同步的基础

Java中的每一个对象都可以作为锁。具体表现以下:

1)对于普通同步方法,锁是当前实例对象。

2)对于静态同步方法,锁是当前类的Class对象。

3) 对于同步方法块,锁是Synchonized括号里配置的对象。

​

二、synchonized在JVM里的实现原理

当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。那么锁到底存在哪里?锁里面会存储什么信息?

​

从JVM规范中可以看到synchonized在JVM里的实现原理,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另外一种方式实现的。但是,方法的同步同样可以使用这两个指令来实现。

​

monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。\

08_synchronized底层原理.jpg

08_synchronized底层原理.jpg

​

三、Java对象头

synchronized用的锁是存在Java对象头里\

image.png

image.png

Java对象头里的Mark Word里默认存储对的HashCode、分代年龄和锁标记位。在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变化为存储以下4种数据。\

image.png

image.png

四、锁的升级与对比

为了减少获得锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态随着竞争情况升级。锁可以升级不能降级,目的是为了提高获得锁和释放锁的效率。

​

4.1、偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

偏向锁升级:当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程再进入和退出同步时不需要进行CAS操作来加锁和解锁,只需要比对一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果是,表示线程已经获得了锁。如果不是,其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还存储线程1的线程ID,那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1不再使用该锁对象,那么将锁状态设置为无锁状态,重新偏向新的线程。\

image.png

image.png

4.2、轻量级锁

轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价大,如果阻塞不久这个锁就被释放,那这个代价有点得不偿失,因此不阻塞这个线程,让它自旋这等待锁释放。

轻量级锁加锁:线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,然后线程尝试使用CAS将对象头中的Mark Work替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

​

轻量级锁升级:如果自旋时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这时轻量级锁就会膨胀为重量级量,重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。

​

image.png

image.png

自旋会消耗CPU,为了避免无用的自旋,一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的抢锁。

​

4.3、锁的优缺点对比

image.png

image.png

\

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

详解Java SPI 机制

发表于 2021-11-23
  1. 什么是 SPI

1. 背景

在面向对象的设计原则中,一般推荐模块之间基于接口编程,通常情况下调用方模块是不会感知到被调用方模块的内部具体实现。一旦代码里面涉及具体实现类,就违反了开闭原则。如果需要替换一种实现,就需要修改代码。

为了实现在模块装配的时候不用在程序里面动态指明,这就需要一种服务发现机制。Java SPI 就是提供了这样一个机制:为某个接口寻找服务实现的机制。这有点类似 IOC 的思想,将装配的控制权移交到了程序之外。

SPI 英文为 Service Provider Interface 字面意思就是:“服务提供者的接口”,我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。

SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。

2. 使用场景

很多框架都使用了 Java 的 SPI 机制,比如:数据库加载驱动,日志接口,以及 dubbo 的扩展实现等等。

3. SPI 和 API 有啥区别

说到 SPI 就不得不说一下 API 了,从广义上来说它们都属于接口,而且很容易混淆。下面先用一张图说明一下:

image.png

一般模块之间都是通过通过接口进行通讯,那我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。

当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API ,这种接口和实现都是放在实现方的。

当接口存在于调用方这边时,就是 SPI ,由接口调用方确定接口规则,然后由不同的厂商去根绝这个规则对这个接口进行实现,从而提供服务,举个通俗易懂的例子:公司 H 是一家科技公司,新设计了一款芯片,然后现在需要量产了,而市面上有好几家芯片制造业公司,这个时候,只要 H 公司指定好了这芯片生产的标准(定义好了接口标准),那么这些合作的芯片公司(服务提供者)就按照标准交付自家特色的芯片(提供不同方案的实现,但是给出来的结果是一样的)。

  1. 实战演示

Spring框架提供的日志服务 SLF4J 其实只是一个日志门面(接口),但是 SLF4J 的具体实现可以有几种,比如:Logback、Log4j、Log4j2 等等,而且还可以切换,在切换日志具体实现的时候我们是不需要更改项目代码的,只需要在 Maven 依赖里面修改一些 pom 依赖就好了。

image.png

这就是依赖 SPI 机制实现的,那我们接下来就实现一个简易版本的日志框架。

1. Service Provider Interface

新建一个 Java 项目 service-provider-interface 目录结构如下:

1
2
3
4
5
6
7
8
9
10
css复制代码├─.idea
└─src
├─META-INF
└─org
└─spi
└─service
├─Logger.java
├─LoggerService.java
├─Main.java
└─MyServicesLoader.java

新建 Logger 接口,这个就是 SPI , 服务提供者接口,后面的服务提供者就要针对这个接口进行实现。

1
2
3
4
5
6
7
java复制代码package org.spi.service;

public interface Logger {
void info(String msg);

void debug(String msg);
}

接下来就是 LoggerService 类,这个主要是为服务使用者(调用方)提供特定功能的。如果存在疑惑的话可以先往后面继续看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
java复制代码package org.spi.service;

import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;

public class LoggerService {
private static final LoggerService SERVICE = new LoggerService();

private final Logger logger;

private final List<Logger> loggerList;

private LoggerService() {
ServiceLoader<Logger> loader = ServiceLoader.load(Logger.class);
List<Logger> list = new ArrayList<>();
for (Logger log : loader) {
list.add(log);
}
// LoggerList 是所有 ServiceProvider
loggerList = list;
if (!list.isEmpty()) {
// Logger 只取一个
logger = list.get(0);
} else {
logger = null;
}
}

public static LoggerService getService() {
return SERVICE;
}

public void info(String msg) {
if (logger == null) {
System.out.println("info 中没有发现 Logger 服务提供者");
} else {
logger.info(msg);
}
}

public void debug(String msg) {
if (loggerList.isEmpty()) {
System.out.println("debug 中没有发现 Logger 服务提供者");
}
loggerList.forEach(log -> log.debug(msg));
}
}

新建 Main 类(服务使用者,调用方),启动程序查看结果。

1
2
3
4
5
6
7
8
9
10
java复制代码package org.spi.service;

public class Main {
public static void main(String[] args) {
LoggerService service = LoggerService.getService();

service.info("Hello SPI");
service.debug("Hello SPI");
}
}

程序结果:

info 中没有发现 Logger 服务提供者

debug 中没有发现 Logger 服务提供者

将整个程序直接打包成 jar 包,可以直接通过 IDEA 将项目打包成一个 jar 包。

image.png

image.png

image.png

image.png

image.png

image.png

2. Service Provider

接下来新建一个项目用来实现 Logger 接口

新建项目 service-provider 目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
vbnet复制代码├─.idea
├─lib
│ └─service-provider-interface.jar
└─src
├─META-INF
│ └─services
│ └─org.spi.service.Logger
└─org
└─spi
└─provider
└─Logback.java

新建 Logback 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码package org.spi.provider;

import org.spi.service.Logger;

public class Logback implements Logger {

@Override
public void info(String msg) {
System.out.println("Logback info 的输出:" + msg);
}

@Override
public void debug(String msg) {
System.out.println("Logback debug 的输出:" + msg);
}
}

将 service-provider-interface 的 jar 导入项目中。
新建 lib 目录,然后将 jar 包拷贝过来,再添加到项目中。

image.png
再点击 OK 。
image.png

接下来就可以在项目中导入 jar 包里面的一些类和方法了,就像 JDK 工具类导包一样的。

实现 Logger 接口,在 src 目录下新建 META-INF/services 文件夹,然后新建文件 org.spi.service.Logger (SPI 的全类名),文件里面的内容是:org.spi.provider.Logback (Logback 的全类名,即 SPI 的实现类的包名 + 类名)。

这是 JDK SPI 机制 ServiceLoader 约定好的标准

接下来同样将 service-provider 项目打包成 jar 包,这个 jar 包就是服务提供方的实现。通常我们导入 maven 的 pom 依赖就有点类似这种,只不过我们现在没有将这个 jar 包发布到 maven 公共仓库中,所以在需要使用的地方只能手动的添加到项目中。

3. 效果展示

接下来再回到 service-provider-interface 项目。

导入 service-provider jar 包,重新运行 Main 方法。
运行结果如下:

Logback info 的输出:Hello SPI

Logback debug 的输出:Hello SPI

说明导入 jar 包中的实现类生效了。

通过使用 SPI 机制,可以看出 服务(LoggerService)和 服务提供者两者之间的耦合度非常低,如果需要替换一种实现(将 Logback 换成另外一种实现),只需要换一个 jar 包即可。这不就是 SLF4J 原理吗?

如果某一天需求变更了,此时需要将日志输出到消息队列,或者做一些别的操作,这个时候完全不需要更改 Logback 的实现,只需要新增一个 服务实现(service-provider)可以通过在本项目里面新增实现也可以从外部引入新的服务实现 jar 包。我们可以在服务(LoggerService)中选择一个具体的 服务实现(service-provider) 来完成我们需要的操作。

loggerList.forEach(log -> log.debug(msg));

或者

loggerList.get(1).debug(msg);

loggerList.get(2).debug(msg);

这里需要先理解一点:ServiceLoader 在加载具体的 服务实现 的时候会去扫描所有包下 src 目录的 META-INF/services 的内容,然后通过反射去生成对应的对象,保存在一个 list 列表里面,所以可以通过迭代或者遍历的方式得到你需要的那个 服务实现。

  1. ServiceLoader

想要使用 Java 的 SPI 机制是需要依赖 ServiceLoader 来实现的,那么我们接下来看看 ServiceLoader 具体是怎么做的:

ServiceLoader 是 JDK 提供的一个工具类, 位于package java.util;包下。

1
css复制代码A facility to load implementations of a service.

这是 JDK 官方给的注释:一种加载服务实现的工具。

再往下看,我们发现这个类是一个 final 类型的,所以是不可被继承修改,同时它实现了 Iterable 接口。之所以实现了迭代器,是为了方便后续我们能够通过迭代的方式得到对应的服务实现。

1
java复制代码public final class ServiceLoader<S> implements Iterable<S>{ xxx...}

可以看到一个熟悉的常量定义:

private static final String PREFIX = "META-INF/services/";

下面是 load 方法:可以发现 load 方法支持两种重载后的入参;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}

public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader) {
return new ServiceLoader<>(service, loader);
}

private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}

public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}

根据代码的调用顺序,在 reload() 方法中是通过一个内部类 LazyIterator 实现的。先继续往下面看。

ServiceLoader 实现了 Iterable 接口的方法后,具有了迭代的能力,在这个 iterator 方法被调用时,首先会在 ServiceLoader 的 Provider 缓存中进行查找,如果缓存中没有命中那么则在 LazyIterator 中进行查找。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码
public Iterator<S> iterator() {
return new Iterator<S>() {

Iterator<Map.Entry<String, S>> knownProviders
= providers.entrySet().iterator();

public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext(); // 调用 LazyIterator
}

public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next(); // 调用 LazyIterator
}

public void remove() {
throw new UnsupportedOperationException();
}

};
}

在调用 LazyIterator 时,具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
java复制代码
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() {
return hasNextService();
}
};
return AccessController.doPrivileged(action, acc);
}
}

private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//通过PREFIX(META-INF/services/)和类名 获取对应的配置文件,得到具体的实现类
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}


public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() {
return nextService();
}
};
return AccessController.doPrivileged(action, acc);
}
}

private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
  1. 总结

其实不难发现,SPI 机制的具体实现本质上还是通过反射完成的。即:我们按照规定将要暴露对外使用的具体实现类在 META-INF/services/ 文件下声明。

image.png

其实 SPI 机制在很多框架中都有应用:Spring 框架的基本原理也是类似的反射。还有 dubbo 框架提供同样的 SPI 扩展机制。
cicada8-spi.md---0082zybply1gc6ue6zubvj30gq0pymyq.jpg

通过 SPI 机制能够大大地提高接口设计的灵活性,但是 SPI 机制也存在一些缺点,比如:

  1. 遍历加载所有的实现类,这样效率还是相对较低的;
  2. 当多个 ServiceLoader 同时 load 时,会有并发问题。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

netty(十二)初识Netty - ByteBuf 内存回

发表于 2021-11-23

「这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战」。

不论我们在前面学习NIO的ByteBuffer,还是现在Netty当中的ByteBuf,其都有使用直接内存的方式。

在Netty当中,我们使用完直接内存,需要去手动进行释放,而不应该等待GC去进行回收,以减少发生内存溢出的风险。

一、ByteBuf的种类

关于其种类,有很多种,我们根据前面提到的池化机制,将其主要分为两大类,每一类当当中又分为堆内存和直接内存:

  • UnpooledHeapByteBuf:非池化堆内存ByteBuf,受JVM内存管理,可以等待GC回收。
  • UnpooledDirectByteBuf:非池化直接内存ByteBuf,不收JVM管理,虽然可以受GC回收,但不是及时的,可能会发生内存溢出,需要手动进行回收。
  • PooledByteBuf:池化ByteBuf,这种有更复杂的回收范式,后面通过源码分析,具体查看其实现细节。
    • PooledHeapByteBuf:池化堆内存ByteBuf
    • PooledDirectByteBuf:池化直接内存ByteBuf

二、直接内存回收原理

在前面的文章中,我们简单聊到过ByteBuf的结构:

public abstract class ByteBuf implements ReferenceCounted

如上所示,其实现了ReferenceCounted的接口,接口翻译过来叫做“引用计数”。

相信学过jvm GC的同学应该有所了解“引用计数法”,当一个对象有引用时,我们就对计数器加1,反之就减1,但是引用计数法无法处理环形垃圾,所以后面提出了“根可达算法”,简单提一下,需要了解细节的朋友可以看我的专题【JVM】。

此处的引用计数,用于ByteBuf的直接内存回收,我们看下其主要的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
csharp复制代码public interface ReferenceCounted {
/**
* 返回当前对象的引用计数
*/
int refCnt();

/**
* 将引用计数增加1
*/
ReferenceCounted retain();

/**
* 按指定的increment增加引用计数
*/
ReferenceCounted retain(int increment);

/**
* 将引用计数减少1,并在引用计数达到0解除分配此对象
*/
boolean release();

/**
* 将引用计数减少指定的decrement ,如果引用计数达到0则取消分配此对象。
*/
boolean release(int decrement);
}

所有的ByteBuf都会实现这个接口,当一个新的ReferenceCounted被实例化时,它以1的引用计数开始。 retain()增加引用计数,而release()减少引用计数。 如果引用计数减少到0 ,对象将被释放,并且访问释放的对象通常会导致访问冲突。

通过下面的代码简单试用一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
csharp复制代码    public static void main(String[] args) {

ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer();

//打印当前的引用计数
System.out.println("初始化后的引用计数" + byteBuf.refCnt());

//释放引用计数
byteBuf.release();
//打印当前的引用计数
System.out.println("释放后的引用计数" + byteBuf.refCnt());

//调用byteBuf
try {
byteBuf.writeInt(888);
} catch (Exception e) {
System.out.println("释放后调用异常:" + e);
}

//增加引用计数
try {
byteBuf.retain();
} catch (Exception e) {
System.out.println("释放后增加引用计数异常:" + e);
}

// 重新分配
byteBuf = ByteBufAllocator.DEFAULT.buffer();
//调用byteBuf
byteBuf.writeInt(888);
System.out.println("重新分配后的引用计数" + byteBuf.refCnt());
}

结果:

1
2
3
4
5
yaml复制代码初始化后的引用计数1
释放后的引用计数0
释放后调用异常:io.netty.util.IllegalReferenceCountException: refCnt: 0
释放后增加引用计数异常:io.netty.util.IllegalReferenceCountException: refCnt: 0, increment: 1
重新分配后的引用计数1

当引用计数变为0后,整个内存就释放了,再次使用会抛出异常,重新尝试增加引用计数也会跑出异常,只能进行重新分配。

三、内存释放使用方式

3.1 手动释放

前面简单了解了关于内存释放的内容,那么我们应该如何使用呢?是不是可以向我们习惯的java代码一样,在finally当中调用呢?

1
2
3
4
5
csharp复制代码try {

} finally {
byteBuf.release();
}

直接给出结论,是不行的。

前面我们介绍时候就说过,会有几率造成内存溢出的,即使不会发生也会造成内存的浪费。

前面的文章当中,我们学习了Pipeline和Handler。通常我们会将一个byteBuf传递给另一个channelHandler去处理,是存在一个传递性的。这里面存在两种情况:

  • 假设一共有5个channelHandler,在第二个当中,将byteBuf转换成了java对象,然后将对象传递给第三个channelHandler,此时byteBuf就没有用了,所以此时就应该释放。
  • 一直以byteBuf传递,直到最后一个channelHandler才进行释放。

总结一句话:最后谁用完了,谁就负责释放。

建议:如果确定这个buf在最后时刻用完了,而又无法确定当前有多少个引用计数,使用如下两种方式释放:

  • 循环调用release(),知道返回true。
  • 通过refCnt()获取当前的引用计数,然后调用release(int refCnt)释放。

3.2 tail和head自动释放

还记得前面将Pipeline和Handler时,提到了关于head和tail的概念,除了我们自己添加的Handler以外,会默认有一个头和尾的处理器。

在这两个处理器当中,也会有自动回收内存的保底能力,但是前提是要求我们将byteBuf传递到head或tail当中才行,对于中途就转换类型的,仍然需要我们自己去释放资源。

前面我们还学习过入站处理器和出栈处理器,其中入站处理器传递内容需要使用channelRead()方法,而在出站处理器传递参数需要使用write方法,这将作为我们跟踪代码的标记。

下面我们简单跟踪下源码,看看是如何实现的内存释放。
我们跟踪pipeline的addLast方法,跟踪到了AbstractChannelHandlerContext这个抽象类,其有两个实现类:

image.png

刚好对应我们的head和tail处理器。

3.2.1 TailContext

首先看tail处理器,实现了ChannelInboundHandler,即入站处理器,进行入站首尾工作。

1
scala复制代码final class TailContext extends AbstractChannelHandlerContext implements ChannelInboundHandler

找到channelRead方法:

1
2
3
typescript复制代码       public void channelRead(ChannelHandlerContext ctx, Object msg) {
DefaultChannelPipeline.this.onUnhandledInboundMessage(ctx, msg);
}

继续跟踪onUnhandledInboundMessage

1
2
3
4
5
6
7
8
typescript复制代码    protected void onUnhandledInboundMessage(Object msg) {
try {
logger.debug("Discarded inbound message {} that reached at the tail of the pipeline. Please check your pipeline configuration.", msg);
} finally {
ReferenceCountUtil.release(msg);
}

}

发现其中的引用计数工具类,调用了release方法:

ReferenceCountUtil.release(msg);

判断msg是否是实现了ReferenceCounted ?是就进行是否,否则返回false。

1
2
3
typescript复制代码    public static boolean release(Object msg) {
return msg instanceof ReferenceCounted ? ((ReferenceCounted)msg).release() : false;
}

3.2.1 HeadContext

查看HeadContext,实现了ChannelOutboundHandler,即出站处理器,进行出站首尾工作。

1
scala复制代码final class HeadContext extends AbstractChannelHandlerContext implements ChannelOutboundHandler, ChannelInboundHandler

找到其write方法:

1
2
3
arduino复制代码        public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
this.unsafe.write(msg, promise);
}

继续跟踪write:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
arduino复制代码        public final void write(Object msg, ChannelPromise promise) {
this.assertEventLoop();
ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
if (outboundBuffer == null) {
this.safeSetFailure(promise, this.newClosedChannelException(AbstractChannel.this.initialCloseCause));
ReferenceCountUtil.release(msg);
} else {
int size;
try {
msg = AbstractChannel.this.filterOutboundMessage(msg);
size = AbstractChannel.this.pipeline.estimatorHandle().size(msg);
if (size < 0) {
size = 0;
}
} catch (Throwable var6) {
this.safeSetFailure(promise, var6);
ReferenceCountUtil.release(msg);
return;
}

outboundBuffer.addMessage(msg, size, promise);
}
}

在上面的代码中,仍然发现了

ReferenceCountUtil.release(msg)

其他代码此文暂时不做讲解了。

无论是head,还是tail,都需要将buf传递过来,才能进行释放。


本文暂时介绍这些,后面继续,有帮助的话点个赞吧。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

“敏捷版”全链路压测 客户的故事 全链路压测 场景梳理 测试

发表于 2021-11-23

简介: PTS 结合 10 多年来阿里的全链路压测的经验,让阿里云的用户可以如同享用满汉全席般的享用全套标准的全链路压测,也可以根据自己的需求,选择最适合自己的方式。

作者:子矜

客户的故事

全链路压测被誉为大促备战的 “核武器” ,如果之前有关注过阿里双 11 相关的技术总结,对 “全链路压测” 一定不会陌生,这个词的出场率几乎 100%。从对双 11 稳定性的价值来看,用 “核武器” 来形容全链路压测毫不为过。

在某知名电商大促中,该电商平台也想用全链路压测来为自己的大促提前排除风险。但是他遇到几个困难:

  1. 全链路压测是一个需要多角色参与的活动:业务方,测试,运维,研发,数据库,都需要参与进来。然而能够像阿里具备成熟的组织体系,可以强有力的推动各种不同的角色,都是需要较长时间来积累的。
  2. 全链路压测,常常涉及到框架的改造:而该电商平台的业务复杂,做结构梳理与业务改造并不现实。

那这个知名电商平台,有什么办法可以在 1 个星期之内,不进行业务改造,不改变业务部署,就能够用上全链路压测呢?

接下来的内容,我们会从全链路压测的原理开始,并引入基于同样原理的 “敏捷版” 全链路压测,让该知名电商平台能够在 2 周之内就能用上全链路压测的方案。

全链路压测

首先,我们来看看阿里的全链路压测,到底解决了什么问题:

全链路压测实际上解决的问题是:在线上的压测。线上压测,能够最快、最直接的发现线上的问题。然而,线上压测会带来数据污染的问题:如何把压测数据和真实数据区分开来,是压测里至关重要的一点。那么,阿里是怎么做的呢?我们一起来看下图:

阿里的全链路压测具有一套成熟又复杂的系统:压测的梳理、构建、准备、发送。然而,这套体系对于一个云上的用户是需要长期建设得到的。那我们如何能够让用户快速,敏捷的享受这套技术呢?

在这里,PTS 把整个流程进行沉淀,都以标准化的输出来提供给云上的用户。用户可以直接享用一整套的全链路压测体系,也可以在压测的关键环节:例如场景梳理、请求构建、压测环境、压测等步骤中,根据自己的需求来定制自己想要的压测效果。

场景梳理

业务场景,即对应的是压测的输入请求。这是压测第一步,也是最重要的一步。最常见的是把涉及到业务的 URL 进行梳理,汇总。例如下图就是一个常见的场景汇总:

然而,这是不够的。当若干个 URL 汇总成一个场景之后,URL 之间的比例、时间间隔,也是影响业务场景的关键。用常见的场景打一个比方:一个用户的下单,可能背后蕴含着 10 个用户登录,每个用户平均浏览了 4 个商品,每个商品中平均被浏览了 5 个评价,最后一个用户在 10 点大促开始的时候,购买了一个商品。

这些 URL 之间的关系、时间点,需要人员有丰富的业务知识才能梳理清楚。为此,PTS 提供服务端流量录制的功能,方便用户来录制流量,并且轻松的得到其中不同维度的比例关系:

如上图所示,用户可以清晰的得到 URL 之间的比例关系、用户 URL 之间的时间行为等等。基于这个梳理好的数据模型,用户可以在这个基础上进行裁剪。

测试数据构造

接下来,就是构造用户数据了。这一步涉及的角色最多,也最为繁琐。整个数据构造由三个步骤构成,如下图所示:

首先是数据发现。通常,我们可以通过人工业务梳理,得到该业务所涉及到的所有表,并进行分析。PTS 为免除这个烦恼,和DMS打通,提供表结构预览,让测试人员方便的看清楚和场景相关联的结构,大大的提升效率。

如果还是觉得太复杂,PTS将提供数据录制工具,安装了这个 agent 之后,该业务所涉及的表,都会被完整的记录下来:

有了这些工具,测试人员就可以无须 DBA 的协助,轻松的得到场景关联的表信息了。

数据闭包

有了这些数据表,并且在这基础之上分析出来数据闭包后,我们可以开始制作压测数据了。通常,我们制作影子表的方式有三种:

  1. 影子库 – 全量的进行影子库映射。该方法的优势是简单,劣势是消耗资源多;
  2. 影子表 – 将表闭包里的表,通过一定规则,进行名字关联。该方法的优势是节省资源,劣势是需要对表进行充分梳理,并且一一对应;
  3. 不新建表,在同一张表内,将影子数据进行大位移偏移。这个将在后面的敏捷版内进行介绍。

这三种方式可以根据需求组合使用。

数据导入/混扰

有了这些前提之后,我们可以利用 DMS 来数据导入,进行数据制作了。

到这里,我们完成了全链路压测中最复杂的两个步骤:压测场景梳理、压测数据制作。

接下来我们通过数据加工,把这两个元素最终加工为压测数据。

数据加工

此时,我们对压测数据做最后一个步骤,进行数据加工。即我们把业务场景、压测数据,按照我们的业务模型进行最后的调整与加工:

到这里,我们可以看到,全链路压测的压测请求,都已经成型了。接下来,我们可以开始设计压测流量在压测对象中的行为了。

测试环境

压测可以在仿真环境、线上环境中进行。不同的环境,选取数据,制造数据都有不同的考量。如下图所示:

简单的说,测试环境关注的是单个组件:例如微服务、接口、但协议(SQL,Redis)等压测;预发环境(通常是VPC环境)则关注链路整合;生产环境则最逼近真实场景。在这里,我们只讨论线上生产环境。

传统全链路压测

下图简单的诠释了传统全链路压测的运作方式;

我们看到,传统的全链路压测,主要通过流量打标,来区分压测流量和真实流量,做到这一点,需要保证这个压测标能够被层层的透传下去。而当流量到了 “写” 的这层,部署好的 agent 根据压测标,来决定 “写” 的行为,是写到真实的数据库呢?还是写到影子区域?道理很简单,但是实施的时候还是会碰到不少的难点。其中,主要涉及的问题是:

  1. 如果应用使用到的框架不标准,则需要进行适配;
  2. 推动开发安装 agent 的流程复杂;
  3. 验证 agent 的覆盖面复杂。

敏捷版的全链路压测

如果我们不想要改造业务,也不想要挂载 agent,我们能如何去做到这一点呢?

我们来看一下抽样测试的原理。在测试的时候,通常有一种手段,即通过选取几个特定的真实用户数据来进行测试,来验证程序的正确性;如果我们把这些真实用户数据,变成假用户,那么需要满足下面这个关键条件:假用户以及假用户在这个业务场景下涉及到的业务数据,以及业务场景下相关的数据,都能够被识别出来。

例如,我们模拟一个假用户,购买某个假商品,这里的用户,商品,都能够有一个特定的特征,这个假用户生成的浏览记录、购买记录,在数据库的表现中都有该用户的 ID;在这个前提下,我们是能够把脏数据从真实数据中识别出来的;

这种压测,需要盘点出以下两点:

  1. 完整的找出业务涉及到的数据表 – 参考上一章节里面的PTS SQL录制功能;
  2. 制作影子数据 – 和传统全链路压测不一样,这里我们选取的是第三种方式,即在一张表里做大位移,而不是制作影子表或者影子库。压测结束后,根据影子数据的特征,巡检数据库并且进行清理;

这种方式,是基于使用者对业务有清晰的了解,制作出来的压测数据有明显的压测标识(比正常数据大的多的偏移量),所有涉及的写压测,都带有这些偏移量;这样,所有压测产生的数据,都能够被识别出来。压测结束之后,根据这个数据特征,来清理压测数据;

流量引擎的选择

为了更好的模拟用户的行为,我们常常会使用压测地域的定制。但是把压测引擎部署到全国各地是不现实的;而PTS 可以方便的让用户选择地域的发起,如下图所示:

总结

PTS 结合 10 多年来阿里的全链路压测的经验,让阿里云的用户可以如同享用满汉全席般的享用全套标准的全链路压测,也可以根据自己的需求,选择最适合自己的方式。

原文链接

本文为阿里云原创内容,未经允许不得转载。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Redis 主从架构之原理篇(一)

发表于 2021-11-23

「这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战」。

前边我们已经讲解了 redis 的相关配置,接下来我们把上边提到的概念串起来,聊一下主从复制的相关原理。

系统的运行依靠三个主要的机制

  • 当一个 master 实例和一个 replica 实例连接正常时, master 会发送一连串的命令流来保持对 replica 的更新,以便于将自身数据集的改变复制给 replica ,包括客户端的写入、key 的过期或被逐出等等。
  • 当 master 和 replica 之间的连接断开之后,因为网络问题、或者是主从意识到连接超时, replica 重新连接上 master 并会尝试进行部分重同步。这意味着它会尝试只获取在断开连接期间内丢失的命令流。
  • 当无法进行部分重同步时, replica 会请求进行全量重同步。这会涉及到一个更复杂的过程,例如 master 需要创建所有数据的快照,将之发送给 replica ,之后在数据集更改时持续发送命令流到 replica 。

Redis 复制功能是如何工作的

每一个 Redis master 都有一个 replication ID :这是一个较大的伪随机字符串,标记了一个给定的数据集。每个 master 也持有一个偏移量,master 将自己产生的复制流发送给 replica 时,发送多少个字节的数据,自身的偏移量就会增加多少,目的是当有新的操作修改自己的数据集时,它可以以此更新 replica 的状态。

复制偏移量即使在没有一个 replica 连接到 master 时,也会自增,所以基本上每一对给定的 Replication ID, offset 都会标识一个 master 数据集的确切版本。

当 replica 连接到 master 时,它使用 PSYNC 命令来发送它记录的旧的 master replication ID 和它至今为止处理的偏移量。通过这种方式, master 能够仅发送 replica 所需的增量部分。但是如果 master 的缓冲区中没有足够的 backlog 或者 replica 引用了 master 不知道的历史记录(replication ID),则会转而进行一个全量重同步:在这种情况下, replica 会得到一个完整的数据集副本,从头开始。

说到这儿,那什么是全量同步,那什么又是增量同步呢?我们在下文中讲解。如果你有不同的意见或者更好的idea,欢迎联系阿Q,添加阿Q可以加入技术交流群参与讨论呦!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

单值二叉树<难度系数⭐>

发表于 2021-11-23

「这是我参与11月更文挑战的第22天,活动详情查看:2021最后一次更文挑战」

1、单值二叉树<难度系数⭐>

📝 题述:如果二叉树每个节点都具有相同的值,那么该二叉树就是单值二叉树。只有给定的树是单值二叉树时,才返回 true;否则返回 false。

💨 示例 1:

输入:[1,1,1,1,1,null,1]

输出:true
在这里插入图片描述

💨 示例 2:

输入:[2,2,2,5,2]

输出:false

在这里插入图片描述

⚠ 注意:

1️⃣ 给定树的节点数范围是 [1, 100]。

2️⃣ 每个节点的值都是整数,范围为 [0, 99] 。

🧷 平台:Visual studio 2017 && windows

🔑 核心思想:

在数学中大家都知道等号具有传递性,如 a = b , b = c,那么 a = c 。

那么这里

在这里插入图片描述

a == b && a == c

b == d && b == e

… …

在每层栈帧中:如果那个节点为空返回 true;判断左右子树与根,如果不相等返回 false ;相等继续递归

❗ 官方(仅供参考) ❕

方法一:深度优先搜索

  深度优先搜索,获取这颗树中的所有节点的值。然后,就可以判断所有节点的值都相等了。

方法二:递归

  一颗树是单值的,当且仅当根节点的子节点所在的子树也是单值的,同时根节点的值与子节点的值相同。这里使用递归实现这个判断的过程。left_correct 表示当前节点的左孩子是正确的,也就是说:左孩子所在的子树是单值的,并且当前节点的值等于左孩子的值。right_correct 对当前节点的右孩子表示同样的事情。递归处理之后,当根节点的这两种属性都为真的时候,我们就可以判定这颗二叉树是单值的。

leetcode原题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
c复制代码#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>

//定义结构体类型
typedef int BTDataType;
typedef struct TreeNode
{
int val;//值
struct TreeNode *left;//左子树
struct TreeNode *right;//右子树
}BTNode;
//单值二叉树
bool isUnivalTree(struct TreeNode* root) {
//root为空就返回真
if (root == NULL)
{
return true;
}
//左树不为空且左树不等于根val,返回假
if (root->left && root->left->val != root->val)
{
return false;
}
//右树不为空且右树不等于根val,返回假
if (root->right && root->right->val != root->val)
{
return false;
}
//递归
return isUnivalTree(root->left) && isUnivalTree(root->right);
}
//malloc空间
BTNode* BuyNode(BTDataType x)
{
BTNode* node = malloc(sizeof(BTNode));
//初始化
node->val = x;
node->left = NULL;
node->right = NULL;

return node;
}
//创建树
BTNode* CreatBinaryTree()
{
BTNode* node1 = BuyNode('A');
BTNode* node2 = BuyNode('A');
BTNode* node3 = BuyNode('A');
BTNode* node4 = BuyNode('A');
BTNode* node5 = BuyNode('A');
BTNode* node6 = BuyNode('A');

node1->left = node2;
node1->right = node3;
node2->left = node4;
node2->right = node5;
node3->right = node6;

return node1;
}

int main()
{
BTNode* root = CreatBinaryTree();
printf("%d\n", isUnivalTree(root));
return 0;
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringloC容器的依赖注入源码解析(7)—— doCr

发表于 2021-11-23

前置文章:

doCreateBean之创建无属性的bean

doCreateBean之处理@Autowired以及@Value标签


doCreateBean完整源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
java复制代码protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException {

// Instantiate the bean.
// bean实例包装类
BeanWrapper instanceWrapper = null;
if (mbd.isSingleton()) {
// 从未完成创建的包装Bean缓存中清理并获取相关中的包装Bean实例,毕竟是单例的,只能存一份
instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
}
if (instanceWrapper == null) {
//创建bean的时候,这里创建bean的实例有三种方法
// 1.工厂方法创建
// 2.构造方法的方式注入
// 3.无参构造方法注入
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
final Object bean = instanceWrapper.getWrappedInstance();
Class<?> beanType = instanceWrapper.getWrappedClass();
if (beanType != NullBean.class) {
mbd.resolvedTargetType = beanType;
}

// Allow post-processors to modify the merged bean definition.
// 调用BeanDefinition属性合并完成后的BeanPostProcessor后置处理器
synchronized (mbd.postProcessingLock) {
if (!mbd.postProcessed) {
try {
// 被@Autowired、@Value标记的属性在这里获取
applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
}
catch (Throwable ex) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"Post-processing of merged bean definition failed", ex);
}
mbd.postProcessed = true;
}
}

// Eagerly cache singletons to be able to resolve circular references
// even when triggered by lifecycle interfaces like BeanFactoryAware.
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
if (logger.isTraceEnabled()) {
logger.trace("Eagerly caching bean '" + beanName +
"' to allow for resolving potential circular references");
}
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

// Initialize the bean instance.
Object exposedObject = bean;
try {
populateBean(beanName, mbd, instanceWrapper);
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
catch (Throwable ex) {
if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {
throw (BeanCreationException) ex;
}
else {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);
}
}

if (earlySingletonExposure) {
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
}
else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
String[] dependentBeans = getDependentBeans(beanName);
Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
for (String dependentBean : dependentBeans) {
if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
actualDependentBeans.add(dependentBean);
}
}
if (!actualDependentBeans.isEmpty()) {
throw new BeanCurrentlyInCreationException(beanName,
"Bean with name '" + beanName + "' has been injected into other beans [" +
StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
"] in its raw version as part of a circular reference, but has eventually been " +
"wrapped. This means that said other beans do not use the final version of the " +
"bean. This is often the result of over-eager type matching - consider using " +
"'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
}
}
}
}

// Register bean as disposable.
try {
registerDisposableBeanIfNecessary(beanName, bean, mbd);
}
catch (BeanDefinitionValidationException ex) {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);
}

return exposedObject;
}

接上文分析到的位置,接下来执行:

1
2
3
4
5
6
7
8
java复制代码// 向容器中缓存单例模式的Bean对象,以防循环引用
// 判断是否是早期引用的bean,如果是,则允许其提前暴露引用
// 这里判断的逻辑主要有三个:
// 1.是否为单例
// 2.是否允许循环引用
// 3.是否是在创建中的bean
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));

之后会来到

1
2
java复制代码// 这里是一个匿名内部类,为了防止循环引用,尽早持有对象的引用
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

进入到addSingletonFactory:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(singletonFactory, "Singleton factory must not be null");
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
// 往三级缓存里添加
this.singletonFactories.put(beanName, singletonFactory);
// 消除此Bean在二级缓存里的缓存信息
this.earlySingletonObjects.remove(beanName);
// 这里为了记录注册单例的顺序
this.registeredSingletons.add(beanName);
}
}
}

消除此Bean在二级缓存里的缓存信息,将其包装成singletonFactory实例往三级缓存里添加

这里有一个重点就是Spring解决循环依赖的真相就在这一段源码中:在这里beanFactory被put进了singletonFactories,此时的bean只是完成了初始化构造的bean,还没有进行set或者注解注入的bean,是bean的一个中间状态,但是已经能被人认出来了,所以Spring此时将这个对象提前曝光出来让大家认识、使用。


回到doCreateBean,进入到getEarlyBeanReference

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
// 如果是SmartInstantiationAwareBeanPostProcessor类型,就进行处理,
// 如果没有相关处理内容,就返回默认的实例。
// 里面的AbstractAutoProxyCreator类是后续AOP的关键
if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
// 对单例进行AOP包装并返回的地方!
exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
}
}
}
return exposedObject;
}

注意这个方法并不是在doCreateBean的

1
java复制代码addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

这一行执行的,这里只是将getEarlyBeanReference注册进去,实际执行的地方是前面尝试从缓存里获取bean的地方,即AbstractBeanFactory的doCreateBean方法里。


回到doCreateBean,此时获取到了bean,并且只在三级缓存里面保存。
之后会执行

1
java复制代码populateBean(beanName, mbd, instanceWrapper);

来将bean的属性真正注入到里面,然后再调用initializeBean进行彻底的初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码// Initialize the bean instance.
// Bean对象的初始化,依赖注入在此触发
// 这个exposedObject在初始化完成之后返回作为依赖注入完成后的Bean
Object exposedObject = bean;
try {
// 填充bean实例的属性
populateBean(beanName, mbd, instanceWrapper);

// 初始化bean,过程如下:
// 1. 判断是否实现了BeanNameAware, BeanClassLoaderAware, BeanFactoryAware方法,如果有,则设置相关的属性
// 2. 调用bean初始化的前置(BeanPostProcessor)操作
// 3. 执行初始化的方法
// 如果有initializingBean,则调用afterPropertiesSet
// 如果有InitMethod,则调用初始方法
// 4. 调用bean初始化的后置(BeanPostProcessor)操作
exposedObject = initializeBean(beanName, exposedObject, mbd);
}

之后判断该bean是否允许提前暴露,

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码if (earlySingletonExposure) {
// 获取指定名称的已注册的单例模式Bean对象
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
// 如果经过initializeBean执行后返回的bean还是同一个(不是代理对象实例,即没有被增强)
if (exposedObject == bean) {
// 确保根据名称获取到的的已注册的Bean和正在实例化的Bean是同一个
exposedObject = earlySingletonReference;
}
......
}
}

if 里面调用了getSingleton方法,进入该方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}

和之前提到的是同一个方法,由于这时bean已经被放到三级缓存里了,所以会被从三级缓存里取出

1
java复制代码singletonObject = singletonFactory.getObject();

获取到bean实例之后会将其从三级缓存里移除

1
java复制代码this.singletonFactories.remove(beanName);

回到doCreateBean,

1
java复制代码Object earlySingletonReference = getSingleton(beanName, false);

执行完毕之后再判断一下

1
2
3
4
java复制代码if (exposedObject == bean) {
// 确保根据名称获取到的的已注册的Bean和正在实例化的Bean是同一个
exposedObject = earlySingletonReference;
}

exposedObject由于之前调用了initializeBean方法,所以有可能会被赋值为全新的对象,一旦被赋值了全新的对象之后if里面是false,进入到前面的initializeBean方法里
请添加图片描述
重点关注applyBeanPostProcessorsAfterInitialization方法,进入到方法里:
请添加图片描述
重点关注实现了postProcessAfterInitialization方法的AbstractAutoProxyCreator类

1
2
3
4
5
6
7
8
9
10
java复制代码@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
if (bean != null) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
if (this.earlyProxyReferences.remove(cacheKey) != bean) {
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}

该方法尝试从缓存里获取bean实例并返回,即如果先前加工过的话就返回bean实例,确保加工过的bean实例和先前是同一个。


回到doCreateBean,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码if (exposedObject == bean) {
// 确保根据名称获取到的的已注册的Bean和正在实例化的Bean是同一个
exposedObject = earlySingletonReference;
}else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
String[] dependentBeans = getDependentBeans(beanName);
Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
// 获取依赖于当前Bean的Bean实例
for (String dependentBean : dependentBeans) {
if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
actualDependentBeans.add(dependentBean);
}
}
if (!actualDependentBeans.isEmpty()) {
throw new BeanCurrentlyInCreationException(beanName,
"Bean with name '" + beanName + "' has been injected into other beans [" +
StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
"] in its raw version as part of a circular reference, but has eventually been " +
"wrapped. This means that said other beans do not use the final version of the " +
"bean. This is often the result of over-eager type matching - consider using " +
"'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
}
}

如果不是同一个bean,则有可能经过initializeBean之后没有返回同样的bean实例,之所以会出现这个问题是因为postProcessor也是供用户使用的,用户可能在自定义逻辑里修改了bean实例,用户的行为对于系统来说是无法预测的。

不一样的话就会检查有没有别的bean依赖于这个bean,如果存在则直接报错,因为既然执行到这里其他bean实例就已经执行过了populate方法将该bean的实例设置上了,然而设置上了之后却又创建出了该bean的新实例,于是就不满足单例。

通过验证之后会注册销毁时候的回调逻辑

1
java复制代码registerDisposableBeanIfNecessary(beanName, bean, mbd);

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…223224225…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%