leetcode经典动态规划解题报告

leetcode70 爬楼梯 题目描述 1 2 3 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢? 解答: 递推公式: 设f(n)为n阶楼梯的爬法 f(n)=f(n-1)+f(n-2) n>2 f(n)=1 n=1 f(n)=2 n=2 到达第n阶楼梯,有两种方法,一种是从第n-1阶楼梯,走一步到;另一种是从n-2阶楼梯,走两步到。 根据以上的递归公式,我们很容易写出下面的代码: 1 2 3 4 5 6 7 8 9 10 11 12 public int climbStairs(int n) { if (n<3){ return n; } int[] dp=new int[n+1]; dp[1]=1; dp[2]=2; for(int i=3;i<=n;i++){ dp[i]=dp[i-1]+dp[i-2]; } return dp[n]; } 通过观察,不难发现我们只需要保存两个状态即可. 1 2 3 4 5 6 7 8 9 10 11 12 13 public int climbStairs(int n) { if (n<3){ return n; } int p1=2; //f(n-1) int p2=1; //f(n-2) for(int i=3;i<=n;i++){ int cur=p1+p2; p2=p1; p1=cur; } return p1; } leetcode198 打家劫舍 题目描述: 1 2 3 你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。 给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。 解答: 设小偷走到第n个房屋旁,他的最大收入为f(n), 那么有递推公式: f(n)=max(f(n-2)+s[n],f(n-1)) 其中s[n]第n个房子可以窃到金额。 这个公式的含义是,当小偷走到第n个房子时,他其实有两种选择,偷或上家偷了不偷这家。小偷只需要选一个最优方案即可。 根据递推公式我们很容易得到以下的代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public int rob(int[] nums) { int n=nums.length; if (n==0){ return 0; }else if (n==1){ return nums[0]; }else if (n==2){ return Math.max(nums[0],nums[1]); } int[] dp=new int[n+1]; dp[1]=nums[0]; dp[2]=Math.max(nums[0],nums[1]); for (int i=3;i<=n;i++){ dp[i]= Math.max(dp[i-1],dp[i-2]+nums[i-1]); } return dp[n]; } 实际上我们只需要保存两个状态即可,也无需这么多的特判代码。简化后的代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 public int rob(int[] nums) { int p1=0; //f(n-1) int p2=0; //f(n-2) for (int i=0;i<nums.length;i++){ int cur=Math.max(p2+nums[i],p1); p2=p1; p1=cur; } return p1; } 这道题还有一些延申题目: leetcode213 打家劫舍II 题目描述 1 2 3 你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。 给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。 解答: 这道题目和上面题目的不同在于房子是环形排布的。也就是说,如果第一个房子被偷了,那么最后一个房子就不能被偷。也就是说第一个房子偷不偷可以影响最后一个房子的情况,我们只需要把两种情况都计算一下即可。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public int rob(int[] nums) { if (nums==null||nums.length==0){ return 0; } if (nums.length==1){ return nums[0]; } return Math.max(helper(nums,0,nums.length-2),helper(nums,1,nums.length-1)); } public int helper(int[] nums,int start,int end){ int p1=0;// f(n-1) int p2=0;// f(n-2) for (int i=start;i<=end;i++){ int cur=Math.max(p2+nums[i],p1); p2=p1; p1=cur; } return p1; } leetcode53最大子序和 题目描述 1 给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 解答: 设以下标n结尾的子序和为f(n) 因此我们可以得到递推公式: f(n)=max(f(n-1),0)+nums[n] 1 2 3 4 5 6 7 8 9 10 11 12 public int maxSubArray(int[] nums) { if (nums==null||nums.length==0){ return 0; } int max=0; for (int i=1;i<nums.length;i++){ nums[i]=Math.max(nums[i-1],0)+nums[i]; max=Math.max(max,nums[i]); } return max; } 从递推公式中可以知道我们需要保存前一个状态,我们可以直接利用原数组即可。 leetcode322 零钱找零 题目描述: 1 给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。 解答: 设总金额n找零所需的最少硬币数为f(n) 那么我们可以得到递推公式 f(n)=min(f(n-c))+1 c 为硬币面值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public int coinChange(int[] coins, int amount) { int[] dp=new int[amount+1]; int max=amount+1; Arrays.fill(dp,max); dp[0]=0; for (int i=1;i<dp.length;i++){ for (int coin : coins) { if (i >= coin) { dp[i]=Math.min(dp[i],dp[i-coin]+1); } } } return dp[amount]>amount?-1:dp[amount]; } leetcode120 三角形最小路径和 题目描述: 1 2 3 给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。 相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。 解答: 1 2 3 4 5 6 [ [2], [3,4], [6,5,7], [4,1,8,3] ] 假如到达第i层的第j个位置,那么只能从第i-1层的j-1/j位置到达。 我们设到达第i层第j个节点的最小路径和为dp[i][j] 那么我们可以得到以下递推公式: dp[i][j]=min(dp[i-1][j-1],dp[i-1][j])+d[i][j] 值得注意的是两边要特殊处理。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public int minimumTotal(List<List<Integer>> triangle) { int n = triangle.size(); int[][] f = new int[n][n]; //左边特殊处理 f[0][0] = triangle.get(0).get(0); for (int i = 1; i < n; ++i) { f[i][0] = f[i - 1][0] + triangle.get(i).get(0); for (int j = 1; j < i; ++j) { f[i][j] = Math.min(f[i - 1][j - 1], f[i - 1][j]) + triangle.get(i).get(j); } //右边特殊处理 f[i][i] = f[i - 1][i - 1] + triangle.get(i).get(i); } int minTotal = f[n - 1][0]; for (int i = 1; i < n; ++i) { minTotal = Math.min(minTotal, f[n - 1][i]); } return minTotal; } leetcode300 最长上升子序列 题目描述: 1 2 3 4 5 6 给定一个无序的整数数组,找到其中最长上升子序列的长度。 示例: 输入: [10,9,2,5,3,7,101,18] 输出: 4 解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。 解答: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public int lengthOfLIS(int[] nums) { if (nums.length==0){ return 0; } int res=0; int[] dp=new int[nums.length]; Arrays.fill(dp,1); for (int end=0;end<nums.length;end++){ for (int start=0;start<end;start++){ if (nums[start]<nums[end]){ dp[end]=Math.max(dp[end],dp[start]+1); } } res=Math.max(res,dp[end]); } return res; } leetcode64 最小路径和 题目描述: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。 说明:每次只能向下或者向右移动一步。 示例: 输入: [ [1,3,1], [1,5,1], [4,2,1] ] 输出: 7 解释: 因为路径 1→3→1→1→1 的总和最小。 解答: 我们假设到达坐标(i,j)的最小路径和为f(i,j) 因为到达(i,j)要么从上面过来,要么从左边过来。 因此我们可以得到这样的递推公式: f(i,j)=min(f(i-1,j),f(i,j-1))+grid[i][j] 注意第一行和第一列要特殊处理。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public int minPathSum(int[][] grid) { int m=grid.length; int n=grid[0].length; int[][] dp=new int[m][n]; dp[0][0]=grid[0][0]; //第一行 for (int col=1;col<n;col++){ dp[0][col]=dp[0][col-1]+grid[0][col]; } //第一列 for (int row=1;row<m;row++){ dp[row][0]=dp[row-1][0]+grid[row][0]; } for (int row=1;row<m;row++){ for (int col=1;col<n;col++){ dp[row][col]=Math.min(dp[row-1][col],dp[row][col-1])+grid[row][col]; } } return dp[m-1][n-1]; } leetcode174 地下城游戏 题目描述: 1 2 3 4 5 6 7 8 一些恶魔抓住了公主(P)并将她关在了地下城的右下角。地下城是由 M x N 个房间组成的二维网格。我们英勇的骑士(K)最初被安置在左上角的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。 骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡。 有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 0),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。 为了尽快到达公主,骑士决定每次只向右或向下移动一步。 编写一个函数来计算确保骑士能够拯救到公主所需的最低初始健康点数。 解答: 这道题我们不能从左上角向右下角进行动态规划;我们需要从右下角到左上角进行动态规划。 设从(i,j)到终点所需的最小初始值为dp[i][j] 也就是说当我们到达坐标(i,j)时,只要路径和不小于dp[i][j]就能到达终点。 我们可以得到状态转移方程: dp[i][j]=max(min(dp[i+1][j],dp[i][j+1])-dungeon(i,j),1) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public int calculateMinimumHP(int[][] dungeon) { int n = dungeon.length, m = dungeon[0].length; int[][] dp = new int[n + 1][m + 1]; for (int i = 0; i <= n; ++i) { Arrays.fill(dp[i], Integer.MAX_VALUE); } dp[n][m - 1] = dp[n - 1][m] = 1; for (int i = n - 1; i >= 0; --i) { for (int j = m - 1; j >= 0; --j) { int minn = Math.min(dp[i + 1][j], dp[i][j + 1]); dp[i][j] = Math.max(minn - dungeon[i][j], 1); } } return dp[0][0]; }

2020/9/10
articleCard.readMore

Netty启动原理

典型的Netty服务端启动代码: 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 public class MyServer { public static void main(String[] args) throws Exception{ EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup,workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); //入站编码处理器 pipeline.addLast(new MyByteToLongDecoder()); //出站的handler进行编码 pipeline.addLast(new MyLongToByteEncoder()); //自定义的handler 处理业务逻辑 pipeline.addLast(new MyServerHandler()); } }); ChannelFuture channelFuture = serverBootstrap.bind(7000).sync(); channelFuture.channel().closeFuture().sync(); }finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } } 启动流程分析 NioEventLoopGroup的创建 在启动Netty服务器之前创建了两个NioEventLoopGroup 那么我们首先来分析它们的创建过程: NioEventLoopGroup的实例化最终调用了它的父类的构造器: 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 protected MultithreadEventExecutorGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory, Object... args) { if (nThreads <= 0) { //参数合法性检测 throw new IllegalArgumentException(String.format("nThreads: %d (expected: > 0)", nThreads)); } if (executor == null) { //如果线程池为空,则创建一个线程池, //这个线程池非常的特殊,他为每个任务都单独创建一个任务 executor = new ThreadPerTaskExecutor(newDefaultThreadFactory()); } //这个children实际上是一个NioEventLoop数组 children = new EventExecutor[nThreads]; for (int i = 0; i < nThreads; i ++) { boolean success = false; //用于标记是否创建成功 try { //这里的newChild实际是子类NioEventLoopGroup实现的 children[i] = newChild(executor, args); success = true; } catch (Exception e) { // TODO: Think about if this is a good exception type throw new IllegalStateException("failed to create a child event loop", e); } finally { if (!success) { //如果在创建的NioEventLoop数组数组中途出现了异常 //那么就将成功创建的NioEventLoop关闭掉 for (int j = 0; j < i; j ++) { children[j].shutdownGracefully(); } for (int j = 0; j < i; j ++) { EventExecutor e = children[j]; try { while (!e.isTerminated()) { e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS); } } catch (InterruptedException interrupted) { // Let the caller handle the interruption. Thread.currentThread().interrupt(); break; } } } } } //chooser实际上是每次进行相关操作时线程的选择的实现,默认使用的是轮询策略 chooser = chooserFactory.newChooser(children); final FutureListener<Object> terminationListener = new FutureListener<Object>() { @Override public void operationComplete(Future<Object> future) throws Exception { if (terminatedChildren.incrementAndGet() == children.length) { terminationFuture.setSuccess(null); } } }; for (EventExecutor e: children) { e.terminationFuture().addListener(terminationListener); } Set<EventExecutor> childrenSet = new LinkedHashSet<EventExecutor>(children.length); Collections.addAll(childrenSet, children); readonlyChildren = Collections.unmodifiableSet(childrenSet); } newChild的实现如下: 1 2 3 4 5 protected EventLoop newChild(Executor executor, Object... args) throws Exception { EventLoopTaskQueueFactory queueFactory = args.length == 4 ? (EventLoopTaskQueueFactory) args[3] : null; return new NioEventLoop(this, executor, (SelectorProvider) args[0], ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2], queueFactory); } 以上就是NioEventLoopGroup的实现,在启动Netty服务端的时候,创建了两个NioEventLoopGroup,分别是boosGroup和workerGroup,它们本质上是一样的,只是作用不同。 ServerBootstrap与NioEventLoopGroup的绑定 1 2 3 4 5 6 7 8 9 public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) { super.group(parentGroup); ObjectUtil.checkNotNull(childGroup, "childGroup"); if (this.childGroup != null) { throw new IllegalStateException("childGroup set already"); } this.childGroup = childGroup; return this; } 从这里开始两个NioEventLoopGroup的作用开始不同了。 ServerBootstrap的bind处理 通过层层调用最终来到了doBind方法: 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 private ChannelFuture doBind(final SocketAddress localAddress) { //对Channel进行初始化和注册操作 final ChannelFuture regFuture = initAndRegister(); final Channel channel = regFuture.channel(); if (regFuture.cause() != null) { return regFuture; } if (regFuture.isDone()) { // At this point we know that the registration was complete and successful. ChannelPromise promise = channel.newPromise(); doBind0(regFuture, channel, localAddress, promise); return promise; } else { // Registration future is almost always fulfilled already, but just in case it's not. final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel); regFuture.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { Throwable cause = future.cause(); if (cause != null) { // Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an // IllegalStateException once we try to access the EventLoop of the Channel. promise.setFailure(cause); } else { // Registration was successful, so set the correct executor to use. // See https://github.com/netty/netty/issues/2586 promise.registered(); doBind0(regFuture, channel, localAddress, promise); } } }); return promise; } } 在进行doBind最开始就进行了Channel的绑定和初始化工作 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 final ChannelFuture initAndRegister() { Channel channel = null; try { //利用Channel工厂创建一个Channel,实际上是通过反射实例化的 channel = channelFactory.newChannel(); init(channel); } catch (Throwable t) { if (channel != null) { channel.unsafe().closeForcibly(); return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t); } return new DefaultChannelPromise(new FailedChannel(), GlobalEventExecutor.INSTANCE).setFailure(t); } ChannelFuture regFuture = config().group().register(channel); if (regFuture.cause() != null) { if (channel.isRegistered()) { channel.close(); } else { channel.unsafe().closeForcibly(); } } return regFuture; } 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 void init(Channel channel) { setChannelOptions(channel, options0().entrySet().toArray(newOptionArray(0)), logger); setAttributes(channel, attrs0().entrySet().toArray(newAttrArray(0))); ChannelPipeline p = channel.pipeline(); final EventLoopGroup currentChildGroup = childGroup; final ChannelHandler currentChildHandler = childHandler; final Entry<ChannelOption<?>, Object>[] currentChildOptions = childOptions.entrySet().toArray(newOptionArray(0)); final Entry<AttributeKey<?>, Object>[] currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(0)); p.addLast(new ChannelInitializer<Channel>() { @Override public void initChannel(final Channel ch) { final ChannelPipeline pipeline = ch.pipeline(); ChannelHandler handler = config.handler(); if (handler != null) { pipeline.addLast(handler); } ch.eventLoop().execute(new Runnable() { @Override public void run() { pipeline.addLast(new ServerBootstrapAcceptor( ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs)); } }); } }); } 该方法主要做Channel的初始化工作,如果我们在启动前设置了参数,这里也会传递过去。 完成Channel的初始化工作之后就需要对Channel进行注册: 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 @Override public final void register(EventLoop eventLoop, final ChannelPromise promise) { ObjectUtil.checkNotNull(eventLoop, "eventLoop"); if (isRegistered()) { promise.setFailure(new IllegalStateException("registered to an event loop already")); return; } if (!isCompatible(eventLoop)) { promise.setFailure( new IllegalStateException("incompatible event loop type: " + eventLoop.getClass().getName())); return; } AbstractChannel.this.eventLoop = eventLoop; if (eventLoop.inEventLoop()) { register0(promise); } else { try { eventLoop.execute(new Runnable() { @Override public void run() { register0(promise); } }); } catch (Throwable t) { logger.warn( "Force-closing a channel whose registration task was not accepted by an event loop: {}", AbstractChannel.this, t); closeForcibly(); closeFuture.setClosed(); safeSetFailure(promise, t); } } } private void register0(ChannelPromise promise) { try { if (!promise.setUncancellable() || !ensureOpen(promise)) { return; } boolean firstRegistration = neverRegistered; doRegister(); neverRegistered = false; registered = true; pipeline.invokeHandlerAddedIfNeeded(); safeSetSuccess(promise); pipeline.fireChannelRegistered(); if (isActive()) { if (firstRegistration) { pipeline.fireChannelActive(); } else if (config().isAutoRead()) { beginRead(); } } } catch (Throwable t) { closeForcibly(); closeFuture.setClosed(); safeSetFailure(promise, t); } } 而doRegister方法完成了最终的注册工作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 protected void doRegister() throws Exception { boolean selected = false; for (;;) { try { selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this); return; } catch (CancelledKeyException e) { if (!selected) { eventLoop().selectNow(); selected = true; } else { throw e; } } } } 这样就把Channle注册到了boss线程的selector多路复用器上,完成了channel的初始化和注册。 那么Server端是何时启动监听呢,其实通过上述代码会发现,每个Channel(不管server还是client)在运行期间,全局绑定一个唯一的线程不变(NioEventLoop),Netty所有的I/O操作都是和这个channel对应NioEventLoop进行操作,也就是很多步骤都会有一个eventLoop.inEventLoop()的判断,判断是否在这个channel对应的线程中,如果不在,则会执行eventLoop.execute(new Runnable() {}这步操作时,会判断IO线程是否启动,如果没有启动,会启动IO线程: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private void execute(Runnable task, boolean immediate) { boolean inEventLoop = inEventLoop(); addTask(task); if (!inEventLoop) { startThread(); if (isShutdown()) { boolean reject = false; try { if (removeTask(task)) { reject = true; } } catch (UnsupportedOperationException e) { } if (reject) { reject(); } } } if (!addTaskWakesUp && immediate) { wakeup(inEventLoop); } } 最终会调用NioEventLoop的run方法: 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 protected void run() { int selectCnt = 0; for (;;) { try { int strategy; try { strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks()); switch (strategy) { case SelectStrategy.CONTINUE: continue; case SelectStrategy.BUSY_WAIT: case SelectStrategy.SELECT: long curDeadlineNanos = nextScheduledTaskDeadlineNanos(); if (curDeadlineNanos == -1L) { curDeadlineNanos = NONE; // nothing on the calendar } nextWakeupNanos.set(curDeadlineNanos); try { if (!hasTasks()) { strategy = select(curDeadlineNanos); } } finally { nextWakeupNanos.lazySet(AWAKE); } // fall through default: } } catch (IOException e) { rebuildSelector0(); selectCnt = 0; handleLoopException(e); continue; } selectCnt++; cancelledKeys = 0; needsToSelectAgain = false; final int ioRatio = this.ioRatio; boolean ranTasks; if (ioRatio == 100) { try { if (strategy > 0) { processSelectedKeys(); } } finally { // Ensure we always run tasks. ranTasks = runAllTasks(); } } else if (strategy > 0) { final long ioStartTime = System.nanoTime(); try { processSelectedKeys(); } finally { final long ioTime = System.nanoTime() - ioStartTime; ranTasks = runAllTasks(ioTime * (100 - ioRatio) / ioRatio); } } else { ranTasks = runAllTasks(0); // This will run the minimum number of tasks } if (ranTasks || strategy > 0) { if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS && logger.isDebugEnabled()) { logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.", selectCnt - 1, selector); } selectCnt = 0; } else if (unexpectedSelectorWakeup(selectCnt)) { // Unexpected wakeup (unusual case) selectCnt = 0; } } catch (CancelledKeyException e) { if (logger.isDebugEnabled()) { logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?", selector, e); } } catch (Throwable t) { handleLoopException(t); } try { if (isShuttingDown()) { closeAll(); if (confirmShutdown()) { return; } } } catch (Throwable t) { handleLoopException(t); } } } 到此整个Netty服务端就启动了。

2020/5/28
articleCard.readMore

web安全基础知识一

常见网站应用攻击方式 XSS攻击 XSS攻击即跨站点脚本攻击(cross site script),指黑客通过篡改网页,注入恶意HTML脚本,在用户浏览网页时,控制用户浏览器进行恶意操作的一种攻击方式。 常见的XSS攻击的类型有两种: 反射型:攻击者诱使用户点击一个嵌入恶意脚本的连接,达到攻击的目的。 持久型:黑客提交包含恶意脚本的请求,保存在被攻击的web站点的数据库中,用户浏览网页时,恶意脚本就被包含在正常页面中,达到攻击的目的。 防范手段 消毒 XSS攻击者一般都是通过在请求中嵌入恶意脚本达到攻击的目的,这些脚本一般用户输入中不使用的,如果进行过滤和消毒处理,即对某些html危险字符转义,如”>””<”等,就可以防范大部分攻击。 HttpOnly 即浏览器禁止页面JavaScript访问带有HttpOnly属性的Cookie。该方法可以防止XSS攻击者窃取Cookie,对于存放敏感信息的Cookie,如用户认证信息等,可通过对该Cookie添加HttpOnly属性,避免被攻击脚本窃取。 注入攻击 注入攻击主要有两种形式,SQL注入攻击和OS注入攻击。 SQL注入攻击的原理就是攻击者在HTTP请求中注入恶意SQL命令,服务器用请求参数构造数据库SQL命令时,恶意SQL被一起构造,并在数据库中执行。SQL注入攻击需要用户对数据库得结构有所了解才能进行,攻击者获取数据库表结构的手段有:网站开源的代码,错误回显,盲注。 SQL注入的防范方法 消毒 和防止XSS攻击一样,请求参数消毒是一种比较简单粗暴又有效的手段。通过正则匹配,过滤请求数据中可能注入的SQL。 参数绑定 使用预编译手段,绑定参数是最好的防SQL注入方法。目前很多数据层很多框架都提供SQL预编译和参数绑定。 CSRF攻击 CSRF(cross site request frogery,跨站点请求伪造),攻击者通过跨站请求,以合法用户的身份进行非常操作,如转账交易、发表评论等。CSRF的主要手段是利用跨站请求,在用户不支持的情况下,以用户的身份伪造请求。核心是利用了浏览器Cookie或服务器Session策略,盗取用户身份。 CSRF的防御手段 CSRF的防范手段主要是识别请求者身份。 表单Token CSRF是一个伪造用户请求的操作,所以需要构造用户请求的所有参数才可以。表单Token通过在请求参数中增加随机数的办法来阻止攻击者获得所有请求参数。服务器检查请求参数中Token的值是否存在并且正确请求提交者是否合法。 验证码 在请求提交时,需要用户输入验证码,以避免在用户不知情的情况下被攻击者伪造请求。 Referer check HTTP请求头的Referer域中记录着请求来源,可通过检查请求来源,验证其是否合法。 其它攻击和漏洞 Error Code 错误回显,许多web服务器默认是打开异常信息输出的,即服务器端未处理的异常堆栈信息回直接输出到客户端浏览器,这种方式虽然对程序调试和错误报告有好处,但同时也给黑客造成可乘之机。 HTML注释 未调式程序或其它不恰当的原因,有时程序开发人员会在PHP,JSP等服务器页面程序中使用HTML注释语法进行程序注释,这些HTML注释信息会显示在客户端浏览器给黑客攻击便利。 文件上传 一般网站都会有文件上传功能,设置头像、分享视频、上传附件等。如果上传的是可执行的程序,并通过该程序获得服务器端命令执行能力,那么攻击者几乎可以在服务器上为所欲为。最有效的防范手段是设置上传文件白名单,只允许上传可靠的文件类型。此外还可以修改文件名、使用专门的存储手段等,保护服务器避免受上传文件攻击。 路径遍历 攻击者在请求的URL中使用相对路径,遍历系统未开放的目录和文件。防御防范主要是讲JS、CSS等资源文件部署在独立的服务器,使用独立域名,其它文件不使用静态URL范围,到你太参数不包含文件路径信息。

2020/5/22
articleCard.readMore

SpringBoot自动配置原理

简介 SpringBoot相较于Spring的一大进步就是它简化了配置。SpringBoot遵循”约定优于配置”的原则,使用注解对一些常规的配置项做默认配置,减少或不使用xml配置。Springboot还提供了大量的starter,只需引入一个starter,就可以直接使用框架。 几个重要的注解 在SpringBoot启动类上添加的SpringBootApplication注解。这个注解实际上是一个复合注解。 1 2 3 4 5 6 7 8 9 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication { 我们主要关注@SpringBootConfiguration,@EnableAutoConfiguration,@ComponeScan. @SpringBootConfiguration注解的底层是@Configuration注解,即支持JavaConfig的方式来进行配置。 @EnableAutoConfiguration注解的作用就是开启自动配置功能。 @ComponentScan注解的作用就是烧苗当前类所属的package,将@Controller、@Service、@Component、@Repository等注解所表示的类加载到IOC容器中。 通过对这几个注解的分析,我们可以知道自动配置工作主要是由EnableAutoConfiguration注解来实现的。 自动配置的关键:@EnableAutoConfiguration注解 该注解的定义如下: 1 2 3 4 5 6 7 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @AutoConfigurationPackage @Import(AutoConfigurationImportSelector.class) public @interface EnableAutoConfiguration { 该注解使用@Import向IOC容器中注入了AutoConfigurationImportSelector类。 该类中提供了获取所有候选的配置的方法: 1 2 3 4 5 6 7 protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()); Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you " + "are using a custom packaging, make sure that file is correct."); return configurations; } 通过loadFactoryNames方法拿到了一个List,这个方法中传入了一个getSpringFactoriesLoaderFactoryClass(),这个方法,实际上就是获取了标记了@EnableAutoConfiguratioin注解的类。 1 2 3 protected Class<?> getSpringFactoriesLoaderFactoryClass() { return EnableAutoConfiguration.class; } 当我们的SpringBoot项目启动的时候,会先导入AutoConfigurationImportSelector,这个类会帮我们选择所有候选的配置,我们需要导入的配置都是SpringBoot帮我们写好的一个一个的配置类,那么这些配置类的位置,存在与META-INF/spring.factories文件中,通过这个文件,Spring可以找到这些配置类的位置,于是去加载其中的配置。 总结 SpringBoot在启动的时候从类路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值 将这些值作为自动配置类导入容器 , 自动配置类就生效 , 帮我们进行自动配置工作; 它将所有需要导入的组件以全类名的方式返回 , 这些组件就会被添加到容器中 ; 它会给容器中导入非常多的自动配置类 (xxxAutoConfiguration), 就是给容器中导入这个场景需要的所有组件 , 并配置好这些组件 ; 有了自动配置类 , 免去了我们手动编写配置注入功能组件等的工作;

2020/5/21
articleCard.readMore

跟踪SpringMVC请求过程

整体流程 所有的请求都被拦截到DispatcherServlet,它也是一个Servlet,执行doService 快照请求中的所有的参数,将框架中的一些对象设置到request对象中。 调用doDispatch(request,response)方法 调用getHandler方法获取对应的Handler 调用getHandlerAdapter拿到对应的HandlerAdapter 应用拦截器的PreHandler,如果拦截器的PreHandeler返回false,则直接返回 调用HandlerAdapter对象的handler得到ModelAndView对象 应用拦截器的postHandle方法 调用processDispatchResult对结果进行处理,其内部调用了拦截器的afterCompletion方法 源码细节 如何拿到对应的Handler? 获取Handler是通过getHandler方法来获取的。 1 2 3 4 5 6 7 8 9 10 11 12 13 protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { for (HandlerMapping hm : this.handlerMappings) { if (logger.isTraceEnabled()) { logger.trace( "Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'"); } HandlerExecutionChain handler = hm.getHandler(request); if (handler != null) { return handler; } } return null; } 这段代码看起来非常的简单,遍历handlerMappings,从HandlerMapping从获取HandlerExecutionChain即我们的handler. 这个HandlerExecutionChain中包含了handler和HandlerInterceptor数组。也就是我们拿到handler实际上是一个处理链。 为什么需要HandlerAdapter,它的如何获取到的? SpringMVC的handler的实现方式比较的多,比如通过继承Controller的,基于注解控制器方式,HttpRequestHandler的方式。因为handler的实现方式不同,因此调用的方式也就不确定了。因此引入了HandlerAdapter来进行适配。 HandlerAdapter接口有三个方法: 1 2 3 4 //判断当前的HandlerAdapter是否支持HandlerMethod boolean supports(Object handler); ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception; long getLastModified(HttpServletRequest request, Object handler); 获取HandlerAdapter是通过getHandlerAdapter方法来获取的。通过对HandlerAdapter使用原因的分析,我们可以直到所谓获取对应的HandlerAdapter实际上从HandlerAdapter列表中找出一个支持当前handler的。 1 2 3 4 5 6 7 8 9 10 11 12 protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException { for (HandlerAdapter ha : this.handlerAdapters) { if (logger.isTraceEnabled()) { logger.trace("Testing handler adapter [" + ha + "]"); } if (ha.supports(handler)) { return ha; } } throw new ServletException("No adapter for handler [" + handler + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler"); } 该方法就是简单的遍历HandlerAdapter列表,从中找出一个支持当前handler的,并返回。 handler方法的执行过程 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 public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //最终拿到了我们的Controller类 Class<?> clazz = ClassUtils.getUserClass(handler); //判断是否使用了@SessionAttributes Boolean annotatedWithSessionAttributes = this.sessionAnnotatedClassesCache.get(clazz); if (annotatedWithSessionAttributes == null) { annotatedWithSessionAttributes = (AnnotationUtils.findAnnotation(clazz, SessionAttributes.class) != null); this.sessionAnnotatedClassesCache.put(clazz, annotatedWithSessionAttributes); } if (annotatedWithSessionAttributes) { // Always prevent caching in case of session attribute management. checkAndPrepare(request, response, this.cacheSecondsForSessionAttributeHandlers, true); // Prepare cached set of session attributes names. } else { // 禁用缓存 checkAndPrepare(request, response, true); } // Execute invokeHandlerMethod in synchronized block if required. if (this.synchronizeOnSession) { HttpSession session = request.getSession(false); if (session != null) { Object mutex = WebUtils.getSessionMutex(session); synchronized (mutex) { return invokeHandlerMethod(request, response, handler); } } } return invokeHandlerMethod(request, response, handler); } 最终来到了invokerhandlerMethod方法了: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { ServletHandlerMethodResolver methodResolver = getMethodResolver(handler); //获取处理请求的方法 Method handlerMethod = methodResolver.resolveHandlerMethod(request); //创建各种组件 ServletHandlerMethodInvoker methodInvoker = new ServletHandlerMethodInvoker(methodResolver); ServletWebRequest webRequest = new ServletWebRequest(request, response); ExtendedModelMap implicitModel = new BindingAwareModelMap(); //调用方法拿到结果 Object result = methodInvoker.invokeHandlerMethod(handlerMethod, handler, webRequest, implicitModel); //获取ModelAndView ModelAndView mav = methodInvoker.getModelAndView(handlerMethod, handler.getClass(), result, implicitModel, webRequest); //更新view中的属性 methodInvoker.updateModelAttributes(handler, (mav != null ? mav.getModel() : null), implicitModel, webRequest); return mav; } 最后调用mappedHandler.applyPostHandle(processedRequest, response, mv);进行后处理。后处理的过程就是调用所有的后置拦截器进行处理。 Filter与Interceptor Filter的实现方式 实现Filter接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @WebFilter(filterName = "filterOne", urlPatterns = {"/*"}) public class FilterOne implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { System.out.println("init"); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("===========before doFilter"); filterChain.doFilter(servletRequest, servletResponse); System.out.println("===========after doFilter"); } @Override public void destroy() { System.out.println("destroy"); } } Interceptor的实现方式 实现HandlerInterceptor接口 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 public class MyInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("Controller调用之前的拦截器。。。"); return true; } /** * 该方法controller调用之后,页面渲染之前执行,要preHandler返回ture才会执行该方法 * @param request * @param response * @param handler * @param modelAndView * @throws Exception */ @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("controller调用之后,页面渲染之前执行"); } /**请求完成之后执行的拦截器,要preHandler返回ture才会执行该方法 * * @param request * @param response * @param handler * @param ex * @throws Exception */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("请求完成之后执行的拦截器"); } } 在springmvc的配置文件中进行配置 1 2 3 4 5 6 7 8 9 <mvc:interceptors> <!-- 使用bean定义一个Interceptor,直接定义在mvc:interceptors根下面的Interceptor将拦截所有的请求 --> <bean class="com.host.app.web.interceptor.AllInterceptor"/> <mvc:interceptor> <mvc:mapping path="/test/number.do"/> <!-- 定义在mvc:interceptor下面的表示是对特定的请求才进行拦截的 --> <bean class="com.host.app.web.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors> 相同点 都可以拦截请求,过滤请求 都是应用了过滤器(责任链)设计模式 区别 Filter过滤器访问较大,配置在web.xml Interceptor范围比较小,配置在springmvc 在进入springmvc处理之前,首先要处理web.xml

2020/5/20
articleCard.readMore

Netty之ChannelHandler

Channel的生命周期 Channel包含4个状态: ChannelUnregisteredChannel已经被创建,但还未注册到EventLoop ChannelRegisteredChannel已经被注册到EventLoop ChannelActiveChannel处于活动状态,它现在可以接受和发送数据了 ChannelInactiveChannel没有连接到远程节点 ChannelHandler的生命周期 ChannelHandler接口定义了一系列生命周期操作: 类型描述 handlerAdded当把ChannelHandler添加到ChannelPipeline中时要调用 handlerRemoved当从ChannelPipeline中移除ChannelHandler时被调用 exceptionCaught当处理过程中在ChannelPipeline中有错误产生时调用 ChannelInboundHandler接口 ChannelOutboundHandler接口 ChannelHandler适配器 资源管理 每当调用ChannelInboundHandler.channelRead()或者ChannelOutboundHandler.write()方法来处理数据时,都需要确保没有任何的资源泄漏。Netty使用引用计数来处理池化的ByteBuf.所以在完全使用某个ByteBuf之后,调整其引用计数是很重要的。 Netty提供了class ResourceLeakDetector,它可以对应用程序缓冲区分配做大约1%的采样率进行内存泄漏检测。相关的开销是非常的小的。 级别描述 DISABLED禁止内存泄漏检测 SIMPLE使用1%的默认采样率检测并报告任何发现的泄漏。(默认) ADVANCED使用默认的采样率,报告所发现的任何的泄漏以及对应的消息被访问的位置。 PARANOID类是于ADVANCED,但是其间会对每次访问都进行采样。这会对性能有较大的影响。 泄漏检测级别可以通过JVM启动选项来设置: java -Dio.netty.leakDetectionLevel=ADVANCED ChannelPipeline接口 ChannelPipeline是一个拦截流经Channel的入站和出站事件的ChannelHandler实例链。每一个新创建的Channel都会被分配给一个新的ChannelPipeline,这项关联式永久性的,Channel既不能附加另外一个ChannelPipeline,也不能分离其当前的。 根据事件的起源事件将会被分为ChannelInboundHandler或者ChannelOutboundHandler处理。 修改ChannelPipeline ChannelPipeline可以通过添加、删除或者替换其它的ChannelHandler来实时地修改ChannelPipeline的布局。 ChannelPipeline还提供了访问ChannelHandler的操作: 触发事件 ChannelPipeline的API公开了用于调用入站和出站操作的附加方法。 ChannelHandlerContext接口 ChannelHandlerContext代表了ChannelHandler和ChannelPipeline之间的关联。每当有ChannelHandler添加到ChannelPipeline中时,都会创建ChannelHandlerContext。ChannelHandlerContext的主要功能就是管理它所关联的ChannelHandler和在同一ChannelPipeline中的其它ChannelHandler之间的交互。 异常处理 Netty提供了几种方式来处理入站和出站过程中出现的异常。 入站异常 如果需要处理入站异常,需要在对应的ChannelInboundHandler中重写exceptionCaught方法。 1 2 3 4 5 6 7 8 public class InboundExceptionHandler extends ChannelInboundHandlerAdapter{ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } } 一般将异常处理的ChannelHandler放到ChannelPipeline的最后一个位置。这样在整个处理路链中无论哪个环节出了异常都可以得到处理。因为exceptionCaught()的默认实现是将异常转发给下一个HandlerHandler. 处理出站异常 处理出站异常基于以下的机制: 每个出站操作都会返回一个ChannelFuture,注册到ChannelFuture的ChannelFutureListener将在操作完成时痛殴之该操作时注册成功还是出错了 几乎所有的ChannelOutboundHandler上的方法都会传入一个ChannelPromise的实例,作为ChannelFuture的子类,ChannelPromise也可以被分配用于异步通知的监听器。但是,ChannelPromise还具有提供立即通知的可写方法。 处理方法: 1 2 3 4 5 6 7 8 9 10 ChannelFuture future = channel.write(someMessage); future.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture f) { if (!f.isSuccess()) { f.cause().printStackTrace(); f.channel().close(); } } }); 另一种方法: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class OutboundExceptionHandler extends ChannelOutboundHandlerAdapter { @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { promise.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture f) { if (!f.isSuccess()) { f.cause().printStackTrace(); f.channel().close(); } } }); } }

