大家好,这篇文章我们来聊下动态线程池开源项目(DynamicTp)的通知告警模块。目前项目提供以下通知告警功能,每一个通知项都可以独立配置是否开启、告警阈值、告警间隔时间、平台等,具体代码请看core模块notify包。

1.核心参数变更通知

2.线程池活跃度告警

3.队列容量告警

4.拒绝策略告警

5.任务执行超时告警

6.任务排队超时告警


DynamicTp项目地址

目前700star,感谢你的star,欢迎pr,业务之余一起给开源贡献一份力量

gitee地址https://gitee.com/yanhom/dynamic-tp

github地址https://github.com/lyh200/dynamic-tp


系列文章

美团动态线程池实践思路,开源了

动态线程池框架(DynamicTp),监控及源码解析篇

动态线程池(DynamicTp),动态调整Tomcat、Jetty、Undertow线程池参数篇


线程池解读

上篇文章里大概讲到了JUC线程池的执行流程,我们这里再仔细回顾下,上图是JUC下线程池ThreadPoolExecutor类的继承体系。

顶级接口Executor提供了一种方式,解耦任务的提交和执行,只定义了一个execute(Runnable command)方法用来提交任务,至于具体任务怎么执行则交给他的实现者去自定义实现。

ExecutorService接口继承Executor,且扩展了生命周期管理的方法、返回Futrue的方法、批量提交任务的方法

  1. void shutdown();
  2. List<Runnable> shutdownNow();
  3. boolean isShutdown();
  4. boolean isTerminated();
  5. boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
  6. <T> Future<T> submit(Callable<T> task);
  7. <T> Future<T> submit(Runnable task, T result);
  8. Future<?> submit(Runnable task);
  9. <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;
  10. <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit) throws InterruptedException;
  11. <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;
  12. <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;

AbstractExecutorService抽象类继承ExecutorService接口,对ExecutorService相关方法提供了默认实现,用RunnableFuture的实现类FutureTask包装Runnable任务,交给execute()方法执行,然后可以从该FutureTask阻塞获取执行结果,并且对批量任务的提交做了编排

  1. protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
  2. return new FutureTask<T>(runnable, value);
  3. }
  4. public Future<?> submit(Runnable task) {
  5. if (task == null) throw new NullPointerException();
  6. RunnableFuture<Void> ftask = newTaskFor(task, null);
  7. execute(ftask);
  8. return ftask;
  9. }

ThreadPoolExecutor继承AbstractExecutorService,采用池化思想管理一定数量的线程来调度执行提交的任务,且定义了一套线程池的生命周期状态,用一个ctl变量来同时保存当前池状态(高3位)和当前池线程数(低29位)。看过源码的小伙伴会发现,ThreadPoolExecutor类里的方法大量有同时需要获取或更新池状态和池当前线程数的场景,放一个原子变量里,可以很好的保证数据的一致性以及代码的简洁性。

  1. // 用此变量保存当前池状态(高3位)和当前线程数(低29位)
  2. private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
  3. private static final int COUNT_BITS = Integer.SIZE - 3;
  4. private static final int CAPACITY = (1 << COUNT_BITS) - 1;
  5. // runState is stored in the high-order bits
  6. // 可以接受新任务提交,也会处理任务队列中的任务
  7. // 结果:111跟29个0:111 00000000000000000000000000000
  8. private static final int RUNNING = -1 << COUNT_BITS;
  9. // 不接受新任务提交,但会处理任务队列中的任务
  10. // 结果:000 00000000000000000000000000000
  11. private static final int SHUTDOWN = 0 << COUNT_BITS;
  12. // 不接受新任务,不执行队列中的任务,且会中断正在执行的任务
  13. // 结果:001 00000000000000000000000000000
  14. private static final int STOP = 1 << COUNT_BITS;
  15. // 任务队列为空,workerCount = 0,线程池的状态在转换为TIDYING状态时,会执行钩子方法terminated()
  16. // 结果:010 00000000000000000000000000000
  17. private static final int TIDYING = 2 << COUNT_BITS;
  18. // 调用terminated()钩子方法后进入TERMINATED状态
  19. // 结果:010 00000000000000000000000000000
  20. private static final int TERMINATED = 3 << COUNT_BITS;
  21. // Packing and unpacking ctl
  22. // 低29位变为0,得到了线程池的状态
  23. private static int runStateOf(int c) { return c & ~CAPACITY; }
  24. // 高3位变为为0,得到了线程池中的线程数
  25. private static int workerCountOf(int c) { return c & CAPACITY; }
  26. private static int ctlOf(int rs, int wc) { return rs | wc; }

