封装ThreadLocal

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

为什么要封装ThreadLocal?

原因有两点:

1、对于Thread,如果希望在Interceptor中存入UserInfo并在Service层通过ThreadLocal把UserInfo出来,必须保证Interceptor和Service此时用的是同一个ThreadLocal。

但是一个对象如何同时出现在Interceptor和Service呢?各自new一个ThreadLocal可不行,因为此时是两个对象了。比如,在Interceptor创建的对象是紫霞,而Service创建的是青霞,紫霞在至尊宝存入的东西,后面的青霞可没办法出来,因为Thread内部的ThreadLocalMap是以ThreadLocal作为key的(看上面爱心的key)。

但如果我们在ThreadLocalUtil中new一个ThreadLocal对象作为成员变量,就可以在Service中取出来了:

即:把ThreadLocal对象封装在ThreadLocalUtil中,分别在Interceptor和Service中使用它。

2、原生的ThreadLocal无法满足复杂的业务场景。

比如现在我封装了一个最简单的ThreadLocal(装饰者模式,为的是解决第一个问题):

/*** @author mx*/
public class MyThreadLocal {private MyThreadLocal() {}private static final ThreadLocal<Object> THREAD_CONTEXT = new ThreadLocal<>();public static void put(Object obj) {THREAD_CONTEXT.set(obj);}public static Object get() {return THREAD_CONTEXT.get();}public static void remove() {THREAD_CONTEXT.remove();}
}

MyThreadLocal确实解决了第一个问题,复用了ThreadLocal,保证了Interceptor和Service用到的ThreadLocal是同一个对象。

但是,有两个缺陷:

  • 无法存取多个不同的值
  • 语意不明

比如,Service层希望往ThreadLocal里再添加一个Score对象,好让DAO层能获取到。你要怎么做?

另外,MyThreadLocal.get()其实很突兀,语意不明,光看代码你根本不知道get出来的是什么东西。

基于以上两个原因,我们必须封装ThreadLocal。对于第二个问题,其实可以考虑把原先的value改为Map类型。比如原本是 threadLocal1:User或者threadLocal1:Score,确实只能存一个值,而且很容易发生覆盖。但是如果把Map作为value存进去,形成双层Map就灵活多了:

{"threadLocal1" : {"USER_INFO" : User,"SCORE" : Score}
}

思路分析到这,我们开始写代码。一般情况下,我们只需要考虑一个ThreadLocal和多个Thread,这也是实际编程最常见的方式,所以下面的代码只会封装一个ThreadLocalUtil,里面也只有一个ThreadLocal。

ThreadLocalUtil第一版