2020/5/17
articleCard.readMore

负载均衡的实现方式与算法

负载均衡的实现方式 HTTP重定向负载均衡 HTTP重定向负载均衡需要一台重定向服务器。它的功能就是根据用户的HTTP请求根据负载均衡算法选择一个真实的服务器地址,并将服务器地址信息写入到重定向响应中返回给用户浏览器。用户浏览器再获取到响应之后,根据返回的信息,重新发送一个请求到真实的服务器上。 优点: 实现比较简单 缺点: 浏览器需要请求两次服务器才能完成一次访问,性能较差。 重定向服务器本身容易成为性能的瓶颈,使得整个集群的伸缩性有限。 另外HTTP返回码302重定向,可能会使搜索引擎判断未SEO作弊,降低搜索排名。 DNS域名解析负载均衡 DNS域名解析负载均衡是在进行域名解析的时候分局负载均衡算法选择一个合适的I{P地址返回,这样来实现负载均衡。大型网站一般将DNS域名解析负载均衡作为第一级负载均衡的手段 优点: 将负载均衡的工作交由DNS,省去了管理完整负载均衡服务器的麻烦 技术实现比较灵活、方便、简单易行,成本低,适用于大多数TCP/IP应用 对于部署的服务而言,无需任何的修改 服务器可以位于互联网的任意位置 一些DNS还支持地址位置的域名解析,即域名解析成距离用户地址位置最近的一个服务器地址,这样可以加快用户的访问速度。 缺点; 因为DNS是多级解析的,因此当集群结构改变后,需要很长时间才能使缓冲的DNS信息刷新。 不能按照服务器的处理能力来分配负载。DNS负载均衡采用的是简单的轮询算法,不能区分服务器之间的差异,不能反映服务器的运行状态。 可能会造成额外的网络问题,为了使本DNS服务器和其它DNS服务器能够即时的交互,保证DNS数据及时更新,使地址能够随机分配,一般都要将DNS的刷新时间设置的较小,但太小将会使DNS流量大增造成额外的网络问题。 反向代理负载均衡 通过反向代理服务器来选择合适的服务器作为目的服务器进行请求的转发。来实现负载均衡 IP负载均衡 IP负载均衡又称为网络层负载均衡,它和原理就是通过内核驱动更改IP的目的地址来完成数据负载均衡。 原理图: IP负载均衡在内核进程完成数据分发,处理性能得到了很好的提高。但是由于所有请求和响应都要经过负载均衡服务器,集群的最大响应数据吞吐量将受到负载均衡服务器网卡带宽的限制。 数据链路负载均衡 数据链路层负载均衡通过修改通信协议数据包的mac地址进行负载均衡。 这种三角传输模式的链路层负载均衡是目前大型网站使用比较广泛的负载均衡手段。在Linux平台下最好的链路层负载均衡开源产品时LVS(Linux Virtual Server)。 基本的负载均衡算法 轮询法 将请求按照顺序轮流的分配到后端服务器上,它均衡地对待后端的每一台服务器,而不关心服务器实际的连接数和当前的系统负载。 随机法 通过随机算法,根据后端服务器的列表大小指来随机的选择其中的一台服务器进行访问。 源地址哈希法 源地址哈希的思想是根据获取客户端的IP,通过hash函数计算得到的一个数值,用改数值对服务器列表的大小进行取模运算,得到的结果便是客服端要访问的服务器的序列。采用源地址哈希法进行负载均衡,同一IP地址的客户端,当后端服务器列表不变时,它每次都会映射到同一台后端服务器进行访问。 加权轮询法 不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不相同。给配置高、负载低的机器配置更高的权重,让其处理更多的请;而配置低、负载高的机器,给其分配较低的权重,降低其系统负载,加权轮询能很好地处理这一问题,并将请求顺序且按照权重分配到后端。 加权随机法 与加权轮询法一样,加权随机法也根据后端机器的配置,系统的负载分配不同的权重。不同的是,它是按照权重随机请求后端服务器,而非顺序。 最小连接数法 最小连接数算法比较灵活和智能,由于后端服务器的配置不尽相同,对于请求的处理有快有慢,它是根据后端服务器当前的连接情况,动态地选取其中当前积压连接数最少的一台服务器来处理当前的请求,尽可能地提高后端服务的利用效率,将负责合理地分流到每一台服务器。 Nginx提供的负载均衡算法 轮询 加权轮询 IP_hash fair 根据后端服务器的响应时间来进行分配,响应时间段的优先分配。 url_hash Dubbo提供的负载均衡算法 加权随机 加权轮询 最小活跃调用 一致性hash算法

2020/5/17
articleCard.readMore

Netty之ByteBuf

简介 ByteBuf是Netty的数据容器,它解决了JDK API的局限性,能为网络应用程序的开发者提供更好的API支持。 ByteBufAPI的优点如下: 它可以被用户自定义的缓冲区类型拓展 通过内置的复合缓冲区类型实现了透明的零拷贝。 容量可以按需增长 在读和写这两种模式下切换不需要调用BuyteBuffer的flip()方法 读和写使用了不同的索引 方式支持链式调用 支持引用计数 支持池化 工作原理 ByteBuf内部维护了两个不同的索引,一个用于读取,一个用于写入。 使用模式 堆缓冲区 最常见的ByteBuf模式,是将数据存储到JVM的堆空间中。这种模式被称为支持数组。它能够在没有使用池化的情况下,提供较为快速的分配和释放。 1 2 3 4 5 6 7 8 ByteBuf heapBuf = ...; //检查是否是数组支撑 if (heapBuf.hasArray()) { byte[] array = heapBuf.array(); int offset = heapBuf.arrayOffset() + heapBuf.readerIndex(); int length = heapBuf.readableBytes(); handleArray(array, offset, length); } 直接缓冲区 直接缓冲区是指内存空间是通过本地调用分配而来的。因此直接缓冲区的内容将驻留在堆外。 通过使用直接缓冲区,避免了依次将JVM堆中的缓冲区复制到直接缓冲区的国产,因此效率更高。但是直接缓冲区的创建和释放的成本比较高。 1 2 3 4 5 6 7 8 9 ByteBuf directBuf = ...; if (!directBuf.hasArray()) { //如果不是数组支撑,那么就是一个直接缓冲区 int length = directBuf.readableBytes(); byte[] array = new byte[length]; //从直接缓冲区中读取数据到array中 directBuf.getBytes(directBuf.readerIndex(), array); handleArray(array, 0, length); } 复合缓冲区 复合缓冲区可以为多个ByteBuf提供一个聚合视图。我们可以根据需要添加或删除ByteBuf实例。Netty通过ByteBuf的一个子类CompositeByteBuf来实现复合缓冲区。 1 2 3 4 5 6 7 8 9 10 11 public static void byteBufComposite() { // 复合缓冲区,只是提供一个视图 CompositeByteBuf messageBuf = Unpooled.compositeBuffer(); ByteBuf headerBuf = Unpooled.buffer(); // can be backing or direct ByteBuf bodyBuf = Unpooled.directBuffer(); // can be backing or direct messageBuf.addComponents(headerBuf, bodyBuf); messageBuf.removeComponent(0); // remove the header for (ByteBuf buf : messageBuf) { System.out.println(buf.toString()); } } 字节级操作 随机访问索引 和普通的Java数组一样,ByteBuf的索引也是从零开始的。 1 2 3 4 5 ByteBuf buffer = ...; for (int i = 0; i < buffer.capacity(); i++) { byte b = buffer.getByte(i); System.out.println((char)b); } 顺序访问索引 ByteBuf同时具有读索引和写索引,因此两个索引把ByteBuf分为了三个部分。分贝是可丢弃字节区,可读字节区,可写字节区。 查找操作 查找ByteBuf指定的指。可以利用indexOf()来直接查询,也可以利用ByteProcessor作为参数来查找某个指定的值。 1 2 3 4 5 6 7 public static void byteProcessor() { ByteBuf buffer = Unpooled.buffer(); //get reference form somewhere // 使用indexOf()方法来查找 buffer.indexOf(buffer.readerIndex(), buffer.writerIndex(), (byte)8); // 使用ByteProcessor查找给定的值 int index = buffer.forEachByte(ByteProcessor.FIND_CR); } 派生缓冲区 派生缓冲区为ByteBuf提供了以专门的方式来呈现其内容的视图,这类视图是通过以下方法被创建的: 1 2 3 4 5 6 duplicate(); slice(); slice(int,int); Upooled.ummodifiableBuffer(...); order(ByteOrder); readSlice(int); ByteBufHolder接口 ByteBufHolder是ByteBuf的容器,可以通过子类实现ByteBufHolder接口,根据自身需要添加自己需要的数据字段。可以用于自定义缓冲区类型扩展字段。 ByteBufHolder接口提供了几种用于访问底层数据和引用计数的方法。 1 2 3 content();//返回持有的所有的ByteBuf copy();//返回一个深拷贝 duplicate();//返回一个浅拷贝 ByteBuf分配 按需分配ByteBufAllocator接口 为了降低分配和释放内存的开销,Netty通过ByteBufAllocator实现ByteBuf池化。 如何获取ByteBufAllocator实例: 1 2 3 4 5 6 7 Channel channel = ...; //从Channel中获取 ByteBufAllocator allocator = channel.alloc(); //从ChannelHandlerContext中获取 ChannelHandlerContext ctx = ...; ByteBufAllocator allocator2 = ctx.alloc(); Unpooled缓冲区 在不能获取到ByteBufAllocator中情况下,可以使用Unpooled获取缓冲区。 引用计数 引用计数是一种通过在某个对象所持有的资源不再被其它对象引用时释放该对象所持有的资源来优化内存使用和性能的计数。 一个ReferencceCounted实现的实例通常以活动的引用计数为1作为开始。只要引用计数大于0,旧能保证对象不会被释放。当活动引用的数量减少到0时,该实例就会被释放。 1 2 3 4 5 6 7 ByteBuf buffer = ... // 引用计数加1 buffer.retain(); // 输出引用计数 buffer.refCnt(); // 引用计数减1 buffer.release(); 零拷贝 零拷贝是指在操作数据的时候,不需要将数据buffer从一个内存区域拷贝到另一个内存区域,因为少了一次内存的拷贝,因此CPU的效率就得到了较大的提升。 OS层面的零拷贝 OS层面的零拷贝通常是指避免在用户态与内核态之间来回进行数据拷贝。比如Linux提供了mmap系统调用,它可以将用户内存空间映射到内核空间。这样用户对这段内存空间的操作就可以直接反映到内核。 Netty层面的零拷贝 Netty层面的零拷贝主要体现在这几个方法: 提供了CompositeByteBuf,它可以将多个ByteBuf合并为一个逻辑上的ByteBif,避免了ByteBuf之间的拷贝。 通过wrap操作,我们可以将byte[]数组,ByteBuf、ByteBuffer包装为一个ByteBuf对象,进而避免了拷贝操作。 1 2 byte[] bytes = ... ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes); ByteBuf 支持 slice操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf, 避免了内存的拷贝. 1 2 3 ByteBuf byteBuf = ... ByteBuf header = byteBuf.slice(0, 5); ByteBuf body = byteBuf.slice(5, 10); 通过 FileRegion 包装的FileChannel.tranferTo 实现文件传输, 可以直接将文件缓冲区的数据发送到目标 Channel, 避免了传统通过循环 write 方式导致的内存拷贝问题. 1 2 3 4 5 6 7 8 9 10 RandomAccessFile srcFile = new RandomAccessFile(srcFileName, "r"); FileChannel srcFileChannel = srcFile.getChannel(); RandomAccessFile destFile = new RandomAccessFile(destFileName, "rw"); FileChannel destFileChannel = destFile.getChannel(); long position = 0; long count = srcFileChannel.size(); //直接传输,而不是通过while进行循环的 srcFileChannel.transferTo(position, count, destFileChannel);

2020/5/17
articleCard.readMore

Netty线程模型

Reactor线程模型 Netty的线程模型实际上就是Reactor模型的一种实现。 Reactor模型是基于事件驱动开发的,核心组成部分是一个Reactor和一个线程池,其中Reactor负责监听和分配事件,线程池负责处理事件。根据Reactor的数量有线程池的数量,又可以将Reactor分为三种模型: 单线程模型(单Reactor,单线程) 多线程模型(单Reactor,多线程) 主从多线程模型(多Reactor,多线程) 单线程模型 Reactor内部通过selector轮询连接,收到事件后,通过dispatch进行分发。 如果是连接事件,则分发给Acceptor处理,Accepter通过accept接受连接,并创建一个Headler来处理连接后的各种事件。 如果是读写事件,那么直接交由对应的Headelr进行处理。 多线程模型 主线程中,Reactor对象通过selector监控连接事件,收到事件后通过dispatch进行分发。 如果是建立连接的事件,则Accepter负责处理,它会通过accept接受请求,并创建一个Headler来处理后序事件,而Headler只负责相应事件,不进行业务操作,也就是只进行read读取数据和write写出数据,业务处理是交给线程池进行处理。 线程池分配一个线程来进行业务的处理,处理结果交由对应的Handler进行转发。 主从多线程模型 存在多个Reactor,每个Reactor都有自己的selector选择器,线程和dispatch 主线程中的mainReactor通过自己的selector监控连接建立事件,收到事件后通过Accepter接受,将任务分配给某个子线程。 子线程中的subReactor将mainReactor分配的连接加入连接队列中通过自己的selector进行监听,并创建一个Handler用于处理后序事件。 Handler完成read->业务处理->send的完整业务流程。 Netty中的线程模型与Reactor的联系 在Netty中主要是通过NioEventLoopGroup线程池来实现具体的线程模型的。 单线程模型 单线程模型就是指定一个线程执行客户端连接和读写操作,也就是在一个Reactor中完成。对应的实现方式就是将NioEventLoopGroup线程数设置为1. Netty中是这样构造单线程模型的: 1 2 3 4 5 6 7 8 NioEventLoopGroup group = new NioEventLoopGroup(1); ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(group) .channel(NioServerSocketChannel.class) .channel(NioServerSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) .option(ChannelOption.SO_BACKLOG, 1024) .childHandler(new ServerHandlerInitializer()); 多线程模型 多线程模型就是当Reactor进行客户端的连接处理,然后业务处理交由线程池来执行。 Netty中是这样构造多线程模型的: 1 2 3 4 5 6 7 NioEventLoopGroup eventGroup = new NioEventLoopGroup(); ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(eventGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) .option(ChannelOption.SO_BACKLOG, 1024) .childHandler(new ServerHandlerInitializer()); 主从多线程模型(最常使用) 主从多线程模型是有多个Reactor,也就是有多个selector,所以我们定义一个bossGroup和一个workGroup 在Netty中是这样构建主从多线程模型的: 1 2 3 4 5 6 7 8 NioEventLoopGroup bossGroup = new NioEventLoopGroup(); NioEventLoopGroup workerGroup = new NioEventLoopGroup(); ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(bossGroup,workerGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) .option(ChannelOption.SO_BACKLOG, 1024) .childHandler(new ServerHandlerInitializer()); 相较于多线程模型,主从多线程模型不会遇到处理连接的瓶颈问题。在多线程模型下,因为只有一个NIO的Acceptor来处理连接请求,所以会出现性能瓶颈。 NioEventLoop源码分析 在Netty线程模型中,NioEventLoop是比较关键的类。下面我们对它的实现进行分析。 它的继承关系图如下: NioEventLoop需要处理网络IO请求,因此有一个多路复用器Selector: 1 2 3 4 5 private Selector selector; private Selector unwrappedSelector; private SelectedSelectionKeySet selectedKeys; private final SelectorProvider provider; 并且在构造方法中完成了初始化: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider, SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler, EventLoopTaskQueueFactory queueFactory) { super(parent, executor, false, newTaskQueue(queueFactory), newTaskQueue(queueFactory), rejectedExecutionHandler); if (selectorProvider == null) { throw new NullPointerException("selectorProvider"); } if (strategy == null) { throw new NullPointerException("selectStrategy"); } provider = selectorProvider; final SelectorTuple selectorTuple = openSelector(); selector = selectorTuple.selector; unwrappedSelector = selectorTuple.unwrappedSelector; selectStrategy = strategy; } 在NioEventLoop中run()方法比较的关键: 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 protected void run() { for (;;) { try { try { //通过hasTasks方法判断队列中是否还有未处理的方法 switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) { case SelectStrategy.CONTINUE: continue; case SelectStrategy.BUSY_WAIT: // fall-through to SELECT since the busy-wait is not supported with NIO //没有任务则执行,select()执行网络IO case SelectStrategy.SELECT: select(wakenUp.getAndSet(false)); if (wakenUp.get()) { selector.wakeup(); } // fall through default: } } catch (IOException e) { //如果本轮Selector的轮询结果为null,那么可能触发了jdk epoll的bug //该bug会导致IO线程处于100%的状态,需要重建Selector来解决 rebuildSelector0(); handleLoopException(e); continue; } cancelledKeys = 0; needsToSelectAgain = false; //处理IO事件所需的事件和花费在处理task的时间的比例,默认为50% final int ioRatio = this.ioRatio; if (ioRatio == 100) { try { //如果比例为100.则表示每次处理完IO后,才开始处理task processSelectedKeys(); } finally { // 执行task任务 runAllTasks(); } } else { //记录处理IO的开始时间 final long ioStartTime = System.nanoTime(); try { //处理IO请求 processSelectedKeys(); } finally { //计算IO请求的耗时 final long ioTime = System.nanoTime() - ioStartTime; //执行task。判断执行task任务时间是否超过配置的比例,如果超过则停止执行task runAllTasks(ioTime * (100 - ioRatio) / ioRatio); } } } catch (Throwable t) { handleLoopException(t); } // Always handle shutdown even if the loop processing threw an exception. try { if (isShuttingDown()) { closeAll(); if (confirmShutdown()) { return; } } } catch (Throwable t) { handleLoopException(t); } } } 重建Selector的方法如下: 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 private void rebuildSelector0() { final Selector oldSelector = selector; final SelectorTuple newSelectorTuple; if (oldSelector == null) { return; } try { //创建一个新的Selector newSelectorTuple = openSelector(); } catch (Exception e) { logger.warn("Failed to create a new Selector.", e); return; } // Register all channels to the new Selector. int nChannels = 0; for (SelectionKey key: oldSelector.keys()) { //将原Selector上注册的所有SelectionKey转移到新的Selector Object a = key.attachment(); try { if (!key.isValid() || key.channel().keyFor(newSelectorTuple.unwrappedSelector) != null) { continue; } int interestOps = key.interestOps(); key.cancel(); SelectionKey newKey = key.channel().register(newSelectorTuple.unwrappedSelector, interestOps, a); if (a instanceof AbstractNioChannel) { // Update SelectionKey ((AbstractNioChannel) a).selectionKey = newKey; } nChannels ++; } catch (Exception e) { logger.warn("Failed to re-register a Channel to the new Selector.", e); if (a instanceof AbstractNioChannel) { AbstractNioChannel ch = (AbstractNioChannel) a; ch.unsafe().close(ch.unsafe().voidPromise()); } else { @SuppressWarnings("unchecked") NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a; invokeChannelUnregistered(task, key, e); } } } //用新的Selector替换旧的 selector = newSelectorTuple.selector; unwrappedSelector = newSelectorTuple.unwrappedSelector; try { // time to close the old selector as everything else is registered to the new one //关闭旧的Selector oldSelector.close(); } catch (Throwable t) { if (logger.isWarnEnabled()) { logger.warn("Failed to close the old Selector.", t); } } if (logger.isInfoEnabled()) { logger.info("Migrated " + nChannels + " channel(s) to the new Selector."); } } 处理IO请求的是由processSelectedKey完成的,它的实现如下: 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 private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) { final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe(); // 省略代码 ...... try { int readyOps = k.readyOps(); // We first need to call finishConnect() before try to trigger a read(...) or write(...) as otherwise // the NIO JDK channel implementation may throw a NotYetConnectedException. if ((readyOps & SelectionKey.OP_CONNECT) != 0) { // remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking // See https://github.com/netty/netty/issues/924 int ops = k.interestOps(); ops &= ~SelectionKey.OP_CONNECT; k.interestOps(ops); unsafe.finishConnect(); } if ((readyOps & SelectionKey.OP_WRITE) != 0) { // Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write ch.unsafe().forceFlush(); } if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) { unsafe.read(); } } catch (CancelledKeyException ignored) { unsafe.close(unsafe.voidPromise()); } } 首先获取 Channel 的 NioUnsafe,所有的读写等操作都在 Channel 的 unsafe 类中操作。 获取 SelectionKey 就绪事件,如果是 OP_CONNECT,则说明已经连接成功,并把注册的 OP_CONNECT 事件取消。 如果是 OP_WRITE 事件,说明可以继续向 Channel 中写入数据,当写完数据后用户自己吧 OP_WRITE 事件取消掉。 如果是 OP_READ 或 OP_ACCEPT 事件,则调用 unsafe.read() 进行读取数据。unsafe.read() 中会调用到 ChannelPipeline 进行读取数据。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 private final class NioMessageUnsafe extends AbstractNioUnsafe { @Override public void read() { // 省略代码 ...... // 获取 Channel 对应的 ChannelPipeline final ChannelPipeline pipeline = pipeline(); boolean closed = false; Throwable exception = null; try { // 省略代码 ...... int size = readBuf.size(); for (int i = 0; i < size; i ++) { readPending = false; // 委托给 pipeline 中的 Handler 进行读取数据 pipeline.fireChannelRead(readBuf.get(i)); } 当 NioEventLoop 读取数据的时候会委托给 Channel 中的 unsafe 对象进行读取数据。 Unsafe中真正读取数据是交由 ChannelPipeline 来处理。 ChannelPipeline 中是注册的我们自定义的 Handler,然后由 ChannelPipeline中的 Handler 一个接一个的处理请求的数据。 作者:jijs 链接:https://www.jianshu.com/p/9e5e45a23309 来源:简书 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2020/5/16
articleCard.readMore

段页式内存管理

内存管理需要解决的问题 内存管理无非就是解决三个问题: 如何使进程的地址空间隔离 如果提高内存的使用效率 如何解决程序运行时的重定位问题 现在的内存管理方案就是引入虚拟内存这一中间层。虚拟内存位于程序和物理内存之间,程序只能看见虚拟内存,不能直接访问物理内存。每个程序都有自己独立的虚拟地址空间,这样就做到了进程地址空间的隔离。 引入了虚拟地址技术后,我们需要解决如何将虚拟地址映射到物理地址。这主要有分段和分页两种技术。 分段机制 这种方法的基本思路是将程序所需要的内存地址空间大小的虚拟空间映射到某个物理地址空间。 分段机制使得每个进程具有独立的进程地址空间,保证了地址空间的隔离性。同时它也解决了程序重定位的问题,因为程序始终是在虚拟地址空间下运行的。 分段机制同样也存在许多的问题,因为它映射的粒度太大,是以程序为单位的,如果内存不足,那么只能换出整个程序。这样内存的使用效率就很低。 分页机制 分页机制就是将内存地址空间分为若干各很小的固定大小的页,每一页的大小由内存来决定。这样映射的粒度更小了,根据局部性原理,我们只需要在内存中保存少部分的页,大部分的页都可以换到磁盘中去。 页式存储管理能够有效的提高内存利用率,而分段存储管理能反映程序的逻辑结构并有利于段的共享 段页式管理 段页式管理就是将程序分为多个逻辑段,在每个段里面又进行分页,即将分段和分页组合起来使用。 在段页式系统中,作业的逻辑地址分为三部分:段号、页号和页内偏移量。 为了实现地址变化,系统为每个进程维护了一个段表,每个分段又有一个页表。段表中包含段号、页表长度和页表的起始地址。页表中包含页号和块号。系统还有一个段表寄存器,存储段表的起始地址和段表长度。 在进行地址变化式,通过段表查到页表的起始地址,然后再通过页表查到物理块的地址。

2020/5/15
articleCard.readMore

fork()与写时复制

没有写时复制时的问题 最初在Unit系统中,在使用fork()系统调用创建子进程的时候,会复制父进程的整个地址空间并把复制的那一份分配给子进程。这种情况比较耗时。因为它需要: 为子进程的页表分配空间 为子进程的页分配页面 初始子进程的页表 把父进程的页复制到子进程相应的页中。 创建一个地址空间的这种方法涉及许多内存访问,消耗许多CPU周期,并且完全破环了高速缓存中的内容。在大多数情况下,这种做法常常时毫无意义的,因为许多子进程通过装入一个新的程序开始它们的执行,这样就完全丢弃了所继承的地址空间。 Linux的fork()使用写时复制 写时复制技术时一种可以推迟甚至避免拷贝数据的技术。内核不需要复制整个地址空间,而是让父子进程共享同一个地址空间,只用在需要写入的时候才会复制地址空间,从而使各个进程拥有自己的地址空间。 写时复制 内核只为新生成的子进程创建虚拟空间结构,它们复制于父进程的虚拟空间结构,但是不为这些段分配物理内存,它们共享父进程的空间,当父进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。 vfork() 这个方案直接利用父进程的虚拟地址空间,vfork()并不会把父进程的地址空间完全复制给子进程,因为子进程会立即调用exec或exit,也就不会访问该地址空间了。在子进程调用exec之前,它在父进程空间中运行。vfork()保证子进程先运行,在子进程调用exec或exit之后父进程才能调度运行。

2020/5/15
articleCard.readMore

Linux内核设计与实现读书笔记一

进程管理 进程 相关概念 进程就是处于执行期的程序。进程是处于执行期的程序以及相关资源的总称。 线程是进程中的活动对象,每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。 内核调度的对象是线程而不是进程。 进程描述符及任务结构 内核吧进程的列表存放在叫做任务队列的双向循环列表中。链表中每一项的类型都是task_struct、被称为进程描述符。该结构包含了内核管理一个进程所需的所有信息。 分配进程描述符 Linux通过slab分配器分配task_struct结构,这样能够达到对象复用和缓存着色的目的。 在2.6之前为了减少对寄存器的使用,task_struct存储在内核栈的尾端,而在2.6之后使用slab分配器动态生成task_stuct,所以只需要在栈第或栈顶创建一个thread_info,它的内部包含了task_struct等信息。 每个任务的thread_info结构在它的内核栈的尾端分配。结构中task域中存放的是指向该任务实际task_struct的指针。 进程描述符的存放 内核通过一个唯一的进程标识符或PID来标识每个进程。 在内核中,任务访问通常需要获得指向其task_struct的指针。内核使用current宏可以计算出当前进程task_struct的指针。 进程管理 进程描述符中state域描述了进程的当前状态。系统中每个进程都必然处于五种状态中的一种。 TASK_RUNNING(运行):进程是可执行的,它或者正在执行,或者在运行队列中等待执行。 TASK_INTERRUPTIBLE(可中断):进程正在随眠,等待某些条件达成。一旦这些条件达成,内核就会把进程状态设置为运行。 TASK_UNINTERRUPTIBLE(不可中断):除了就算接收到信号也不会被唤醒或准备投入运行外,这个状态与可打断状态相同。 _TASK_TRACED:被其它进程跟踪的进程,例如通过ptrace对调试程序进行跟踪。 _TASK_STOOPED:进程停止运行。 进程上下文 一般的程序是在用户空间执行,当程序进行了系统调用或触发了某一异常,那么它就会陷入到内核空间。此时,我们称内核“代表进程执行”并处于进程上下文中。在此上下问中current宏是有效的。 进程创建 在Unix操作系统中进程的创建分为两步完成,第一步是调用fork()方法拷贝当前进程创建一个子进程(父子进程的唯一区别就是PID),然后调用exec()方法载入可执行文件并开始执行。 而Linux对整个进程的创建做出了优化: 写时拷贝 写时拷贝时一种可以推迟甚至免除拷贝数据的技术,在创建进程的时候,内核并不需要再一开始就复制整个进程地址空间,而是让父子进程共享一个拷贝。只有再需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是所,资源的复制只有在需要写入的时候才进行,在此之前,只是以只读的方式共享,这种技术使得地址空间上的页的拷贝被推迟到实际发生写入的时候才进行,在页根本不会被写入的情况下(比如fork后立即调用exec)它们就无需复制了。 也就是只有进程空间的各段的内容要发生变化时,才将父进程的内容复制一份给子进程。 fork() Linux通过clone()系统调用来实现fork()。然后clone()去调用do_fork()。do_fork()完成了创建中的大部分工作,该函数调用了copy_process()函数,然后让进程开始运行。copy_process()函数完成的工作如下: 调用dup_task_struct()为新建成创建一个内核栈,thread_info结构和task_struct,这些只与当前进程的只相同。此时,子进程和父进程的描述符使完全相同的。 检测创建了这个子进程之后,当前用户所拥有的进程数目有没有超出给它分配资源的限制。 子进程着手使自己与父进程区别开来。将进程描述符内的许多成员清零或设置为初始值。 将子进程的状态设置为TASK_UNINTERRUPTIBLE,以保证它不会投入运行。 copy_process()调用copy_flags()以更新task_struct的flag成员。 调用alloc_pid()为新进程分配一个有效的PID。 根据传递给clone()的参数标志,决定拷贝或共享打开的资源。 最后copy_process做扫尾工作并返回一个指向子进程的指针。 vfork() 除了不拷贝父进程的页表项外,vfork()系统调用和fork()的功能相同。子进程作为父进程的一个单独的线程在他的地址空间里运行,父进程被阻塞,直到子进程退出或执行exec(). vfork()系统调用的实现是通过项clone()系统调用实现的: 在调用copy_process()时,task_struct的vfor_done成员被设置为NULL。 在执行do_fork()时,如果给定特别标志,则vfork_done会指向特定地址。 子进程先开始指向后,父进程不是马上恢复执行,而是一直等待,直到父进程通过vfork_done指针向它发送信号。 在调用mm_release()时,该函数用于进程退出内存地址空间,并且检查vfork_done是否为空,如果不为空,则会向父进程发送信号。 回到do_fork(),父进程醒来并返回。 线程在Linux中的实现 线程机制时现代编程技术中常用的一种抽象概念,该机制提供了在同一程序内共享内存地址空间运行的一组线程。这些线程还可以共享打开的文件或其它资源,线程机制支持并发程序设计技术,在多处理器系统上,它也能保证真正的并行处理。 在Linux中的内核实现中,它不严格区分线程和进程,线程仅仅被视为一个与其它进程共享某些资源的进程。 创建线程 线程的创建于普通进程的创建类似,只不过在调用clone()的时候徐娅传递一些参数标志来指明需要共享的资源。 内核线程 内核进程需要在后台执行一些操作,这些操作由内核线程来完成。 内核线程和普通的进程的区别在于内核线程没有独立的地址空间。它们只在内核空间运行,从来不切换到用户空间去。内核进程和普通进程一样,可以被调度,也可以被抢占。内核线程也只能由其它内核线程创建。 进程终结 当一个进程终结时,内核必须要释放它所占有的资源并通知其父进程。 进程终结时,最终大多都是通过do_exit()来完成的。它主要完成了以下工作: 将task_struct中的标志成员设置为PF_EXITING 调用del_timer_sync()删除任一内核定时器,根据返回的结果,保证没有定时器在排队,也没有定时器处理程序在运行。 如果BSD的进程计帐功能时开启的,那么会调用acct_update_integerals()来输出记账信息。 调用exit_mm()函数释放进程占用mm_struct,如果没有别的进程使用它,就释放。 接下来调用sem_exit()函数,如果进程排队等候IPC信号,它则离开队列。 调用exit_files()和exit_fs(),以分别递减文件描述符,文件系统数据的引用计数。如果引用计数降为0,那么它代表没有进程使用相应的资源,此时就可以释放。 接着把存放在task_struct的exit_code成员中的任务退出代码置为由exit()提供的退出代码。 调用exit_notify()向父进程发送行信号,给子进程重新找养父,养父为线程组中的其它进程或者init进程,并把进程状态设置为EXIT_ZOMBIE. do_exit()调用schedule()切换到新的进程,因为处于EXIT_ZOMBIE状态的进程不会再调度,所以这是进程所执行的最后一段代码。do_exit()永不返回。 删除进程描述符 调用do_exit()之后,尽管线程已经僵死不能再运行了,但是系统还是保留了它们的进程描述符。这样做的好处就是有办法再进程终结后仍然能够获得它的信息。 当需要释放进程描述符的时候,会调用release_task(). 处理孤儿进程 如果父进程在子进程之前退出,必须有机制来保证子进程能找到一个新的父进程,否则这些成为孤儿的进程就会在退出时永远处于僵死状态,白白的消耗内存。 处理孤儿进程的方案是,首先会尝试在当前线程组内找一个线程作为父进程,如果不行,就让init做它们的父进程。 进程调度 多任务 多任务操作系统就是能够同时并发地交互执行多个进程地操作系统。 多任务系统可以划分为两类:非抢占式多任务和抢占式多任务。Linux提供了抢占式多任务模式。 在此模式下,由调度程序来决定什么时候停止一个进程的运行,以便其它进程能够得到执行的机会。这个强制挂起的动作叫做抢占。进程在被抢占之前能够运行的时间式预先分配号的,叫做进程的时间篇。 在非抢占式多任务模式下,除非进程自己主动停止运行,否则它会一直执行。进程主动挂起自己的操作称为让步。 Linux的进程调度 Linux2.5之后采用了一种叫做O(1)的调度程序。 调度策略 IO消耗型和处理器消耗型的进程 进程可以分为IO消耗型和处理器消耗性。前者指进程的大部分时间用来提交IO请求或是等待IO请求。因此,这样的进程进程处于可运行的状态,但通常都是运行短短的一会,因为它在等待更多的IO请求时最后总会阻塞。 而处理器消耗型进程把时间大多用在执行代码上,除非被抢占,否则它门通常都一直不停的运行,因为它们没有太多的IO需求。 调递策略通常要在两个矛盾的目标中间寻找平衡:进程响应迅速(响应时间短)和最大系统利用率(高吞吐率)。 进程优先级 调度算法中最基本的一类就是基于优先级的调度。这是一种根据进程的价值和对处理器时间的需求来对进程分级的想法。 Linux采用了两种不同的优先级范围,第一种是用nice值,它的范围是从-20到+19,nice值越小,优先级越高。第二种范围是实时范围,其值是可配置的,默认情况下它的变化范围是从0到99,值越大,优先级越大。 实时优先级和nice优先级处于互不相交的两个范畴。 时间片 时间片是一个数值,它表示进程在被抢占前所能持续运行的时间。调度策略必须规定一个默认的时间片。 一个处于就绪状态的进程能够进入运行态的完全是由进程优先级和是否有时间片决定的。 Linux调度算法 Linux中提供了一种CFS调度算法,即完全公平调度算法。CFS的实现思路就是根据进程的权重分配运行时间。这里的权重其实是和进程的nice值之间有一一对应的关系,可以通过全局数组prio_to_weight来转换。 $$ 分配给进程的运行时间=调度周期*进程权重/所有进程权重之和 $$ CFS调度算法实现 调度器实体结构 CFS不再有时间片的概念,但是它也必须维护每个进程运行的时间记账,因为它需要确保每个进程只在分配给它的时间片内运行。CFS使用调度器实体结构来最终进程运行记账。 虚拟实时 vruntime变量存放进程的虚拟运行时间,该运行时间的计算是经过了所有可运行进程总数的标准化。 进程选择 当CFS需要选择下一个运行进程是,它会挑一个具有最小vruntime的进程。CFS使用红黑树来组织可运行进程队列,并利用其迅速找到vruntime值最小的进程。 休眠和唤醒 等待队列 休眠通过等待队列进行处理。等待队列是由等待某些事件发生的进程组成的简单链表。 唤醒 唤醒操作通过函数wake_up()进行,它会唤醒指定的等待队列上的所有进程。它调用函数wake_up()进行,它会唤醒指定的等待队列上的所有进程。 抢占 用户抢占 用户抢占在以下情况下产生: 从系统调用返回用户空间时 从中断处理程序返回用户空间时 内核抢占 内核抢占会发生在: 中断处理程序正在执行,且返回内核空间之前。 内核代码再一次具有可抢占性的时候。 如果内核中的任务显示地调用schedule() 如果内核中的任务阻塞。

2020/5/15
articleCard.readMore

Dubbo小知识点总结

Dubbo的几种集群容错方案 集群容错方案解释 Failover(默认)快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。 Failsafe安全失败,出现异常时,直接忽略,通常用于写入日志 Failback失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知等。 Forking并行调用多个服务器,只要有一个成功即放hi。通常用于对实时性要求较高的读操作。 Broadcast广播所有的调用者,逐个调用,任意一台报错即报错。通常用于通知所有的提供者更新缓存本地资源信息。 它的配置方式是这样的: 1 <dubbo:service cluster="failsafe" /> Dubbo的负载均衡算法有哪些 Dubbo内部提供了4种负载均衡算法。 基于权重随机算法的RandomLoadBalance(默认) 基于最少活跃调用数算法的LeastActiveBalance 基于一致性hash算法的ConsistentHashLoadBalance 基于加权轮询算法的RoundRobinLoadBalance 基于加权的轮询算法会根据每台服务器的性能为服务器设置一个权值,加权后,每台服务器能够得到的请求数比例,接近或等于他们的权重比。比如服务器 A、B、C 权重比为 5:2:1。那么在8次请求中,服务器 A 将收到其中的5次请求,服务器 B 会收到其中的2次请求,服务器 C 则收到其中的1次请求。 1 2 3 4 <dubbo:service interface="com.alibaba.hello.api.WorldService" version="1.0.0" ref="helloService" timeout="300" retries="2" loadbalance="random" actives="0" > <dubbo:method name="findAllPerson" timeout="10000" retries="9" loadbalance="leastactive" actives="5" /> <dubbo:service/> 服务化推荐用法 分包 将服务接口、服务模型、服务异常均放在API包中,因为服务模型和异常也是API的一部分,这样做服务分包原则:重用发布等价原则(REP),共同重用原则(CRP) 粒度 服务接口应该尽可能大粒度,每个服务方法应代表一个功能,而不是某功能的一个步骤,否则将面临分布式事务问题,而Dubbo并未提供分布式事务解决方案。 版本 每个接口都应该定义版本号,为后序不兼容升级提供可能。 兼容性 服务接口增加方法,或服务模型增加字段,可向后兼容,删除方法或删除字段,将不兼容,枚举类型新增字段也不兼容,需要通过变更版本号升级。 枚举值 如果是完备集,可以使用Enum 如果后期可能会有变更,建议使用String代替。 序列化 服务参数和返回值建议使用POJO对象。 异常 建议使用异常汇报错误,而不是返回错误码,异常信息能够携带更多信息,并且语义更加友好。 调用 不要只是因为是 Dubbo 调用,而把调用 try...catch 起来。try...catch 应该加上合适的回滚边界上。 Provider 端需要对输入参数进行校验。如有性能上的考虑,服务实现者可以考虑在 API 包上加上服务 Stub 类来完成检验。 Dubbo推荐用法 在Provider端尽可能多配置consumer端属性 因为作为服务的提供方,比服务消费方更清楚服务的性能。在服务提供端配置后,消费端不配置则会使用服务提供端的配置。如果在消费端进行配置,那么对服务端来讲就是不可控的。 一个配置示例: 1 2 3 4 5 6 7 <dubbo:service interface="com.alibaba.hello.api.HelloService" version="1.0.0" ref="helloService" timeout="300" retries="2" loadbalance="random" actives="0" /> <dubbo:service interface="com.alibaba.hello.api.WorldService" version="1.0.0" ref="helloService" timeout="300" retries="2" loadbalance="random" actives="0" > <dubbo:method name="findAllPerson" timeout="10000" retries="9" loadbalance="leastactive" actives="5" /> <dubbo:service/> 建议在服务提供端配置的消费端属性有: timeout、retries 、 loadbalance actives 在服务提供端配置合理的提供端属性 比如这样: 1 2 3 4 5 <dubbo:protocol threads="200" /> <dubbo:service interface="com.alibaba.hello.api.HelloService" version="1.0.0" ref="helloService" executes="200" > <dubbo:method name="findAllPerson" executes="50" /> </dubbo:service> 建议在服务提供端配置的提供端属性有: threads (服务线程池的大小)、executes(一个服务提供者并行执行请求的上限)

2020/5/12
articleCard.readMore

MyBatis的使用回顾

Mapper文件的编写 select 1 2 3 <select id = “ selectPerson” parameterType = “ int” resultType = “ hashmap” > SELECT * FROM PERSON WHERE ID =#{id} </ select> 这里有两点需要注意的,一是区分#和$的区别.#的底层是基于预编译语句来实现了,这样可以避免SQL注入的风险。而$在底层是通过字符串的直接拼接来实现了,因此有SQL注入的风险。 上面是常见的select的用法,实际上它还有许多其它的属性供我们选用。 比如: 1 2 3 4 5 6 7 8 9 10 11 12 <select id="selectPerson" parameterType="int" parameterMap="deprecated" resultType="hashmap" resultMap="personResultMap" flushCache="false" useCache="true" timeout="10" fetchSize="256" statementType="PREPARED" resultSetType="FORWARD_ONLY"> 下面列举了一些相对比较常用的属性: 属性描述 id parameterType将传递到该语句中的参数的标准类名或别名 resultType从该语句返回的结果预期的标准类名或别名 resultMap对外部resultMap的引用,用于结果集映射 flushCache若将该属性设置为true,那么调用此语句时会刷新本地和二级缓存 useCache将此属性设置为true,这该语句的结果会被缓存到二级缓存中。默认值为true timeout超时时间 fetchSize设置成批的返回的行数 statementType设置statement的类型,有STATEMENT,PREPARED或CALLABLE.默认是第二种,即PreparedStatement insert,update and delete 这三个标签常见的用法如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <insert id="insertAuthor" parameterType="domain.blog.Author" flushCache="true" statementType="PREPARED" keyProperty="" keyColumn="" useGeneratedKeys="" timeout="20"> <update id="updateAuthor" parameterType="domain.blog.Author" flushCache="true" statementType="PREPARED" timeout="20"> <delete id="deleteAuthor" parameterType="domain.blog.Author" flushCache="true" statementType="PREPARED" timeout="20"> 一些属性的用法补充: 属性解释 useGeneratedKeys当设置为true的时候,使用数据库内部生成的主键。默认为false keyProperty该属性一般与useGeneratedKeys结合使用,它标识的是Java对象的属性名。配置了该属性之后,会将数据库中自动生成的主键存到对应的java属性中。 keyColumn这几个属性一般是结合使用的,keyColumn指定数据库主键字段名。 sql片段 sql片段是mybatis动态sql的基础。它的使用方法如下: 1 2 3 4 5 6 7 8 9 10 <sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql> <select id="selectUsers" resultType="map"> select <include refid="userColumns"><property name="alias" value="t1"/></include>, <include refid="userColumns"><property name="alias" value="t2"/></include> from some_table t1 cross join some_table t2 </select> 这里通过属性,为sql片段传递数据,使用起来非常的灵活: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <sql id="sometable"> ${prefix}Table </sql> <sql id="someinclude"> from <include refid="${include_target}"/> </sql> <select id="select" resultType="map"> select field1, field2, field3 <include refid="someinclude"> <property name="prefix" value="Some"/> <property name="include_target" value="sometable"/> </include> </select> resultMap 结果集映射是mybatis非常重要的特性 它的简单的使用方法如下: 1 2 3 4 5 <resultMap id="userResultMap" type="User"> <id property="id" column="user_id" /> <result property="username" column="user_name"/> <result property="password" column="hashed_password"/> </resultMap> 在<resultMap>还可以使用<constructor>标签,通过构造器完成结果集Java对象的映射。 1 2 3 4 5 <constructor> <idArg column="id" javaType="int"/> <arg column="username" javaType="String"/> <arg column="age" javaType="_int"/> </constructor> 用于一对一关联查询结果映射的<assocation>标签的使用方式: 1 2 3 4 5 6 7 8 9 10 11 <resultMap id="blogResult" type="Blog"> <association property="author" column="author_id" javaType="Author" select="selectAuthor"/> </resultMap> <select id="selectBlog" resultMap="blogResult"> SELECT * FROM BLOG WHERE ID = #{id} </select> <select id="selectAuthor" resultType="Author"> SELECT * FROM AUTHOR WHERE ID = #{id} </select> 另一个例子: 1 2 3 4 5 6 7 8 9 10 11 <resultMap id="blogResult" type="Blog"> <id property="id" column="blog_id" /> <result property="title" column="blog_title"/> <association property="author" javaType="Author"> <id property="id" column="author_id"/> <result property="username" column="author_username"/> <result property="password" column="author_password"/> <result property="email" column="author_email"/> <result property="bio" column="author_bio"/> </association> </resultMap> 一对多的关系映射的标签<collection>标签的使用方式: 1 2 3 4 5 <collection property="posts" ofType="domain.blog.Post"> <id property="id" column="post_id"/> <result property="subject" column="post_subject"/> <result property="body" column="post_body"/> </collection> 1 private List<Post> posts; 另一个例子是这样的: 1 2 3 4 5 6 7 8 9 <resultMap id="blogResult" type="Blog"> <id property="id" column="blog_id" /> <result property="title" column="blog_title"/> <collection property="posts" ofType="Post"> <id property="id" column="post_id"/> <result property="subject" column="post_subject"/> <result property="body" column="post_body"/> </collection> </resultMap> 鉴别器<discriminator>,它可以根据 1 2 3 4 5 6 7 8 9 10 11 12 13 14 <resultMap id="vehicleResult" type="Vehicle"> <id property="id" column="id" /> <result property="vin" column="vin"/> <result property="year" column="year"/> <result property="make" column="make"/> <result property="model" column="model"/> <result property="color" column="color"/> <discriminator javaType="int" column="vehicle_type"> <case value="1" resultMap="carResult"/> <case value="2" resultMap="truckResult"/> <case value="3" resultMap="vanResult"/> <case value="4" resultMap="suvResult"/> </discriminator> </resultMap> 动态SQL if 1 2 3 4 5 6 7 8 <select id="findActiveBlogWithTitleLike" resultType="Blog"> SELECT * FROM BLOG WHERE state = ‘ACTIVE’ <if test="title != null"> AND title like #{title} </if> </select> choose, when, otherwise 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <select id="findActiveBlogLike" resultType="Blog"> SELECT * FROM BLOG WHERE state = ‘ACTIVE’ <choose> <when test="title != null"> AND title like #{title} </when> <when test="author != null and author.name != null"> AND author_name like #{author.name} </when> <otherwise> AND featured = 1 </otherwise> </choose> </select> trim, where, set 1 2 3 <trim prefix="WHERE" prefixOverrides="AND |OR "> ... </trim> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <select id="findActiveBlogLike" resultType="Blog"> SELECT * FROM BLOG <where> <if test="state != null"> state = #{state} </if> <if test="title != null"> AND title like #{title} </if> <if test="author != null and author.name != null"> AND author_name like #{author.name} </if> </where> </select> 1 2 3 4 5 6 7 8 9 10 <update id="updateAuthorIfNecessary"> update Author <set> <if test="username != null">username=#{username},</if> <if test="password != null">password=#{password},</if> <if test="email != null">email=#{email},</if> <if test="bio != null">bio=#{bio}</if> </set> where id=#{id} </update> foreach 1 2 3 4 5 6 7 8 9 <select id="selectPostIn" resultType="domain.blog.Post"> SELECT * FROM POST P WHERE ID in <foreach item="item" index="index" collection="list" open="(" separator="," close=")"> #{item} </foreach> </select>

2020/5/12
articleCard.readMore

Dubbo集群容错

简介 Dubbo集群容错方面的源码包括四个部分,分别式服务目录Directory、服务路由Router、集群Cluster和负载均衡LoadBalance。 它们之间的关系是这样的: 服务目录 简介 服务目录中存储了服务提供者有关的信息,通过服务目录,服务消费者可以获取到服务提供者的信息,比如IP、端口、服务协议等。通过这些信息,服务消费者就可以进行远程服务调用了。服务提供者的信息是有变动的,因此服务目录中的信息也有要做相应的变更。 而服务目录中的信息,其实又是从注册中心中获取的,然后根据从注册中心中获取的信息为每条配置信息生成一个Invoker对象。 因此简单来讲服务目录就是一个会根据注册中心的有关信息进行相应调整的Invoker集合。 Dubbo中服务目录的继承体系如图: 源码分析 针对服务目录,我们主要分析一个AbstractDirectory和它的两个子类。 下面我们来看AbstractDirectory的具体实现: 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 public List<Invoker<T>> list(Invocation invocation) throws RpcException { if (destroyed) { throw new RpcException("Directory already destroyed..."); } // 调用 doList 方法列举 Invoker,doList 是模板方法,由子类实现 List<Invoker<T>> invokers = doList(invocation); // 获取路由 Router 列表 List<Router> localRouters = this.routers; if (localRouters != null && !localRouters.isEmpty()) { for (Router router : localRouters) { try { // 获取 runtime 参数,并根据参数决定是否进行路由 if (router.getUrl() == null || router.getUrl().getParameter(Constants.RUNTIME_KEY, false)) { // 进行服务路由 invokers = router.route(invokers, getConsumerUrl(), invocation); } } catch (Throwable t) { logger.error("Failed to execute router: ..."); } } } return invokers; } // 模板方法,由子类实现 protected abstract List<Invoker<T>> doList(Invocation invocation) throws RpcException; AbstractDirectory的list方法,主要完成两件事情: 通过doList获取Invoker列表 根据Router的getUrl返回值为空与否,以及runtime参数决定是否进行服务路由。 这里的doList方法其实是一个模板方法,由它的子类来负责具体的实现。 那么下面我们就来看一看它的两个子类是如何实现这个方法的。 StaticDirectory StaticDirectory即静态服务目录,它内部存放的Invoker集合是不会变动的。它的源码实现如下: 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 public class StaticDirectory<T> extends AbstractDirectory<T> { // Invoker 列表 private final List<Invoker<T>> invokers; // 省略构造方法 @Override public Class<T> getInterface() { // 获取接口类 return invokers.get(0).getInterface(); } // 检测服务目录是否可用 @Override public boolean isAvailable() { if (isDestroyed()) { return false; } for (Invoker<T> invoker : invokers) { if (invoker.isAvailable()) { // 只要有一个 Invoker 是可用的,就认为当前目录是可用的 return true; } } return false; } @Override public void destroy() { if (isDestroyed()) { return; } // 调用父类销毁逻辑 super.destroy(); // 遍历 Invoker 列表,并执行相应的销毁逻辑 for (Invoker<T> invoker : invokers) { invoker.destroy(); } invokers.clear(); } @Override protected List<Invoker<T>> doList(Invocation invocation) throws RpcException { // 列举 Inovker,也就是直接返回 invokers 成员变量 return invokers; } } 它的实现非常的简单。 RegistryDirctory RegistryDirectory是一种动态服务目录,它会根据注册中心中服务配置的变化而动态的变化。因此RegistryDirectory中比较关键的点就在于,它是如何进行Invoker列举的?它是如何接收服务配置信息变更的?它是如何刷新Invoker列表的。 列举Invoker 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 public List<Invoker<T>> doList(Invocation invocation) { if (forbidden) { // 服务提供者关闭或禁用了服务,此时抛出 No provider 异常 throw new RpcException(RpcException.FORBIDDEN_EXCEPTION, "No provider available from registry ..."); } List<Invoker<T>> invokers = null; // 获取 Invoker 本地缓存 Map<String, List<Invoker<T>>> localMethodInvokerMap = this.methodInvokerMap; if (localMethodInvokerMap != null && localMethodInvokerMap.size() > 0) { // 获取方法名和参数列表 String methodName = RpcUtils.getMethodName(invocation); Object[] args = RpcUtils.getArguments(invocation); // 检测参数列表的第一个参数是否为 String 或 enum 类型 if (args != null && args.length > 0 && args[0] != null && (args[0] instanceof String || args[0].getClass().isEnum())) { // 通过 方法名 + 第一个参数名称 查询 Invoker 列表,具体的使用场景暂时没想到 invokers = localMethodInvokerMap.get(methodName + "." + args[0]); } if (invokers == null) { // 通过方法名获取 Invoker 列表 invokers = localMethodInvokerMap.get(methodName); } if (invokers == null) { // 通过星号 * 获取 Invoker 列表 invokers = localMethodInvokerMap.get(Constants.ANY_VALUE); } // 冗余逻辑,pull request #2861 移除了下面的 if 分支代码 if (invokers == null) { Iterator<List<Invoker<T>>> iterator = localMethodInvokerMap.values().iterator(); if (iterator.hasNext()) { invokers = iterator.next(); } } } // 返回 Invoker 列表 return invokers == null ? new ArrayList<Invoker<T>>(0) : invokers; } Invoker的列举逻辑还是比较简单的,主要就是从localMethodInvokerMap中获取对应的Invoker 接收服务变更通知 RegistryDirectory是一个动态服务目录,会随注册中心配置的变化而进行动态调整,因此RegistryDirectory实现了NotifyListener接口,通过这个接口获取注册中心变更通知。 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 public synchronized void notify(List<URL> urls) { // 定义三个集合,分别用于存放服务提供者 url,路由 url,配置器 url List<URL> invokerUrls = new ArrayList<URL>(); List<URL> routerUrls = new ArrayList<URL>(); List<URL> configuratorUrls = new ArrayList<URL>(); for (URL url : urls) { String protocol = url.getProtocol(); // 获取 category 参数 String category = url.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY); // 根据 category 参数将 url 分别放到不同的列表中 if (Constants.ROUTERS_CATEGORY.equals(category) || Constants.ROUTE_PROTOCOL.equals(protocol)) { // 添加路由器 url routerUrls.add(url); } else if (Constants.CONFIGURATORS_CATEGORY.equals(category) || Constants.OVERRIDE_PROTOCOL.equals(protocol)) { // 添加配置器 url configuratorUrls.add(url); } else if (Constants.PROVIDERS_CATEGORY.equals(category)) { // 添加服务提供者 url invokerUrls.add(url); } else { // 忽略不支持的 category logger.warn("Unsupported category ..."); } } if (configuratorUrls != null && !configuratorUrls.isEmpty()) { // 将 url 转成 Configurator this.configurators = toConfigurators(configuratorUrls); } if (routerUrls != null && !routerUrls.isEmpty()) { // 将 url 转成 Router List<Router> routers = toRouters(routerUrls); if (routers != null) { setRouters(routers); } } List<Configurator> localConfigurators = this.configurators; this.overrideDirectoryUrl = directoryUrl; if (localConfigurators != null && !localConfigurators.isEmpty()) { for (Configurator configurator : localConfigurators) { // 配置 overrideDirectoryUrl this.overrideDirectoryUrl = configurator.configure(overrideDirectoryUrl); } } // 刷新 Invoker 列表 refreshInvoker(invokerUrls); } notify 方法首先是根据 url 的 category 参数对 url 进行分门别类存储,然后通过 toRouters 和 toConfigurators 将url 列表转成 Router 和Configurator 列表。最后调用 refreshInvoker 方法刷新 Invoker 列表。 刷新Invoker列表 refreshInvoker 方法是保证 RegistryDirectory 随注册中心变化而变化的关键所在。 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 private void refreshInvoker(List<URL> invokerUrls) { // invokerUrls 仅有一个元素,且 url 协议头为 empty,此时表示禁用所有服务 if (invokerUrls != null && invokerUrls.size() == 1 && invokerUrls.get(0) != null && Constants.EMPTY_PROTOCOL.equals(invokerUrls.get(0).getProtocol())) { // 设置 forbidden 为 true this.forbidden = true; this.methodInvokerMap = null; // 销毁所有 Invoker destroyAllInvokers(); } else { this.forbidden = false; Map<String, Invoker<T>> oldUrlInvokerMap = this.urlInvokerMap; if (invokerUrls.isEmpty() && this.cachedInvokerUrls != null) { // 添加缓存 url 到 invokerUrls 中 invokerUrls.addAll(this.cachedInvokerUrls); } else { this.cachedInvokerUrls = new HashSet<URL>(); // 缓存 invokerUrls this.cachedInvokerUrls.addAll(invokerUrls); } if (invokerUrls.isEmpty()) { return; } // 将 url 转成 Invoker Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls); // 将 newUrlInvokerMap 转成方法名到 Invoker 列表的映射 Map<String, List<Invoker<T>>> newMethodInvokerMap = toMethodInvokers(newUrlInvokerMap); // 转换出错,直接打印异常,并返回 if (newUrlInvokerMap == null || newUrlInvokerMap.size() == 0) { logger.error(new IllegalStateException("urls to invokers error ...")); return; } // 合并多个组的 Invoker this.methodInvokerMap = multiGroup ? toMergeMethodInvokerMap(newMethodInvokerMap) : newMethodInvokerMap; this.urlInvokerMap = newUrlInvokerMap; try { // 销毁无用 Invoker destroyUnusedInvokers(oldUrlInvokerMap, newUrlInvokerMap); } catch (Exception e) { logger.warn("destroyUnusedInvokers error. ", e); } } } refreshInvoker 方法首先会根据入参 invokerUrls 的数量和协议头判断是否禁用所有的服务,如果禁用,则将 forbidden设为 true,并销毁所有的 Invoker。若不禁用,则将 url 转成 Invoker,得到 <url, Invoker> 的映射关系。然后进一步进行转换,得到 <methodName, Invoker 列表>映射关系。之后进行多组 Invoker合并操作,并将合并结果赋值给 methodInvokerMap。methodInvokerMap 变量在 doList 方法中会被用到,doList 会对该变量进行读操作,在这里是写操作。当新的 Invoker 列表生成后,还要一个重要的工作要做,就是销毁无用的 Invoker,避免服务消费者调用已下线的服务的服务。 到此就实现了Invoker的刷新。 服务路由 简介 服务路由就是包含一条路由规则,路由规则决定了服务消费者的调用目标,即规定了服务消费者可调用可调用哪些服务提供者。Dubbo目前提供了三种服务路由实现,分别是条件路ConditionRouter、脚本路由ScriptRounter和标签路路由TagRounter。其中条件路由是我们最常用的。 源码分析 下面我们就以条件路由为例进行源码分析。 条件路由规则有两个条件组成,分别用于对服务消费者和提供者进行匹配。比如有这样一条规则: host=10.20.153.10 => host=12.20.153.11 这条规则表明IP10.20.153.10的服务消费者只能调用IP为10.20.153.11机器上的服务,不可调用其它机器上的服务。条件路由规则的格式如下: 1 [服务消费者匹配条件] => [服务提供者匹配条件] 表达式解析 路由规则是一条字符串表达式,在进行路由之前会先进行条件表达式解析,具体的解析过程这里就不看源码了。 只需要知道通过解之后,得到一个Map<String, MatchPair> condition.解析后的信息,以这样的格式进行表示: 1 2 3 4 5 6 7 8 9 10 { "host": { "matches": ["2.2.2.2"], "mismatches": ["1.1.1.1"] }, "method": { "matches": ["hello"], "mismatches": [] } } 路由服务 服务路由的入口方法是ConditionRouter的route方法,该方法定义在Router接口中,实现代码如下: 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 public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException { if (invokers == null || invokers.isEmpty()) { return invokers; } try { // 先对服务消费者条件进行匹配,如果匹配失败,表明服务消费者 url 不符合匹配规则, // 无需进行后续匹配,直接返回 Invoker 列表即可。比如下面的规则: // host = 10.20.153.10 => host = 10.0.0.10 // 这条路由规则希望 IP 为 10.20.153.10 的服务消费者调用 IP 为 10.0.0.10 机器上的服务。 // 当消费者 ip 为 10.20.153.11 时,matchWhen 返回 false,表明当前这条路由规则不适用于 // 当前的服务消费者,此时无需再进行后续匹配,直接返回即可。 if (!matchWhen(url, invocation)) { return invokers; } List<Invoker<T>> result = new ArrayList<Invoker<T>>(); // 服务提供者匹配条件未配置,表明对指定的服务消费者禁用服务,也就是服务消费者在黑名单中 if (thenCondition == null) { logger.warn("The current consumer in the service blacklist..."); return result; } // 这里可以简单的把 Invoker 理解为服务提供者,现在使用服务提供者匹配规则对 // Invoker 列表进行匹配 for (Invoker<T> invoker : invokers) { // 若匹配成功,表明当前 Invoker 符合服务提供者匹配规则。 // 此时将 Invoker 添加到 result 列表中 if (matchThen(invoker.getUrl(), url)) { result.add(invoker); } } // 返回匹配结果,如果 result 为空列表,且 force = true,表示强制返回空列表, // 否则路由结果为空的路由规则将自动失效 if (!result.isEmpty()) { return result; } else if (force) { logger.warn("The route result is empty and force execute ..."); return result; } } catch (Throwable t) { logger.error("Failed to execute condition router rule: ..."); } // 原样返回,此时 force = false,表示该条路由规则失效 return invokers; } route 方法先是调用 matchWhen 对服务消费者进行匹配,如果匹配失败,直接返回 Invoker 列表。如果匹配成功,再对服务提供者进行匹配,匹配逻辑封装在了 matchThen 方法中。 集群 简介 为了避免单点故障,现在应用通常至少会部署在两台服务器上。对于一些负载比较高的服务,会部署更多的服务器。对于服务消费者来说,同一环境下出现了多个服务提供者。这时会出现一个问题,服务消费者需要决定选择哪个服务提供者进行调用。另外服务调用失败时的处理措施也是需要考虑的。为了处理这些问题,Dubbo定义了集群接口Cluster以及Cluster Invoker.集群Cluster 用途是将多个服务提供者合并为一个 Cluster Invoker,并将这个 Invoker 暴露给服务消费者。这样一来,服务消费者只需通过这个 Invoker 进行远程调用即可,至于具体调用哪个服务提供者,以及调用失败后如何处理等问题,现在都交给集群模块去处理。集群模块是服务提供者和服务消费者的中间层,为服务消费者屏蔽了服务提供者的情况,这样服务消费者就可以专心处理远程调用相关事宜。 集群工作过程可分为两个阶段,第一个阶段是在服务消费者初始化期间,集群 Cluster 实现类为服务消费者创建 Cluster Invoker 实例,即上图中的merge 操作。 第二个阶段是在服务消费者进行远程调用时。以 FailoverClusterInvoker 为例,该类型 Cluster Invoker 首先会调用Directory 的list 方法列举 Invoker 列表(可将 Invoker 简单理解为服务提供者)。Directory的用途是保存 Invoker,可简单类比为 List<Invoker>。其实现类 RegistryDirectory 是一个动态服务目录,可感知注册中心配置的变化,它所持有的 Invoker 列表会随着注册中心内容的变化而变化。每次变化后,RegistryDirectory 会动态增删 Invoker,并调用 Router 的 route方法进行路由,过滤掉不符合路由规则的 Invoker。当 FailoverClusterInvoker 拿到Directory 返回的 Invoker 列表后,它会通过LoadBalance从 Invoker 列表中选择一个 Invoker。最后 FailoverClusterInvoker 会将参数传给 LoadBalance 选择出的 Invoker 实例的 invoke 方法,进行真正的远程调用。 Dubbo集群提供了以下几种容错机制: 1 2 3 4 5 6 Failover Cluster - 失败自动切换 Failfast Cluster - 快速失败 Failsafe Cluster - 失败安全 Failback Cluster - 失败自动恢复 Forking Cluster - 并行调用多个服务提供者 源码分析 Cluster实现类分析 Cluster的实现类负责生成Cluster Invoker. 1 2 3 4 5 6 7 8 9 10 public class FailoverCluster implements Cluster { public final static String NAME = "failover"; @Override public <T> Invoker<T> join(Directory<T> directory) throws RpcException { // 创建并返回 FailoverClusterInvoker 对象 return new FailoverClusterInvoker<T>(directory); } } 它的实现类的逻辑比较简单. Cluster Invoker分析 我们首先从各种 Cluster Invoker的父类 AbstractClusterInvoker 源码开始说起。前面说过,集群工作过程可分为两个阶段,第一个阶段是在服务消费者初始化期间,即服务引出。第二个阶段是在服务消费者进行远程调用时,此时AbstractClusterInvoker的 invoke 方法会被调用。列举 Invoker,负载均衡等操作均会在此阶段被执行。因此下面先来看一下 invoke方法的逻辑。 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 public Result invoke(final Invocation invocation) throws RpcException { checkWhetherDestroyed(); LoadBalance loadbalance = null; // 绑定 attachments 到 invocation 中. Map<String, String> contextAttachments = RpcContext.getContext().getAttachments(); if (contextAttachments != null && contextAttachments.size() != 0) { ((RpcInvocation) invocation).addAttachments(contextAttachments); } // 列举 Invoker List<Invoker<T>> invokers = list(invocation); if (invokers != null && !invokers.isEmpty()) { // 加载 LoadBalance loadbalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(invokers.get(0).getUrl() .getMethodParameter(RpcUtils.getMethodName(invocation), Constants.LOADBALANCE_KEY, Constants.DEFAULT_LOADBALANCE)); } RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation); // 调用 doInvoke 进行后续操作 return doInvoke(invocation, invokers, loadbalance); } // 抽象方法,由子类实现 protected abstract Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException; AbstractClusterInvoker 的 invoke 方法主要用于列举Invoker,以及加载LoadBalance,最后在调用模板方法doInvoke进行后序操作。 下面我们来看FailoverClusterInvoker是如何实现doInvoke的,它在调用失败后,会自动切换Invoke进行重试。它是缺省的Cluster Invoker实现。 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 public class FailoverClusterInvoker<T> extends AbstractClusterInvoker<T> { // 省略部分代码 @Override public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException { List<Invoker<T>> copyinvokers = invokers; checkInvokers(copyinvokers, invocation); // 获取重试次数 int len = getUrl().getMethodParameter(invocation.getMethodName(), Constants.RETRIES_KEY, Constants.DEFAULT_RETRIES) + 1; if (len <= 0) { len = 1; } RpcException le = null; List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyinvokers.size()); Set<String> providers = new HashSet<String>(len); // 循环调用,失败重试 for (int i = 0; i < len; i++) { if (i > 0) { checkWhetherDestroyed(); // 在进行重试前重新列举 Invoker,这样做的好处是,如果某个服务挂了, // 通过调用 list 可得到最新可用的 Invoker 列表 copyinvokers = list(invocation); // 对 copyinvokers 进行判空检查 checkInvokers(copyinvokers, invocation); } // 通过负载均衡选择 Invoker Invoker<T> invoker = select(loadbalance, invocation, copyinvokers, invoked); // 添加到 invoker 到 invoked 列表中 invoked.add(invoker); // 设置 invoked 到 RPC 上下文中 RpcContext.getContext().setInvokers((List) invoked); try { // 调用目标 Invoker 的 invoke 方法 Result result = invoker.invoke(invocation); return result; } catch (RpcException e) { if (e.isBiz()) { throw e; } le = e; } catch (Throwable e) { le = new RpcException(e.getMessage(), e); } finally { providers.add(invoker.getUrl().getAddress()); } } // 若重试失败,则抛出异常 throw new RpcException(..., "Failed to invoke the method ..."); } } FailoverClusterInvoker 的 doInvoke 方法首先是获取重试次数,然后根据重试次数进行循环调用,失败后进行重试。在 for 循环内,首先是通过负载均衡组件选择一个 Invoker,然后再通过这个 Invoker 的 invoke方法进行远程调用。如果失败了,记录下异常,并进行重试。重试时会再次调用父类的list 方法列举 Invoker。 在选择Invoker的时候,使用了select方法主要就是对粘滞连接特性的处理。它的实现如下: 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 protected Invoker<T> select(LoadBalance loadbalance, Invocation invocation, List<Invoker<T>> invokers, List<Invoker<T>> selected) throws RpcException { if (invokers == null || invokers.isEmpty()) return null; // 获取调用方法名 String methodName = invocation == null ? "" : invocation.getMethodName(); // 获取 sticky 配置,sticky 表示粘滞连接。所谓粘滞连接是指让服务消费者尽可能的 // 调用同一个服务提供者,除非该提供者挂了再进行切换 boolean sticky = invokers.get(0).getUrl().getMethodParameter(methodName, Constants.CLUSTER_STICKY_KEY, Constants.DEFAULT_CLUSTER_STICKY); { // 检测 invokers 列表是否包含 stickyInvoker,如果不包含, // 说明 stickyInvoker 代表的服务提供者挂了,此时需要将其置空 if (stickyInvoker != null && !invokers.contains(stickyInvoker)) { stickyInvoker = null; } // 在 sticky 为 true,且 stickyInvoker != null 的情况下。如果 selected 包含 // stickyInvoker,表明 stickyInvoker 对应的服务提供者可能因网络原因未能成功提供服务。 // 但是该提供者并没挂,此时 invokers 列表中仍存在该服务提供者对应的 Invoker。 if (sticky && stickyInvoker != null && (selected == null || !selected.contains(stickyInvoker))) { // availablecheck 表示是否开启了可用性检查,如果开启了,则调用 stickyInvoker 的 // isAvailable 方法进行检查,如果检查通过,则直接返回 stickyInvoker。 if (availablecheck && stickyInvoker.isAvailable()) { return stickyInvoker; } } } // 如果线程走到当前代码处,说明前面的 stickyInvoker 为空,或者不可用。 // 此时继续调用 doSelect 选择 Invoker Invoker<T> invoker = doSelect(loadbalance, invocation, invokers, selected); // 如果 sticky 为 true,则将负载均衡组件选出的 Invoker 赋值给 stickyInvoker if (sticky) { stickyInvoker = invoker; } return invoker; } 从这段代码我们也可以轻松的明白什么是粘滞连接。 在这个方法中又调用了doSelect方法,这个方法的作用就是根据负载均衡策略选择合适的Invoker. 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 private Invoker<T> doSelect(LoadBalance loadbalance, Invocation invocation, List<Invoker<T>> invokers, List<Invoker<T>> selected) throws RpcException { if (invokers == null || invokers.isEmpty()) return null; if (invokers.size() == 1) return invokers.get(0); if (loadbalance == null) { // 如果 loadbalance 为空,这里通过 SPI 加载 Loadbalance,默认为 RandomLoadBalance loadbalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(Constants.DEFAULT_LOADBALANCE); } // 通过负载均衡组件选择 Invoker Invoker<T> invoker = loadbalance.select(invokers, getUrl(), invocation); // 如果 selected 包含负载均衡选择出的 Invoker,或者该 Invoker 无法经过可用性检查,此时进行重选 if ((selected != null && selected.contains(invoker)) || (!invoker.isAvailable() && getUrl() != null && availablecheck)) { try { // 进行重选 Invoker<T> rinvoker = reselect(loadbalance, invocation, invokers, selected, availablecheck); if (rinvoker != null) { // 如果 rinvoker 不为空,则将其赋值给 invoker invoker = rinvoker; } else { // rinvoker 为空,定位 invoker 在 invokers 中的位置 int index = invokers.indexOf(invoker); try { // 获取 index + 1 位置处的 Invoker,以下代码等价于: // invoker = invokers.get((index + 1) % invokers.size()); invoker = index < invokers.size() - 1 ? invokers.get(index + 1) : invokers.get(0); } catch (Exception e) { logger.warn("... may because invokers list dynamic change, ignore."); } } } catch (Throwable t) { logger.error("cluster reselect fail reason is : ..."); } } return invoker; } doSelect 主要做了两件事,第一是通过负载均衡组件选择 Invoker。第二是,如果选出来的 Invoker 不稳定,或不可用,此时需要调用 reselect 方法进行重选。若 reselect 选出来的 Invoker为空,此时定位 invoker 在invokers 列表中的位置 index,然后获取index + 1 处的 invoker,这也可以看做是重选逻辑的一部分。 负责重选的reselect方法的实现如下: 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 private Invoker<T> reselect(LoadBalance loadbalance, Invocation invocation, List<Invoker<T>> invokers, List<Invoker<T>> selected, boolean availablecheck) throws RpcException { List<Invoker<T>> reselectInvokers = new ArrayList<Invoker<T>>(invokers.size() > 1 ? (invokers.size() - 1) : invokers.size()); // 下面的 if-else 分支逻辑有些冗余,pull request #2826 对这段代码进行了简化,可以参考一下 // 根据 availablecheck 进行不同的处理 if (availablecheck) { // 遍历 invokers 列表 for (Invoker<T> invoker : invokers) { // 检测可用性 if (invoker.isAvailable()) { // 如果 selected 列表不包含当前 invoker,则将其添加到 reselectInvokers 中 if (selected == null || !selected.contains(invoker)) { reselectInvokers.add(invoker); } } } // reselectInvokers 不为空,此时通过负载均衡组件进行选择 if (!reselectInvokers.isEmpty()) { return loadbalance.select(reselectInvokers, getUrl(), invocation); } // 不检查 Invoker 可用性 } else { for (Invoker<T> invoker : invokers) { // 如果 selected 列表不包含当前 invoker,则将其添加到 reselectInvokers 中 if (selected == null || !selected.contains(invoker)) { reselectInvokers.add(invoker); } } if (!reselectInvokers.isEmpty()) { // 通过负载均衡组件进行选择 return loadbalance.select(reselectInvokers, getUrl(), invocation); } } { // 若线程走到此处,说明 reselectInvokers 集合为空,此时不会调用负载均衡组件进行筛选。 // 这里从 selected 列表中查找可用的 Invoker,并将其添加到 reselectInvokers 集合中 if (selected != null) { for (Invoker<T> invoker : selected) { if ((invoker.isAvailable()) && !reselectInvokers.contains(invoker)) { reselectInvokers.add(invoker); } } } if (!reselectInvokers.isEmpty()) { // 再次进行选择,并返回选择结果 return loadbalance.select(reselectInvokers, getUrl(), invocation); } } return null; } reselect 方法总结下来其实只做了两件事情,第一是查找可用的 Invoker,并将其添加到 reselectInvokers 集合中。第二,如果 reselectInvokers 不为空,则通过负载均衡组件再次进行选择. 还有一些容错处理的实现类,这里就不分析了。 负载均衡 简介 LoadBalance 中文意思为负载均衡,它的职责是将网络请求,或者其他形式的负载“均摊”到不同的机器上。避免集群中部分服务器压力过大,而另一些服务器比较空闲的情况。通过负载均衡,可以让每台服务器获取到适合自己处理能力的负载。 Dubbo提供了4种负载均衡的实现: RandomLoadBalance:基于权重随机算法 LeastActiveLoadBalance:基于最少活跃连接数算法 ConsistentHashLoadBalance:基于一致性hash算法 RoundRobinLoadBalance:基于加权轮询算法 源码分析 在Dubbo种所有的负载均衡策略均是AbstractLoadBalance的子类,该类实现了LoadBalance接口,并封装了一些公共逻辑。 下面我们来分析一下AbstractLoadBalance中的公共逻辑。 整个负载均衡的入口方法select的实现: 1 2 3 4 5 6 7 8 9 10 11 12 13 @Override public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) { if (invokers == null || invokers.isEmpty()) return null; // 如果 invokers 列表中仅有一个 Invoker,直接返回即可,无需进行负载均衡 if (invokers.size() == 1) return invokers.get(0); // 调用 doSelect 方法进行负载均衡,该方法为抽象方法,由子类实现 return doSelect(invokers, url, invocation); } protected abstract <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation); 它还提供了计算服务提供者权重的计算方法getWeight 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 protected int getWeight(Invoker<?> invoker, Invocation invocation) { // 从 url 中获取权重 weight 配置值 int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.WEIGHT_KEY, Constants.DEFAULT_WEIGHT); if (weight > 0) { // 获取服务提供者启动时间戳 long timestamp = invoker.getUrl().getParameter(Constants.REMOTE_TIMESTAMP_KEY, 0L); if (timestamp > 0L) { // 计算服务提供者运行时长 int uptime = (int) (System.currentTimeMillis() - timestamp); // 获取服务预热时间,默认为10分钟 int warmup = invoker.getUrl().getParameter(Constants.WARMUP_KEY, Constants.DEFAULT_WARMUP); // 如果服务运行时间小于预热时间,则重新计算服务权重,即降权 if (uptime > 0 && uptime < warmup) { // 重新计算服务权重 weight = calculateWarmupWeight(uptime, warmup, weight); } } } return weight; } static int calculateWarmupWeight(int uptime, int warmup, int weight) { // 计算权重,下面代码逻辑上形似于 (uptime / warmup) * weight。 // 随着服务运行时间 uptime 增大,权重计算值 ww 会慢慢接近配置值 weight int ww = (int) ((float) uptime / ((float) warmup / (float) weight)); return ww < 1 ? 1 : (ww > weight ? weight : ww); } 上面是权重的计算过程,该过程主要用于保证当服务运行时长小于服务预热时间时,对服务进行降权,避免让服务在启动之初就处于高负载状态。服务预热是一个优化手段,与此类似的还有 JVM 预热。主要目的是让服务启动后“低功率”运行一段时间,使其效率慢慢提升至最佳状态。 下面我们就以Dubbo的默认负载均衡策略RandomLoadBalance的实现为例来分析这些负载均衡策略是如何实现的。 RandomLoadBalance是加权随机算法的具体实现,它的算法思想很简单。假设我们有一组服务器 servers = [A, B, C],他们对应的权重为 weights = [5, 3, 2],权重总和为10。现在把这些权重值平铺在一维坐标值上,[0, 5)区间属于服务器 A,[5, 8)区间属于服务器 B,[8, 10) 区间属于服务器 C。接下来通过随机数生成器生成一个范围在[0, 10) 之间的随机数,然后计算这个随机数会落到哪个区间上.基于这个思路,它的代码实现也是非常简单的。 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 public class RandomLoadBalance extends AbstractLoadBalance { public static final String NAME = "random"; private final Random random = new Random(); @Override protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) { int length = invokers.size(); int totalWeight = 0; boolean sameWeight = true; // 下面这个循环有两个作用,第一是计算总权重 totalWeight, // 第二是检测每个服务提供者的权重是否相同 for (int i = 0; i < length; i++) { int weight = getWeight(invokers.get(i), invocation); // 累加权重 totalWeight += weight; // 检测当前服务提供者的权重与上一个服务提供者的权重是否相同, // 不相同的话,则将 sameWeight 置为 false。 if (sameWeight && i > 0 && weight != getWeight(invokers.get(i - 1), invocation)) { sameWeight = false; } } // 下面的 if 分支主要用于获取随机数,并计算随机数落在哪个区间上 if (totalWeight > 0 && !sameWeight) { // 随机获取一个 [0, totalWeight) 区间内的数字 int offset = random.nextInt(totalWeight); // 循环让 offset 数减去服务提供者权重值,当 offset 小于0时,返回相应的 Invoker。 // 举例说明一下,我们有 servers = [A, B, C],weights = [5, 3, 2],offset = 7。 // 第一次循环,offset - 5 = 2 > 0,即 offset > 5, // 表明其不会落在服务器 A 对应的区间上。 // 第二次循环,offset - 3 = -1 < 0,即 5 < offset < 8, // 表明其会落在服务器 B 对应的区间上 for (int i = 0; i < length; i++) { // 让随机值 offset 减去权重值 offset -= getWeight(invokers.get(i), invocation); if (offset < 0) { // 返回相应的 Invoker return invokers.get(i); } } } // 如果所有服务提供者权重值相同,此时直接随机返回一个即可 return invokers.get(random.nextInt(length)); } } 到此这个Dubbo集群的源码就分析完毕了。

2020/5/11
articleCard.readMore

Dubbo服务调用过程

简介 Dubbo服务调用的基本过程如图: 首先服务消费者通过代理对象Proxy发起远程调用,接着通过网络客户端将编码后的请求发送给服务提供方的网络层上。Server收到请求之后,首先要做的就是对数据包进行解码。然后将解码后的请求发送至分发器,再由分发器将请求发送到指定的线程池上,最后由线程池调用具体的服务。这就是一个远程调用请求的发送过程。 服务调用过程源码分析 服务消费端进行服务调用过程 我们知道服务消费端是通过为接口生成的代理对象进行服务调用的,他的代理对象的实现如下: 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 public class proxy0 implements ClassGenerator.DC, EchoService, DemoService { // 方法数组 public static Method[] methods; private InvocationHandler handler; public proxy0(InvocationHandler invocationHandler) { this.handler = invocationHandler; } public proxy0() { } public String sayHello(String string) { // 将参数存储到 Object 数组中 Object[] arrobject = new Object[]{string}; // 调用 InvocationHandler 实现类的 invoke 方法得到调用结果 Object object = this.handler.invoke(this, methods[0], arrobject); // 返回调用结果 return (String)object; } /** 回声测试方法 */ public Object $echo(Object object) { Object[] arrobject = new Object[]{object}; Object object2 = this.handler.invoke(this, methods[1], arrobject); return object2; } } 这个代理类的实现逻辑比较简单,首先将参数存储到数组中,然后调用InvocationHandler的实现类的invoke方法,然后得到一个调用结果,最后将这个结果返回给调用端。 也就是说这个代理类只做了三件事情: 将参数进行封装 调用invoke方法,进行服务调用 返回结果 下面我们来分析这个InvocationHandler的实现类的代码实现; 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 public class InvokerInvocationHandler implements InvocationHandler { private final Invoker<?> invoker; public InvokerInvocationHandler(Invoker<?> handler) { this.invoker = handler; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); Class<?>[] parameterTypes = method.getParameterTypes(); // 拦截定义在 Object 类中的方法(未被子类重写),比如 wait/notify if (method.getDeclaringClass() == Object.class) { return method.invoke(invoker, args); } // 如果 toString、hashCode 和 equals 等方法被子类重写了,这里也直接调用 if ("toString".equals(methodName) && parameterTypes.length == 0) { return invoker.toString(); } if ("hashCode".equals(methodName) && parameterTypes.length == 0) { return invoker.hashCode(); } if ("equals".equals(methodName) && parameterTypes.length == 1) { return invoker.equals(args[0]); } // 将 method 和 args 封装到 RpcInvocation 中,并执行后续的调用 return invoker.invoke(new RpcInvocation(method, args)).recreate(); } } 这个类的invoke的实现逻辑非常的简单,如果调用的是Object中的一些方法,那么直接进行处理即可(这些方法根本不需要远程调用),否则通过Invoker接口的实现类的invoke方法进行远程调用。 这里的Invoker的实现类其实是MockClusterInvoker,它的内部封装了服务降级的逻辑,下面我们来看它的具体实现: 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 public class MockClusterInvoker<T> implements Invoker<T> { private final Invoker<T> invoker; public Result invoke(Invocation invocation) throws RpcException { Result result = null; // 获取 mock 配置值 String value = directory.getUrl().getMethodParameter(invocation.getMethodName(), Constants.MOCK_KEY, Boolean.FALSE.toString()).trim(); if (value.length() == 0 || value.equalsIgnoreCase("false")) { // 无 mock 逻辑,直接调用其他 Invoker 对象的 invoke 方法, // 比如 FailoverClusterInvoker result = this.invoker.invoke(invocation); } else if (value.startsWith("force")) { // force:xxx 直接执行 mock 逻辑,不发起远程调用 result = doMockInvoke(invocation, null); } else { // fail:xxx 表示消费方对调用服务失败后,再执行 mock 逻辑,不抛出异常 try { // 调用其他 Invoker 对象的 invoke 方法 result = this.invoker.invoke(invocation); } catch (RpcException e) { if (e.isBiz()) { throw e; } else { // 调用失败,执行 mock 逻辑 result = doMockInvoke(invocation, e); } } } return result; } // 省略其他方法 } 这里主要是对服务降级的处理,如果没有配置服务降级的逻辑,那么直接进行远程调用;如果服务降级信息配置为force那么直接降级,不发起远程调用;如果服务降级信息配置为fail,那么会尝试进行远程调用,如果失败那么就进行服务降级逻辑。 在这里我们就不深究这个服务降级的实现了,主要来看一看远程调用。在这个类中进行远程调用使用的是实现了Invoker接口的AbstractInvoker。 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 public abstract class AbstractInvoker<T> implements Invoker<T> { public Result invoke(Invocation inv) throws RpcException { if (destroyed.get()) { throw new RpcException("Rpc invoker for service ..."); } RpcInvocation invocation = (RpcInvocation) inv; // 设置 Invoker invocation.setInvoker(this); if (attachment != null && attachment.size() > 0) { // 设置 attachment invocation.addAttachmentsIfAbsent(attachment); } Map<String, String> contextAttachments = RpcContext.getContext().getAttachments(); if (contextAttachments != null && contextAttachments.size() != 0) { // 添加 contextAttachments 到 RpcInvocation#attachment 变量中 invocation.addAttachments(contextAttachments); } if (getUrl().getMethodParameter(invocation.getMethodName(), Constants.ASYNC_KEY, false)) { // 设置异步信息到 RpcInvocation#attachment 中 invocation.setAttachment(Constants.ASYNC_KEY, Boolean.TRUE.toString()); } RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation); try { // 抽象方法,由子类实现 return doInvoke(invocation); } catch (InvocationTargetException e) { // ... } catch (RpcException e) { // ... } catch (Throwable e) { return new RpcResult(e); } } protected abstract Result doInvoke(Invocation invocation) throws Throwable; // 省略其他方法 } 上面的代码的主要逻辑就是将信息添加到RpcInvocation#attachment,然后调用doInvoke执行后序逻辑。doInvoke是一个抽象方法,由子类DubboInvoker实现。 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 public class DubboInvoker<T> extends AbstractInvoker<T> { private final ExchangeClient[] clients; protected Result doInvoke(final Invocation invocation) throws Throwable { RpcInvocation inv = (RpcInvocation) invocation; final String methodName = RpcUtils.getMethodName(invocation); // 设置 path 和 version 到 attachment 中 inv.setAttachment(Constants.PATH_KEY, getUrl().getPath()); inv.setAttachment(Constants.VERSION_KEY, version); ExchangeClient currentClient; if (clients.length == 1) { // 从 clients 数组中获取 ExchangeClient currentClient = clients[0]; } else { currentClient = clients[index.getAndIncrement() % clients.length]; } try { // 获取异步配置 boolean isAsync = RpcUtils.isAsync(getUrl(), invocation); // isOneway 为 true,表示“单向”通信 boolean isOneway = RpcUtils.isOneway(getUrl(), invocation); int timeout = getUrl().getMethodParameter(methodName, Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT); // 异步无返回值 if (isOneway) { boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false); // 发送请求 currentClient.send(inv, isSent); // 设置上下文中的 future 字段为 null RpcContext.getContext().setFuture(null); // 返回一个空的 RpcResult return new RpcResult(); } // 异步有返回值 else if (isAsync) { // 发送请求,并得到一个 ResponseFuture 实例 ResponseFuture future = currentClient.request(inv, timeout); // 设置 future 到上下文中 RpcContext.getContext().setFuture(new FutureAdapter<Object>(future)); // 暂时返回一个空结果 return new RpcResult(); } // 同步调用 else { RpcContext.getContext().setFuture(null); // 发送请求,得到一个 ResponseFuture 实例,并调用该实例的 get 方法进行等待 return (Result) currentClient.request(inv, timeout).get(); } } catch (TimeoutException e) { throw new RpcException(..., "Invoke remote method timeout...."); } catch (RemotingException e) { throw new RpcException(..., "Failed to invoke remote method: ..."); } } // 省略其他方法 } 这段代码包含了Dubbo对同步调用和一步调用的处理逻辑。Dubbo实现同步调用和异步调用比较关键的点是由谁负责调用ResponseFuture 的get方法,同步调用模式下是由框架来调用get方法的,而异步调用是由用户来调用get方法的。 这个方法主要完成的就是利用client进行远程调用,并处理同步调用和异步调用的细节。 下面我们来看一看ResponseFuture的一个默认实现DefaultFuture. 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 public class DefaultFuture implements ResponseFuture { private static final Map<Long, Channel> CHANNELS = new ConcurrentHashMap<Long, Channel>(); private static final Map<Long, DefaultFuture> FUTURES = new ConcurrentHashMap<Long, DefaultFuture>(); private final long id; private final Channel channel; private final Request request; private final int timeout; private final Lock lock = new ReentrantLock(); private final Condition done = lock.newCondition(); private volatile Response response; public DefaultFuture(Channel channel, Request request, int timeout) { this.channel = channel; this.request = request; // 获取请求 id,这个 id 很重要,后面还会见到 this.id = request.getId(); this.timeout = timeout > 0 ? timeout : channel.getUrl().getPositiveParameter(Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT); // 存储 <requestId, DefaultFuture> 映射关系到 FUTURES 中 FUTURES.put(id, this); CHANNELS.put(id, channel); } @Override public Object get() throws RemotingException { return get(timeout); } @Override public Object get(int timeout) throws RemotingException { if (timeout <= 0) { timeout = Constants.DEFAULT_TIMEOUT; } // 检测服务提供方是否成功返回了调用结果 if (!isDone()) { long start = System.currentTimeMillis(); lock.lock(); try { // 循环检测服务提供方是否成功返回了调用结果 while (!isDone()) { // 如果调用结果尚未返回,这里等待一段时间 done.await(timeout, TimeUnit.MILLISECONDS); // 如果调用结果成功返回,或等待超时,此时跳出 while 循环,执行后续的逻辑 if (isDone() || System.currentTimeMillis() - start > timeout) { break; } } } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); } // 如果调用结果仍未返回,则抛出超时异常 if (!isDone()) { throw new TimeoutException(sent > 0, channel, getTimeoutMessage(false)); } } // 返回调用结果 return returnFromResponse(); } @Override public boolean isDone() { // 通过检测 response 字段为空与否,判断是否收到了调用结果 return response != null; } private Object returnFromResponse() throws RemotingException { Response res = response; if (res == null) { throw new IllegalStateException("response cannot be null"); } // 如果调用结果的状态为 Response.OK,则表示调用过程正常,服务提供方成功返回了调用结果 if (res.getStatus() == Response.OK) { return res.getResult(); } // 抛出异常 if (res.getStatus() == Response.CLIENT_TIMEOUT || res.getStatus() == Response.SERVER_TIMEOUT) { throw new TimeoutException(res.getStatus() == Response.SERVER_TIMEOUT, channel, res.getErrorMessage()); } throw new RemotingException(channel, res.getErrorMessage()); } // 省略其他方法 } 其实这段代码非常逻辑非常的简单,如果服务消费者还没有收到结果时,那么调用get方法,就会被阻塞。同步调用模式下,是由框架来执行get方法的,它会阻塞直至收到结果。而异步模式下将该对象封装到FutureAdapter对象中,然后设置到RpcContext中,供用户使用。这个适配器的主要作用就是将Dubbo的ResponseFuture和JDK中的Future进行适配,这样用户可以调用Future的get方法的时候经过了FutureAdapter的适配,最终调用ResponseFuture的get方法。 到此,整个执行的流程如下: 服务消费端发送请求 通过之前的源码分析,我们可以看出服务调用是通过client发送请求完成的,下面我们就来分析,这个请求发送的具体流程。 在之前提到的client其实就是实现ExchangeClient接口的ReferenceCountExchangeClient. 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 final class ReferenceCountExchangeClient implements ExchangeClient { private final URL url; private final AtomicInteger referenceCount = new AtomicInteger(0); public ReferenceCountExchangeClient(ExchangeClient client, ConcurrentMap<String, LazyConnectExchangeClient> ghostClientMap) { this.client = client; // 引用计数自增 referenceCount.incrementAndGet(); this.url = client.getUrl(); // ... } @Override public ResponseFuture request(Object request) throws RemotingException { // 直接调用被装饰对象的同签名方法 return client.request(request); } @Override public ResponseFuture request(Object request, int timeout) throws RemotingException { // 直接调用被装饰对象的同签名方法 return client.request(request, timeout); } /** 引用计数自增,该方法由外部调用 */ public void incrementAndGetCount() { // referenceCount 自增 referenceCount.incrementAndGet(); } @Override public void close(int timeout) { // referenceCount 自减 if (referenceCount.decrementAndGet() <= 0) { if (timeout == 0) { client.close(); } else { client.close(timeout); } client = replaceWithLazyClient(); } } // 省略部分方法 } ReferenceCountExchangeClient内部主要进行的是引用计数的处理,其它均调用的是被装饰对象的相关方法。 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 public class HeaderExchangeClient implements ExchangeClient { private static final ScheduledThreadPoolExecutor scheduled = new ScheduledThreadPoolExecutor(2, new NamedThreadFactory("dubbo-remoting-client-heartbeat", true)); private final Client client; private final ExchangeChannel channel; private ScheduledFuture<?> heartbeatTimer; private int heartbeat; private int heartbeatTimeout; public HeaderExchangeClient(Client client, boolean needHeartbeat) { if (client == null) { throw new IllegalArgumentException("client == null"); } this.client = client; // 创建 HeaderExchangeChannel 对象 this.channel = new HeaderExchangeChannel(client); // 以下代码均与心跳检测逻辑有关 String dubbo = client.getUrl().getParameter(Constants.DUBBO_VERSION_KEY); this.heartbeat = client.getUrl().getParameter(Constants.HEARTBEAT_KEY, dubbo != null && dubbo.startsWith("1.0.") ? Constants.DEFAULT_HEARTBEAT : 0); this.heartbeatTimeout = client.getUrl().getParameter(Constants.HEARTBEAT_TIMEOUT_KEY, heartbeat * 3); if (heartbeatTimeout < heartbeat * 2) { throw new IllegalStateException("heartbeatTimeout < heartbeatInterval * 2"); } if (needHeartbeat) { // 开启心跳检测定时器 startHeartbeatTimer(); } } @Override public ResponseFuture request(Object request) throws RemotingException { // 直接 HeaderExchangeChannel 对象的同签名方法 return channel.request(request); } @Override public ResponseFuture request(Object request, int timeout) throws RemotingException { // 直接 HeaderExchangeChannel 对象的同签名方法 return channel.request(request, timeout); } @Override public void close() { doClose(); channel.close(); } private void doClose() { // 停止心跳检测定时器 stopHeartbeatTimer(); } private void startHeartbeatTimer() { stopHeartbeatTimer(); if (heartbeat > 0) { heartbeatTimer = scheduled.scheduleWithFixedDelay( new HeartBeatTask(new HeartBeatTask.ChannelProvider() { @Override public Collection<Channel> getChannels() { return Collections.<Channel>singletonList(HeaderExchangeClient.this); } }, heartbeat, heartbeatTimeout), heartbeat, heartbeat, TimeUnit.MILLISECONDS); } } private void stopHeartbeatTimer() { if (heartbeatTimer != null && !heartbeatTimer.isCancelled()) { try { heartbeatTimer.cancel(true); scheduled.purge(); } catch (Throwable e) { if (logger.isWarnEnabled()) { logger.warn(e.getMessage(), e); } } } heartbeatTimer = null; } // 省略部分方法 } HeaderExchangeClient封装了心跳检测逻辑,然后通过调用HeaderExchangeChannel对象的同签名方法。下面我们来分析HeaderExchangeChannel的代码实现: 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 final class HeaderExchangeChannel implements ExchangeChannel { private final Channel channel; HeaderExchangeChannel(Channel channel) { if (channel == null) { throw new IllegalArgumentException("channel == null"); } // 这里的 channel 指向的是 NettyClient this.channel = channel; } @Override public ResponseFuture request(Object request) throws RemotingException { return request(request, channel.getUrl().getPositiveParameter(Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT)); } @Override public ResponseFuture request(Object request, int timeout) throws RemotingException { if (closed) { throw new RemotingException(..., "Failed to send request ...); } // 创建 Request 对象 Request req = new Request(); req.setVersion(Version.getProtocolVersion()); // 设置双向通信标志为 true req.setTwoWay(true); // 这里的 request 变量类型为 RpcInvocation req.setData(request); // 创建 DefaultFuture 对象 DefaultFuture future = new DefaultFuture(channel, req, timeout); try { // 调用 NettyClient 的 send 方法发送请求 channel.send(req); } catch (RemotingException e) { future.cancel(); throw e; } // 返回 DefaultFuture 对象 return future; } } 这个类的request方法,首先定义了一个Request对象,然后再将该对象传给NettyClient的send方法,进行后序的调用。而NettyClient本身并没有实现send方法,这个方法是通过继承AbstractPeer得到得。 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 public abstract class AbstractPeer implements Endpoint, ChannelHandler { @Override public void send(Object message) throws RemotingException { // 该方法由 AbstractClient 类实现 send(message, url.getParameter(Constants.SENT_KEY, false)); } // 省略其他方法 } public abstract class AbstractClient extends AbstractEndpoint implements Client { @Override public void send(Object message, boolean sent) throws RemotingException { if (send_reconnect && !isConnected()) { connect(); } // 获取 Channel,getChannel 是一个抽象方法,具体由子类实现 Channel channel = getChannel(); if (channel == null || !channel.isConnected()) { throw new RemotingException(this, "message can not send ..."); } // 继续向下调用 channel.send(message, sent); } protected abstract Channel getChannel(); // 省略其他方法 } 再默认得情况下,Dubbo使用得是Netty作为底层得通信框架,下面我们来分析一下NettyClient类中getChannel`方法得实现逻辑。 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 public class NettyClient extends AbstractClient { // 这里的 Channel 全限定名称为 org.jboss.netty.channel.Channel private volatile Channel channel; @Override protected com.alibaba.dubbo.remoting.Channel getChannel() { Channel c = channel; if (c == null || !c.isConnected()) return null; // 获取一个 NettyChannel 类型对象 return NettyChannel.getOrAddChannel(c, getUrl(), this); } } final class NettyChannel extends AbstractChannel { private static final ConcurrentMap<org.jboss.netty.channel.Channel, NettyChannel> channelMap = new ConcurrentHashMap<org.jboss.netty.channel.Channel, NettyChannel>(); private final org.jboss.netty.channel.Channel channel; /** 私有构造方法 */ private NettyChannel(org.jboss.netty.channel.Channel channel, URL url, ChannelHandler handler) { super(url, handler); if (channel == null) { throw new IllegalArgumentException("netty channel == null;"); } this.channel = channel; } static NettyChannel getOrAddChannel(org.jboss.netty.channel.Channel ch, URL url, ChannelHandler handler) { if (ch == null) { return null; } // 尝试从集合中获取 NettyChannel 实例 NettyChannel ret = channelMap.get(ch); if (ret == null) { // 如果 ret = null,则创建一个新的 NettyChannel 实例 NettyChannel nc = new NettyChannel(ch, url, handler); if (ch.isConnected()) { // 将 <Channel, NettyChannel> 键值对存入 channelMap 集合中 ret = channelMap.putIfAbsent(ch, nc); } if (ret == null) { ret = nc; } } return ret; } } 拿到NettyChannel实例之后,就可以进行后序的调用。 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 public void send(Object message, boolean sent) throws RemotingException { super.send(message, sent); boolean success = true; int timeout = 0; try { // 发送消息(包含请求和响应消息) ChannelFuture future = channel.write(message); // sent 的值源于 <dubbo:method sent="true/false" /> 中 sent 的配置值,有两种配置值: // 1. true: 等待消息发出,消息发送失败将抛出异常 // 2. false: 不等待消息发出,将消息放入 IO 队列,即刻返回 // 默认情况下 sent = false; if (sent) { timeout = getUrl().getPositiveParameter(Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT); // 等待消息发出,若在规定时间没能发出,success 会被置为 false success = future.await(timeout); } Throwable cause = future.getCause(); if (cause != null) { throw cause; } } catch (Throwable e) { throw new RemotingException(this, "Failed to send message ..."); } // 若 success 为 false,这里抛出异常 if (!success) { throw new RemotingException(this, "Failed to send message ..."); } } 到此调用请求就发出去了,当然再Netty中还有出站数据的编码操作,这里就不分析了。 整个调用路径是这个样子的: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 proxy0#sayHello(String) —> InvokerInvocationHandler#invoke(Object, Method, Object[]) —> MockClusterInvoker#invoke(Invocation) —> AbstractClusterInvoker#invoke(Invocation) —> FailoverClusterInvoker#doInvoke(Invocation, List<Invoker<T>>, LoadBalance) —> Filter#invoke(Invoker, Invocation) // 包含多个 Filter 调用 —> ListenerInvokerWrapper#invoke(Invocation) —> AbstractInvoker#invoke(Invocation) —> DubboInvoker#doInvoke(Invocation) —> ReferenceCountExchangeClient#request(Object, int) —> HeaderExchangeClient#request(Object, int) —> HeaderExchangeChannel#request(Object, int) —> AbstractPeer#send(Object) —> AbstractClient#send(Object, boolean) —> NettyChannel#send(Object, boolean) —> NioClientSocketChannel#write(Object) 服务提供方处理逻辑 接收请求 前面说过,默认情况下 Dubbo 使用 Netty 作为底层的通信框架。Netty 检测到有数据入站后,首先会通过解码器对数据进行解码,并将解码后的数据传递给下一个入站处理器的指定方法。 这里直接分析请求数据的解码逻辑,忽略中间过程. 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 public class ExchangeCodec extends TelnetCodec { @Override public Object decode(Channel channel, ChannelBuffer buffer) throws IOException { int readable = buffer.readableBytes(); // 创建消息头字节数组 byte[] header = new byte[Math.min(readable, HEADER_LENGTH)]; // 读取消息头数据 buffer.readBytes(header); // 调用重载方法进行后续解码工作 return decode(channel, buffer, readable, header); } @Override protected Object decode(Channel channel, ChannelBuffer buffer, int readable, byte[] header) throws IOException { // 检查魔数是否相等 if (readable > 0 && header[0] != MAGIC_HIGH || readable > 1 && header[1] != MAGIC_LOW) { int length = header.length; if (header.length < readable) { header = Bytes.copyOf(header, readable); buffer.readBytes(header, length, readable - length); } for (int i = 1; i < header.length - 1; i++) { if (header[i] == MAGIC_HIGH && header[i + 1] == MAGIC_LOW) { buffer.readerIndex(buffer.readerIndex() - header.length + i); header = Bytes.copyOf(header, i); break; } } // 通过 telnet 命令行发送的数据包不包含消息头,所以这里 // 调用 TelnetCodec 的 decode 方法对数据包进行解码 return super.decode(channel, buffer, readable, header); } // 检测可读数据量是否少于消息头长度,若小于则立即返回 DecodeResult.NEED_MORE_INPUT if (readable < HEADER_LENGTH) { return DecodeResult.NEED_MORE_INPUT; } // 从消息头中获取消息体长度 int len = Bytes.bytes2int(header, 12); // 检测消息体长度是否超出限制,超出则抛出异常 checkPayload(channel, len); int tt = len + HEADER_LENGTH; // 检测可读的字节数是否小于实际的字节数 if (readable < tt) { return DecodeResult.NEED_MORE_INPUT; } ChannelBufferInputStream is = new ChannelBufferInputStream(buffer, len); try { // 继续进行解码工作 return decodeBody(channel, is, header); } finally { if (is.available() > 0) { try { StreamUtils.skipUnusedStream(is); } catch (IOException e) { logger.warn(e.getMessage(), e); } } } } } 上面方法通过检测消息头中的魔数是否与规定的魔数相等,提前拦截掉非常规数据包,比如通过 telnet 命令行发出的数据包。接着再对消息体长度,以及可读字节数进行检测。最后调用 decodeBody 方法进行后续的解码工作,ExchangeCodec 中实现了 decodeBody 方法,但因其子类 DubboCodec 覆写了该方法,所以在运行时 DubboCodec 中的 decodeBody 方法会被调用。下面我们来看一下该方法的代码。 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 public class DubboCodec extends ExchangeCodec implements Codec2 { @Override protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException { // 获取消息头中的第三个字节,并通过逻辑与运算得到序列化器编号 byte flag = header[2], proto = (byte) (flag & SERIALIZATION_MASK); Serialization s = CodecSupport.getSerialization(channel.getUrl(), proto); // 获取调用编号 long id = Bytes.bytes2long(header, 4); // 通过逻辑与运算得到调用类型,0 - Response,1 - Request if ((flag & FLAG_REQUEST) == 0) { // 对响应结果进行解码,得到 Response 对象。这个非本节内容,后面再分析 // ... } else { // 创建 Request 对象 Request req = new Request(id); req.setVersion(Version.getProtocolVersion()); // 通过逻辑与运算得到通信方式,并设置到 Request 对象中 req.setTwoWay((flag & FLAG_TWOWAY) != 0); // 通过位运算检测数据包是否为事件类型 if ((flag & FLAG_EVENT) != 0) { // 设置心跳事件到 Request 对象中 req.setEvent(Request.HEARTBEAT_EVENT); } try { Object data; if (req.isHeartbeat()) { // 对心跳包进行解码,该方法已被标注为废弃 data = decodeHeartbeatData(channel, deserialize(s, channel.getUrl(), is)); } else if (req.isEvent()) { // 对事件数据进行解码 data = decodeEventData(channel, deserialize(s, channel.getUrl(), is)); } else { DecodeableRpcInvocation inv; // 根据 url 参数判断是否在 IO 线程上对消息体进行解码 if (channel.getUrl().getParameter( Constants.DECODE_IN_IO_THREAD_KEY, Constants.DEFAULT_DECODE_IN_IO_THREAD)) { inv = new DecodeableRpcInvocation(channel, req, is, proto); // 在当前线程,也就是 IO 线程上进行后续的解码工作。此工作完成后,可将 // 调用方法名、attachment、以及调用参数解析出来 inv.decode(); } else { // 仅创建 DecodeableRpcInvocation 对象,但不在当前线程上执行解码逻辑 inv = new DecodeableRpcInvocation(channel, req, new UnsafeByteArrayInputStream(readMessageData(is)), proto); } data = inv; } // 设置 data 到 Request 对象中 req.setData(data); } catch (Throwable t) { // 若解码过程中出现异常,则将 broken 字段设为 true, // 并将异常对象设置到 Reqeust 对象中 req.setBroken(true); req.setData(t); } return req; } } } 如上,decodeBody 对部分字段进行了解码,并将解码得到的字段封装到 Request 中。随后会调用 DecodeableRpcInvocation 的 decode 方法进行后续的解码工作。此工作完成后,可将调用方法名、attachment、以及调用参数解析出来。下面我们来看一下 DecodeableRpcInvocation 的 decode 方法逻辑。 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 public class DecodeableRpcInvocation extends RpcInvocation implements Codec, Decodeable { @Override public Object decode(Channel channel, InputStream input) throws IOException { ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), serializationType) .deserialize(channel.getUrl(), input); // 通过反序列化得到 dubbo version,并保存到 attachments 变量中 String dubboVersion = in.readUTF(); request.setVersion(dubboVersion); setAttachment(Constants.DUBBO_VERSION_KEY, dubboVersion); // 通过反序列化得到 path,version,并保存到 attachments 变量中 setAttachment(Constants.PATH_KEY, in.readUTF()); setAttachment(Constants.VERSION_KEY, in.readUTF()); // 通过反序列化得到调用方法名 setMethodName(in.readUTF()); try { Object[] args; Class<?>[] pts; // 通过反序列化得到参数类型字符串,比如 Ljava/lang/String; String desc = in.readUTF(); if (desc.length() == 0) { pts = DubboCodec.EMPTY_CLASS_ARRAY; args = DubboCodec.EMPTY_OBJECT_ARRAY; } else { // 将 desc 解析为参数类型数组 pts = ReflectUtils.desc2classArray(desc); args = new Object[pts.length]; for (int i = 0; i < args.length; i++) { try { // 解析运行时参数 args[i] = in.readObject(pts[i]); } catch (Exception e) { if (log.isWarnEnabled()) { log.warn("Decode argument failed: " + e.getMessage(), e); } } } } // 设置参数类型数组 setParameterTypes(pts); // 通过反序列化得到原 attachment 的内容 Map<String, String> map = (Map<String, String>) in.readObject(Map.class); if (map != null && map.size() > 0) { Map<String, String> attachment = getAttachments(); if (attachment == null) { attachment = new HashMap<String, String>(); } // 将 map 与当前对象中的 attachment 集合进行融合 attachment.putAll(map); setAttachments(attachment); } // 对 callback 类型的参数进行处理 for (int i = 0; i < args.length; i++) { args[i] = decodeInvocationArgument(channel, this, pts, i, args[i]); } // 设置参数列表 setArguments(args); } catch (ClassNotFoundException e) { throw new IOException(StringUtils.toString("Read invocation data failed.", e)); } finally { if (in instanceof Cleanable) { ((Cleanable) in).cleanup(); } } return this; } } 上面的方法通过反序列化将诸如 path、version、调用方法名、参数列表等信息依次解析出来,并设置到相应的字段中,最终得到一个具有完整调用信息的 DecodeableRpcInvocation 对象。 到这里,请求数据解码的过程就分析完了。 调用服务 解码器将数据包解析成 Request 对象后,NettyHandler 的 messageReceived 方法紧接着会收到这个对象,并将这个对象继续向下传递。这期间该对象会被依次传递给 NettyServer、MultiMessageHandler、HeartbeatHandler 以及 AllChannelHandler。最后由 AllChannelHandler 将该对象封装到 Runnable 实现类对象中,并将 Runnable 放入线程池中执行后续的调用逻辑。整个调用栈如下: 1 2 3 4 5 6 NettyHandler#messageReceived(ChannelHandlerContext, MessageEvent) —> AbstractPeer#received(Channel, Object) —> MultiMessageHandler#received(Channel, Object) —> HeartbeatHandler#received(Channel, Object) —> AllChannelHandler#received(Channel, Object) —> ExecutorService#execute(Runnable) // 由线程池执行后续的调用逻辑 线程派发模型 Dubbo将底层通信框架中接收请求的线程称为IO线程。如果一些事件处理逻辑可以很快执行完,比如只再内存打一个标记,此时直接在IO线程上执行该段逻辑即可。但如果事件的处理逻辑比较耗时,比如该段逻辑发起数据库查询或者HTTP请求。此时我就不应该让事件处理逻辑在IO线程上执行,而是应该派发到线程池中执行。原因也很简单,IO线程主要用于接收请求,如果IO线程被占满,将导致它请求接收新的请求。 在前文提到的原理图中,Dispatcher就是线程派发器。它的真实职责是创建具有线程派发能力的Channelhandler,比如AllChannelHandler,MessageOnlyChannelHandler和ExecutionChannelHandler等,其本身并不具有线程派发能力。 Dubbo支持的不同线程派发策略 策略用途 all(默认)所有消息都派发到线程池,包括请求,响应,连接事件,断开事件等。 direct所有消息都不派发的线程池,全部在IO线程上直接执行。 message只是请求和响应消息派发到线程池,其它消息均在IO线程上执行 execution只有请求派发到线程池,不含响应。其它消息均在IO线程上执行。 connection在IO线程上,将连接断开事件放入队列,有序逐个执行,其它消息派发到线程池。 在默认配置下,Dubbo使用all派发策略,即将所有的消息都派发到线程池中。下面我们来分析一下AllChannelHandler的代码。 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 public class AllChannelHandler extends WrappedChannelHandler { public AllChannelHandler(ChannelHandler handler, URL url) { super(handler, url); } /** 处理连接事件 */ @Override public void connected(Channel channel) throws RemotingException { // 获取线程池 ExecutorService cexecutor = getExecutorService(); try { // 将连接事件派发到线程池中处理 cexecutor.execute(new ChannelEventRunnable(channel, handler, ChannelState.CONNECTED)); } catch (Throwable t) { throw new ExecutionException(..., " error when process connected event .", t); } } /** 处理断开事件 */ @Override public void disconnected(Channel channel) throws RemotingException { ExecutorService cexecutor = getExecutorService(); try { cexecutor.execute(new ChannelEventRunnable(channel, handler, ChannelState.DISCONNECTED)); } catch (Throwable t) { throw new ExecutionException(..., "error when process disconnected event .", t); } } /** 处理请求和响应消息,这里的 message 变量类型可能是 Request,也可能是 Response */ @Override public void received(Channel channel, Object message) throws RemotingException { ExecutorService cexecutor = getExecutorService(); try { // 将请求和响应消息派发到线程池中处理 cexecutor.execute(new ChannelEventRunnable(channel, handler, ChannelState.RECEIVED, message)); } catch (Throwable t) { if(message instanceof Request && t instanceof RejectedExecutionException){ Request request = (Request)message; // 如果通信方式为双向通信,此时将 Server side ... threadpool is exhausted // 错误信息封装到 Response 中,并返回给服务消费方。 if(request.isTwoWay()){ String msg = "Server side(" + url.getIp() + "," + url.getPort() + ") threadpool is exhausted ,detail msg:" + t.getMessage(); Response response = new Response(request.getId(), request.getVersion()); response.setStatus(Response.SERVER_THREADPOOL_EXHAUSTED_ERROR); response.setErrorMessage(msg); // 返回包含错误信息的 Response 对象 channel.send(response); return; } } throw new ExecutionException(..., " error when process received event .", t); } } /** 处理异常信息 */ @Override public void caught(Channel channel, Throwable exception) throws RemotingException { ExecutorService cexecutor = getExecutorService(); try { cexecutor.execute(new ChannelEventRunnable(channel, handler, ChannelState.CAUGHT, exception)); } catch (Throwable t) { throw new ExecutionException(..., "error when process caught event ..."); } } } 请求对象会被封装 ChannelEventRunnable 中,ChannelEventRunnable 将会是服务调用过程的新起点。所以接下来我们以 ChannelEventRunnable 为起点向下探索。 调用服务 我们从ChannelEventRunnable开始分析。 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 public class ChannelEventRunnable implements Runnable { private final ChannelHandler handler; private final Channel channel; private final ChannelState state; private final Throwable exception; private final Object message; @Override public void run() { // 检测通道状态,对于请求或响应消息,此时 state = RECEIVED if (state == ChannelState.RECEIVED) { try { // 将 channel 和 message 传给 ChannelHandler 对象,进行后续的调用 handler.received(channel, message); } catch (Exception e) { logger.warn("... operation error, channel is ... message is ..."); } } // 其他消息类型通过 switch 进行处理 else { switch (state) { case CONNECTED: try { handler.connected(channel); } catch (Exception e) { logger.warn("... operation error, channel is ..."); } break; case DISCONNECTED: // ... case SENT: // ... case CAUGHT: // ... default: logger.warn("unknown state: " + state + ", message is " + message); } } } } ChannelEventRunnable仅仅是一个中转站,它的run方法中并不包含具体的调用逻辑,仅用于将参数传给其它的ChannelHandler对象进行处理,该对象类型为DecodeHandler. 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 public class DecodeHandler extends AbstractChannelHandlerDelegate { public DecodeHandler(ChannelHandler handler) { super(handler); } @Override public void received(Channel channel, Object message) throws RemotingException { if (message instanceof Decodeable) { // 对 Decodeable 接口实现类对象进行解码 decode(message); } if (message instanceof Request) { // 对 Request 的 data 字段进行解码 decode(((Request) message).getData()); } if (message instanceof Response) { // 对 Request 的 result 字段进行解码 decode(((Response) message).getResult()); } // 执行后续逻辑 handler.received(channel, message); } private void decode(Object message) { // Decodeable 接口目前有两个实现类, // 分别为 DecodeableRpcInvocation 和 DecodeableRpcResult if (message != null && message instanceof Decodeable) { try { // 执行解码逻辑 ((Decodeable) message).decode(); } catch (Throwable e) { if (log.isWarnEnabled()) { log.warn("Call Decodeable.decode failed: " + e.getMessage(), e); } } } } } DecodeHandler主要是包含了一些解码逻辑。之前提到请求解码可以在IO线程上执行,也可以在线程池中执行,这取决于运行时配置。DecodeHandler存在意义就是保证请求或响应对象可在线程池中被解码。解码完毕后,完全解码后的Request对象会继续先后传递,下一站是HeaderExchangeHandler. 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 public class HeaderExchangeHandler implements ChannelHandlerDelegate { private final ExchangeHandler handler; public HeaderExchangeHandler(ExchangeHandler handler) { if (handler == null) { throw new IllegalArgumentException("handler == null"); } this.handler = handler; } @Override public void received(Channel channel, Object message) throws RemotingException { channel.setAttribute(KEY_READ_TIMESTAMP, System.currentTimeMillis()); ExchangeChannel exchangeChannel = HeaderExchangeChannel.getOrAddChannel(channel); try { // 处理请求对象 if (message instanceof Request) { Request request = (Request) message; if (request.isEvent()) { // 处理事件 handlerEvent(channel, request); } // 处理普通的请求 else { // 双向通信 if (request.isTwoWay()) { // 向后调用服务,并得到调用结果 Response response = handleRequest(exchangeChannel, request); // 将调用结果返回给服务消费端 channel.send(response); } // 如果是单向通信,仅向后调用指定服务即可,无需返回调用结果 else { handler.received(exchangeChannel, request.getData()); } } } // 处理响应对象,服务消费方会执行此处逻辑,后面分析 else if (message instanceof Response) { handleResponse(channel, (Response) message); } else if (message instanceof String) { // telnet 相关,忽略 } else { handler.received(exchangeChannel, message); } } finally { HeaderExchangeChannel.removeChannelIfDisconnected(channel); } } Response handleRequest(ExchangeChannel channel, Request req) throws RemotingException { Response res = new Response(req.getId(), req.getVersion()); // 检测请求是否合法,不合法则返回状态码为 BAD_REQUEST 的响应 if (req.isBroken()) { Object data = req.getData(); String msg; if (data == null) msg = null; else if (data instanceof Throwable) msg = StringUtils.toString((Throwable) data); else msg = data.toString(); res.setErrorMessage("Fail to decode request due to: " + msg); // 设置 BAD_REQUEST 状态 res.setStatus(Response.BAD_REQUEST); return res; } // 获取 data 字段值,也就是 RpcInvocation 对象 Object msg = req.getData(); try { // 继续向下调用 Object result = handler.reply(channel, msg); // 设置 OK 状态码 res.setStatus(Response.OK); // 设置调用结果 res.setResult(result); } catch (Throwable e) { // 若调用过程出现异常,则设置 SERVICE_ERROR,表示服务端异常 res.setStatus(Response.SERVICE_ERROR); res.setErrorMessage(StringUtils.toString(e)); } return res; } } 到这里,我们看到了比较清晰的请求和响应逻辑。对于双向通信,HeaderExchangeHandler 首先向后进行调用,得到调用结果。然后将调用结果封装到 Response 对象中,最后再将该对象返回给服务消费方。如果请求不合法,或者调用失败,则将错误信息封装到 Response 对象中,并返回给服务消费方。接下来我们继续向后分析,把剩余的调用过程分析完。下面分析定义在 DubboProtocol 类中的匿名类对象逻辑,如下: 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 public class DubboProtocol extends AbstractProtocol { public static final String NAME = "dubbo"; private ExchangeHandler requestHandler = new ExchangeHandlerAdapter() { @Override public Object reply(ExchangeChannel channel, Object message) throws RemotingException { if (message instanceof Invocation) { Invocation inv = (Invocation) message; // 获取 Invoker 实例 Invoker<?> invoker = getInvoker(channel, inv); if (Boolean.TRUE.toString().equals(inv.getAttachments().get(IS_CALLBACK_SERVICE_INVOKE))) { // 回调相关,忽略 } RpcContext.getContext().setRemoteAddress(channel.getRemoteAddress()); // 通过 Invoker 调用具体的服务 return invoker.invoke(inv); } throw new RemotingException(channel, "Unsupported request: ..."); } // 忽略其他方法 } Invoker<?> getInvoker(Channel channel, Invocation inv) throws RemotingException { // 忽略回调和本地存根相关逻辑 // ... int port = channel.getLocalAddress().getPort(); // 计算 service key,格式为 groupName/serviceName:serviceVersion:port。比如: // dubbo/com.alibaba.dubbo.demo.DemoService:1.0.0:20880 String serviceKey = serviceKey(port, path, inv.getAttachments().get(Constants.VERSION_KEY), inv.getAttachments().get(Constants.GROUP_KEY)); // 从 exporterMap 查找与 serviceKey 相对应的 DubboExporter 对象, // 服务导出过程中会将 <serviceKey, DubboExporter> 映射关系存储到 exporterMap 集合中 DubboExporter<?> exporter = (DubboExporter<?>) exporterMap.get(serviceKey); if (exporter == null) throw new RemotingException(channel, "Not found exported service ..."); // 获取 Invoker 对象,并返回 return exporter.getInvoker(); } // 忽略其他方法 } 以上逻辑用于获取与指定服务对应的 Invoker 实例,并通过 Invoker 的 invoke 方法调用服务逻辑。invoke 方法定义在 AbstractProxyInvoker 中,代码如下。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public abstract class AbstractProxyInvoker<T> implements Invoker<T> { @Override public Result invoke(Invocation invocation) throws RpcException { try { // 调用 doInvoke 执行后续的调用,并将调用结果封装到 RpcResult 中,并 return new RpcResult(doInvoke(proxy, invocation.getMethodName(), invocation.getParameterTypes(), invocation.getArguments())); } catch (InvocationTargetException e) { return new RpcResult(e.getTargetException()); } catch (Throwable e) { throw new RpcException("Failed to invoke remote proxy method ..."); } } protected abstract Object doInvoke(T proxy, String methodName, Class<?>[] parameterTypes, Object[] arguments) throws Throwable; } 如上,doInvoke 是一个抽象方法,这个需要由具体的 Invoker 实例实现。Invoker 实例是在运行时通过 JavassistProxyFactory 创建的,创建逻辑如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class JavassistProxyFactory extends AbstractProxyFactory { // 省略其他方法 @Override public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) { final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type); // 创建匿名类对象 return new AbstractProxyInvoker<T>(proxy, type, url) { @Override protected Object doInvoke(T proxy, String methodName, Class<?>[] parameterTypes, Object[] arguments) throws Throwable { // 调用 invokeMethod 方法进行后续的调用 return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments); } }; } } Wrapper 是一个抽象类,其中 invokeMethod 是一个抽象方法。Dubbo 会在运行时通过 Javassist 框架为 Wrapper 生成实现类,并实现 invokeMethod 方法,该方法最终会根据调用信息调用具体的服务。以 DemoServiceImpl 为例,Javassist 为其生成的代理类如下。 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 /** Wrapper0 是在运行时生成的,大家可使用 Arthas 进行反编译 */ public class Wrapper0 extends Wrapper implements ClassGenerator.DC { public static String[] pns; public static Map pts; public static String[] mns; public static String[] dmns; public static Class[] mts0; // 省略其他方法 public Object invokeMethod(Object object, String string, Class[] arrclass, Object[] arrobject) throws InvocationTargetException { DemoService demoService; try { // 类型转换 demoService = (DemoService)object; } catch (Throwable throwable) { throw new IllegalArgumentException(throwable); } try { // 根据方法名调用指定的方法 if ("sayHello".equals(string) && arrclass.length == 1) { return demoService.sayHello((String)arrobject[0]); } } catch (Throwable throwable) { throw new InvocationTargetException(throwable); } throw new NoSuchMethodException(new StringBuffer().append("Not found method \"").append(string).append("\" in class com.alibaba.dubbo.demo.DemoService.").toString()); } } 到这里,整个服务调用过程就分析完了。最后把调用过程贴出来,如下: 1 2 3 4 5 6 7 8 9 ChannelEventRunnable#run() —> DecodeHandler#received(Channel, Object) —> HeaderExchangeHandler#received(Channel, Object) —> HeaderExchangeHandler#handleRequest(ExchangeChannel, Request) —> DubboProtocol.requestHandler#reply(ExchangeChannel, Object) —> Filter#invoke(Invoker, Invocation) —> AbstractProxyInvoker#invoke(Invocation) —> Wrapper0#invokeMethod(Object, String, Class[], Object[]) —> DemoServiceImpl#sayHello(String) 服务提供方返回调用结果 服务提供方调用指定服务后,会将调用结果封装到 Response 对象中,并将该对象返回给服务消费方。服务提供方也是通过 NettyChannel 的 send 方法将 Response 对象返回。具体的就不再分析了。 服务消费方接收调用结果 服务消费方在收到响应数据后,首先要做的事情是对响应数据进行解码,得到 Response 对象。然后再将该对象传递给下一个入站处理器,这个入站处理器就是 NettyHandler。接下来 NettyHandler 会将这个对象继续向下传递,最后 AllChannelHandler 的 received 方法会收到这个对象,并将这个对象派发到线程池中。这个过程和服务提供方接收请求的过程是一样的,因此这里就不重复分析了。本节我们重点分析两个方面的内容,一是响应数据的解码过程,二是 Dubbo 如何将调用结果传递给用户线程的。下面先来分析响应数据的解码过程。 响应数据解码 响应数据解码的逻辑主要封装在DubboCodec中。 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 public class DubboCodec extends ExchangeCodec implements Codec2 { @Override protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException { byte flag = header[2], proto = (byte) (flag & SERIALIZATION_MASK); Serialization s = CodecSupport.getSerialization(channel.getUrl(), proto); // 获取请求编号 long id = Bytes.bytes2long(header, 4); // 检测消息类型,若下面的条件成立,表明消息类型为 Response if ((flag & FLAG_REQUEST) == 0) { // 创建 Response 对象 Response res = new Response(id); // 检测事件标志位 if ((flag & FLAG_EVENT) != 0) { // 设置心跳事件 res.setEvent(Response.HEARTBEAT_EVENT); } // 获取响应状态 byte status = header[3]; // 设置响应状态 res.setStatus(status); // 如果响应状态为 OK,表明调用过程正常 if (status == Response.OK) { try { Object data; if (res.isHeartbeat()) { // 反序列化心跳数据,已废弃 data = decodeHeartbeatData(channel, deserialize(s, channel.getUrl(), is)); } else if (res.isEvent()) { // 反序列化事件数据 data = decodeEventData(channel, deserialize(s, channel.getUrl(), is)); } else { DecodeableRpcResult result; // 根据 url 参数决定是否在 IO 线程上执行解码逻辑 if (channel.getUrl().getParameter( Constants.DECODE_IN_IO_THREAD_KEY, Constants.DEFAULT_DECODE_IN_IO_THREAD)) { // 创建 DecodeableRpcResult 对象 result = new DecodeableRpcResult(channel, res, is, (Invocation) getRequestData(id), proto); // 进行后续的解码工作 result.decode(); } else { // 创建 DecodeableRpcResult 对象 result = new DecodeableRpcResult(channel, res, new UnsafeByteArrayInputStream(readMessageData(is)), (Invocation) getRequestData(id), proto); } data = result; } // 设置 DecodeableRpcResult 对象到 Response 对象中 res.setResult(data); } catch (Throwable t) { // 解码过程中出现了错误,此时设置 CLIENT_ERROR 状态码到 Response 对象中 res.setStatus(Response.CLIENT_ERROR); res.setErrorMessage(StringUtils.toString(t)); } } // 响应状态非 OK,表明调用过程出现了异常 else { // 反序列化异常信息,并设置到 Response 对象中 res.setErrorMessage(deserialize(s, channel.getUrl(), is).readUTF()); } return res; } else { // 对请求数据进行解码,前面已分析过,此处忽略 } } } 解码之后,通过DecodeableRpcResult进行调用结果的反序列化。 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 public class DecodeableRpcResult extends RpcResult implements Codec, Decodeable { private Invocation invocation; @Override public void decode() throws Exception { if (!hasDecoded && channel != null && inputStream != null) { try { // 执行反序列化操作 decode(channel, inputStream); } catch (Throwable e) { // 反序列化失败,设置 CLIENT_ERROR 状态到 Response 对象中 response.setStatus(Response.CLIENT_ERROR); // 设置异常信息 response.setErrorMessage(StringUtils.toString(e)); } finally { hasDecoded = true; } } } @Override public Object decode(Channel channel, InputStream input) throws IOException { ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), serializationType) .deserialize(channel.getUrl(), input); // 反序列化响应类型 byte flag = in.readByte(); switch (flag) { case DubboCodec.RESPONSE_NULL_VALUE: break; case DubboCodec.RESPONSE_VALUE: // ... break; case DubboCodec.RESPONSE_WITH_EXCEPTION: // ... break; // 返回值为空,且携带了 attachments 集合 case DubboCodec.RESPONSE_NULL_VALUE_WITH_ATTACHMENTS: try { // 反序列化 attachments 集合,并存储起来 setAttachments((Map<String, String>) in.readObject(Map.class)); } catch (ClassNotFoundException e) { throw new IOException(StringUtils.toString("Read response data failed.", e)); } break; // 返回值不为空,且携带了 attachments 集合 case DubboCodec.RESPONSE_VALUE_WITH_ATTACHMENTS: try { // 获取返回值类型 Type[] returnType = RpcUtils.getReturnTypes(invocation); // 反序列化调用结果,并保存起来 setValue(returnType == null || returnType.length == 0 ? in.readObject() : (returnType.length == 1 ? in.readObject((Class<?>) returnType[0]) : in.readObject((Class<?>) returnType[0], returnType[1]))); // 反序列化 attachments 集合,并存储起来 setAttachments((Map<String, String>) in.readObject(Map.class)); } catch (ClassNotFoundException e) { throw new IOException(StringUtils.toString("Read response data failed.", e)); } break; // 异常对象不为空,且携带了 attachments 集合 case DubboCodec.RESPONSE_WITH_EXCEPTION_WITH_ATTACHMENTS: try { // 反序列化异常对象 Object obj = in.readObject(); if (obj instanceof Throwable == false) throw new IOException("Response data error, expect Throwable, but get " + obj); // 设置异常对象 setException((Throwable) obj); // 反序列化 attachments 集合,并存储起来 setAttachments((Map<String, String>) in.readObject(Map.class)); } catch (ClassNotFoundException e) { throw new IOException(StringUtils.toString("Read response data failed.", e)); } break; default: throw new IOException("Unknown result flag, expect '0' '1' '2', get " + flag); } if (in instanceof Cleanable) { ((Cleanable) in).cleanup(); } return this; } } 向用户线程传递调用结果 响应数据解码完成后,Dubbo 会将响应对象派发到线程池上。要注意的是,线程池中的线程并非用户的调用线程,所以要想办法将响应对象从线程池线程传递到用户线程上。我们之前分析过用户线程在发送完请求后的动作,即调用 DefaultFuture 的 get 方法等待响应对象的到来。当响应对象到来后,用户线程会被唤醒,并通过调用编号获取属于自己的响应对象。下面我们来看一下整个过程对应的代码。 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 public class HeaderExchangeHandler implements ChannelHandlerDelegate { @Override public void received(Channel channel, Object message) throws RemotingException { channel.setAttribute(KEY_READ_TIMESTAMP, System.currentTimeMillis()); ExchangeChannel exchangeChannel = HeaderExchangeChannel.getOrAddChannel(channel); try { if (message instanceof Request) { // 处理请求,前面已分析过,省略 } else if (message instanceof Response) { // 处理响应 handleResponse(channel, (Response) message); } else if (message instanceof String) { // telnet 相关,忽略 } else { handler.received(exchangeChannel, message); } } finally { HeaderExchangeChannel.removeChannelIfDisconnected(channel); } } static void handleResponse(Channel channel, Response response) throws RemotingException { if (response != null && !response.isHeartbeat()) { // 继续向下调用 DefaultFuture.received(channel, response); } } } public class DefaultFuture implements ResponseFuture { private final Lock lock = new ReentrantLock(); private final Condition done = lock.newCondition(); private volatile Response response; public static void received(Channel channel, Response response) { try { // 根据调用编号从 FUTURES 集合中查找指定的 DefaultFuture 对象 DefaultFuture future = FUTURES.remove(response.getId()); if (future != null) { // 继续向下调用 future.doReceived(response); } else { logger.warn("The timeout response finally returned at ..."); } } finally { CHANNELS.remove(response.getId()); } } private void doReceived(Response res) { lock.lock(); try { // 保存响应对象 response = res; if (done != null) { // 唤醒用户线程 done.signal(); } } finally { lock.unlock(); } if (callback != null) { invokeCallback(callback); } } } 以上逻辑是将响应对象保存到相应的 DefaultFuture 实例中,然后再唤醒用户线程,随后用户线程即可从 DefaultFuture 实例中获取到相应结果。 为什么要有调用编号? 一般情况下,服务消费方会并发调用多个服务,每个用户线程发送请求后,会调用不同 DefaultFuture 对象的 get 方法进行等待。 一段时间后,服务消费方的线程池会收到多个响应对象。这个时候要考虑一个问题,如何将每个响应对象传递给相应的 DefaultFuture 对象,且不出错。答案是通过调用编号。

2020/5/10
articleCard.readMore

InnoDB的锁

什么是锁? 锁是数据库系统区别于文件系统的一个关键特性,锁机制用于管理对共享资源的并发访问。 lock和latch latch一般称为闩锁,因为其要求锁定的时间必须非常短,则应用的性能会非常的差。在InnoDB存储引擎中,latch由可以分为matuex(互斥量)和rwlock(读写锁)。其目的是用来保证并发线程操作临界资源的正确性,并且通常没有死锁检查机制。 lock的对象是事务,用来锁定的是数据库中的对象,如表、页、行。并且一般lock的对象仅在事务commit或rollback后进行释放。此外lock是有死锁检查机制的。 InnoDB存储引擎中的锁 锁的类型 InnoDB存储引擎实现了两种标准的行级锁: 共享锁(S Lock),允许事务读一行数据 排他锁(X Lock),允许事务删除或更新一行数据 InnoDB存储引擎支持多粒度锁定,这种锁定允许事务在行级锁和标记锁上的锁定同时存在。为了支持不同粒度上进行加锁操作,InnoDB存储引擎支持一个额外的锁方式,称为意向锁。意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁。 InnoDB存储引擎支持意向锁设计比较简练,其意向锁即为表级别的锁。设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型。其支持两种意向锁: 意向共享锁(IS Lock),事务想要获得一张表中某几行的共享锁。 意向排他锁(IX Lock),事务想要获得一张表中某几行的排他锁。 一致性非锁定读 一致性非锁定读是指InnoDB存储引擎通过多版本控制的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行DELETE或UPDATE操作,这式读取操作不会因此区等待行上锁的释放。相反的,InnoDB存储引擎会区读取行的一个快照数据。快照数据是只该行的之前版本的数据,该实现是通过indo断来完成,而undo用来在事务中回滚数据,因此快照数据本身是没有额外的开销。此外,读取快照数据是不需要上锁的,因为没有事务需要对历史的数据进行修改操作。这种并发控制,其实就是多版本并发控制MVCC。 一致性锁定读 在默认的配置下,事务的隔离级别为可重复读,innoDB存储引擎的select操作使用一致性非锁定读,但是在某些情况下,用户需要显示地对数据库读取操作进行加锁一保证数据逻辑地一致性。而这个时候就需要数据库支持加锁语句。InnoDB存储引擎对于select语句支持两种一致性地锁定读操作。 select … for update(对读取地行加一个X锁) select …lock in share mode(对读取地行加一个S锁) 自增长与锁 在InnoDB存储引擎地内存结构中,对每个含有自镇长只地表都有一个自增长计数器。档含有自增长计数器地表进行插入操作时,这个计数器会被初始化。插入操作会根据这个自增长地计数器加一赋予自增长列。这个实现方式叫 AUTO-INC Locking.这种锁其实是采用一种特殊地表锁机制,为了提高插入地性能,锁不是在一个事务完成后才释放,而是在完成对自增长值插入地SQL语句后立即释放。 ​ 外键和锁 外键的主要用于引用完整性的约束检查,在InnoDB存储引擎中,对于一个外键列,如果没有显示地对这个列加索引,InnoDB存储引擎自动对其加一个索引,因为这样可以避免表锁。 对于外键值地插入或更新,首先要查询父表中地记录,即select父表,但是对于父表的select操作,不是使用一致性非锁定读的方式,因为这样会发生数据不一致的问题,因此这时使用的时select … lock in share mode方式,即主动对父表加一个s锁。如果这时父表上已经加了x锁,子表上的操作会被阻塞。 锁的算法 行锁的三种算法 InnoDB存储引擎有三种行锁的算法,其分别是: Record Lock:单个行记录上的锁 Gap Lock:间隙锁,锁定一个范围,但不包含记录本身 Next-Key Lock:Gap Lock+record Lock,锁定一个范围,并且锁定记录本身。 record lock总是会区锁定索引记录,如果innodb存储引擎表在建立的时候没有设置任何一个索引,那么这时InnoDB存储引擎会使用隐式的主键来进行锁定。 解决幻读问题 幻读问题是指在同一事务中,连续两次同样的sql语句可能导致不同的结果,第二次的sql语句可能会返回之前不存在的行。InnoDB存储引擎采用Next-Key Locking的算法来避免幻读问题。 死锁 死锁的概念 死锁是指两个或两个以上的事务在执行过程中,因争夺资源而造成的一种互相等待的现象。 解决方案 解决死锁的一种方案是超时,即当两个事务互相等待时,当一个等待时间超过设置的某一阈值时,其中一个事务进行回滚,另一个继续进行。 这种超时机制虽然简单,但是其仅通过超时后对事务进行回滚的方式来处理,或者说其时根据FIFO的顺序选择回滚对象。当若超时的事务所占的权重较大,如果事务操作更新了很多行,占用了较多的undo log,这时FIFO的方式,就显得不合适了,因为回滚这个事务的时间相对于另一个事务所占用的时间可能会很多。 因此除了超时机制之外,当前数据库还普遍采用了等待图的方式来进行死锁检测。较之超时的解决方案,这是一种更为主动的死锁检测方式。InnoDB存储引擎也采用这种方式。等待图要求数据库保存以下两种信息。 锁的信息链表 事务等待链表 通过上述信息可以够着出一张图,而在这个图种若存在回路,就代表存在死锁,因此资源间互相发生等待。 锁升级 锁升级时至当前锁粒度降低,举例来说,数据库可以把一个表的1000个行锁升级未页锁,或者间页锁升级未表锁。如果在数据库的设计中,认为锁是一种稀有资源,而且想要避免锁的开销,那数据库中会频繁出现锁升级现象。 InnoDB存储引擎不存在锁升级这个问题,因为其不是根据每个记录来产生行锁的,相反,其根据每个事务访问的每个页对锁进行管理的,采用的是位图的方式,因此不管一个事务锁住页中一个记录还是多个记录,其开销都是一致的。

2020/5/4
articleCard.readMore

SpringBoot启动原理

SpringBoot启动 SpringBoot因为内置了tomcat或jetty服务器,不需要直接部署War文件,所以SpringBoot的程序起点是一个普通的主函数。主函数如下: 1 2 3 4 5 6 @SpringBootApplication public class SpringbootStudyApplication { public static void main(String[] args) { SpringApplication.run(SpringbootStudyApplication.class, args); } } 整个SpringBoot的启动过程其实都是通过@SpringBootApplication注解和SpringApplication.run方法来实现的。 整个启动的过程可以概括为: 读取所有依赖的META-INF/spring.factories文件,该文件指明了哪些依赖可以被自动加载。 根据importSelector类选择加载哪些依赖,使用conditionOn系列注解排除掉不需要的配置文件 将剩余的配置文件所代表的bean加载到IOC容器中。 比如spring-boot-2.1.8RELEASE.jar中的spring.factories文件的内容是整个样子的(节选): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # PropertySource Loaders org.springframework.boot.env.PropertySourceLoader=\ org.springframework.boot.env.PropertiesPropertySourceLoader,\ org.springframework.boot.env.YamlPropertySourceLoader # Run Listeners org.springframework.boot.SpringApplicationRunListener=\ org.springframework.boot.context.event.EventPublishingRunListener # Error Reporters org.springframework.boot.SpringBootExceptionReporter=\ org.springframework.boot.diagnostics.FailureAnalyzers # Application Context Initializers org.springframework.context.ApplicationContextInitializer=\ org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\ org.springframework.boot.context.ContextIdApplicationContextInitializer,\ org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\ org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer 这个文件中的内容最终会被解析为Map<K,List<V>>这种格式。键和值都是一个类的全限定名。 跟踪源码,探索原理 我们从这段代码开始跟踪SpringApplication.run(SpringbootStudyApplication.class, args); 这段代码经过重重调用最终来到了: 1 2 3 public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) { return new SpringApplication(primarySources).run(args); } 这个方法完成了SpringApplication的实例化。具体的实例化过程如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) { this.resourceLoader = resourceLoader; Assert.notNull(primarySources, "PrimarySources must not be null"); this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources)); this.webApplicationType = WebApplicationType.deduceFromClasspath(); //将初始化器放到数组中 setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); //把初始化的监听器加入到数组中 setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); //获得主类 this.mainApplicationClass = deduceMainApplicationClass(); } 整个Application的实例化过程中,下面这两行代码比较关键。 1 2 setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); 方法的具体实现如下: 1 2 3 4 5 6 7 8 9 10 private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) { ClassLoader classLoader = getClassLoader(); // Use names and ensure unique to protect against duplicates //获取FactoryClass的全限定名 Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader)); //直接利用反射实例化对象 List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names); AnnotationAwareOrderComparator.sort(instances); return instances; } 这里的loadFactoryNames方法,其实就是从我们之前提到的spring.factories读取数据,然后以Map的形式进行存储的,loadFactoryNames就是从这个Map`中取数据(类的全限定名)。 读取spring.factories的具体实现: 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 private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) { MultiValueMap<String, String> result = cache.get(classLoader); if (result != null) { return result; } try { Enumeration<URL> urls = (classLoader != null ? // FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories" classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); result = new LinkedMultiValueMap<>(); while (urls.hasMoreElements()) { URL url = urls.nextElement(); UrlResource resource = new UrlResource(url); Properties properties = PropertiesLoaderUtils.loadProperties(resource); for (Map.Entry<?, ?> entry : properties.entrySet()) { String factoryClassName = ((String) entry.getKey()).trim(); for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) { result.add(factoryClassName, factoryName.trim()); } } } cache.put(classLoader, result); return result; } catch (IOException ex) { throw new IllegalArgumentException("Unable to load factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex); } // output // like: // org.springframework.boot.autoconfigure.EnableAutoConfiguration -> {LinkedList@1446} size = 118 // | // |- org.springframework.boot.autoconfigure.EnableAutoConfiguration } 拿到需要加载的类的全限定名之后,就通过反射进行实例化,然后返回。在Application的构造器中,拿到这些对象后,存入到List中。 1 2 private List<ApplicationContextInitializer<?>> initializers; private List<ApplicationListener<?>> listeners; 到这个地方,我们已经拿到了所有的依赖类,那么SpringBoot是如何进行自动配置的呢? 其实前面我们看到的源码都是SpingApplication的实例化,整个实例化过程就完成了依赖类信息,而run方法其实就是完成装配的。具体的看下面的分析: 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 public ConfigurableApplicationContext run(String... args) { StopWatch stopWatch = new StopWatch(); stopWatch.start(); ConfigurableApplicationContext context = null; Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>(); configureHeadlessProperty(); SpringApplicationRunListeners listeners = getRunListeners(args); listeners.starting(); try { ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); configureIgnoreBeanInfo(environment); Banner printedBanner = printBanner(environment); context = createApplicationContext(); exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context); prepareContext(context, environment, listeners, applicationArguments, printedBanner); //关键代码 refreshContext(context); afterRefresh(context, applicationArguments); stopWatch.stop(); if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch); } listeners.started(context); callRunners(context, applicationArguments); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, listeners); throw new IllegalStateException(ex); } try { listeners.running(context); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, null); throw new IllegalStateException(ex); } return context; } run方法中最关键的就是refreshContext(context);。它实际上是调用了refresh方法,这个方法对应读过Spring源码的同学不会陌生。而我们bean的装配过程实际上就是由它完成的。 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 public void refresh() throws BeansException, IllegalStateException { synchronized (this.startupShutdownMonitor) { // Prepare this context for refreshing. prepareRefresh(); // Tell the subclass to refresh the internal bean factory. ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // Prepare the bean factory for use in this context. prepareBeanFactory(beanFactory); try { // Allows post-processing of the bean factory in context subclasses. postProcessBeanFactory(beanFactory); // Invoke factory processors registered as beans in the context. invokeBeanFactoryPostProcessors(beanFactory); // Register bean processors that intercept bean creation. registerBeanPostProcessors(beanFactory); // Initialize message source for this context. initMessageSource(); // Initialize event multicaster for this context. initApplicationEventMulticaster(); // Initialize other special beans in specific context subclasses. onRefresh(); // Check for listener beans and register them. registerListeners(); // Instantiate all remaining (non-lazy-init) singletons. finishBeanFactoryInitialization(beanFactory); // Last step: publish corresponding event. finishRefresh(); } catch (BeansException ex) { if (logger.isWarnEnabled()) { logger.warn("Exception encountered during context initialization - " + "cancelling refresh attempt: " + ex); } // Destroy already created singletons to avoid dangling resources. destroyBeans(); // Reset 'active' flag. cancelRefresh(ex); // Propagate exception to caller. throw ex; } finally { // Reset common introspection caches in Spring's core, since we // might not ever need metadata for singleton beans anymore... resetCommonCaches(); } } } 其中invokeBeanFactoryPostProcessors会解析@import注解,并根据@import的属性进行下一步操作。 1 2 3 4 protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) { PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors()); // 省略 } 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 public static void invokeBeanFactoryPostProcessors( ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) { // Invoke BeanDefinitionRegistryPostProcessors first, if any. Set<String> processedBeans = new HashSet<>(); if (beanFactory instanceof BeanDefinitionRegistry) { BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; List<BeanFactoryPostProcessor> regularPostProcessors = new ArrayList<>(); List<BeanDefinitionRegistryPostProcessor> registryProcessors = new ArrayList<>(); // 记录是否是定义类的 Processor 或者普通的 Processor // Do not initialize FactoryBeans here: We need to leave all regular beans // uninitialized to let the bean factory post-processors apply to them! // Separate between BeanDefinitionRegistryPostProcessors that implement // PriorityOrdered, Ordered, and the rest. List<BeanDefinitionRegistryPostProcessor> currentRegistryProcessors = new ArrayList<>(); // ... // 应用 Bean 定义类的后置处理器 invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry); // ... } private static void invokeBeanDefinitionRegistryPostProcessors( Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry) { for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) { postProcessor.postProcessBeanDefinitionRegistry(registry); } } invokerBeandefintionRegistryPostProcessors函数对每一个定义类的后置处理器分别进行应用,@Configure的解析就在这个函数中。 1 2 3 4 5 6 7 // 从注册表中的配置类派生更多的bean定义 public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { // ... this.registriesPostProcessed.add(registryId); // Build and validate a configuration model based on the registry of Configuration classes. processConfigBeanDefinitions(registry); } 进入最关键的类ConfigurationClassPostProcessor,这个类用户来注册所有的@Configure和@Bean, 它的processConfigbeanDefinitions函数如下: 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 public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) { List<BeanDefinitionHolder> configCandidates = new ArrayList<>(); String[] candidateNames = registry.getBeanDefinitionNames(); // 记录所有候选的未加载的配置 // Return immediately if no @Configuration classes were found if (configCandidates.isEmpty()) { return; } // 按照 Ordered 对配置进行排序 // 加载自定义 bean 名命策略 if (this.environment == null) { this.environment = new StandardEnvironment(); } // Parse each @Configuration class ConfigurationClassParser parser = new ConfigurationClassParser( this.metadataReaderFactory, this.problemReporter, this.environment, this.resourceLoader, this.componentScanBeanNameGenerator, registry); Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates); Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size()); do { // 解译候选集 parser.parse(candidates); parser.validate(); Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses()); configClasses.removeAll(alreadyParsed); // Read the model and create bean definitions based on its content this.reader.loadBeanDefinitions(configClasses); alreadyParsed.addAll(configClasses); // ... } while (!candidates.isEmpty()); // Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) { sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry()); } if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) { // Clear cache in externally provided MetadataReaderFactory; this is a no-op // for a shared cache since it'll be cleared by the ApplicationContext. ((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache(); } } 在解释候选集parser.parse(candidates)中,会调用suorceClass=doProcessConfigurationClass(configClass,sourceClass)方法依次解析注解,得到所有的候选集。该方法顺次解析@PropertySource,@componentScan,@Import,@importResource,@Bean父类。 解析完成之后,会找到所有以 @PropertySource、@ComponentScan、@Import、@ImportResource、@Bean 注解的类及其对象,如果有 DeferredImportSelector,会将其加入到 deferredImportSelectorHandler 中,并调用 this.deferredImportSelectorHandler.process() 对这些 DeferredImportSelector 进行处理。 实际上,在 spring boot 中,容器初始化的时候,主要就是对 AutoConfigurationImportSelector 进行处理。 Spring 会将 AutoConfigurationImportSelector 封装成一个 AutoConfigurationGroup,用于处理。最终会调用 AutoConfigurationGroup 的 process 方法。 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 @Override public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) { // 主要通过该函数找到所有需要自动配置的类 AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector) .getAutoConfigurationEntry(getAutoConfigurationMetadata(), annotationMetadata); this.autoConfigurationEntries.add(autoConfigurationEntry); for (String importClassName : autoConfigurationEntry.getConfigurations()) { this.entries.putIfAbsent(importClassName, annotationMetadata); } } protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return EMPTY_ENTRY; } AnnotationAttributes attributes = getAttributes(annotationMetadata); List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes); configurations = removeDuplicates(configurations); Set<String> exclusions = getExclusions(annotationMetadata, attributes); checkExcludedClasses(configurations, exclusions); configurations.removeAll(exclusions); configurations = filter(configurations, autoConfigurationMetadata); fireAutoConfigurationImportEvents(configurations, exclusions); return new AutoConfigurationEntry(configurations, exclusions); } protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()); return configurations; } 如上,我们可以看到 process 最终调用了我们非常熟悉的函数 SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());,该方法以 EnableAutoConfiguration 类为键(org.springframework.boot.autoconfigure.EnableAutoConfiguration),取得所有的值。 在该函数中,还会调用 configurations = filter(configurations, autoConfigurationMetadata) 方法,将不需要的候选集全部排除。(该方法内部使用 AutoConfigurationImportFilter 的实现类排除)。 我们看一个常见的 configuration,即 org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,这个类中有大量的 @Bean 注解的方法,用来产生 bean,如下: 1 2 3 4 5 6 7 8 @Bean @Override public RequestMappingHandlerAdapter requestMappingHandlerAdapter() { RequestMappingHandlerAdapter adapter = super.requestMappingHandlerAdapter(); adapter.setIgnoreDefaultModelOnRedirect( this.mvcProperties == null || this.mvcProperties.isIgnoreDefaultModelOnRedirect()); return adapter; } spring 通过读取所有所需要的 AutoConfiguration,就可以加载默认的上下文容器,实现自动注入。 SpringBoot常用注解简介 @Configuration 作用于类,用于定义配置类,可替换xml配置文件,被注解的类内部包含有一个或多个被@Bean注解的方法,这些方法会被AnnotationConfigApplicationContext或AnncationConfigWebApplicationContext类进行扫描,并用于构建bean定义,初始化spring容器。 @ComponentScan 该注解会扫描@Controller,@Service,@Repository,@component注解到类到spring容器中。 @SpringBootApplication 该注解包含了@ComponentScan注解,所以在使用中我们可以通过@SpringBootApplication注解的scanBasepackages属性进行配置。 @Conditional 该注解作用于类,它可以根据代码中的条件装载不同的bean,在设置注解之前类需要实现Condition接口,然后对该实现接口的类设置是否装载条件。 @Import 通过导入的方式实现吧实例加入spring容器中,可以在需要时间没有被spring管理的类导入至Spring容器中。 @ImportResource 和@Import类似,区别就是该注解导入的是配置文件。 Component 该注解是一个元注解,意思是可以注解其它类注解,如@Controller @Service @Repository。带此注解的类被看作组件,当使用基于注解的配置和类路径扫描的时候,这些类就会被实例化。 @SpringBootApplication 这个注解是Spring Boot最核心的注解,用在SpringBoot的主类上,标识这是一个Spring Boot应用。用来开启Spring Boot的各项能力,实际上这个注解@Configuration,@EnableAutoConfiguration,@ComponentScan三个注解的组合.由于这些注解一般都是一起使用的。 @EnableAutoConfiguration 允许Spring Boot自动配置注解,开启这个注解之后,Spring Boot就能根据当前类路径下的包或者类来配置Spring Bean。配置信息是从META-INF/spring.factories加载的。

2020/5/3
articleCard.readMore

Maven和Git命令的一些总结

Maven命令 创建Maven的普通Java项目 1 2 3 mvn archetype:create -DgroupId=packageName - DartifactId=projectName 创建Maven的Web项目: 1 2 3 4 mvn archetype:create -DgroupId=packageName -DartifactId=webappName -DarchetypeArtifactId=maven-archetype-webapp 编译源代码 1 mvc compile 编译测试代码 1 mvn test-compile 运行测试 1 mvc test 产生site 1 mvn site 打包 1 mvn package 在本地的仓库中安装jar 1 mvc install 清除产生的项目 1 mvn clean 上传到私服 1 mvn deploy mvn compile与mvn install,mvn deloy的区别 mvn compile编译类文件 mvn install包含mvn compile,mvn package然后上传到本地仓库 mvn deploy包含mvn install然后上传到私服 Git命令 新建代码库 1 2 3 git init # 在当前目录生成一个git代码库 git init [project-name] # 新建一个目录,将其初始化为git代码库 git clone [url] # 下载一个项目和它的整个代码历史 增加/删除文件 1 2 3 4 5 git add [file] # 添加指定文件到暂存区 git add [dir] # 添加指定目录到暂存区 git add . # 添加当前目录的所有文件到暂存区 git rm [file] # 删除工作区文件,并且将这次删除放入暂存区 git mv [file-original][fule-renamed] # 改名文件 代码提交 1 git commit -m [message] # 提交暂存区到仓库区 分支管理 1 2 3 4 5 git branch # 列出所有的本地分支 git branch -r # 列出所有的远程分支 git branch -a # 列出所有本地分支和远程分支 git branch [branch-name] # 新建一个分支,但依然停留在当前分支 git chechout -b [branch] # 新建一个分支,并切换到该分支 标签 1 2 3 4 5 6 7 git tag # 列出所有tag git tag [tag] # 新建一个tag在指定commit git tag -d [tag] # 删除本地tag git push origin :refs/tags/[tagName] # 删除远程分支 git show [tag] # 查看tag信息 git push [remote][tag] # 提交指定tag git checkout -b [branch][tag] # 新建一个分支,指向某个tag 查看信息 1 2 3 4 git status # 显示有变更的文件 git log # 显示当前分支的版本历史 git diff # 显示暂存区和工作区的代码差异 git reflog # 显示当前分支的最近几次提价 远程同步 1 2 3 4 5 git remore update # 更新远程仓库 git fetch [remote] # 下载远程仓库的所有变动 git remote -v # 显示所有远程仓库 git pull [remote][branch] #取回远程仓库的变换,并与本地分支合并 git push [remote][branch] # 上传本地指定分支到远程仓库 撤销 1 2 3 4 5 6 7 8 git checkout [file] #恢复缓冲区的指定文件到工作区 git checkout [commit][file] # 恢复某个commit的指定文件到暂存区和工作区 git reset [file] # 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变 git reset --hard # 重置暂存区与工作区,与上一次commit保持一致 git reset [commit] #重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变 git reset --hard [commit] # 重置当前分支的head为指定commit,同时重置暂存区和工作区,与指定commit一致 git revert [commit] # 新建一个commit,用于册小指定commit,后置的所有变换都会被前者抵消,并且应用当前分支

2020/5/2
articleCard.readMore

高可用Redis:Redis Cluster

Redis Cluster是Redis官方提供的Redis集群功能。 为什么现需要Redis Cluster 主从复制不能满足高可用的要求 随着业务的发展,需要更高的QPS 数据量的增长导致服务器内存不足以存储 网络流量的增长,业务的流量已经超过了服务器的网卡的上限值,可以考虑使用分布式技术来进行分流。 离线计算,需要中间环节缓冲等别的需求。 数据分布 当单机的redis节点无法满足要求,按照分区规则把数据分到若干个子集中。 常见的数据分布方法 顺序分布 比如有1-100个数据,要保存的三个节点上,那么1-33号数据放在第一个节点上,34-66号数据放在第二个节点上,依此类推。 哈希分布 对键进行hash后,根据哈希码,来进行分区。 常见的分区方式有: 节点取余分区:对key进行hash之后,与节点数进行取余运算,根据余数不同保存在不同的节点上。该分区方式的问题就是不利于节点数量的调整。但节点数量变动时,大量的数据需要迁移。 一致性哈希分区:将所有的数据当作一个token环,token环中的数据范围时0到2的32次方,然后为每一个数据节点分配一个token范围值,这个节点就负责保存这个节点范围的数据。对每一个key进行hash运算,将哈希后的结果在哪个token范围内,则按照顺时针去找最近的节点,这个key就会被保存在这个节点上。 虚拟槽分区:虚拟槽分区时Redis Cluster采用的分区方式,预设虚拟槽,每个槽就相当于一个数字,有一定的范围。每个槽映射一个数据子集,一般比节点数大。Redis cluster中预设的虚拟槽的范围为0到16383 虚拟槽分区的步骤: 1 2 3 4 5 6 7 1.把16384槽按照节点数量进行平均分配,由节点进行管理 2.对每个key按照CRC16规则进行hash运算 3.把hash结果对16383进行取余 4.把余数发送给Redis节点 5.节点接收到数据,验证是否在自己管理的槽编号的范围 如果在自己管理的槽编号范围内,则把数据保存到数据槽中,然后返回执行结果 如果在自己管理的槽编号范围外,则会把数据发送给正确的节点,由正确的节点来把数据保存在对应的槽中 虚拟槽分区的特点: 使用服务器端话管理节点,槽,数据 可以对数据进行打散,又可以保证数据分布均匀。 Redis Cluster基本架构 节点 Redis Cluster是分布式架构,即Redis Cluster中有多个节点,每个节点购汇负责数据读写操作,每个节点之间会进行通信, meet操作 节点之间会互相通信 meet操作时节点之间完成相互通信的基础,meet操作有一定的频率和规则。 分配槽 把16384个槽平均分配给节点进行管理,每个节点只能对自己负责的槽进行读写。由于每个节点之间都彼此通信,每个节点都知道另外节点负责管理的槽范围。 客户端访问任意任意节点时,对数据key按照CRC16规则进行hash运算,然后对运算结果对16383进行取余,如果余数在当前访问的节点管理的槽的范围内,则直接返回对应的数据,否则会告诉客户端去哪个节点获取数据,由客户端去正确的节点获取数据。 复制 保证高可用,每个主节点都有一个从节点,当主节点故障,cluster会按照规则实现主备的高可用 客户端路由 moved重定向 1 2 3 4 5 6 1. 每个节点通过通信都会共享redis cluster中槽和集群中对应节点的关系 2. 客户端向redis cluster的任意节点发送命令,接收命令的节点会根据CRC16规则进行hash算法与16383取余,计算自己的槽和对应节点 3. 如果保存数据槽被分配给当前节点,则去槽中执行命令,并把命令执行结果返回给客户端 4. 如果保存数据的槽不再当前节点的管理范围内,则向客户端返回moved重定向异常 5. 客户端接收到节点返回的节点,如果时moved异常,则从moved异常中获取目标节点的信息 6. 客户端向目标节点发送命令,获取命令执行命令。 ask重定向 对集群进行扩容和缩容时,需要对槽及槽中的数据进行迁移。 当客户端向某个节点发送命令,节点向客户端返回moved异常,告诉客户端数据对应的槽节点信息。如果此时正在进行集群拓展或者缩空操作,当客户端向正确的节点发送命令时,槽及槽中数据已经被迁移到被的节点了,就会返回ask。 步骤: 1 2 3 1. 客户端向目标节点发送命令,目标节点中的槽已经迁移到别的节点上,此时目标节点会返回ask转向给客户端 2. 客户端向新的节点发送asking命令给新的节点,然后再次给新节点发送命令 3. 新节点执行命令,把命令执行结果返回给客户端。 故障发现 Redis cluster通过pin/pong消息实现故障发现,不需要sentinel。 ping/pong不仅能传递节点与槽的对应消息,也能传递其它状态,比如:节点主从状态,节点故障等。 主观下线 主观下线只代表一个节点对另一个节点的判断,不代表所有节点的认知。 1 2 3 4 1. 节点1定期发送ping消息给节点2 2. 如果发送成功,代表节点2正常运行,节点2会响应pong消息给节点1,节点1更新与节点2的最后通信时间 3. 如果发送失败,则节点1和节点2之间的通信异常判断连接,在下一个定时任务周期时,仍然会与节点2发送ping消息。 4. 如果节点1发送与节点2最后通信时间超过node-timeout,则把节点2标识为pfail状态 客观下线 当半数以上持有槽的主节点都标记某节点主观下线时,可以保证判断的公平性。 客观下线流程: 1 2 1. 某个节点接收到其它节点发送的ping信息,如果接收到的ping消息中包含了其它pfail节点,这个节点会将主观下线消息添加到自身的故障列表中,故障列表中包含了当前节点接收到的每一个节点对其它节点的状态信息。 2. 当前节点把主观下线的消息内容添加到故障列表之后,会尝试对故障节点进行客观下线操作。 故障恢复 资格检查 1 2 3 4 5 对从节点的资格进行检查,只有难过检查的从节点才可以开始进行故障恢复 每个从节点检查与故障主节点的断线时间 超过cluster-node-timeout * cluster-slave-validity-factor数字,则取消资格 cluster-node-timeout默认为15秒,cluster-slave-validity-factor默认值为10 如果这两个参数都使用默认值,则每个节点都检查与故障主节点的断线时间,如果超过150秒,则这个节点就没有成为替换主节点的可能性 准备选举时间 使偏移量最大的从节点具备优先级成为主节点的条件。 选举投票 对选举出来的多个从节点进行投票,选出新的主节点 替换主节点 当前从节点取消复制变成离节点。执行cluster del slot撤销故障主节点负责的槽,并执行cluster add slot把这些槽分配给自己 向集群广播自己的pong消息,表明已经替换了故障从节点

2020/5/1
articleCard.readMore