核心入口execute()方法执行逻辑如下:

  1. public void execute(Runnable command) {
  2. if (command == null)
  3. throw new NullPointerException();
  4. int c = ctl.get();
  5. if (workerCountOf(c) < corePoolSize) {
  6. if (addWorker(command, true))
  7. return;
  8. c = ctl.get();
  9. }
  10. if (isRunning(c) && workQueue.offer(command)) {
  11. int recheck = ctl.get();
  12. if (! isRunning(recheck) && remove(command))
  13. reject(command);
  14. else if (workerCountOf(recheck) == 0)
  15. addWorker(null, false);
  16. }
  17. else if (!addWorker(command, false))
  18. reject(command);
  19. }

可以总结出如下主要执行流程,当然看上述代码会有一些异常分支判断,可以自己顺理加到下述执行流程里

1.判断线程池的状态,如果不是RUNNING状态,直接执行拒绝策略

2.如果当前线程数 < 核心线程池,则新建一个线程来处理提交的任务

3.如果当前线程数 > 核心线程数且任务队列没满,则将任务放入任务队列等待执行

4.如果 核心线程池 < 当前线程池数 < 最大线程数,且任务队列已满,则创建新的线程执行提交的任务

5.如果当前线程数 > 最大线程数,且队列已满,则拒绝该任务