/*** @author mx*/
public class ThreadLocalUtil {private ThreadLocalUtil() {}/*** ThreadLocal是紫霞仙子,至尊宝是Thread* ThreadLocal的泛型规定了紫霞仙子劈开至尊宝时,能给他心里塞的东西的类型。* <p>* 比如* 将ThreadLocal泛型指定为String,那么造了一个ThreadLocalMap后,这个map只能存 threadLocal:"这是字符串" 这样的键值对* 将ThreadLocal泛型指定为Integer,那么造了一个ThreadLocalMap后,这个map只能存 threadLocal:1111111111 这样的键值对** 由于单纯的value会发生值覆盖,所以我们使用Map<String, Object>作为value*/private static final ThreadLocal<Map<String, Object>> THREAD_CONTEXT = new ThreadLocal<>();/*** 存入线程变量** @param key* @param object*/public static void put(String key, Object object) {/*** 至尊宝(一个Thread)经过这段代码,遇到了紫霞(THREAD_CONTEXT)。大家可以点进get()看看,内部操作是:* 1.把至尊宝的心取出来(从Thread中取出ThreadLocalMap)** ThreadLocalMap的构造类似于这样* {* ...THREAD_CONTEXT: {* ........."USER_INFO":"{'name':'bravo', 'age':18}",* ........."SCORE":"{'Math':99, 'English': 97}"* ......}* }** 2.ThreadLocalMap.Entry e = map.getEntry(this); 把自己(THREAD_CONTEXT)作为key,取出属于自己的value,此时value是一个Map<String, Object>。* 3.所以最终THREAD_CONTEXT.get()返回的Map<String, Object> map**/Map<String, Object> map = THREAD_CONTEXT.get();// 第一次从ThreadLocalMap中根据threadLocal取出的value可能是nullif (map == null) {map = new HashMap<>();// 把map作为value放进去THREAD_CONTEXT.set(map);}/*** 假设本次存的是 USER_INFO:{"name":"bravo", "age":18}* 此时ThreadLocalMap中的结构是* {* ...THREAD_CONTEXT: {* ........."USER_INFO":"{'name':'bravo', 'age':18}",* ......}* }**/map.put(key, object);}/*** 取出线程变量** @param key* @return*/public static Object get(String key) {// 先获取MapMap<String, Object> map = THREAD_CONTEXT.get();// 从Map中得到USER_INFOreturn map != null ? map.get(key) : null;}/*** 移除当前线程的指定变量* 比如把* {* ...THREAD_CONTEXT: {* ........."USER_INFO":"{'name':'bravo', 'age':18}",* ........."SCORE":"{'Math':99, 'English': 97}"* ......}* }* 变成* {* ...THREAD_CONTEXT: {* ........."SCORE":"{'Math':99, 'English': 97}"* ......}* }* 并不是移除所有,而是只移除USER_INFO** @param key*/public static void remove(String key) {Map<String, Object> map = THREAD_CONTEXT.get();map.remove(key);}/*** 移除当前线程的所有变量* 比如把* {* ...THREAD_CONTEXT: {* ........."USER_INFO":"{'name':'bravo', 'age':18}",* ........."SCORE":"{'Math':99, 'English': 97}"* ......}* }* 变成* {* }*/public static void clear() {THREAD_CONTEXT.remove();}
}

建议大家从上面的MyThreadLocal开始,尝试自己一步步封装,ThreadLocalUtil第一步也不难,完全可以自己写。

ThreadLocalUtil第二版

上面的版本其实马马虎虎能用了,就是看起来不是特别优雅,很多地方需要判断null。如果你希望自己的工具类优雅些,逼格高一点,可以尝试下面这种:

/*** @author mx*/
public class ThreadLocalUtil {private ThreadLocalUtil() {}/*** 注意右边new的不是原生的ThreadLocal,而是我自定义的MapThreadLocal,它继承自ThreadLocal** @see MapThreadLocal*/private final static ThreadLocal<Map<String, Object>> THREAD_CONTEXT = new MapThreadLocal();/*** 根据key获取value* 比如key为USER_INFO,则返回"{'name':'bravo', 'age':18}"* {* ...THREAD_CONTEXT: {* ........."USER_INFO":"{'name':'bravo', 'age':18}",* ........."SCORE":"{'Math':99, 'English': 97}"* ...}* }** @param key* @return*/public static Object get(String key) {// getContextMap()表示要先获取THREAD_CONTEXT的value,也就是Map<String, Object>。然后再从Map<String, Object>中根据key获取return getContextMap().get(key);}/*** put操作,原理同上** @param key* @param value*/public static void put(String key, Object value) {getContextMap().put(key, value);}/*** 清除map里的某个值* 比如把* {* ...THREAD_CONTEXT: {* ........."USER_INFO":"{'name':'bravo', 'age':18}",* ........."SCORE":"{'Math':99, 'English': 97}"* ...}* }* 变成* {* ...THREAD_CONTEXT: {* ........."SCORE":"{'Math':99, 'English': 97}"* ...}* }** @param key* @return*/public static Object remove(String key) {return getContextMap().remove(key);}/*** 清除整个Map<String, Object>* 比如把* {* ...THREAD_CONTEXT: {* ........."USER_INFO":"{'name':'bravo', 'age':18}",* ........."SCORE":"{'Math':99, 'English': 97}"* ...}* }* 变成* {* ...THREAD_CONTEXT: {}* }*/public static void remove() {getContextMap().clear();}/*** 从ThreadLocalMap中清除当前ThreadLocal存储的内容* 比如把* {* ...THREAD_CONTEXT: {* ........."USER_INFO":"{'name':'bravo', 'age':18}",* ........."SCORE":"{'Math':99, 'English': 97}"* ...}* }* 变成* {* }*/public static void clear() {THREAD_CONTEXT.remove();}/*** 从ThreadLocalMap* {* ...THREAD_CONTEXT: {* ........."USER_INFO":"{'name':'bravo', 'age':18}",* ........."SCORE":"{'Math':99, 'English': 97}"* ...}* }* 中获取Map<String, Object>* {* ..."USER_INFO":"{'name':'bravo', 'age':18}",* ..."SCORE":"{'Math':99, 'English': 97}"* }** @return*/private static Map<String, Object> getContextMap() {return THREAD_CONTEXT.get();}/*** 内部类,继承自ThreadLocal,和第一版一样,仍旧指定value为Map<String, Object>* 之所以要自定义MapThreadLocal,是为了重写原生ThreadLocal的initialValue()* 把ThreadLocal第一版中判断null的操作隐藏掉,让代码优雅一些(但对于初学者来说,理解难度也提升了)*/private static class MapThreadLocal extends ThreadLocal<Map<String, Object>> {@Overrideprotected Map<String, Object> initialValue() {return new HashMap<String, Object>(8) {private static final long serialVersionUID = 3637958959138295593L;@Overridepublic Object put(String key, Object value) {return super.put(key, value);}};}}
}

第二版的难点有两个:

