<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Java 技术探秘</title>
    <link>https://www.szlinkroutes.com/</link>
    <description>Recent content on Java 技术探秘</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Tue, 28 Jun 2022 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://www.szlinkroutes.com/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>XxlJob09：路由策略</title>
      <link>https://www.szlinkroutes.com/post/xxljob09%E8%B7%AF%E7%94%B1%E7%AD%96%E7%95%A5/</link>
      <pubDate>Tue, 28 Jun 2022 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/post/xxljob09%E8%B7%AF%E7%94%B1%E7%AD%96%E7%95%A5/</guid>
      <description>注：本系列源码分析基于XxlJob 2.3.0，gitee仓库链接：https://gitee.com/funcy/xxl-job.git.&#xA;在前面配置xxl-job任务时，提到过xxl-job的路由策略，本文将来分析这些策略。&#xA;在任务的编辑页面，可以配置路由策略：&#xA;所谓的路由策略，是指在多个executor存在时，选择哪一个executor来执行任务的策略。xxl-job支持的路由策略有如下几种：&#xA;第一个：选择第一个executor 最后一个：选择最后一个executor 轮询：依次选择executor 随机：随机选择一个executor 一致性hash：使用一致性hash算法来选择executor 最不经常使用：选择最不经常使用的executor 最近最久未使用：选择最久未使用的executor 故障转移：判断当前选择的executor是否可用，若不可用，则选择另一个executor 忙碌转移：判断当前选择的executor是否处于忙碌状态，若处于，则选择另一executor 分片广播：每一个executor都会执行任务 路由策略的选择 路由策略的选择在XxlJobTrigger#processTrigger，相关代码如下：&#xA;// 获取路由策略枚举类，jobInfo.getExecutorRouteStrategy() 来自于数据库 ExecutorRouteStrategyEnum executorRouteStrategyEnum = ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null); .... // 路由策略的处理，区分分片广播与其他策略 if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST == executorRouteStrategyEnum) { if (index &amp;lt; group.getRegistryList().size()) { address = group.getRegistryList().get(index); } else { address = group.getRegistryList().get(0); } } else { // 执行策略，仅仅只是为了得到执行器的地址 routeAddressResult = executorRouteStrategyEnum.getRouter() .route(triggerParam, group.getRegistryList()); if (routeAddressResult.getCode() == ReturnT.SUCCESS_CODE) { address = routeAddressResult.getContent(); } } 在任务的配置时，任务的路由策略会保存在数据库中，在XxlJobTrigger#processTrigger方法中，会把路由策略拿出来以便获取路由策略的枚举类，jobInfo.getExecutorRouteStrategy()就是数据库中保存的策略。</description>
    </item>
    <item>
      <title>XxlJob08：任务执行流程（三）之执行器揭秘</title>
      <link>https://www.szlinkroutes.com/post/xxljob08%E4%BB%BB%E5%8A%A1%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B%E4%B8%89%E4%B9%8B%E6%89%A7%E8%A1%8C%E5%99%A8%E6%8F%AD%E7%A7%98/</link>
      <pubDate>Tue, 21 Jun 2022 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/post/xxljob08%E4%BB%BB%E5%8A%A1%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B%E4%B8%89%E4%B9%8B%E6%89%A7%E8%A1%8C%E5%99%A8%E6%8F%AD%E7%A7%98/</guid>
      <description>注：本系列源码分析基于XxlJob 2.3.0，gitee仓库链接：https://gitee.com/funcy/xxl-job.git.&#xA;在上一篇文章中，要执行的任务终于通过ExecutorBiz.run(...)发往了executor，由此开始了executor上任务的执行操作。关于admin到executor之间的通讯，在《admin与executor通讯》一文中已详细分析过了，这里我们直接看executor进程中ExecutorBiz.run(...)方法的操作。&#xA;ExecutorBizImpl#run 获取 jobHandler 进入 ExecutorBizImpl#run 方法，内容如下：&#xA;public ReturnT&amp;lt;String&amp;gt; run(TriggerParam triggerParam) { // load old：jobHandler + jobThread JobThread jobThread = XxlJobExecutor.loadJobThread(triggerParam.getJobId()); IJobHandler jobHandler = jobThread!=null?jobThread.getHandler():null; String removeOldReason = null; // 先分析这么多，剩下的接下来再分析 ... 方法一开始，就从XxlJobExecutor中获取JobThread，然后从JobThread中获取IJobHandler。这简单的3行代码，一下子出现了3个类，下面一一来分析。&#xA;XxlJobExecutor.loadJobThread 关于XxlJobExecutor，从名称上来看，就是处理xxlJob任务执行逻辑的，在《执行器启动流程》一文中对该类的部分功能也做过分析，这里我们直接来看loadJobThread(...)相关方法的内容：&#xA;private static ConcurrentMap&amp;lt;Integer, JobThread&amp;gt; jobThreadRepository = new ConcurrentHashMap&amp;lt;Integer, JobThread&amp;gt;(); public static JobThread loadJobThread(int jobId){ JobThread jobThread = jobThreadRepository.get(jobId); return jobThread; } 在XxlJobExecutor中，有一个ConcurrentMap结构，key是jobId，value是jobThread，loadJobThread(xxx)方法就是从这个ConcurrentMap中获取jobThread的操作了。&#xA;关于JobThread放入jobThreadRepository中的操作，我们下面会分析到。&#xA;IJobHandler IJobHandler是一个接口，其中定义了3个方法：&#xA;/** * job handler * * @author xuxueli 2015-12-19 19:06:38 */ public abstract class IJobHandler { /** * 处理任务的执行，当executor收到调度请求时被调用 * execute handler, invoked when executor receives a scheduling request * * @throws Exception */ public abstract void execute() throws Exception; /*@Deprecated public abstract ReturnT&amp;lt;String&amp;gt; execute(String param) throws Exception;*/ /** * 初始化 handler，当 JobThread 初始化时被调用 * init handler, invoked when JobThread init */ public void init() throws Exception { // do something } /** * 销毁 handler，当 JobThread 销毁时被调用 * destroy handler, invoked when JobThread destroy */ public void destroy() throws Exception { // do something } } 每个方法的作用代码中已给出了注释，这个IJobHandler就是执行任务的组件了，它有3个子类：</description>
    </item>
    <item>
      <title>XxlJob07：任务执行流程（二）之触发器揭秘</title>
      <link>https://www.szlinkroutes.com/post/xxljob07%E4%BB%BB%E5%8A%A1%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B%E4%BA%8C%E4%B9%8B%E8%A7%A6%E5%8F%91%E5%99%A8%E6%8F%AD%E7%A7%98/</link>
      <pubDate>Tue, 14 Jun 2022 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/post/xxljob07%E4%BB%BB%E5%8A%A1%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B%E4%BA%8C%E4%B9%8B%E8%A7%A6%E5%8F%91%E5%99%A8%E6%8F%AD%E7%A7%98/</guid>
      <description>注：本系列源码分析基于XxlJob 2.3.0，gitee仓库链接：https://gitee.com/funcy/xxl-job.git.&#xA;上一篇文章中，任务调度完成后，最终会调用JobTriggerPoolHelper.trigger(...)方法进行触发操作，本文将来分析xxl-job任务触发流程。&#xA;JobTriggerPoolHelper#trigger 关于任务的触发，直观感受应该来自于管理后台界面：&#xA;点击执行一次后，调用的接口是/jobinfo/trigger，方法是JobInfoController#triggerJob代码如下：&#xA;@RequestMapping(&amp;#34;/trigger&amp;#34;) @ResponseBody public ReturnT&amp;lt;String&amp;gt; triggerJob(int id, String executorParam, String addressList) { if (executorParam == null) { executorParam = &amp;#34;&amp;#34;; } JobTriggerPoolHelper.trigger(id, TriggerTypeEnum.MANUAL, -1, null, executorParam, addressList); return ReturnT.SUCCESS; } 同上一文触发操作一样，最终调用的也是JobTriggerPoolHelper.trigger(...)方法.&#xA;继续，看看JobTriggerPoolHelper.trigger(...)方法的内容：&#xA;public static void trigger(int jobId, TriggerTypeEnum triggerType, int failRetryCount, String executorShardingParam, String executorParam, String addressList) { helper.addTrigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList); } 仅仅只是调用了JobTriggerPoolHelper#addTrigger方法，那我们继续下去，看看这个方法：&#xA;public void addTrigger(final int jobId, final TriggerTypeEnum triggerType, final int failRetryCount, final String executorShardingParam, final String executorParam, final String addressList) { // choose thread pool ThreadPoolExecutor triggerPool_ = fastTriggerPool; AtomicInteger jobTimeoutCount = jobTimeoutCountMap.</description>
    </item>
    <item>
      <title>XxlJob06：任务执行流程（一）之调度器揭密</title>
      <link>https://www.szlinkroutes.com/post/xxljob06%E4%BB%BB%E5%8A%A1%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B%E4%B8%80%E4%B9%8B%E8%B0%83%E5%BA%A6%E5%99%A8%E6%8F%AD%E5%AF%86/</link>
      <pubDate>Mon, 06 Jun 2022 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/post/xxljob06%E4%BB%BB%E5%8A%A1%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B%E4%B8%80%E4%B9%8B%E8%B0%83%E5%BA%A6%E5%99%A8%E6%8F%AD%E5%AF%86/</guid>
      <description>注：本系列源码分析基于XxlJob 2.3.0，gitee仓库链接：https://gitee.com/funcy/xxl-job.git.&#xA;从本文开始，我们将用三篇文章来介绍xxl-job最核心的功能——xxl-job任务执行流程。xxl-job任务的执行包含3个组件：调度器、触发器、执行器，本文将重点介绍调度器的流程。&#xA;xxl-job调度器位于admin进程，主要功能为精准获取将要执行的任务，处理调度器的方法为JobScheduleHelper#start，在《admin启动流程》一文中也提到过该方法，不过当时并未展开讨论，在本文中将详细分析该方法。&#xA;JobScheduleHelper#start 代码如下：&#xA;public void start(){ // schedule thread scheduleThread = new Thread(new Runnable() { public void run() { // 省略run()方法的内容，下面再展开 ... } }); scheduleThread.setDaemon(true); scheduleThread.setName(&amp;#34;xxl-job, admin JobScheduleHelper#scheduleThread&amp;#34;); scheduleThread.start(); // ring thread ringThread = new Thread(new Runnable() { public void run() { // 省略run()方法的内容，下面再展开 ... } }); ringThread.setDaemon(true); ringThread.setName(&amp;#34;xxl-job, admin JobScheduleHelper#ringThread&amp;#34;); ringThread.start(); } 可以看到，这个方法启动了两个线程：&#xA;scheduleThread：调度线程，用来获取需要执行的任务 ringThread：时间轮线程，用来精准控制任务的执行时间点 任务的调度就是依靠以上两个线程来完成的，我们继续。&#xA;scheduleThread 我们先来分析scheduleThread，查看其 run()方法：&#xA;@Override public void run() { // 休眠，表示线程启动5s后才去执行任务 try { TimeUnit.</description>
    </item>
    <item>
      <title>XxlJob05：执行器注册流程</title>
      <link>https://www.szlinkroutes.com/post/xxljob05%E6%89%A7%E8%A1%8C%E5%99%A8%E6%B3%A8%E5%86%8C%E6%B5%81%E7%A8%8B/</link>
      <pubDate>Sat, 28 May 2022 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/post/xxljob05%E6%89%A7%E8%A1%8C%E5%99%A8%E6%B3%A8%E5%86%8C%E6%B5%81%E7%A8%8B/</guid>
      <description>注：本系列源码分析基于XxlJob 2.3.0，gitee仓库链接：https://gitee.com/funcy/xxl-job.git.&#xA;本文来分析执行器注册到admin的流程，也就是EmbedServer#startRegistry方法。&#xA;有了前面两篇文章的铺垫，我们终于可以分析executor的注册流程了，注册流程在executor启动流程中的位置如下：&#xA;我们直接进入ExecutorRegistryThread#start的方法：&#xA;public void start(final String appname, final String address){ // 判断参数的合法性 if (appname==null || appname.trim().length()==0) { logger.warn(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, executor registry config fail, &amp;#34; + &amp;#34;appname is null.&amp;#34;); return; } // 如果没有admin服务，就不注册了 if (XxlJobExecutor.getAdminBizList() == null) { logger.warn(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, executor registry config fail, &amp;#34; + &amp;#34;adminAddresses is null.&amp;#34;); return; } registryThread = new Thread(new Runnable() { @Override public void run() { ... } }); registryThread.setDaemon(true); registryThread.setName(&amp;#34;xxl-job, executor ExecutorRegistryThread&amp;#34;); registryThread.</description>
    </item>
    <item>
      <title>XxlJob04：admin与executor通讯</title>
      <link>https://www.szlinkroutes.com/post/xxljob04admin%E4%B8%8Eexecutor%E9%80%9A%E8%AE%AF/</link>
      <pubDate>Fri, 20 May 2022 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/post/xxljob04admin%E4%B8%8Eexecutor%E9%80%9A%E8%AE%AF/</guid>
      <description>注：本系列源码分析基于XxlJob 2.3.0，gitee仓库链接：https://gitee.com/funcy/xxl-job.git.&#xA;本文将分析执行器（executor）与admin之间的通讯。&#xA;在xxl-job中，executor与admin并不是相互独立工作的，他们之前会通过网络通讯相互协作完成任务的调度流程，比如，executor启动时，需要把自己的地址信息（ip:端口）信息告知admin；任务调度时，admin会把任务信息发送给executor，在executor上真正执行任务。这两类通讯中，前者是executor到admin的通讯，后者是admin到executor的通讯，本文接下来就分析这两类通讯。&#xA;executor到admin 处理类：AdminBiz admin对executor提供的服务定义在com.xxl.job.core.biz.AdminBiz接口中，内容如下：&#xA;public interface AdminBiz { /** * callback * * @param callbackParamList * @return */ public ReturnT&amp;lt;String&amp;gt; callback(List&amp;lt;HandleCallbackParam&amp;gt; callbackParamList); /** * registry * * @param registryParam * @return */ public ReturnT&amp;lt;String&amp;gt; registry(RegistryParam registryParam); /** * registry remove * * @param registryParam * @return */ public ReturnT&amp;lt;String&amp;gt; registryRemove(RegistryParam registryParam); } 这是一个接口，可以看到它共有3个方法：&#xA;callback：处理任务回调 registry：处理执行器(executor)的注册，即将executor注册到admin registryRemove：处理执行器(executor)的下线，即将executor从admin中删除 AdminBiz有2个实现类：&#xA;com.xxl.job.core.biz.client.AdminBizClient：从名称上看，它是一个客户端类，位于executor进程中，内容如下：&#xA;public class AdminBizClient implements AdminBiz { // 省略属性及构造方法 .</description>
    </item>
    <item>
      <title>XxlJob03：执行器启动流程</title>
      <link>https://www.szlinkroutes.com/post/xxljob03%E6%89%A7%E8%A1%8C%E5%99%A8%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B/</link>
      <pubDate>Sat, 14 May 2022 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/post/xxljob03%E6%89%A7%E8%A1%8C%E5%99%A8%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B/</guid>
      <description>注：本系列源码分析基于XxlJob 2.3.0，gitee仓库链接：https://gitee.com/funcy/xxl-job.git.&#xA;本文将分析xxl-job执行器（executor）的启动流程。&#xA;执行器接入流程 在分析xxl-job执行器启动流程之前，我们先来看下如何让自己的项目变成xxl-job执行器，这里以springboot框架集成为例，示例项目为xxl-job-executor-sample-springboot。&#xA;1. 引入xxl-job-core包 要使用xxl-job的功能，第一步当然是引入其依赖包了，xxl-job-core的GAV坐标如下：&#xA;&amp;lt;!-- xxl-job-core --&amp;gt; &amp;lt;dependency&amp;gt; &amp;lt;groupId&amp;gt;com.xuxueli&amp;lt;/groupId&amp;gt; &amp;lt;artifactId&amp;gt;xxl-job-core&amp;lt;/artifactId&amp;gt; &amp;lt;version&amp;gt;${latest.version}&amp;lt;/version&amp;gt; &amp;lt;/dependency&amp;gt; 2. 新增配置类 这块是为了在项目中引入xxl-job的配置，配置类为 com.xxl.job.executor.core.config.XxlJobConfig:&#xA;@Configuration public class XxlJobConfig { private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class); @Value(&amp;#34;${xxl.job.admin.addresses}&amp;#34;) private String adminAddresses; @Value(&amp;#34;${xxl.job.accessToken}&amp;#34;) private String accessToken; @Value(&amp;#34;${xxl.job.executor.appname}&amp;#34;) private String appname; @Value(&amp;#34;${xxl.job.executor.address}&amp;#34;) private String address; @Value(&amp;#34;${xxl.job.executor.ip}&amp;#34;) private String ip; @Value(&amp;#34;${xxl.job.executor.port}&amp;#34;) private int port; @Value(&amp;#34;${xxl.job.executor.logpath}&amp;#34;) private String logPath; @Value(&amp;#34;${xxl.job.executor.logretentiondays}&amp;#34;) private int logRetentionDays; @Bean public XxlJobSpringExecutor xxlJobExecutor() { logger.info(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job config init.</description>
    </item>
    <item>
      <title>XxlJob02：admin启动流程</title>
      <link>https://www.szlinkroutes.com/post/xxljob02admin%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B/</link>
      <pubDate>Sun, 08 May 2022 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/post/xxljob02admin%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B/</guid>
      <description>注：本系列源码分析基于XxlJob 2.3.0，gitee仓库链接：https://gitee.com/funcy/xxl-job.git.&#xA;本文我们将分析xxl-job-admin的启动流程。xxl-job-admin 是一个springboot项目，直接启动com.xxl.job.admin.XxlJobAdminApplication就可以了，但是在启动过程中，xxl-job相关功能是如何初始化的呢？&#xA;spring 配置类: XxlJobAdminConfig 经过本人的一番探索，发现xxl-job 相关组件的启动类为com.xxl.job.admin.core.conf.XxlJobAdminConfig：&#xA;@Component public class XxlJobAdminConfig implements InitializingBean, DisposableBean { private XxlJobScheduler xxlJobScheduler; @Override public void afterPropertiesSet() throws Exception { adminConfig = this; xxlJobScheduler = new XxlJobScheduler(); xxlJobScheduler.init(); } @Override public void destroy() throws Exception { xxlJobScheduler.destroy(); } ... // 省略了属性以及setter、getter方法 } 关于这个类，几点说明如下：&#xA;该类被@Component标记，表明这是个spring bean，享有spring bean的生命同期； 该类实现了InitializingBean与DisposableBean两个接口，提供了spring bean在初始化及销毁阶段的一些操作，对应的两个方法如下： afterPropertiesSet：来自InitializingBean接口，用来处理bean在初始化时的一些操作 destroy：来自DisposableBean接口，用来处理bean在销毁时的一些操作 而实际上，XxlJobAdminConfig的afterPropertiesSet与destroy两个方法就是xxl-job启动与关闭的关键所在，而这两个方法都是调用的xxlJobScheduler的方法，接下来我们就来分析XxlJobScheduler#init与XxlJobScheduler#destroy方法。&#xA;这两个方法代码如下：&#xA;两个方法对比下可以看到，init方法正序启动了一系列组件，而destroy方法逆序关闭了一系列组件，正所谓“先启动的后关闭”。他们启动或关闭的组件如下：&#xA;JobTriggerPool：任务的触发线程，用来把需要执行的任务提交到执行器 JobRegistry：任务执行器注册监听，用来监听执行器注册操作，及时移除无效的执行器 JobFailMonitor：失败任务监听，用来监听失败任务，发送告警，对于重试次数大于0的失败任务会发再次触发执行 JobComplete：任务完成监听，用来监听任务是否完成，把长时间处于运行中的任务标记为失败 JobLogReport：任务报表，用来汇总任务的整体执行情况，也是管理后台“运行报表”菜单的数据来源 JobSchedule：任务调度，用来获取接下来要执行的任务，将这些任务提交给触发线程 接下来我们就来看下这些组件的实现。&#xA;触发线程池：JobTriggerPoolHelper 触发线程池的处理类为JobTriggerPoolHelper，我们来看看它的启动方法JobTriggerPoolHelper.toStart()：</description>
    </item>
    <item>
      <title>XxlJob01：环境准备</title>
      <link>https://www.szlinkroutes.com/post/xxljob01%E7%8E%AF%E5%A2%83%E5%87%86%E5%A4%87/</link>
      <pubDate>Wed, 04 May 2022 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/post/xxljob01%E7%8E%AF%E5%A2%83%E5%87%86%E5%A4%87/</guid>
      <description>关于定时任务，想必大家都不会陌生，简单的定时任务有jdk提供的定时任务、spring提供的定时任务，稍微复杂一些有quartz定时任务，分布式任务有xxl-job、elastic-job。由于项目中使用的定时任务是xxl-job，因此本系列将从源码的角度来分析xxl-job定时任务的设计与实现。&#xA;关于xxl-job的操作，xxl-job社区贴心地提供了操作文档：https://www.xuxueli.com/xxl-job/，文档不长，一遍看下来xxl-job的操作基本就会了，这块就不过多叙述了。&#xA;1. 获取xxl-job源码 既然是源码分析，当然要先获取源码了，xxl-job的github地址为https://github.com/xuxueli/xxl-job。一般来说，直接frok这个仓库就可以了，不过由于国内网络的原因，github 的访问并不稳定，比如checkout到一半网络断开、push超时等。为了规避这些问题，建议使用国内的git仓库——gitee，操作方式如下：&#xA;1.1 创建新的gitee仓库 登录个人的gitee，点击“+”号，选择“从github/gitlab导入仓库”：&#xA;1.2 导入xxl-job仓库 复制xxl-job的github地址，点击导入：&#xA;等待一会，就得到了自己的xxl-job的gitee地址了，本人得到的地址为https://gitee.com/funcy/xxl-job，并且gitee上也提供一个按钮，可以很方便地同步github上的代码：&#xA;如果有一天，你发现作者在github提交了大把代码，想及时同步到自己的gitee仓库，只要点击这个按钮就可以了，不过这样可能会覆盖自己的代码，可以通过新建分支来规避。&#xA;1.3 创建新分支 接下来，就可以基于该仓库自由操作了，继续，选择一个自己准备放项目的目录，然后clone：&#xA;git clone https://gitee.com/funcy/xxl-job 接下来，需要基于 tag 创建分支（新分支命为2.3.0.LEARN）&#xA;git tag git checkout 2.3.0 git checkout -b 2.3.0.LEARN 2.3.0 git push -u origin 2.3.0.LEARN 后续我们所有的分析操作就都在2.3.0.LEARN上进行了。&#xA;关于创建分支一些小疑问：&#xA;为什么要创建新分支？&#xA;主要是防止点击同步时，代码被覆盖。如果直接在master分支，或其他在github中已有的分支上进行修改，点击同步后，所作的修改都会被github上的分支覆盖，而在gitee上新建的分支则不会被覆盖&#xA;为什么要基于tag创建新分支而不是master？&#xA;一般来说，master上的代码是最新稳定版本的代码，因此会不断变化，而tag上的代码，表示打tag那一刻的代码，之后不再变化，这样就保证了同一个tag上的代码，无论什么时候都是一样的。&#xA;事实上，了解了git的tag概念后，会发现tag上的代码checkout下来后，无法再怎么修改都不能提交，即不会被修改；相反地，master或其他分支，修改后可以再提交。由于tag上的代码是不变的，为了能提交代码，必须要基于tag上的代码创建新分支&#xA;为什么选择的tag版本是2.3.0而不是其他？ 因为当前最新版的xxl-job是2.3.0，当然要分析最新版的代码了。&#xA;2. xxl-job 项目结构 拿到代码后，我们来看下xxl-job的项目结构：&#xA;xxl-job-admin：管理后台，提供任务管理界面，同时也是任务的调度器、触发器，本系列文章中简称为admin。 xxl-job-core：xxl-job的核心组件，同时也是xxl-job对外提供的jar包，提供了任务执行相关逻辑。 xxl-job-executor-samples：执行器示例，在本系列文章中，执行器是指集成了xxl-job任务执行功能的项目，也称为executor xxl-job-executor-sample-frameless：不集成任何框架的示例 xxl-job-executor-sample-springboot：集成了 springboot 框架的示例 关于该项目的更多细节，可以参考官方文档https://www.xuxueli.com/xxl-job/，这里截取官方的架构图：&#xA;建议先了解上图中xxl-job的组件，后面的源码分析会一步步揭开这些组件工作的原理。&#xA;3. 启动xxl-job 大致了解xxl-job的结构后，接下来我们在idea中启动xxl-job，这样也是方便我们后面调试。&#xA;3.1 执行sql脚本 xxl-job的sql脚本位于xxl-job/doc/db目录下，文件名为tables_xxl_job.sql&#xA;准备一个mysql数据库，直接执行该脚本即可。需要注意的是，如果数据库中已存在名为xxl_job数据库，执行该脚本会覆盖原来的库，需要注意下。&#xA;执行成功后，数据库中会出现如下表：&#xA;各表的功能如下：&#xA;xxl_job_user: 用户信息，用来验证登录到管理后台的用户是否合法 xxl_job_info: 任务信息表，用来配置需要执行的任务 xxl_job_log: 日志表，用来记录任务的执行日志 xxl_job_log_report: 任务执行报表，用来汇总、统计任务的执行情况 xxl_job_logglue: gule模式任务表，主要记录glue模式任务的执行代码 xxl_job_registry: 执行器的注册记录表，每一个执行器都保存一条注册记录 xxl_job_group: 执行器的注册汇总表，多个执行器汇总成一条记录 xxl_job_lock: 锁表，xxl-job用来实现分布式锁的表 3.</description>
    </item>
    <item>
      <title>会员体系中，积分过期的设计方案</title>
      <link>https://www.szlinkroutes.com/categories/%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/%E4%BC%9A%E5%91%98%E4%BD%93%E7%B3%BB%E4%B8%AD%E7%A7%AF%E5%88%86%E8%BF%87%E6%9C%9F%E7%9A%84%E8%AE%BE%E8%AE%A1%E6%96%B9%E6%A1%88/</link>
      <pubDate>Thu, 21 Oct 2021 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/categories/%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/%E4%BC%9A%E5%91%98%E4%BD%93%E7%B3%BB%E4%B8%AD%E7%A7%AF%E5%88%86%E8%BF%87%E6%9C%9F%E7%9A%84%E8%AE%BE%E8%AE%A1%E6%96%B9%E6%A1%88/</guid>
      <description>背景：积分项目，每次添加的积分都有一个有效期，有效期为一年，如2017-01-02添加了一条积分记录，到2018-01-02这条记录应该是过期的。当前项目设计有两张表：积分明细表（存放积分添加、使用明细）、积分总额表（用户当前的积分额度）。由于每条积分的过期时间各不相同，如何正确地将过期的积分作废？消费时，如何优先使用即将过期的积分？&#xA;1.问题的提出 刚开始系统有两张表：积分明细表、积分总额表，积分总额直接由积分明细表统计。&#xA;以下是积分明细表，需要将内容展示给用户，让用户知道自己积分的添加、消耗情况：&#xA;记录id 会员id 积分值 添加时间 过期时间 备注 1 1 10 2017-01-02 2018-01-02 新增积分 2 1 20 2017-01-04 2018-01-04 新增积分 3 1 -30 2017-01-08 2018-01-08 使用积分 假设id为1的会在2017-01-08后，再无积分添加与消耗的情况。&#xA;现在有一张积分总额表，记录的了会员的积分额度，由于积分是过期的，那么按不同时间来统计，就会出现以下情况：&#xA;在2018-01-01统计时，id为1的会员积分额度是0，这没问题。&#xA;会员id 积分总值 更新时间 1 0 2018-01-01 那在2018-01-02统计时呢，记录id为1的记录已经过期了，只统计记录id为2与3的记录，会出现以下情况：&#xA;会员id 积分总值 更新时间 1 -10 2018-01-02 在2018-01-04统计时，记录id为1与3的记录已经过期了，只统计记录id为3的记录，差额就更大了：&#xA;会员id 积分总值 更新时间 1 -30 2018-01-04 只有在2018-01-08统计时，记录id为1、2、3的记录已经过期了，积分值才会正常：&#xA;会员id 积分总值 更新时间 1 0 2018-01-08 从上面的情况来看，单纯从积分明细表来统计，会出现积分值为负数的情况。&#xA;2.解决方案 项目当前的方案中，积分明细表记录了用户使用添加与使用情况，需要展现给用户，所以无法在这上面下手，为此新添加了一张表来记录当前可用积分明细。该表的关键字段如下：&#xA;字段 含义 积分值 该记录的积分值 添加时间 该记录的添加时间 过期时间 该记录的过期时间 如，在2017-01-02该会员的可用积分记录为</description>
    </item>
    <item>
      <title>idea下，Jetty采用main方法启动web项目</title>
      <link>https://www.szlinkroutes.com/categories/%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/idea%E4%B8%8Bjetty%E9%87%87%E7%94%A8main%E6%96%B9%E6%B3%95%E5%90%AF%E5%8A%A8web%E9%A1%B9%E7%9B%AE/</link>
      <pubDate>Wed, 21 Jul 2021 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/categories/%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/idea%E4%B8%8Bjetty%E9%87%87%E7%94%A8main%E6%96%B9%E6%B3%95%E5%90%AF%E5%8A%A8web%E9%A1%B9%E7%9B%AE/</guid>
      <description>对于maven多模块的spring web项目，本地开发时，启动的方式一般有如下几种：&#xA;使用容器（tomcat/jetty/resin等），该方式需要ide支持，而社区版的idea并不支持； maven插件（jetty/tomcat），该方式只需在web模块的pom文件中加入插件依赖，然后运行 mvn jetty:run 或 mvn tomcat7:run 即可，不过对于多模块项目而言，代码时改动需要使用maven进行构建，而maven构建需耗时良久； jetty的main()方法启动，该方式启动时，并不需要使用maven进行构建，代码改动时仅需重新编译改动的源文件，且社区版的idea支持良好，本地开发时，是个不错的选择。 1. maven引入包 &amp;lt;!-- jetty相关依赖包，注意scope为test--&amp;gt; &amp;lt;dependency&amp;gt; &amp;lt;groupId&amp;gt;org.eclipse.jetty&amp;lt;/groupId&amp;gt; &amp;lt;artifactId&amp;gt;jetty-webapp&amp;lt;/artifactId&amp;gt; &amp;lt;version&amp;gt;9.3.2.v20150730&amp;lt;/version&amp;gt; &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt; &amp;lt;/dependency&amp;gt; &amp;lt;dependency&amp;gt; &amp;lt;groupId&amp;gt;org.eclipse.jetty&amp;lt;/groupId&amp;gt; &amp;lt;artifactId&amp;gt;jetty-annotations&amp;lt;/artifactId&amp;gt; &amp;lt;version&amp;gt;9.3.2.v20150730&amp;lt;/version&amp;gt; &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt; &amp;lt;/dependency&amp;gt; &amp;lt;dependency&amp;gt; &amp;lt;groupId&amp;gt;org.eclipse.jetty&amp;lt;/groupId&amp;gt; &amp;lt;artifactId&amp;gt;apache-jsp&amp;lt;/artifactId&amp;gt; &amp;lt;version&amp;gt;9.3.2.v20150730&amp;lt;/version&amp;gt; &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt; &amp;lt;/dependency&amp;gt; &amp;lt;dependency&amp;gt; &amp;lt;groupId&amp;gt;org.eclipse.jetty&amp;lt;/groupId&amp;gt; &amp;lt;artifactId&amp;gt;apache-jstl&amp;lt;/artifactId&amp;gt; &amp;lt;version&amp;gt;9.3.2.v20150730&amp;lt;/version&amp;gt; &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt; &amp;lt;/dependency&amp;gt; 2. 编写启动类 由于引入包时，scope为test，因此该类需位于src/test/java下。&#xA;import org.eclipse.jetty.jsp.JettyJspServlet; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.webapp.WebAppContext; import java.io.File; public class JettyStart { public static void main(String[] args) throws Exception { //添加你的模块名，对于maven多模块项目，需要指定 String moduleName = &amp;#34;module&amp;#34;; String dir = System.</description>
    </item>
    <item>
      <title>04.Apache thrift 之传输协议</title>
      <link>https://www.szlinkroutes.com/categories/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/thrift/04.-apache-thrift-%E4%B9%8B%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE/</link>
      <pubDate>Thu, 03 Jun 2021 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/categories/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/thrift/04.-apache-thrift-%E4%B9%8B%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE/</guid>
      <description>我们都知道，数据在网络上是以二进制方式传输的。&#xA;对于一个java对象，从客户端通过网络传输到服务端时，客户端需要将其转换为二进制，然后写入网络IO；服务端从网络IO接收到数据时，也需要将二进制数据转换为对象，然后再进行操作。&#xA;这两个转换过程，统称为编解码操作。&#xA;在Thrift中，编解码操作又叫传输协议，由抽象类 TProtocol 来定义，它的方法主要分为两大类：&#xA;TProtocol写入方法： TProtocol读取方法： 写入方法规定了数据转换成二进制的方式，读取方法规定了将二进制转换为数据的方式。&#xA;thrift中常用的传输协议有以下几种：&#xA;TBinaryProtocol：二进制编码格式进行数据传输 TCompactProtocol：高效率的、密集的二进制编码格式进行数据传输 TJSONProtocol： 使用JSON文本的数据编码协议进行数据传输 TSimpleJSONProtocol：只提供JSON只写的协议，适用于通过脚本语言解析 TMultiplexedProtocol：复合协议，同时处理多个服务 这些传输协议都是TProtocol的子类，接下来我们来分析下这些方法的操作。&#xA;TBinaryProtocol TBinaryProtocol 是最基本的实现，得到的二进制数据是原始数据（相比于压缩数据而言），这里我们选取几个方法了解转换过程。&#xA;读写 I32 类型数据 所谓的I32数据，是指32位的整型数据，也就是java中的int类型，写入方法如下：&#xA;public void writeI32(int i32) throws TException { // 将i32类型的数据转换为byte数组 inoutTemp[0] = (byte)(0xff &amp;amp; (i32 &amp;gt;&amp;gt; 24)); inoutTemp[1] = (byte)(0xff &amp;amp; (i32 &amp;gt;&amp;gt; 16)); inoutTemp[2] = (byte)(0xff &amp;amp; (i32 &amp;gt;&amp;gt; 8)); inoutTemp[3] = (byte)(0xff &amp;amp; (i32)); trans_.write(inoutTemp, 0, 4); } 代码中主要是通过位运算，将int类型转换为长度为4的byte数组。&#xA;int类型的读取操作如下：&#xA;@Override public int readI32() throws TException { byte[] buf = inoutTemp; int off = 0; // 读取4个byte if (trans_.</description>
    </item>
    <item>
      <title>03.Apache thrift 之网络模型</title>
      <link>https://www.szlinkroutes.com/categories/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/thrift/03.-apache-thrift-%E4%B9%8B%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%9E%8B/</link>
      <pubDate>Tue, 01 Jun 2021 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/categories/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/thrift/03.-apache-thrift-%E4%B9%8B%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%9E%8B/</guid>
      <description>本文我们来分析thrift的网络传输模式。&#xA;客户端 Thrift 客户端常用的传输层有以下几种：&#xA;TSocket：使用阻塞式I/O进行传输，是最常见的模式&#xA;TNonblockingSocket：使用非阻塞方式，用于构建异步客户端&#xA;TFramedTransport：使用非阻塞方式，按块的大小进行传输，类似于Java中的NIO&#xA;接下来我们从源码的角度来分析这几种传输层的实现。&#xA;阻塞式IO：TSocket TSocket使用了阻塞式I/O进行传输，代码实现如下：&#xA;public class TSocket extends TIOStreamTransport { private Socket socket_; /** * 构造方法 */ public TSocket(String host, int port) throws TTransportException { this(new TConfiguration(), host, port, 0); } /** * 构造方法 */ public TSocket(TConfiguration config, String host, int port, int socketTimeout, int connectTimeout) throws TTransportException { super(config); host_ = host; port_ = port; socketTimeout_ = socketTimeout; connectTimeout_ = connectTimeout; initSocket(); } /** * 初始化方法 */ private void initSocket() { // 创建 Socket 对象 socket_ = new Socket(); try { // 对 socket 进行一些配置 socket_.</description>
    </item>
    <item>
      <title>02.Apache thrift 之请求处理流程</title>
      <link>https://www.szlinkroutes.com/categories/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/thrift/02.-apache-thrift-%E4%B9%8B%E8%AF%B7%E6%B1%82%E5%A4%84%E7%90%86%E6%B5%81%E7%A8%8B/</link>
      <pubDate>Sat, 29 May 2021 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/categories/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/thrift/02.-apache-thrift-%E4%B9%8B%E8%AF%B7%E6%B1%82%E5%A4%84%E7%90%86%E6%B5%81%E7%A8%8B/</guid>
      <description>本文我们以 HelloService 为例，来分析thrfit的请求处理流程。&#xA;服务端启动 HelloService 的服务端启动在 HelloServer，这是我们自定义的类，其中就只有一个main方法：&#xA;public static void main(String[] args) { try { // 创建处理器，这个就是最终处理请求的类 HelloService.Processor processor = new HelloService.Processor&amp;lt;&amp;gt;(new HelloServiceImpl()); // 配置传输类型 TServerTransport transport = new TServerSocket(SERVER_PORT); // 配置服务器 TServer server = new TSimpleServer(new TServer.Args(transport).processor(processor)); System.out.println(&amp;#34;Starting the simple server...&amp;#34;); // 对外提供服务 server.serve(); } catch (Exception e) { e.printStackTrace(); } } 该方法的关键部分在注释中已经详细说明了，接下来我们来一步步分析这些步骤。&#xA;创建处理器 创建处理器的代码为&#xA;HelloService.Processor processor = new HelloService.Processor&amp;lt;&amp;gt;(new HelloServiceImpl()) 我们进入HelloService.Processor#Processor(I)方法：&#xA;public Processor(I iface) { super(iface, getProcessMap(new java.util.HashMap&amp;lt;java.lang.String, org.</description>
    </item>
    <item>
      <title>01.Apache thrift 之使用示例</title>
      <link>https://www.szlinkroutes.com/categories/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/thrift/01.-apache-thrift-%E4%B9%8B%E4%BD%BF%E7%94%A8%E7%A4%BA%E4%BE%8B/</link>
      <pubDate>Thu, 27 May 2021 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/categories/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/thrift/01.-apache-thrift-%E4%B9%8B%E4%BD%BF%E7%94%A8%E7%A4%BA%E4%BE%8B/</guid>
      <description>最近看到一些招聘说明有关于thrift的要求：&#xA;作为一个面向简历编程的程序员，多年来使用的都是springmvc下的http调用，不免对这个thrift有了一丝好奇，于是thrift的学习就提上了日程。&#xA;Thrift 基本概念 百度百科的定义如下：&#xA;Thrift是一种接口描述语言和二进制通讯协议，它被用来定义和创建跨语言的服务。它被当作一个远程过程调用（RPC）框架来使用，是由Facebook为“大规模跨语言服务开发”而开发的。&#xA;关于那些大而泛的概念，本文就不多介绍了，直接进入thfrit的学习吧。&#xA;Thrift的数据类型 Thrift 脚本可定义的数据类型包括以下几种类型：&#xA;基本类型：&#xA;bool: 布尔值 byte: 8位有符号整数 i16: 16位有符号整数 i32: 32位有符号整数 i64: 64位有符号整数 double: 64位浮点数 string: UTF-8编码的字符串 binary: 二进制串 结构体类型：&#xA;struct: 定义的结构体对象 容器类型：&#xA;list: 有序元素列表 set: 无序无重复元素集合 map: 有序的key/value集合 异常类型：&#xA;exception: 异常类型 服务类型：&#xA;service: 具体对应服务的类 Thrift的协议 Thrift可以让用户选择客户端与服务端之间传输通信协议的类别，在传输协议上总体划分为**文本(text)和二进制(binary)**传输协议。为节约带宽，提高传输效率，一般情况下使用二进制类型的传输协议为多数，有时还会使用基于文本类型的协议，这需要根据项目/产品中的实际需求。常用协议有以下几种：&#xA;TBinaryProtocol：二进制编码格式进行数据传输 TCompactProtocol：高效率的、密集的二进制编码格式进行数据传输 TJSONProtocol： 使用JSON文本的数据编码协议进行数据传输 TSimpleJSONProtocol：只提供JSON只写的协议，适用于通过脚本语言解析 Thrift的传输层 常用的传输层有以下几种：&#xA;TSocket：使用阻塞式I/O进行传输，是最常见的模式 TNonblockingTransport：使用非阻塞方式，用于构建异步客户端 TFramedTransport：使用非阻塞方式，按块的大小进行传输，类似于Java中的NIO Thrift的服务端类型 TSimpleServer：单线程服务器端，使用标准的阻塞式I/O TThreadPoolServer：多线程服务器端，使用标准的阻塞式I/O TNonblockingServer：单线程服务器端，使用非阻塞式I/O THsHaServer：半同步半异步服务器端，基于非阻塞式IO读写和多线程工作任务处理 TThreadedSelectorServer：多线程选择器服务器端，对THsHaServer在异步IO模型上进行增强 简单了解完上面的概念后，接下来我们将通过一个实例来演示thrift的使用。&#xA;安装 使用thrift前，需要安装thrift命令行工具，该工具用来将编写的thfirt文件编译成指定的源码文件。&#xA;在 Mac 上用如下命令安装：&#xA;$ brew install thrift 安装成功后，查看版本：</description>
    </item>
    <item>
      <title>springboot 多数据源下，开启事务后，数据源切换失败的处理</title>
      <link>https://www.szlinkroutes.com/categories/%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/springboot-%E5%A4%9A%E6%95%B0%E6%8D%AE%E6%BA%90%E4%B8%8B%E5%BC%80%E5%90%AF%E4%BA%8B%E5%8A%A1%E5%90%8E%E6%95%B0%E6%8D%AE%E6%BA%90%E5%88%87%E6%8D%A2%E5%A4%B1%E8%B4%A5%E7%9A%84%E5%A4%84%E7%90%86/</link>
      <pubDate>Sat, 01 May 2021 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/categories/%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/springboot-%E5%A4%9A%E6%95%B0%E6%8D%AE%E6%BA%90%E4%B8%8B%E5%BC%80%E5%90%AF%E4%BA%8B%E5%8A%A1%E5%90%8E%E6%95%B0%E6%8D%AE%E6%BA%90%E5%88%87%E6%8D%A2%E5%A4%B1%E8%B4%A5%E7%9A%84%E5%A4%84%E7%90%86/</guid>
      <description>springboot 多数据源下，开启事务后，数据源切换失败的处理 1. 问题的产生 在之前的文章中，整合了spring多数据源后，开始进行了愉快地编码中。不过这种&amp;quot;愉快&amp;quot;并没有持续多久，就碰到了一个诡异的问题：数据源切换突然不起作用了。经多次代码比对，发现无法切换的方法上加了个注解：@Transactional.&#xA;2. 验证 首先，对无@Transactional注解的代码进行访问：&#xA;@Override public List&amp;lt;User&amp;gt; selectByQuery(UserQuery query) { //省略其他代码 ... } 访问http://localhost:8084/api/user/get?appId=3，结果如下：&#xA;[ { &amp;#34;uid&amp;#34;: 1, &amp;#34;nick&amp;#34;: &amp;#34;db3_张三&amp;#34; }, { &amp;#34;uid&amp;#34;: 2, &amp;#34;nick&amp;#34;: &amp;#34;db3_李四&amp;#34; }, { &amp;#34;uid&amp;#34;: 3, &amp;#34;nick&amp;#34;: &amp;#34;db3_王五&amp;#34; } ] 可以看到，结果正常。&#xA;接着，添加@Transactional注解，再次访问：&#xA;@Override @Transactional(rollbackFor = Exception.class) public List&amp;lt;User&amp;gt; selectByQuery(UserQuery query) { //省略其他代码 ... } 访问http://localhost:8084/api/user/get?appId=3，结果如下：&#xA;[ { &amp;#34;uid&amp;#34;:1, &amp;#34;nick&amp;#34;:&amp;#34;main_张三&amp;#34; }, { &amp;#34;uid&amp;#34;:2, &amp;#34;nick&amp;#34;:&amp;#34;main_李四&amp;#34; }, { &amp;#34;uid&amp;#34;:3, &amp;#34;nick&amp;#34;:&amp;#34;main_王五&amp;#34; } ] 可以看到，获取到的是main数据源中的数据。&#xA;3. 分析 这里引进csdn上一位博主《springboot+mybatis解决多数据源切换事务控制不生效的问题》的分析：</description>
    </item>
    <item>
      <title>java抽奖工具类：按概率抽奖</title>
      <link>https://www.szlinkroutes.com/categories/%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/java%E6%8A%BD%E5%A5%96%E5%B7%A5%E5%85%B7%E7%B1%BB%E6%8C%89%E6%A6%82%E7%8E%87%E6%8A%BD%E5%A5%96/</link>
      <pubDate>Thu, 22 Apr 2021 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/categories/%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/java%E6%8A%BD%E5%A5%96%E5%B7%A5%E5%85%B7%E7%B1%BB%E6%8C%89%E6%A6%82%E7%8E%87%E6%8A%BD%E5%A5%96/</guid>
      <description>在一些抽奖活动中，会有按概率抽取奖品的操作，本文将提供一个抽奖概率的解决方案。&#xA;假设抽奖集合如下： 奖品id 概率 1 10% 2 10% 3 20% 4 20% 生成集合 生成的连续集合为{(0.0, 10.0],(10.0, 30.0],(30.0, 60.0]}，如下： 奖品id 区间 1 (0.0, 10.0] 2 (10.0, 20.0] 3 (20.0, 40.0] 4 (40.0, 60.0] 最大的概率值 maxElement = 60.0，这个值下面会用到。&#xA;生成随机数 生成方法为 random.nextDouble() * maxElement，maxElement 就是上一步得到的。&#xA;判断随机数在哪个区间内，返回该区间对应的奖品id 假如 生成了随机数12.001，则它属于(10.0, 20.0]，对应的奖品id为2.&#xA;代码如下：&#xA;LotteryUtil&#xA;public class LotteryUtil { private List&amp;lt;Continuous&amp;gt; lotteryList; private double maxElement; /** * 得到一个抽奖对象 * @param map 抽奖概率，key:奖品id，value:概率 * @param &amp;lt;T&amp;gt; * @return */ public static&amp;lt;T&amp;gt; LotteryUtil getInstance(Map&amp;lt;T, Double&amp;gt; map) { return new LotteryUtil(map); } /** * 构造抽奖集合 * @param map 为奖品的概率 */ private &amp;lt;T&amp;gt; LotteryUtil(Map&amp;lt;T, Double&amp;gt; map) { if(map.</description>
    </item>
    <item>
      <title>【docker实践】之二：安装redis</title>
      <link>https://www.szlinkroutes.com/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/docker/docker%E5%AE%9E%E8%B7%B5%E4%B9%8B%E4%BA%8C%E5%AE%89%E8%A3%85redis/</link>
      <pubDate>Sun, 10 Jan 2021 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/docker/docker%E5%AE%9E%E8%B7%B5%E4%B9%8B%E4%BA%8C%E5%AE%89%E8%A3%85redis/</guid>
      <description>1.获取镜像 # 先查询镜像，找到自己想要的 docker search redis # 拉取镜像，可指定tag，如拉取redis 5.0：docker pull redis:5.0 # 如果未指定tag，则拉取最新版本 docker pull redis:5.0 2.编写redis配置文件 由于docker容器的独立性，在容器删除时，redis中持久化的文件也会删除。为了解决这个问题，我们在启动docker时，需要将redis的持久化文件目录映射到宿主机，这样只即使docker删除了，只要不删除宿主机的文件，数据文件会一直保留，下次要使用时，只须重新运行一个docker容器，将redis的持久化文件目录映射到宿主机的相应目录即可。&#xA;目录规划如下：&#xA;新建/data/docker_data/redis_5.0目录 进入/data/docker_data/redis_5.0，创建conf、data、logs目录 进入/data/docker_data/redis_5.0/conf目录，创建文本文件，内容如下： bind 0.0.0.0 protected-mode yes port 6379 daemonize no pidfile /var/run/redis_6379.pid loglevel notice logfile &amp;#34;&amp;#34; databases 16 always-show-logo yes requirepass 2v8P5#ai1b%i save 900 1 save 300 10 save 60 10000 stop-writes-on-bgsave-error yes rdbcompression yes rdbchecksum yes dbfilename dump.rdb dir /data appendonly yes appendfilename &amp;#34;appendonly.aof&amp;#34; appendfsync everysec no-appendfsync-on-rewrite no auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb aof-load-truncated yes aof-use-rdb-preamble yes 将文件名保存为redis.</description>
    </item>
    <item>
      <title>【docker实践】之一：安装mysql</title>
      <link>https://www.szlinkroutes.com/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/docker/docker%E5%AE%9E%E8%B7%B5%E4%B9%8B%E4%B8%80%E5%AE%89%E8%A3%85mysql/</link>
      <pubDate>Sat, 09 Jan 2021 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/docker/docker%E5%AE%9E%E8%B7%B5%E4%B9%8B%E4%B8%80%E5%AE%89%E8%A3%85mysql/</guid>
      <description>1.获取镜像 # 先查询镜像，找到自己想要的 docker search mysql # 拉取镜像，可指定tag，如拉取mysql 5.7：docker pull mysql:5.7 # 如果未指定tag，则拉取最新版本 docker pull mysql 2.配置mysql目录及文件 由于docker容器的独立性，在容器删除时，mysql的文件也会删除。为了解决这个问题，我们在启动docker时，需要将mysql的数据文件目录映射到宿主机，这样只即使docker删除了，只要不删除宿主机的文件，数据文件会一直保留，下次要使用时，只须重新运行一个docker容器，将mysql的数据文件目录映射到宿主机的相应目录即可。&#xA;目录规划如下：&#xA;在~目录下新建mysql文件 进入mysql文件夹，再创建conf、data文件夹(data目录存放mysql数据，可酌情存放到其他盘) 进行conf文件夹，新建txt文件，内容如下： [mysqld] pid-file = /var/run/mysqld/mysqld.pid socket = /var/run/mysqld/mysqld.sock datadir = /var/lib/mysql secure-file-priv= NULL # Disabling symbolic-links is recommended to prevent assorted security risks symbolic-links=0 sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION lower_case_table_names=1 并将文件名保存为my.cnf。更多mysql配置，请参阅官方文档。&#xA;整个目录结构如下：&#xA;mysql ├─conf │ └─my.cnf └─data 3.启动docker docker run -p 3306:3306 --name mysql -v ~/mysql/conf/my.cnf:/etc/mysql/my.cnf -v ~/mysql/logs:/data/docker_data/mysql_5.7/logs:/var/log/mysql/ -v ~/mysql/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123 -d mysql:latest 命令解释如下：</description>
    </item>
    <item>
      <title>docker入门与实践之【08-外部访问容器】</title>
      <link>https://www.szlinkroutes.com/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/docker/docker%E5%85%A5%E9%97%A8%E4%B8%8E%E5%AE%9E%E8%B7%B5%E4%B9%8B08-%E5%A4%96%E9%83%A8%E8%AE%BF%E9%97%AE%E5%AE%B9%E5%99%A8/</link>
      <pubDate>Fri, 08 Jan 2021 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/docker/docker%E5%85%A5%E9%97%A8%E4%B8%8E%E5%AE%9E%E8%B7%B5%E4%B9%8B08-%E5%A4%96%E9%83%A8%E8%AE%BF%E9%97%AE%E5%AE%B9%E5%99%A8/</guid>
      <description>1.端口映射 容器中可以运行一些网络应用，要让外部也可以访问这些应用，可以通过 -P 或 -p 参数来指定端口映射。&#xA;示例：&#xA;$ docker run -d -P training/webapp python app.py $ docker container ls -l CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES bc533791f3f5 training/webapp:latest python app.py 5 seconds ago Up 2 seconds 0.0.0.0:49155-&amp;gt;5000/tcp nostalgic_morse -p 则可以指定要映射的端口，并且，在一个指定端口上只可以绑定一个容器。支持的格式有&#xA;ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort。 1.1 映射所有接口地址 使用 hostPort:containerPort 格式本地的 5000 端口映射到容器的 5000 端口，可以执行&#xA;$ docker run -d -p 5000:5000 training/webapp python app.py 此时默认会绑定本地所有接口上的所有地址。&#xA;1.2 映射到指定地址的指定端口 可以使用 ip:hostPort:containerPort 格式指定映射使用一个特定地址，比如 localhost 地址 127.</description>
    </item>
    <item>
      <title>docker入门与实践之【07-数据管理】</title>
      <link>https://www.szlinkroutes.com/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/docker/docker%E5%85%A5%E9%97%A8%E4%B8%8E%E5%AE%9E%E8%B7%B5%E4%B9%8B07-%E6%95%B0%E6%8D%AE%E7%AE%A1%E7%90%86/</link>
      <pubDate>Thu, 07 Jan 2021 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/docker/docker%E5%85%A5%E9%97%A8%E4%B8%8E%E5%AE%9E%E8%B7%B5%E4%B9%8B07-%E6%95%B0%E6%8D%AE%E7%AE%A1%E7%90%86/</guid>
      <description>1.数据卷 数据卷 是一个可供一个或多个容器使用的特殊目录，它绕过UFS，可以提供很多有用的特性：&#xA;数据卷 可以在容器之间共享和重用 对 数据卷 的修改会立马生效 对 数据卷 的更新，不会影响镜像数据卷 默认会一直存在，即使容器被删除 注意：数据卷 的使用，类似于 Linux 下对目录或文件进行 mount，镜像中的被指定为挂载点的目录中的文件会隐藏掉，能显示看的是挂载的 数据卷。&#xA;1.1 创建一个数据卷 $ docker volume create my-vol 查看所有的 数据卷&#xA;$ docker volume ls 查看指定的数据卷信息&#xA;$ docker volume inspect my-vol [ { &amp;#34;Driver&amp;#34;: &amp;#34;local&amp;#34;, &amp;#34;Labels&amp;#34;: {}, &amp;#34;Mountpoint&amp;#34;: &amp;#34;/var/lib/docker/volumes/my-vol/_data&amp;#34;, &amp;#34;Name&amp;#34;: &amp;#34;my-vol&amp;#34;, &amp;#34;Options&amp;#34;: {}, &amp;#34;Scope&amp;#34;: &amp;#34;local&amp;#34; } ] 1.2 启动一个挂载数据卷的容器 在用 docker run 命令的时候，使用 --mount 标记来将 数据卷 挂载到容器里。在一次 docker run 中可以挂载多个 数据卷。&#xA;示例：&#xA;$ docker run -d -P \ --name web \ # -v my-vol:/wepapp \ --mount source=my-vol,target=/webapp \ training/webapp \ python app.</description>
    </item>
    <item>
      <title>docker入门与实践之【06-操作容器】</title>
      <link>https://www.szlinkroutes.com/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/docker/docker%E5%85%A5%E9%97%A8%E4%B8%8E%E5%AE%9E%E8%B7%B5%E4%B9%8B06-%E6%93%8D%E4%BD%9C%E5%AE%B9%E5%99%A8/</link>
      <pubDate>Wed, 06 Jan 2021 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/docker/docker%E5%85%A5%E9%97%A8%E4%B8%8E%E5%AE%9E%E8%B7%B5%E4%B9%8B06-%E6%93%8D%E4%BD%9C%E5%AE%B9%E5%99%A8/</guid>
      <description>1.启动 启动容器有两种方式，一种是基于镜像新建一个容器并启动，另外一个是将在终止状态（stopped）的容器重新启动。&#xA;1.1 新建并启动 所需要的命令主要为 docker run。&#xA;例如，下面的命令输出一个 “Hello World”，之后终止容器。&#xA;$ docker run ubuntu:14.04 /bin/echo &amp;#39;Hello world&amp;#39; Hello world 下面的命令则启动一个 bash 终端，允许用户进行交互。&#xA;$ docker run -t -i ubuntu:14.04 /bin/bash root@af8bae53bdd3:/# 其中，&#xA;-t 选项让Docker分配一个伪终端（pseudo-tty）并绑定到容器的标准输入上， -i 则让容器的标准输入保持打开。 1.2 启动已终止容器 可以利用 docker container start 命令，直接将一个已经终止的容器启动运行。 2. 后台运行 很多时候，需要让 Docker 在后台运行而不是直接把执行命令的结果输出在当前宿主机下。此时，可以通过添加 -d 参数来实现。&#xA;示例：&#xA;$ docker run -d ubuntu:17.10 /bin/sh -c &amp;#34;while true; do echo hello world; sleep 1; done&amp;#34; 77b2dc01fe0f3f1265df143181e7b9af5e05279a884f4776ee75350ea9d8017a 使用 -d 参数启动后会返回一个唯一的 id，也可以通过 docker container ls 命令来查看容器信息。</description>
    </item>
    <item>
      <title>docker入门与实践之【05-其他镜像制作方式】</title>
      <link>https://www.szlinkroutes.com/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/docker/docker%E5%85%A5%E9%97%A8%E4%B8%8E%E5%AE%9E%E8%B7%B5%E4%B9%8B05-%E5%85%B6%E4%BB%96%E9%95%9C%E5%83%8F%E5%88%B6%E4%BD%9C%E6%96%B9%E5%BC%8F/</link>
      <pubDate>Tue, 05 Jan 2021 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/docker/docker%E5%85%A5%E9%97%A8%E4%B8%8E%E5%AE%9E%E8%B7%B5%E4%B9%8B05-%E5%85%B6%E4%BB%96%E9%95%9C%E5%83%8F%E5%88%B6%E4%BD%9C%E6%96%B9%E5%BC%8F/</guid>
      <description>1.从压缩包导入 格式：docker import [选项] &amp;lt;文件&amp;gt;|&amp;lt;URL&amp;gt;|- [&amp;lt;仓库名&amp;gt;[:&amp;lt;标签&amp;gt;]]&#xA;# 从网络导入ubuntu14.04镜像 $ docker import \ http://download.openvz.org/template/precreated/ubuntu-14.04-x86_64-minimal.tar.gz \ openvz/ubuntu:14.04 可以使用history命令查询构建记录：&#xA;docker history openvz/ubuntu:14.04 IMAGE CREATED CREATED BY SIZE COMMENT f477a6e18e98 About a minute ago 214.9 MB Imported from http://download.openvz.org/template/precreated/ubuntu-14.04-x86_64-minimal.tar.gz 2.docker save 和 docker load 使用 docker save 命令可以将镜像保存为归档文件。&#xA;比如我们希望保存这个 alpine 镜像。&#xA;$ docker image ls alpine REPOSITORY TAG IMAGE ID CREATED SIZE alpine latest baa5d63471ea 5 weeks ago 4.803 MB 保存镜像的命令为：&#xA;$ docker save alpine | gzip &amp;gt; alpine-latest.</description>
    </item>
    <item>
      <title>docker入门与实践之【04-Dockfile指令】</title>
      <link>https://www.szlinkroutes.com/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/docker/docker%E5%85%A5%E9%97%A8%E4%B8%8E%E5%AE%9E%E8%B7%B5%E4%B9%8B04-dockfile%E6%8C%87%E4%BB%A4/</link>
      <pubDate>Mon, 04 Jan 2021 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/docker/docker%E5%85%A5%E9%97%A8%E4%B8%8E%E5%AE%9E%E8%B7%B5%E4%B9%8B04-dockfile%E6%8C%87%E4%BB%A4/</guid>
      <description>1.COPY 复制文件 格式：&#xA;COPY &amp;lt;源路径&amp;gt;... &amp;lt;目标路径&amp;gt; COPY [&amp;#34;&amp;lt;源路径1&amp;gt;&amp;#34;,... &amp;#34;&amp;lt;目标路径&amp;gt;&amp;#34;] COPY 指令将从构建上下文目录中 &amp;lt;源路径&amp;gt; 的文件/目录复制到新的一层的镜像内的 &amp;lt;目标路径&amp;gt; 位置。比如：&#xA;COPY package.json /usr/src/app/ &amp;lt;源路径&amp;gt; 可以是多个，甚至可以是通配符，其通配符规则要满足 Go 的 filepath.Match 规则&#xA;&amp;lt;目标路径&amp;gt; 可以是容器内的绝对路径，也可以是相对于工作目录的相对路径（工作目录可以用 WORKDIR 指令来指定）。&#xA;2.ADD 更高级的复制文件 ADD使用格式与COPY一致，不同的是，如果 &amp;lt;源路径&amp;gt; 为一个 tar 压缩文件的话，压缩格式为 gzip, bzip2 以及 xz 的情况下，ADD 指令将会自动解压缩这个压缩文件到 &amp;lt;目标路径&amp;gt; 去。 比如官方镜像 ubuntu 中：&#xA;FROM scratch ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz / ... COPY与ADD选用原则： 所有的文件复制应使用 COPY 指令，仅在需要自动解压缩的场合使用 ADD。&#xA;3.CMD 容器启动命令 CMD 指令的格式和 RUN 相似，也是两种格式：&#xA;shell 格式：CMD &amp;lt;命令&amp;gt; exec 格式：CMD [&amp;#34;可执行文件&amp;#34;, &amp;#34;参数1&amp;#34;, &amp;#34;参数2&amp;#34;.</description>
    </item>
    <item>
      <title>docker入门与实践之【03-使用dockerfile定制镜像】</title>
      <link>https://www.szlinkroutes.com/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/docker/docker%E5%85%A5%E9%97%A8%E4%B8%8E%E5%AE%9E%E8%B7%B5%E4%B9%8B03-%E4%BD%BF%E7%94%A8dockerfile%E5%AE%9A%E5%88%B6%E9%95%9C%E5%83%8F/</link>
      <pubDate>Sun, 03 Jan 2021 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/docker/docker%E5%85%A5%E9%97%A8%E4%B8%8E%E5%AE%9E%E8%B7%B5%E4%B9%8B03-%E4%BD%BF%E7%94%A8dockerfile%E5%AE%9A%E5%88%B6%E9%95%9C%E5%83%8F/</guid>
      <description>利用dockerfile定制镜像 Dockerfile 是一个文本文件，其内包含了一条条的指令(Instruction)，每一条指令构建一层，因此每一条指令的内容，就是描述该层应当如何构建。&#xA;以定制nginx 镜像为例，使用 Dockerfile 来定制。&#xA;在一个空白目录中，建立一个文本文件，并命名为 Dockerfile：&#xA;$ mkdir mynginx $ cd mynginx $ touch Dockerfile 其内容为：&#xA;FROM nginx RUN echo &amp;#39;&amp;lt;h1&amp;gt;Hello, Docker!&amp;lt;/h1&amp;gt;&amp;#39; &amp;gt; /usr/share/nginx/html/index.html 1 FROM 指定基础镜像 所谓定制镜像，那一定是以一个镜像为基础，在其上进行定制.FROM 就是指定基础镜像，一个 Dockerfile 中 FROM 是必备的指令，并且必须是第一条指令。&#xA;2 RUN 执行命令 RUN 指令是用来执行命令行命令的。由于命令行的强大能力，RUN 指令在定制镜像时是最常用的指令之一。其格式有两种：&#xA;shell 格式：RUN &amp;lt;命令&amp;gt;，就像直接在命令行中输入的命令一样。刚才写的 Dockerfile 中的 RUN 指令就是这种格式。 RUN echo &amp;#39;&amp;lt;h1&amp;gt;Hello, Docker!&amp;lt;/h1&amp;gt;&amp;#39; &amp;gt; /usr/share/nginx/html/index.html exec 格式：RUN [&amp;quot;可执行文件&amp;quot;, &amp;quot;参数1&amp;quot;, &amp;quot;参数2&amp;quot;]，这更像是函数调用中的格式。 Dockerfile 中每一个指令都会建立一层，RUN 也不例外。每一个 RUN 的行为，就和刚才我们手工建立镜像的过程一样：新建立一层，在其上执行这些命令，执行结束后，commit 这一层的修改，构成新的镜像。&#xA;Union FS 是有最大层数限制的，比如 AUFS，曾经是最大不得超过 42 层，现在是不得超过 127 层。为了不生成非必要层，使用RUN指令时，可以使用如下方法：</description>
    </item>
    <item>
      <title>docker入门与实践之【02-镜像】</title>
      <link>https://www.szlinkroutes.com/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/docker/docker%E5%85%A5%E9%97%A8%E4%B8%8E%E5%AE%9E%E8%B7%B5%E4%B9%8B02-%E9%95%9C%E5%83%8F/</link>
      <pubDate>Sat, 02 Jan 2021 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/docker/docker%E5%85%A5%E9%97%A8%E4%B8%8E%E5%AE%9E%E8%B7%B5%E4%B9%8B02-%E9%95%9C%E5%83%8F/</guid>
      <description>1. 获取镜像 1.1 获取镜像 docker获取镜像的命令是pull，命令格式为：&#xA;docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签] 具体的选项可以通过 docker pull &amp;ndash;help 命令看到，这里我们说一下镜像名称的格式。&#xA;Docker 镜像仓库地址：地址的格式一般是 &amp;lt;域名/IP&amp;gt;[:端口号]。默认地址是 Docker Hub。 仓库名：如之前所说，这里的仓库名是两段式名称，即 &amp;lt;用户名&amp;gt;/&amp;lt;软件名&amp;gt;。对于 Docker Hub，如果不给出用户名，则默认为 library，也就是官方镜像。 示例：&#xA;docker pull ubuntu:16.04 1.2 运行 有了镜像后，我们就能够以这个镜像为基础启动并运行一个容器。以上面的 ubuntu:16.04 为例，如果我们打算启动里面的 bash 并且进行交互式操作的话，可以执行下面的命令。&#xA;$ docker run -it --rm \ ubuntu:16.04 \ bash 说明： docker run 就是运行容器的命令，后面再细讲。&#xA;-it：这是两个参数，一个是 -i：交互式操作，一个是 -t 终端。我们这里打算进入 bash 执行一些命令并查看返回结果，因此我们需要交互式终端。 &amp;ndash;rm：这个参数是说容器退出后随之将其删除。默认情况下，为了排障需求，退出的容器并不会立即删除，除非手动 docker rm。我们这里只是随便执行个命令，看看结果，不需要排障和保留结果，因此使用 &amp;ndash;rm 可以避免浪费空间。 ubuntu:16.04：这是指用 ubuntu:16.04 镜像为基础来启动容器。 bash：放在镜像名后的是命令，这里我们希望有个交互式 Shell，因此用的是 bash。 1.3 搜索镜像 可以通过 docker search 命令来查找官方仓库中的镜像</description>
    </item>
    <item>
      <title>docker入门与实践之【01-基本概念】</title>
      <link>https://www.szlinkroutes.com/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/docker/docker%E5%85%A5%E9%97%A8%E4%B8%8E%E5%AE%9E%E8%B7%B5%E4%B9%8B01-%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5/</link>
      <pubDate>Fri, 01 Jan 2021 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/docker/docker%E5%85%A5%E9%97%A8%E4%B8%8E%E5%AE%9E%E8%B7%B5%E4%B9%8B01-%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5/</guid>
      <description>1.docker镜像 Docker 镜像（Image），就相当于是一个 root 文件系统。比如官方镜像 ubuntu:16.04 就包含了完整的一套 Ubuntu 16.04 最小系统的 root 文件系统。&#xA;2.docker容器 镜像（Image）和容器（Container）的关系，就像是面向对象程序设计中的 类 和 实例 一样，镜像是静态的定义，容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。&#xA;容器的实质是进程，但与直接在宿主执行的进程不同，容器进程运行于属于自己的独立的 命名空间。因此容器可以拥有自己的 root 文件系统、自己的网络配置、自己的进程空间，甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里，使用起来，就好像是在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。也因为这种隔离的特性，很多人初学 Docker 时常常会混淆容器和虚拟机。&#xA;==按照 Docker 最佳实践的要求==，容器不应该向其存储层内写入任何数据，容器存储层要保持无状态化。所有的文件写入操作，都应该使用 数据卷（Volume）、或者绑定宿主目录，在这些位置的读写会跳过容器存储层，直接对宿主（或网络存储）发生读写，其性能和稳定性更高。&#xA;数据卷的生存周期独立于容器，容器消亡，数据卷不会消亡。因此，使用数据卷后，容器删除或者重新运行之后，数据却不会丢失。&#xA;3.仓库 通常，一个仓库会包含同一个软件不同版本的镜像，而标签就常用于对应该软件的各个版本。我们可以通过 &amp;lt;仓库名&amp;gt;:&amp;lt;标签&amp;gt; 的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签，将以 latest 作为默认标签。&#xA;以 Ubuntu 镜像 为例，ubuntu 是仓库的名字，其内包含有不同的版本标签，如，14.04, 16.04。我们可以通过 ubuntu:14.04，或者 ubuntu:16.04 来具体指定所需哪个版本的镜像。如果忽略了标签，比如 ubuntu，那将视为 ubuntu:latest。&#xA;仓库名经常以 两段式路径 形式出现，比如 jwilder/nginx-proxy，前者往往意味着 Docker Registry 多用户环境下的用户名，后者则往往是对应的软件名。但这并非绝对，取决于所使用的具体 Docker Registry 的软件或服务。</description>
    </item>
    <item>
      <title>centos安装mysql</title>
      <link>https://www.szlinkroutes.com/categories/%E6%97%A5%E5%B8%B8%E8%AE%B0%E5%BD%95/2020-12-04-centos%E5%AE%89%E8%A3%85mysql/</link>
      <pubDate>Fri, 04 Dec 2020 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/categories/%E6%97%A5%E5%B8%B8%E8%AE%B0%E5%BD%95/2020-12-04-centos%E5%AE%89%E8%A3%85mysql/</guid>
      <description>1、下载 MySQL 官方的 Yum Repository wget http://repo.mysql.com//mysql57-community-release-el7-8.noarch.rpm 2.安装 yum Repository yum -y install mysql57-community-release-el7-8.noarch.rpm 3.安装 MySQL 服务端 #查看 mysql server yum search mysql-com #安装 yum -y install mysql-community-server.x86_64 4.启动 mysql 服务 #启动 systemctl start mysqld.service #查看启动是否成功 systemctl status mysqld.service 5.登陆数据库 然后就可以进入数据库了，这里注意一点，MySQL5.7.6 之后会在启动 mysql 进程的时候生成一个用户密码，首次登陆需要这个密码才行。密码保存在 mysql 进程的日志里，即(/var/log/mysqld.log)&#xA;[root@centos-linux ~]# cat /var/log/mysqld.log | grep &amp;#39;password&amp;#39; 2016-07-16T03:07:53.587995Z 1 [Note] A temporary password is generated for root@localhost: 2=s6NZk.t:fz #然后登陆再登录 mysql -uroot -p [密码] 6.修改 root 密码 mysql&amp;gt; ALTER USER &#39;root&#39;@&#39;localhost&#39; IDENTIFIED BY &#39;new_password&#39;; 7.</description>
    </item>
    <item>
      <title>linux远程脚本执行</title>
      <link>https://www.szlinkroutes.com/categories/%E6%97%A5%E5%B8%B8%E8%AE%B0%E5%BD%95/2020-12-03-linux%E8%BF%9C%E7%A8%8B%E8%84%9A%E6%9C%AC%E6%89%A7%E8%A1%8C/</link>
      <pubDate>Thu, 03 Dec 2020 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/categories/%E6%97%A5%E5%B8%B8%E8%AE%B0%E5%BD%95/2020-12-03-linux%E8%BF%9C%E7%A8%8B%E8%84%9A%E6%9C%AC%E6%89%A7%E8%A1%8C/</guid>
      <description> 经常需要远程到其他节点上执行一些shell命令，如果分别ssh到每台主机上再去执行很麻烦，因此能有个集中管理的方式就好了。一下介绍两种shell命令远程执行的方法。&#xA;前提条件： 配置ssh免密码登陆 对于简单的命令： 如果是简单执行几个命令，则：&#xA;ssh user@remoteNode &amp;quot;cd /home ; ls&amp;quot; 以上命令基本能完成常用的对于远程节点的管理了，几个注意的点：&#xA;双引号，必须有。如果不加双引号，第二个ls命令在本地执行 分号，两个命令之间用分号隔开 对于脚本的方式： 有些远程执行的命令内容较多，单一命令无法完成，考虑脚本方式实现：&#xA;#!/bin/bash ssh user@remoteNode &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;lt;&amp;lt; eeooff cd /home touch abcdefg.txt exit eeooff echo done! 远程执行的内容在“&amp;laquo; eeooff ” 至“ eeooff”之间，在远程机器上的操作就位于其中，注意的点：&#xA;&amp;laquo; eeooff，ssh后直到遇到eeooff这样的内容结束，eeooff可以随便修改成其他形式。 重定向目的在于不显示远程的输出了 在结束前，加exit退出远程节点 </description>
    </item>
    <item>
      <title>关于作者</title>
      <link>https://www.szlinkroutes.com/about/</link>
      <pubDate>Thu, 03 Dec 2020 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/about/</guid>
      <description>男，90后，坐标广州，毕业于广州某双非学校(非985/211)，目前就职于广州某语音社交公司，热爱技术，热爱钻研源码，欢迎各位小伙伴与我一起交流，一起成长！</description>
    </item>
    <item>
      <title>deepin下深度终端使用ssh-agent</title>
      <link>https://www.szlinkroutes.com/categories/%E6%97%A5%E5%B8%B8%E8%AE%B0%E5%BD%95/2020-12-02-deepin%E4%B8%8B%E6%B7%B1%E5%BA%A6%E7%BB%88%E7%AB%AF%E4%BD%BF%E7%94%A8ssh-agentxshell%E4%B8%AD%E7%9A%84xagent%E5%8A%9F%E8%83%BD/</link>
      <pubDate>Wed, 02 Dec 2020 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/categories/%E6%97%A5%E5%B8%B8%E8%AE%B0%E5%BD%95/2020-12-02-deepin%E4%B8%8B%E6%B7%B1%E5%BA%A6%E7%BB%88%E7%AB%AF%E4%BD%BF%E7%94%A8ssh-agentxshell%E4%B8%AD%E7%9A%84xagent%E5%8A%9F%E8%83%BD/</guid>
      <description>背景：从windows10换到deepin后，在连接公司的服务器遇到了问题：windows下用的是xshell，开启xagent后，可直接从公司的跳转板上连接生产服务器；在deepin的深度终端上，从公司的跳板机连接到生产服务器时，提示需要public key。baidu+google几次之后，终于解决了这个问题。&#xA;在深度终端中，实现像xshell下那样，快速地登录生产服务器，至少要解决两个问题：&#xA;开启ssh-agent（在xshell下只需勾选xagent即可） 免密码输入 深度终端中自带了远程服务器的管理功能，可惜的是，并不支持ssh-agent功能，因此只能自主实现了。&#xA;关于如何开启ssh-agent方法，主要是使用以下命令：&#xA;eval `ssh-agent` 对于如何免密码输入，可以使用expect功能，实现密码自动输入。幸运的是，expect在deepin上已经默认安装了，不能再去折腾安装过程了。&#xA;我使用的shell脚本如下：&#xA;#!/bin/bash # # ssh 连接方法，开启ssh-agent # connect() { #开启ssh-agent eval `ssh-agent` #使用expect实现密码自动输入 expect -c &amp;#34; spawn ssh-add $1 #Enter passphrase for /home/root/.ssh/id_rsa: expect &amp;#34;*passphrase*&amp;#34; send -- $3\r spawn ssh -A fangchengyan@$2 -p $4 -i $1 #Enter passphrase for key &amp;#39;/home/root/.ssh/id_rsa&amp;#39;: expect &amp;#34;*passphrase*&amp;#34; send -- $3\r interact exit &amp;#34; exit } PASSWORD=&amp;#34;password&amp;#34; SSH_KEY=&amp;#34;/home/root/.ssh/id_rsa&amp;#34; SERVER=&amp;#34;192.168.0.1&amp;#34; PORT=22 # # 使用时，可根据编号，自主选择要连接的服务器 # echo &amp;#39;请选择：&amp;#39; echo &amp;#39;1) 跳板机1-192.</description>
    </item>
    <item>
      <title>如何使网站启动https</title>
      <link>https://www.szlinkroutes.com/categories/%E6%97%A5%E5%B8%B8%E8%AE%B0%E5%BD%95/2020-12-01-%E5%A6%82%E4%BD%95%E4%BD%BF%E7%BD%91%E7%AB%99%E5%90%AF%E5%8A%A8https/</link>
      <pubDate>Tue, 01 Dec 2020 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/categories/%E6%97%A5%E5%B8%B8%E8%AE%B0%E5%BD%95/2020-12-01-%E5%A6%82%E4%BD%95%E4%BD%BF%E7%BD%91%E7%AB%99%E5%90%AF%E5%8A%A8https/</guid>
      <description>我用的是 Let’s Encrypt这个免费的解决方案。Let’s Encrypt 是一个于2015年推出的数字证书认证机构，将通过旨在消除当前手动创建和安装证书的复杂过程的自动化流程，为安全网站提供免费的SSL/TLS证书。&#xA;为网站来安装一个证书十分简单，只需要使用 Let’s Encrypt 的 Certbot，就可以完成。&#xA;首先，打开 https://certbot.eff.org 网页。&#xA;在那个机器上图标下面，你需要选择一下你用的 Web 接入软件 和你的 操作系统。比如，我选的，nginx 和 centos7&#xA;然后就会跳转到一个安装教程网页。你就照着做一遍就好了。&#xA;这里将所用的命令整理如下：&#xA;# 安装certbot wget https://dl.eff.org/certbot-auto sudo mv certbot-auto /usr/local/bin/certbot-auto sudo chown root /usr/local/bin/certbot-auto sudo chmod 0755 /usr/local/bin/certbot-auto # 配置证书 sudo /usr/local/bin/certbot-auto --nginx # 由于证书会过期，因此需要定期更新 echo &amp;#34;0 0,12 * * * root python -c &amp;#39;import random; import time; time.sleep(random.random() * 3600)&amp;#39; &amp;amp;&amp;amp; certbot renew&amp;#34; | sudo tee -a /etc/crontab &amp;gt; /dev/null 运行sudo /usr/local/bin/certbot-auto --nginx命令时，会有一些选项需要配置，命令运行过程如下：</description>
    </item>
    <item>
      <title>spring&#43;redis自主实现分布式session</title>
      <link>https://www.szlinkroutes.com/categories/%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/spring&#43;redis%E8%87%AA%E4%B8%BB%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8Fsession%E9%9D%9Espring-session%E6%96%B9%E5%BC%8F/</link>
      <pubDate>Sun, 13 Sep 2020 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/categories/%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/spring&#43;redis%E8%87%AA%E4%B8%BB%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8Fsession%E9%9D%9Espring-session%E6%96%B9%E5%BC%8F/</guid>
      <description>背景：最近对一个老项目进行改造，使其支持多机部署，其中最关键的一点就是实现多机session共享。项目有多老呢，jdk版本是1.6，spring版本是3.2，jedis版本是2.2。&#xA;1.方案的确定 接到这项目任务后，理所当然地google了，一搜索，发现解决方案分为两大类：&#xA;tomcat的session管理 spring-session 对于“tomcat的session管理”，很不幸，线上代码用的是resin，直接pass了；&#xA;对于“spring-session”，这是spring全家桶系列，项目中正好使用了spring，可以很方便集成，并且原业务代码不用做任何发动，似乎是个不错的选择。但是，在引入spring-session过程中发生了意外：项目中使用的jedis版本不支持！项目中使用的jedis版本是2.2,而spring-session中使用的jedis版本是2.5，有些命令像&amp;quot;set PX/EX NX/XX&amp;quot;，项目中使用的redis是不支持的，但spring-session引入的jedis支持，直接引入的话，风险难以把控，而升级项目中的redis版本的话，代价就比较高了。&#xA;综上所述，以上两个方案都行不能，既然第三方组件行不通，那就只能自主实现了。&#xA;通过参考一些开源项目的实现，自主实现分布式session的关键点有以下几点：&#xA;使用servlet的filter功能来接管session； 使用redis来管理全局session 2.使用filter来接管session 为了实现此功能，我们定义如下几个类：&#xA;SessionFilter：servlet的过滤器，替换原始的session DistributionSession：分布式session，实现了HttpSession类； SessionRequestWrapper：在filter中用来接管session； 类的具体实现如下：&#xA;SessionFilter类&#xA;/** * 该类实现了Filter */ public class SessionFilter implements Filter { /** redis的相关操作 */ @Autowired private RedisExtend redisExtend; @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { //这里将request转换成自主实现的SessionRequestWrapper //经过传递后，项目中获取到的request就是SessionRequestWrapper ServletRequest servletRequest = new SessionRequestWrapper((HttpServletRequest)request, (HttpServletResponse)response, redisExtend); chain.doFilter(servletRequest, response); } @Override public void destroy() { } } ServletRequestWrap类</description>
    </item>
    <item>
      <title>springboot &#43; druid 实现数据源下动态切换</title>
      <link>https://www.szlinkroutes.com/categories/%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/springboot-&#43;-druid-%E5%AE%9E%E7%8E%B0%E6%95%B0%E6%8D%AE%E6%BA%90%E4%B8%8B%E5%8A%A8%E6%80%81%E5%88%87%E6%8D%A2/</link>
      <pubDate>Wed, 03 Jun 2020 00:00:00 +0000</pubDate>
      <guid>https://www.szlinkroutes.com/categories/%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/springboot-&#43;-druid-%E5%AE%9E%E7%8E%B0%E6%95%B0%E6%8D%AE%E6%BA%90%E4%B8%8B%E5%8A%A8%E6%80%81%E5%88%87%E6%8D%A2/</guid>
      <description>springboot + druid 实现数据源下动态切换 1. 项目背景 最近接到一个需求：为多个业务系统开发统一的管理后台，这几个业务系统的相关业务惊人地相似，用的是同一份代码，只是地区、受众不同，因此服务器、数据库都单独分开了。&#xA;项目很明显要用到多数据源，经过在google多方打探，终于了解到基于AbstractRoutingDataSource的多数据源解决方案了。废话不多说，上代码介绍我的实现方式。&#xA;2. 代码实现 2.1 引入多数据源 在 application.yml 引入多数据源配置，这里使用的是阿里的 Druid 连接池：&#xA;# 数据源配置 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driverClassName: com.mysql.cj.jdbc.Driver druid: # 主库数据源 main: url: jdbc:mysql://127.0.0.1:3306/main?useUnicode=true&amp;amp;characterEncoding=utf8&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;useSSL=true&amp;amp;serverTimezone=GMT%2B8 username: root password: 123 # 应用数据源 app: # db1 db1: url: jdbc:mysql://127.0.0.1:3306/db1?useUnicode=true&amp;amp;characterEncoding=utf8&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;useSSL=true&amp;amp;serverTimezone=GMT%2B8 username: root password: 123 # db2 db2: url: jdbc:mysql://127.0.0.1:3306/db2?useUnicode=true&amp;amp;characterEncoding=utf8&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;useSSL=true&amp;amp;serverTimezone=GMT%2B8 username: root password: 123 # ds3 db3: url: jdbc:mysql://127.0.0.1:3306/db3?useUnicode=true&amp;amp;characterEncoding=utf8&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;useSSL=true&amp;amp;serverTimezone=GMT%2B8 username: root password: 123 这里一共引入了4个数据源：main、db1、db2、db3&#xA;main: 管理后台的数据源，存放管理后台相关数据，如用户登录数据、系统菜单等 db1: 业务系统1的数据源 db2: 业务系统2的数据源 db3: 业务系统3的数据源 2.</description>
    </item>
  </channel>
</rss>