addWorker()方法逻辑

  1. private boolean addWorker(Runnable firstTask, boolean core) {
  2. retry:
  3. for (;;) {
  4. int c = ctl.get();
  5. // 获取当前池状态
  6. int rs = runStateOf(c);
  7. // 1.判断如果线程池状态 > SHUTDOWN,直接返回false,否则2
  8. // 2.如果线程池状态 = SHUTDOWN,并且firstTask不为null则直接返回false,因为SHUTDOWN状态的线程池不能在接受新任务,否则3
  9. // 3.如果线程池状态 = SHUTDOWN,并且firstTask == null,此时如果任务队列为空,则直接返回false
  10. if (rs >= SHUTDOWN &&
  11. ! (rs == SHUTDOWN &&
  12. firstTask == null &&
  13. ! workQueue.isEmpty()))
  14. return false;
  15. for (;;) {
  16. int wc = workerCountOf(c);
  17. // 1.如果当前线程池线程数大于等于CAPACITY(理论上的最大值5亿),则返回fasle
  18. // 2.如果创建核心线程情况下当前池线程数 >= corePoolSize,则返回false
  19. // 3.如果创建非核心线程情况下当前池线程数 >= maximumPoolSize,则返回false
  20. if (wc >= CAPACITY ||
  21. wc >= (core ? corePoolSize : maximumPoolSize))
  22. return false;
  23. // cas 增加当前池线程数量,成功则退出循环
  24. if (compareAndIncrementWorkerCount(c))
  25. break retry;
  26. c = ctl.get(); // Re-read ctl
  27. // cas 增加当前池线程数量失败(多线程并发),则重新获取ctl,计算出当前线程池状态,如果不等于上述计算的状态rs,则说明线程池状态发生了改变,需要跳到外层循环重新进行状态判断
  28. if (runStateOf(c) != rs)
  29. continue retry;
  30. // else CAS failed due to workerCount change; retry inner loop
  31. }
  32. }
  33. boolean workerStarted = false;
  34. boolean workerAdded = false;
  35. Worker w = null;
  36. try {
  37. // 至此说明线程池状态校验通过,且增加池线程数量成功,则创建一个Worker线程来执行任务
  38. w = new Worker(firstTask);
  39. final Thread t = w.thread;
  40. if (t != null) {
  41. final ReentrantLock mainLock = this.mainLock;
  42. // 访问worker set时需要获取mainLock
  43. mainLock.lock();
  44. try {
  45. // Recheck while holding lock.
  46. // Back out on ThreadFactory failure or if
  47. // shut down before lock acquired.
  48. int rs = runStateOf(ctl.get());
  49. // 1.当前池状态 < SHUTDOWN,也就是RUNNING状态,如果已经started,抛出异常
  50. // 2.当前池状态 = SHUTDOWN,且firstTask == null,需要处理任务队列中的任务,如果已经started,抛出异常
  51. if (rs < SHUTDOWN ||
  52. (rs == SHUTDOWN && firstTask == null)) {
  53. if (t.isAlive()) // precheck that t is startable
  54. throw new IllegalThreadStateException();
  55. // 添加到workers集合中
  56. workers.add(w);
  57. int s = workers.size();
  58. // 判断更新历史最大线程数量
  59. if (s > largestPoolSize)
  60. largestPoolSize = s;
  61. workerAdded = true;
  62. }
  63. } finally {
  64. mainLock.unlock();
  65. }
  66. if (workerAdded) {
  67. // 启动新建线程
  68. t.start();
  69. workerStarted = true;
  70. }
  71. }
  72. } finally {
  73. if (! workerStarted)
  74. // 启动失败,workerCount--,workers里移除该worker
  75. addWorkerFailed(w);
  76. }
  77. return workerStarted;
  78. }

线程池中的线程并不是直接用的Thread类,而是定义了一个内部工作线程Worker类,实现了AQS以及Runnable接口,然后持有一个Thread类的引用及一个firstTask(创建后第一个要执行的任务),每个Worker线程启动后会执行run()方法,该方法会调用执行外层runWorker(Worker w)方法

  1. final void runWorker(Worker w) {
  2. Thread wt = Thread.currentThread();
  3. Runnable task = w.firstTask;
  4. w.firstTask = null;
  5. w.unlock(); // allow interrupts
  6. boolean completedAbruptly = true;
  7. try {
  8. // 1.如果task不为空,则作为该线程的第一个任务直接执行
  9. // 2.如果task为空,则通过getTask()方法从任务队列中获取任务执行
  10. while (task != null || (task = getTask()) != null) {
  11. w.lock();
  12. // If pool is stopping, ensure thread is interrupted;
  13. // if not, ensure thread is not interrupted. This
  14. // requires a recheck in second case to deal with
  15. // shutdownNow race while clearing interrupt
  16. // 线程池状态 >= STOP,则中断线程
  17. if ((runStateAtLeast(ctl.get(), STOP) ||
  18. (Thread.interrupted() &&
  19. runStateAtLeast(ctl.get(), STOP))) &&
  20. !wt.isInterrupted())
  21. wt.interrupt();
  22. try {
  23. // 实际执行任务前调用的钩子方法
  24. beforeExecute(wt, task);
  25. Throwable thrown = null;
  26. try {
  27. // 实际执行任务
  28. task.run();
  29. } catch (RuntimeException x) {
  30. thrown = x; throw x;
  31. } catch (Error x) {
  32. thrown = x; throw x;
  33. } catch (Throwable x) {
  34. thrown = x; throw new Error(x);
  35. } finally {
  36. // 任务执行后调用的钩子方法
  37. afterExecute(task, thrown);
  38. }
  39. } finally {
  40. // 任务置为null,重新获取新任务,完成数++
  41. task = null;
  42. w.completedTasks++;
  43. w.unlock();
  44. }
  45. }
  46. completedAbruptly = false;
  47. } finally {
  48. // 无任务可执行,执行worker销毁逻辑
  49. processWorkerExit(w, completedAbruptly);
  50. }
  51. }