  • 多了一个getContextMap(),部分人会晕。其实这个操作就是得到当前ThreadLocal对应Map<String, Object>
  • 为什么重写initialValue()可以避免判断null?

另外,不用担心每次都会创建新的Map覆盖原有的,get()方法内部本身会判断,如果已经有ThreadLocalMap其实是直接取值返回的。

如果还是觉得难理解,我建议取消getContextMap(),把里面的代码拷贝到各个方法中,好理解些。

最后的最后,不要因为这个工具类是自己封装的就怀疑是不是会重新导致线程安全问题。

只有同时满足下面3个条件,才有可能发生线程安全问题:

  • 多线程环境
  • 有共享数据
  • 有多条语句操作共享数据/单条语句本身非原子操作

但实际上,ThreadLocal的机制本身就避免了资源共享...因为每个线程内部都有自己的ThreadLocalMap(每个线程都有自己的资源,相互独立)。

所以记住,ThreadLocal本身和线程安全没啥关系,但你可以用它来解决线程安全问题,而且它的解决办法很粗暴,就是从根源上杜绝了资源共享。

之前说最后应该调用threadLocal.remove(),而对应ThreadLocalUtil,应该调用clear(),它对应的才是threadLocal.remove()。

ThreadLocalUtil第三版

之前封装ThreadLocal时一直在解决两个问题:

  • 原生的ThreadLocal对每个Thread的操作是基于单值的Key-Value,而我们期望基于Key-MapValue的操作
  • 如果不重写initValue(),需要在外部处理Map的初始化问题

对于initValue()的重写,其实不需要专门写一个内部类(很多人不习惯内部类),有两种替代方式:

  • 给THREAD_CONTEXT赋值时,直接new ThreadLocal()并用匿名类方式重写initValue()
  • 让ThreadLocalUtil继承ThreadLocal,然后重写initValue()

第一种方式最简单,这里演示第二种。

另外,之前在知乎专栏讨论过,ThreadLocalMap是定义在ThreadLocal内部的,由于包权限问题,我们无法直接使用。而我们的ThreadLocalUtil其实本质是就像个Map,所以第三版我改了名字,干脆咱也叫ThreadLocalMap,就当Map使用,只不过是线程内共享的。

当然,你也可以有更好的封装,可以下方留言:

/*** @author mx*/
public class ThreadLocalMap extends ThreadLocal<Map<String, Object>> {private ThreadLocalMap() {}private final static ThreadLocal<Map<String, Object>> THREAD_CONTEXT = new ThreadLocalMap();/*** 根据key获取value* 比如key为USER_INFO,则返回"{'name':'bravo', 'age':18}"* {* ...THREAD_CONTEXT: {* ........."USER_INFO":"{'name':'bravo', 'age':18}",* ........."SCORE":"{'Math':99, 'English': 97}"* ...}* }** @param key* @return*/public static Object get(String key) {// getContextMap()表示要先获取THREAD_CONTEXT的value,也就是Map<String, Object>。然后再从Map<String, Object>中根据key获取return THREAD_CONTEXT.get().get(key);}/*** put操作,原理同上** @param key* @param value*/public static void put(String key, Object value) {THREAD_CONTEXT.get().put(key, value);}/*** 清除map里的某个值* 比如把* {* ...THREAD_CONTEXT: {* ........."USER_INFO":"{'name':'bravo', 'age':18}",* ........."SCORE":"{'Math':99, 'English': 97}"* ...}* }* 变成* {* ...THREAD_CONTEXT: {* ........."SCORE":"{'Math':99, 'English': 97}"* ...}* }** @param key* @return*/public static Object remove(String key) {return THREAD_CONTEXT.get().remove(key);}/*** 清除整个Map<String, Object>* 比如把* {* ...THREAD_CONTEXT: {* ........."USER_INFO":"{'name':'bravo', 'age':18}",* ........."SCORE":"{'Math':99, 'English': 97}"* ...}* }* 变成* {* ...THREAD_CONTEXT: {}* }*/public static void clear() {THREAD_CONTEXT.get().clear();}/*** 从ThreadLocalMap中清除当前ThreadLocal存储的内容* 比如把* {* ...THREAD_CONTEXT: {* ........."USER_INFO":"{'name':'bravo', 'age':18}",* ........."SCORE":"{'Math':99, 'English': 97}"* ...}* }* 变成* {* }*/public static void clearAll() {THREAD_CONTEXT.remove();}@Overrideprotected Map<String, Object> initialValue() {return new HashMap<String, Object>(8) {private static final long serialVersionUID = 3637958959138295593L;@Overridepublic Object put(String key, Object value) {return super.put(key, value);}};}}

测试用例:

/*** @author mx*/
public class ThreadLocalMapTest {public static void main(String[] args) {ThreadLocalMap.put("mainKey", "mainValue");new Thread(()->{ThreadLocalMap.put("threadKey", "threadValue");System.out.println("get main value in thread:" + ThreadLocalMap.get("mainKey"));System.out.println("get thread value in thread:" + ThreadLocalMap.get("threadKey"));}).start();System.out.println("get thread value in main:" + ThreadLocalMap.get("threadKey"));System.out.println("get main value in main:" + ThreadLocalMap.get("mainKey"));}}

ThreadLocalUtil第四版

public class ThreadLocalMap {private ThreadLocalMap() {}/*** ThreadLocal的静态方法withInitial()会返回一个SuppliedThreadLocal对象* 而SuppliedThreadLocal<T> extends ThreadLocal<T>* 我们存进去的Map会作为的返回值:* protected T initialValue() {*    return supplier.get();* }* * 所以也相当于重写了initialValue()* */private final static ThreadLocal<Map<String, Object>> THREAD_CONTEXT = ThreadLocal.withInitial(() -> new HashMap<>(8));/*** 根据key获取value* 比如key为USER_INFO,则返回"{'name':'bravo', 'age':18}"* {* ...THREAD_CONTEXT: {* ........."USER_INFO":"{'name':'bravo', 'age':18}",* ........."SCORE":"{'Math':99, 'English': 97}"* ...}* }** @param key* @return*/public static Object get(String key) {// getContextMap()表示要先获取THREAD_CONTEXT的value,也就是Map<String, Object>。然后再从Map<String, Object>中根据key获取return THREAD_CONTEXT.get().get(key);}/*** put操作,原理同上** @param key* @param value*/public static void put(String key, Object value) {THREAD_CONTEXT.get().put(key, value);}/*** 清除map里的某个值* 比如把* {* ...THREAD_CONTEXT: {* ........."USER_INFO":"{'name':'bravo', 'age':18}",* ........."SCORE":"{'Math':99, 'English': 97}"* ...}* }* 变成* {* ...THREAD_CONTEXT: {* ........."SCORE":"{'Math':99, 'English': 97}"* ...}* }** @param key* @return*/public static Object remove(String key) {return THREAD_CONTEXT.get().remove(key);}/*** 清除整个Map<String, Object>* 比如把* {* ...THREAD_CONTEXT: {* ........."USER_INFO":"{'name':'bravo', 'age':18}",* ........."SCORE":"{'Math':99, 'English': 97}"* ...}* }* 变成* {* ...THREAD_CONTEXT: {}* }*/public static void clear() {THREAD_CONTEXT.get().clear();}/*** 从ThreadLocalMap中清除当前ThreadLocal存储的内容* 比如把* {* ...THREAD_CONTEXT: {* ........."USER_INFO":"{'name':'bravo', 'age':18}",* ........."SCORE":"{'Math':99, 'English': 97}"* ...}* }* 变成* {* }*/public static void clearAll() {THREAD_CONTEXT.remove();}}

Spring对ThreadLocal的封装

比如编写AOP日志时,经常会用到的RequestContextHolder,其实内部也维护了ThreadLocal。

那么Spring是如何做到remove的呢?使用过滤器(我们使用了拦截器)。

对于ThreadLocal的应用还有很多很多,这里就举这么一个例子叭~

补充:线程重用导致用户信息错乱的Bug

虽然小册两篇ThreadLocal相关的文章都反复强调用完之后最好及时remove(),但似乎都没有给出特别具有说服力的案例。最近在看极客时间朱晔老师的《Java业务开发常见错误100例》时,发现一个很不错的案例,这里特别拿来补充。

贴一段里面的代码:


private static final ThreadLocal<Integer> currentUser = ThreadLocal.withInitial(() -> null);@GetMapping("wrong")
public Map wrong(@RequestParam("userId") Integer userId) {//设置用户信息之前先查询一次ThreadLocal中的用户信息String before  = Thread.currentThread().getName() + ":" + currentUser.get();//设置用户信息到ThreadLocalcurrentUser.set(userId);//设置用户信息之后再查询一次ThreadLocal中的用户信息String after  = Thread.currentThread().getName() + ":" + currentUser.get();//汇总输出两次查询结果Map result = new HashMap();result.put("before", before);result.put("after", after);return result;
}

为了更明显地看到这个BUG,可以将Tomcat线程池的最大连接数设置为1:

server.tomcat.max-threads=1

分别请求两次:

也就是说,由于Tomcat连接池的线程数有限(比如极端情况下max-thread=1),所以必然存在线程复用。如果两个请求复用一个Thread且ThreadLocal没有及时remove,那么上一个请求设置在Thread.ThreadLocalMap中的值就会污染本次请求。

所以应该保证每次使用后及时remove():


@GetMapping("right")
public Map right(@RequestParam("userId") Integer userId) {String before  = Thread.currentThread().getName() + ":" + currentUser.get();currentUser.set(userId);try {String after = Thread.currentThread().getName() + ":" + currentUser.get();Map result = new HashMap();result.put("before", before);result.put("after", after);return result;} finally {//在finally代码块中删除ThreadLocal中的数据,确保数据不串currentUser.remove();}
}

或者像Spring一样放在filter或者interceptor中remove()。

说一个小插曲:

之前有一次面试时,我提到项目中使用了BaseController,里面封装了ThreadLocal,可以获取Interceptor中存入的用户信息,然后面试官问我是否了解分布式场景下ThreadLocal导致的用户信息混乱的问题。我当时有点懵逼,关注点全在分布式场景上是否会产生这种BUG。其实不论是单体应用还是分布式应用,都有可能出现这个BUG。但为什么我们没遇到呢?不是因为项目小、并发低,而是我们根本不会像上面demo那样,上来就获取ThreadLocal里的内容,我们通常是在Interceptor中先设置值,然后Controller/Service中获取值,也就是说每次都是先覆盖、再取值,此时上次的value早就没了。

当然,还是推荐本次请求的值在响应时就remove,不要留到下次请求去覆盖,很容易出错,也容易造成内存泄漏。

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/201164.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++入门【2-C++ 数据类型】

C 数据类型 使用编程语言进行编程时&#xff0c;需要用到各种变量来存储各种信息。变量保留的是它所存储的值的内存位置。这意味着&#xff0c;当您创建一个变量时&#xff0c;就会在内存中保留一些空间。 您可能需要存储各种数据类型&#xff08;比如字符型、宽字符型、整型…

android实战项目之实现预览图片、视频、音频

引言 项目中经常用遇到视频、图片、音频的预览需要&#xff0c;采用PictureSelector第三方库&#xff0c;方便解决 方式 1. 引入第三方库&#xff0c;目前最新的版本v3.11.1。其他的版本可能用法不一样&#xff0c;有很多差异 implementation io.github.lucksiege:picturese…

玄子Share-CSS3 弹性布局知识手册

玄子Share-CSS3 弹性布局知识手册 Flexbox Layout&#xff08;弹性盒布局&#xff09;是一种在 CSS 中用于设计复杂布局结构的模型。它提供了更加高效、简便的方式来对容器内的子元素进行排列、对齐和分布 主轴和交叉轴 使用弹性布局&#xff0c;最重要的一个概念就是主轴与…

CoDeF视频处理——视频风格转化部署使用与源码解析

一、算法简介与功能 CoDef是作为一种新型的视频表示形式&#xff0c;它包括一个规范内容场&#xff0c;聚合整个视频中的静态内容&#xff0c;以及一个时间变形场&#xff0c;记录了从规范图像&#xff08;即从规范内容场渲染而成&#xff09;到每个单独帧的变换过程。针对目标…

JavaScript中的this指向:如何避免常见的this陷阱

​&#x1f308;个人主页&#xff1a;前端青山 &#x1f525;系列专栏&#xff1a;JavaScript篇 &#x1f516;人终将被年少不可得之物困其一生 依旧青山,本期给大家带来JavaScript篇专栏内容:JavaScript-this指向 目录 this指向详解 强行改变 this 指向 修改上下文中的this…

17、pytest自动使用fixture

官方实例 # content of test_autouse_fixture.py import pytestpytest.fixture def first_entry():return "a"pytest.fixture def order():return []pytest.fixture(autouseTrue) def append_first(order, first_entry):return order.append(first_entry)def test_s…

数学建模-基于机器学习的家政行业整体素质提升因素分析

基于机器学习的家政行业整体素质提升因素分析 整体求解过程概述(摘要) 家政服务业即为家庭提供多种类服务的专门行业&#xff0c;在第三产业中占有重要地位。但近年来&#xff0c;由于人工智能家居产业的发展与客户对家政从业者的要求水平不断提高&#xff0c;家政行业仍面对较…

graphics.h安装后依旧报错

问题解决一&#xff1a; 我在网上找了很多&#xff0c;都说找到graphics.h这个文件&#xff0c;放到include这个目录下&#xff0c;我照做了&#xff0c;然后 当我进行编译时&#xff0c;自动跳到graphics.h这个文件并出现一堆报错 问题解决二&#xff1a; 看一下这两个文件是…

c++拷贝与替换算法

一&#xff1a; copy 算法是 C STL 中的一个常用算法&#xff0c;用于将一个范围内的元素复制到另一个范围。它的函数原型如下&#xff1a; template<class InputIt, class OutputIt> OutputIt copy(InputIt first, InputIt last, OutputIt d_first); 这个算法接受三…

Linux库之动态库静态库

一、什么是库&#xff08;Library&#xff09; 二、库的分类 三、静态库、动态库优缺点 四、静态库的制作和使用 五、动态库的制作和使用 SO-NAME–解决主版本号之间的兼容问题 基于符号的版本机制 共享库系统路径 共享库的查找过程 有用的环境变量 gcc 编译器常用选项 Linux共…

STM32F1外部中断EXTI

目录 1. EXTI简介 2. EXTI基本结构 3. AFIO复用IO口 4. EXTI框图 5. EXTI程序配置 5.1 首先先配置要使用的GPIO口的引脚 5.2 配置AFIO数据选择器&#xff0c;选择想要中断的引脚 5.3 EXTI配置 1. EXTI简介 EXTI&#xff08;Extern Interrupt&#xff09;外部中…

思腾云计算中心 | 5千平米超大空间,基础设施完善,提供裸金属GPU算力租赁业务

2021年&#xff0c;思腾合力全资收购包头市易慧信息科技有限公司&#xff0c;正式开启云计算业务。思腾云计算中心占地2400平米&#xff0c;位于包头市稀土高新区&#xff0c;毗邻多家知名企业&#xff0c;地理位置优越&#xff0c;交通便利&#xff0c;是区内重要的信息化产业…

配置集群免密登录

文章目录 前言配置集群免密登录1. 设置主机名与 IP 地址的映射关系2. 生成 SSH 密钥对3. 将公钥复制到集群节点4. 测试免密登录5. 配置节点之间互相免密登录 总结 前言 本文介绍了如何配置集群之间免密登录&#xff0c;以便在搭建集群环境时方便地进行节点之间的通信。通过设置…

【开源】基于Vue.js的公司货物订单管理系统

文末获取源码&#xff0c;项目编号&#xff1a; S 082 。 \color{red}{文末获取源码&#xff0c;项目编号&#xff1a;S082。} 文末获取源码&#xff0c;项目编号&#xff1a;S082。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 客户管理模块2.2 商品维护模块2.3 供…

19、pytest通过mark标记测试函数

官方实例 [pytest] markers slow:marks tests as slow(deselect with -m "not slow")serial# content of test_mark.py import pytestpytest.mark.slow def test_mark_function():print("test_mark_function was invoked")assert 0解读与实操 通过使用p…

FairGuard无缝兼容小米澎湃OS、ColorOS 14 、鸿蒙4!

随着移动互联网时代的发展&#xff0c;各大手机厂商为打造生态系统、构建自身的技术壁垒&#xff0c;纷纷投身自研操作系统。 而对于一款游戏安全产品&#xff0c;在不同操作系统下&#xff0c;是否能够无缝兼容并且提供稳定的、高强度的加密保护&#xff0c;成了行业的一大痛…

7、信息收集(2)

文章目录 一、目录扫描1.1 目录扫描的原因1.2 目录扫描方法 二、指纹识别2.1 系统指纹识别2.2 中间件指纹识别2.3 web程序指纹识别2.4 防火墙指纹识别2.5 其他工具使用 三、Google Hacking3.1 逻辑运算符3.2 基本语法3.3 相关案例 一、目录扫描 1.1 目录扫描的原因 寻找网站后…

docker中mysql的数据迁移

在Docker中进行MySQL数据迁移通常涉及将数据从一个MySQL容器导出&#xff0c;并将其导入到另一个容器或主机上的MySQL实例中。以下是一般步骤&#xff1a; 步骤 1: 在源 MySQL 容器中导出数据 进入源 MySQL 容器&#xff1a; docker exec -it <source_mysql_container_name…

翻译: 大语言模型LLMs能做什么和不能做什么 保存笔记What LLMs can and cannot do

生成式 AI 是一项惊人的技术&#xff0c;但它并非万能。在这个视频中&#xff0c;我们将仔细看看大型语言模型&#xff08;LLM&#xff09;能做什么&#xff0c;不能做什么。我们将从我发现的一个有用的心理模型开始&#xff0c;了解它能做什么&#xff0c;然后一起看看 LLM 的…

怎么验证公钥和私钥是一对

文章目录 前言ssh-keygen 和 openssl两种证书的内容对比 从私钥提取公钥OPENSSH和PEM两种密钥格式相互转化OpenSSH 格式私钥转换成 OpenSSL PEM 格式私钥OpenSSL PEM 格式私钥转换成 OpenSSH 格式私钥OpenSSH 格式公钥生成 OpenSSL PEM 格式公钥OpenSSL PEM 格式公钥生成 OpenS…