getTask()方法逻辑

  1. private Runnable getTask() {
  2. boolean timedOut = false; // Did the last poll() time out?
  3. for (;;) {
  4. int c = ctl.get();
  5. int rs = runStateOf(c);
  6. // 以下两种情况递减工作线程数量
  7. // 1. rs >= STOP
  8. // 2. rs == SHUTDOWN && workQueue.isEmpty()
  9. if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
  10. decrementWorkerCount();
  11. return null;
  12. }
  13. int wc = workerCountOf(c);
  14. // Are workers subject to culling?
  15. // 允许核心线程超时 或者 当前线程数 > 核心线程数,有可能发生超时关闭
  16. boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
  17. // wc什么情况 > maximumPoolSize,调用setMaximumPoolSize()方法将maximumPoolSize调小了,会发生这种情况,此时需要关闭多余线程
  18. if ((wc > maximumPoolSize || (timed && timedOut))
  19. && (wc > 1 || workQueue.isEmpty())) {
  20. if (compareAndDecrementWorkerCount(c))
  21. return null;
  22. continue;
  23. }
  24. try {
  25. // 阻塞队列获取任务
  26. Runnable r = timed ?
  27. workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
  28. workQueue.take();
  29. if (r != null)
  30. return r;
  31. timedOut = true;
  32. } catch (InterruptedException retry) {
  33. // 发生中断,进行重试
  34. timedOut = false;
  35. }
  36. }
  37. }

以上内容比较详细的介绍了ThreadPoolExecutor的继承体系,以及相关的核心源码,基于此,现在我们来看DynamicTp提供的告警通知能力。


核心参数变更通知

对应配置中心的监听端监听到配置变更后,封装到DtpProperties中然后交由DtpRegistry类中的refresh()方法去做配置更新,同时通知时会高亮显示有变更的字段


线程池活跃度告警

活跃度 = activeCount / maximumPoolSize

服务启动后会开启一个定时监控任务,每隔一定时间(可配置)去计算线程池的活跃度,达到配置的threshold阈值后会触发一次告警,告警间隔内多次触发不会发送告警通知


队列容量告警

容量使用率 = queueSize / queueCapacity

服务启动后会开启一个定时监控任务,每隔一定时间去计算任务队列的使用率,达到配置的threshold阈值后会触发一次告警,告警间隔内多次触发不会发送告警通知


拒绝策略告警

  1. /**
  2. * Do sth before reject.
  3. * @param executor ThreadPoolExecutor instance
  4. */
  5. default void beforeReject(ThreadPoolExecutor executor) {
  6. if (executor instanceof DtpExecutor) {
  7. DtpExecutor dtpExecutor = (DtpExecutor) executor;
  8. dtpExecutor.incRejectCount(1);
  9. Runnable runnable = () -> AlarmManager.doAlarm(dtpExecutor, REJECT);
  10. AlarmManager.triggerAlarm(dtpExecutor.getThreadPoolName(), REJECT.getValue(), runnable);
  11. }
  12. }

线程池线程数达到配置的最大线程数,且任务队列已满,再提交任务会触发拒绝策略。DtpExecutor线程池用到的RejectedExecutionHandler是经过动态代理包装过的,在执行具体的拒绝策略之前会执行RejectedAware类beforeReject()方法,此方法会去做拒绝数量累加(总数值累加、周期值累加)。且判断如果周期累计值达到配置的阈值,则会触发一次告警通知(同时重置周期累加值为0及上次告警时间为当前时间),告警间隔内多次触发不会发送告警通知


任务队列超时告警

重写ThreadPoolExecutor的execute()方法和beforeExecute()方法,如果配置了执行超时或排队超时值,则会用DtpRunnable包装任务,同时记录任务的提交时间submitTime,beforeExecute根据当前时间和submitTime的差值就可以计算到该任务在队列中的等待时间,然后判断如果差值大于配置的queueTimeout则累加排队超时任务数量(总数值累加、周期值累加)。且判断如果周期累计值达到配置的阈值,则会触发一次告警通知(同时重置周期累加值为0及上次告警时间为当前时间),告警间隔内多次触发不会发送告警通知

  1. @Override
  2. public void execute(Runnable command) {
  3. if (CollUtil.isNotEmpty(taskWrappers)) {
  4. for (TaskWrapper t : taskWrappers) {
  5. command = t.wrap(command);
  6. }
  7. }
  8. if (runTimeout > 0 || queueTimeout > 0) {
  9. command = new DtpRunnable(command);
  10. }
  11. super.execute(command);
  12. }
  1. @Override
  2. protected void beforeExecute(Thread t, Runnable r) {
  3. if (!(r instanceof DtpRunnable)) {
  4. super.beforeExecute(t, r);
  5. return;
  6. }
  7. DtpRunnable runnable = (DtpRunnable) r;
  8. long currTime = System.currentTimeMillis();
  9. if (runTimeout > 0) {
  10. runnable.setStartTime(currTime);
  11. }
  12. if (queueTimeout > 0) {
  13. long waitTime = currTime - runnable.getSubmitTime();
  14. if (waitTime > queueTimeout) {
  15. queueTimeoutCount.incrementAndGet();
  16. Runnable alarmTask = () -> AlarmManager.doAlarm(this, QUEUE_TIMEOUT);
  17. AlarmManager.triggerAlarm(this.getThreadPoolName(), QUEUE_TIMEOUT.getValue(), alarmTask);
  18. }
  19. }
  20. super.beforeExecute(t, r);
  21. }


任务执行超时告警

重写ThreadPoolExecutor的afterExecute()方法,根据当前时间和beforeExecute()中设置的startTime的差值即可算出任务的实际执行时间,然后判断如果差值大于配置的runTimeout则累加排队超时任务数量(总数值累加、周期值累加)。且判断如果周期累计值达到配置的阈值,则会触发一次告警通知(同时重置周期累加值为0及上次告警时间为当前时间),告警间隔内多次触发不会发送告警通知

  1. @Override
  2. protected void afterExecute(Runnable r, Throwable t) {
  3. if (runTimeout > 0) {
  4. DtpRunnable runnable = (DtpRunnable) r;
  5. long runTime = System.currentTimeMillis() - runnable.getStartTime();
  6. if (runTime > runTimeout) {
  7. runTimeoutCount.incrementAndGet();
  8. Runnable alarmTask = () -> AlarmManager.doAlarm(this, RUN_TIMEOUT);
  9. AlarmManager.triggerAlarm(this.getThreadPoolName(), RUN_TIMEOUT.getValue(), alarmTask);
  10. }
  11. }
  12. super.afterExecute(r, t);
  13. }


告警通知相关配置项

如果想使用通知告警功能,配置文件必须要配置platforms字段,且可以配置多个平台,如钉钉、企微等;notifyItems配置具体告警项,包括阈值、平台、告警间隔等。

  1. spring:
  2. dynamic:
  3. tp:
  4. # 省略其他项
  5. platforms: # 通知平台
  6. - platform: wechat
  7. urlKey: 38a98-0c5c3b649c
  8. receivers: test
  9. - platform: ding
  10. urlKey: f80db3e801d593604f4a08dcd6a
  11. secret: SECb5444a6f375d5b9d21
  12. receivers: 17811511815
  13. executors: # 动态线程池配置,都有默认值,采用默认值的可以不配置该项,减少配置量
  14. - threadPoolName: dtpExecutor1
  15. executorType: common # 线程池类型common、eager:适用于io密集型
  16. corePoolSize: 2
  17. maximumPoolSize: 4
  18. queueCapacity: 200
  19. queueType: VariableLinkedBlockingQueue # 任务队列,查看源码QueueTypeEnum枚举类
  20. rejectedHandlerType: CallerRunsPolicy # 拒绝策略,查看RejectedTypeEnum枚举类
  21. keepAliveTime: 50
  22. allowCoreThreadTimeOut: false
  23. threadNamePrefix: dtp1 # 线程名前缀
  24. waitForTasksToCompleteOnShutdown: false # 参考spring线程池设计
  25. awaitTerminationSeconds: 5 # 单位(s)
  26. preStartAllCoreThreads: false # 是否预热核心线程,默认false
  27. runTimeout: 200 # 任务执行超时阈值,目前只做告警用,单位(ms)
  28. queueTimeout: 100 # 任务在队列等待超时阈值,目前只做告警用,单位(ms)
  29. taskWrapperNames: ["ttl"] # 任务包装器名称,集成TaskWrapper接口
  30. notifyItems: # 报警项,不配置自动会按默认值配置(变更通知、容量报警、活性报警、拒绝报警、任务超时报警)
  31. - type: capacity # 报警项类型,查看源码 NotifyTypeEnum枚举类
  32. threshold: 80 # 报警阈值
  33. platforms: [ding,wechat] # 可选配置,不配置默认拿上层platforms配置的所以平台
  34. interval: 120 # 报警间隔(单位:s)
  35. - type: change
  36. - type: liveness
  37. threshold: 80
  38. interval: 120
  39. - type: reject
  40. threshold: 1
  41. interval: 160
  42. - type: run_timeout
  43. threshold: 1
  44. interval: 120
  45. - type: queue_timeout
  46. threshold: 1
  47. interval: 140

总结

本文开头介绍了线程池ThreadPoolExecutor的继承体系,核心流程的源码解读。然后介绍了DynamicTp提供的以上6种告警通知能力,希望通过监控+告警可以让我们及时感知到我们业务线程池的执行负载情况,第一时间做出调整,防止事故的发生。


联系我

对项目有什么想法或者建议,可以加我微信交流,或者创建issues,一起完善项目

公众号:CodeFox

微信:yanhom1314

美团动态线程池实践思路开源项目(DynamicTp),线程池源码解析及通知告警篇的更多相关文章

  1. Scala 深入浅出实战经典 第65讲:Scala中隐式转换内幕揭秘、最佳实践及其在Spark中的应用源码解析

    王家林亲授<DT大数据梦工厂>大数据实战视频 Scala 深入浅出实战经典(1-87讲)完整视频.PPT.代码下载:百度云盘:http://pan.baidu.com/s/1c0noOt6 ...

  2. 一个Python开源项目-哈勃沙箱源码剖析(下)

    前言 在上一篇中,我们讲解了哈勃沙箱的技术点,详细分析了静态检测和动态检测的流程.本篇接着对动态检测的关键技术点进行分析,包括strace,sysdig,volatility.volatility的介 ...

  3. vs2008编译QT开源项目--太阳神三国杀源码分析(三) 皮肤

    太阳神三国杀的界面很绚丽,界面上按钮的图标,鼠标移入移出时图标的变化,日志和聊天Widget的边框和半透明等效果,既可以通过代码来控制,也可以使用皮肤文件qss进行控制.下面我们分析一下三国杀的qss ...

  4. airbnb 开源reAir 工具 用法及源码解析(一)

    reAir 有批量复制与增量复制功能 今天我们先来看看批量复制功能 批量复制使用方式: cd reair ./gradlew shadowjar -p main -x test # 如果是本地tabl ...

  5. elementUi源码解析(1)--项目结构篇

    因为在忙其他事情好久没有更新iview的源码,也是因为后面的一些组件有点复杂在考虑用什么方式把复杂的功能逻辑简单的展示出来,还没想到方法,突然想到element的组件基本也差不多,内部功能的逻辑也差不 ...

  6. Android 开源项目源码解析(第二期)

    Android 开源项目源码解析(第二期) 阅读目录 android-Ultra-Pull-To-Refresh 源码解析 DynamicLoadApk 源码解析 NineOldAnimations ...

  7. Python优秀开源项目Rich源码解析

    这篇文章对优秀的开源项目Rich的源码进行解析,OMG,盘他.为什么建议阅读源码,有两个原因,第一,单纯学语言很难在实践中灵活应用,通过阅读源码可以看到每个知识点的运用场景,印象会更深,以后写代码的时 ...

  8. 【原】Android热更新开源项目Tinker源码解析系列之一:Dex热更新

    [原]Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Tinker是微信的第一个开源项目,主要用于安卓应用bug的热修复和功能的迭代. Tinker github地址:http ...

  9. 【原】Android热更新开源项目Tinker源码解析系列之二:资源文件热更新

    上一篇文章介绍了Dex文件的热更新流程,本文将会分析Tinker中对资源文件的热更新流程. 同Dex,资源文件的热更新同样包括三个部分:资源补丁生成,资源补丁合成及资源补丁加载. 本系列将从以下三个方 ...

随机推荐

  1. egg-jwt的使用

    安装 npm install egg-jwt --save 配置 // config/config.default.js config.jwt = { secret: 'zidingyi', // 自 ...

  2. java 中判断输入是否合法 if (变量名.hasNextInt())

    //案例: Scanner sc = new Scanner(System.in); System.out.println("你选择了新修改商品功能!"); System.out. ...

  3. BM 学习笔记

    两个 BM 哟 1.Bostan-Mori 常系数其次线性递推. 实际上这个算法是用来计算 \([x^n]\frac {F(x)}{G(x)}\) 的... 我们考虑一个神奇的多项式:\(F(x)F( ...

  4. Linux卸载源码编译安装的软件

    使用auto-apt 和 checkinstall,具体命令如下 #安装auto-apt和checkinstall apt install auto-apt checkinstall #在源码目录中 ...

  5. kali linux 更换国内源报GPG error解决办法

    wget -q -O - https://archive.kali.org/archive-key.asc | apt-key add

  6. Django-Multitenant,分布式多租户数据库项目实战(Python/Django+Postgres+Citus)

    Python/Django 支持分布式多租户数据库,如 Postgres+Citus. 通过将租户上下文添加到您的查询来实现轻松横向扩展,使数据库(例如 Citus)能够有效地将查询路由到正确的数据库 ...

  7. Flask 之 蓝图

    蓝图,听起来就是一个很宏伟的东西 在Flask中的蓝图 blueprint 也是非常宏伟的 它的作用就是将 功能 与 主服务 分开怎么理解呢? 比如说,你有一个客户管理系统,最开始的时候,只有一个查看 ...

  8. P1030

    题面 给出一棵二叉树的中序排列与后序排列.求出它的先序排列.(约定树结点用不同的大写字母表示,长度≤8). 输入格式 2行,均为大写字母组成的字符串,表示一棵二叉树的中序排列与后序排列. 输出格式 1 ...

  9. TiDB 5.0认证指南之PCTA PCTP

    1. TiDB简介 TiDB 是 PingCAP 公司自主设计.研发的开源分布式关系型数据库,是一款同时支持在线事务处理与在线分析处理 (Hybrid Transactional and Analyt ...

  10. Python中将字典转为成员变量

    技术背景 当我们在Python中写一个class时,如果有一部分的成员变量需要用一个字典来命名和赋值,此时应该如何操作呢?这个场景最常见于从一个文件(比如json.npz之类的文件)中读取字典变量到内 ...