xiaozhigang

长风破浪会有时,直挂云帆济沧海。

简述

redis有两种持久化方式:RDB(Redis DataBase)持久化和 AOF(Append Only File)。

RDB:通过创建数据的快照,在指定时间间隔内将redis某个时刻的数据状态保存到磁盘RDB文件中。

AOF:通过记录每个操作命令并将其追加到AOF文件中,恢复时通过执行这些命令来重建数据。

image-20240331163759309

RDB持久化

RBD以快照的方式保存全量的数据,可通过save和bgsave两个命令来触发持久化。

1、save命令:会同步地将 Redis 的所有数据保存到磁盘上的一个 RDB 文件中。这个操作会阻塞所有客户端请求直到 RDB 文件被完全写入磁盘。

2、bgsave命令:会在后台异步地创建 Redis 的数据快照,并将快照保存到磁盘上的 RDB 文件中。这个命令会立即返回,Redis 服务器可以继续处理客户端请求。

image-20240331165755573

AOF持久化

AOF以追加的方式保存增量的数据,AOF 的主要作用是解决了数据持久化的实时性,目前已经是 Redis 持久化的主流方式。

AOF 的工作流程操作:命令写入 (append)、文件同步(sync)、文件重写(rewrite)、重启加载 (load)。

image-20240331171433901

流程如下:

1)当 AOF 持久化功能被启用时,Redis 服务器会将接收到的所有写命令(比如 SET, LPUSH, SADD 等修改数据的命令)追加到 AOF 缓冲区(buffer)的末尾。

2)为了将缓冲区中的命令持久化到磁盘中的 AOF 文件,Redis 提供了几种不同的同步策略:

  • always:每次写命令都会同步到 AOF 文件,这提供了最高的数据安全性,但可能因为磁盘 I/O 的延迟而影响性能。
  • everysec(默认):每秒同步一次,这是一种折衷方案,提供了较好的性能和数据安全性。
  • no:不主动进行同步,交由操作系统决定何时将缓冲区数据写入磁盘,这种方式性能最好,但在系统崩溃时可能会丢失最近一秒的数据。

3)随着操作的不断执行,AOF 文件会不断增长,为了减小 AOF 文件大小,Redis 可以重写 AOF 文件:

  • 重写过程不会解析原始的 AOF 文件,而是将当前内存中的数据库状态转换为一系列写命令,然后保存到一个新的 AOF 文件中。
  • AOF 重写操作由 BGREWRITEAOF 命令触发,它会创建一个子进程来执行重写操作,因此不会阻塞主进程。
  • 重写过程中,新的写命令会继续追加到旧的 AOF 文件中,同时也会被记录到一个缓冲区中。一旦重写完成,Redis 会将这个缓冲区中的命令追加到新的 AOF 文件中,然后切换到新的 AOF 文件上,以确保数据的完整性。

4)当 Redis 服务器启动时,如果配置为使用 AOF 持久化方式,它会读取 AOF 文件中的所有命令并重新执行它们,以恢复数据库的状态。

两者优缺点

RDB-优点

  1. 集中备份,则文件紧凑, dump.rdb,非常适合备份、全量复制的场景。
  2. 容灾性好,RDB 文件可以直接拷贝使用,用于容灾恢复。
  3. 恢复速度快,RDB文件集中紧凑, 则速度快

RDB-缺点

  1. 实时性低,RDB 是间隔一段时间进行持久化,无法实时持久化。如果在这时间段发生故障,则数据会丢失。
  2. 存在兼容问题,Redis 演进过程存在多个格式的 RDB 版本,存在老版本 Redis 无法兼容新版本 RDB 的问题。

AOF-优点

  1. 实时性好,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次命令操作就记录到 aof 文件中一次。
  2. 通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。

AOF-缺点

  1. AOF 文件比 RDB 文件大,且 恢复速度慢
  2. 数据集大 的时候,比 RDB 启动效率低

两者如何选择

1、只选RDB,可能会有数分钟的数据丢失。

2、只选AOF,不利于备份,恢复速度也慢。

3、两者配合,两种持久化方式都选,这种情况下Redis重启时会优先载入AOF文件来恢复数据,因为AOF文件的数据集比RDB全。

4、都不选,不持久化。

故障数据恢复

Redis 启动时加载数据的流程:

1、判断AOF开启且AOF文件是否存在,存在则优先加载

2、如果AOF关闭或者不存在,则加载RDB文件

3、文件加载成功后则Redis启动成功

4、加载失败,则启动失败

image-20240331174017060

REST


RESTful本身是一种风格而不是规范,本文为该风格的规范实现的最佳实践,本文档详细说明了HTTP RESTful API的定义和使用规范,作为接口调用者和实现者的重要参考。

接口风格

遵循RESTful设计风格,同时控制复杂度及易于使用,仅遵循大部分原则。 遵循原则:

  • 使用https协议
  • 版本号放入URL或Header
  • 只提供json返回格式
  • post,put上使用json作为输入
  • 使用http状态码作为错误提示
  • Path(路径)尽量使用名词,不使用动词,把每个URL看成一个资源
  • 使用HTTP动词(GET,POST,PUT,DELETE)作为action操作URL资源
  • 过滤信息
    • limit:指定返回记录数量
    • offset:记录开始位置
    • direction:请求数据的方向,取值prev-上一页数据;next-下一页数据
    • page:第几页
    • per_page:每页条数
    • total_count:总记录数
    • total_pages:总页数,等于page时,表示当前是最后一页
    • sort:column1,column2排序字段
    • orderby:排序规则,desc或asc
    • q:搜索关键字(uri encode之后的)
  • 返回结果
    • GET:返回资源对象
    • POST:返回新生成的资源对象
    • PUT:返回完整的资源对象
    • DELETE:返回一个空文档
  • 速率限制
    • X-RateLimit-Limit: 每个IP每个时间窗口最大请求数
    • X-RateLimit-Remaining: 当前时间窗口剩余请求数
    • X-RateLimit-Reset: 下次更新时间窗口的时间(UNIX时间戳),达到下个时间窗口时,Remaining恢复为Limit

未遵循原则:

  • Hypermedia API(HATEOAS),通过接口URL获取接口地址及帮助文档地址信息
  • 限制返回值的域,fields=id,subject,customer_name
  • 缓存,使用ETag和Last-Modified

参考:

模块和版本说明

接口模块相互对立且有版本管理,模块名作为APP配置项进行存储,每个模块的版本号version和endpoint在应用初始化时调用api模块信息接口(通过传递客户端应用名称和版本号获取各个API模块的endpoint和version)获取并存储。

  • 示例模块及最新版本号:

模块模块用途最新版本号account帐户v1sms短信v1open一些开放接口,不需要公共参数v1

公共参数

Headers

公共请求参数是指每个接口都可能需要传递的参数,公共参数通过header传递。

参数是否必须说明及header格式app所有接口必须请求客户端应用标识,取值*-ios、*-android、*-pc、*-h5
header格式:
X-Co-App: $appuser_idApp登录后所有接口都传,
Web通过session机制获取用户标识
header格式:
Authorization: CoAPI base64(user_id:token)tokenApp登录后所有接口都传,
Web通过session机制获取授权访问令牌
header格式:
Authorization: CoAPI base64(user_id:token)

  • Web应用通过cookies传递session id,user_id和token无需传递,接口会从session自动获取;
  • 同一token值在App和Web各应用间通用(token即为session id);
  • APP修改user-agent,在原有user-agent的尾部添加$app/$versionNetType/$value。如:
    • Dalvik/2.1.0 (Linux; U; Android 6.0.1; MI 4LTE MIUI/V7.5.3.0.MXGCNDE) $app-android/3.0.0 NetType/4G
    • Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_2 like Mac OS X) AppleWebKit/603.2.4 (KHTML, like Gecko) $app-ios/3.0.0 NetType/WIFI
  • app取值及释义示例

app取值客户端名称【域名】admin-pc管理中心PC网页版【admin.url.com】admin-h5管理中心手机网页版【admin.url.com】admin-ios管理中心iOS版admin-android管理中心Android版

Cookies

  • 用于告知服务端是否支持Webp的Cookie:cookie name是supportWebp,取值是1(支持)和0(不支持),未传递时服务端默认取值为0。
  • Webview植入Session的Cookie:

JWT & OAuth2

  • Json Web Token可用于替代session-cookie机制。但会存在一些问题,比如为过期token强制失效问题(用户修改了密码后,无法强制其他的终端token全部失效)。
  • OAuth2是授权其他开发者访问自己应用有限权限的授权机制。

权限

  • 权限分为
    • none:无需任何授权;
    • token:需要用户登录授权,可通过header AuthorizationCookie CoSID传递;
    • admintoken:需要管理员登录授权,可通过header AuthorizationCookie CoCPSID传递;
    • token || admintoken:用户登录授权或管理员登录授权都可以;
    • sign:需要签名,一般用于服务端内部相互调用。

状态码说明

正确
接口正常访问情况下,服务器返回2××的HTTP状态码。

HTTP状态码200 OK - 表示已在响应中发出、资源更改成功(GET、PUT)201 Created - 新资源被创建(POST)204 No Content - 资源被删除(DELETE)

错误
当用户访问接口出错时,服务器会返回给一个合适的4××或者5××的HTTP状态码;以及一个application/json格式的消息体,消息体中包含错误码code和错误说明message。

  • 5××错误(500=<status code)为服务器或程序出错,客户端只需要提示“服务异常,请稍后重试”即可,该类错误不在每个接口中列出。
  • 4××错误(400=<status code<500)为客户端的请求错误,需要根据具体的code做相应的提示和逻辑处理,message仅供开发时参考,不建议作为用户提示。
  • 部分错误示例:

codemessageHTTP状态码InvalidToken未登录或授权过期,请登录401 UnauthorizedValidationError输入字段验证出错,缺少字段或字段格式有误422 Unprocessable EntityAccountNotExist账户名不存在404 Not FoundInvalidPassword密码错误401 UnauthorizedNotFound请求的资源不存在404 Not FoundAccountHasExist账户名已经存在409 ConflictMobileHasBinded手机号已经绑定其他账户409 ConflictInvalidSign参数签名验证未通过403 ForbiddenInvalidSMSCode短信验证码错误403 ForbiddenExpiredSMSCode过期的短信验证码403 ForbiddenFrequencyLimit发送过于频繁,请稍后再试403 ForbiddenTimesExceeded达到最大发送次数限制,请明天再试403 ForbiddenVerifyTimesExceeded达到最大校验次数,请明天再试403 ForbiddenRateLimitExceeded接口调用次数超过限制,请稍后再试429 Too Many Requests InternalError服务异常,请稍后再试500 Internal Server Error

参数传递

遵循RESTful规范,使用了GET, POST, PUT, DELETE共4种请求方法。

  1. GET:请求资源,返回资源对象
  2. POST:新建资源,返回新生成的资源对象
  3. PUT:新建/更新资源,返回完整的资源对象
  4. DELETE:删除资源,返回body为空
  • GET请求不允许有body, 所有参数通过拼接在URL之后传递,所有的请求参数都要进行遵循RFC 3986的URL Encode。
  • DELETE删除单个资源时,资源标识通过path传递,批量删除时,通过在body中传递JSON。
  • POST, PUT请求,所有参数通过JSON传递,可选的请求参数,只传有值的,无值的不要传递,contentType为application/json。

4种请求动作中,GET、PUT、DELETE是幂等的;只有POST是非幂等的。幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。 是非幂等是判断接口使用POST还是PUT的决定条件

注意: APP端获取json数据时,对于数值类型字段必须以数值类型转换,无论传递过来的值是否带引号。

速率限制Rate Limiting

  • 为了防止API被恶意调用,对API调用进行速率限制。
  • 速率限制为每IP每15分钟5000次(dev/qa为10W)调用(15分钟是一个时间窗口)。
  • 限制是针对所有接口模块一起计算的(Redis key为APIRL:{IP}),暂时没有特殊的模块或单个接口(未来可能有)。
  • 你可以通过每个接口返回的HTTP headers了解当前速率限制的情况:
    • X-RateLimit-Limit: 每个IP每个时间窗口最大请求数
    • X-RateLimit-Remaining: 当前时间窗口剩余请求数
    • X-RateLimit-Reset: 下次更新时间窗口的时间(UNIX时间戳),达到下个时间窗口时,Remaining恢复为Limit
  • 超出速率限制,返回以下错误

安全注意事项

  • 用户登录后用户的token;aliyun OSS的bucket、AccessKey ID与AccessKey secret;微视频的appid、sign、bucket;这些关键数据通过调用接口获得,需要在客户端以安全的方式存储。
  • 音频视频在APP内的存储,不允许被拷贝(即使越狱或root后拿走也无法使用)。

测试工具

推荐Chrome浏览器插件Postman作为接口测试工具, Postman下载地址

文档生成工具

调用示例

  • 伪代码
  • PHP

API模块信息获取

  • App配置文件中仅存储api模块名,App初始化时请求获取api模块信息,获取各个api模块的信息(endpoint和version)。

需求描述

我有一个仓库AAA,目录结构如下:

1
2
3
4
5
6
7
8
$ tree
├── .git
├── common-api
├── commons-pojo
├── commons-utils
├── account
└── payment
└── product

现在我需要把common-api、commons-pojo、commons-utils独立为一个新仓库xxx-common
从网上找到可以用”git filter-branch”和”git subtree split”两种方式独立目录为仓库,但是”git subtree split”只能独立一个目录为仓库,如果需要独立多个目录为仓库需要使用”git filter-branch”。

阅读全文 »

分层架构图

DDD

简述

  1. Controller层:接口层,负责对前端展示的路由和适配;如果需要的话,消费事件和发送消息通知等也可以放在这里;

  2. Application层:主要负责获取输入,组装上下文,参数校验,调用领域层做业务处理

  3. Domain层:领域是应用的核心,主要是封装了核心业务逻辑,通过领域服务(Domain Service)、领域对象(Entity)的方法对App层提供业务实体和业务逻辑计算;

  4. Infrastructure层:主要负责技术细节问题的处理,比如数据库的CRUD、搜索引擎、文件系统、分布式服务的RPC等;

分层说明

  • APP层采用CQE模型,查询和命令分离
  • AppService调用实体和IRepo进行编排实现简单业务,通过调用DomainService实现复杂业务场景
  • AppService和DomainService均为无状态服务
  • App只允许调用仓储以及实体自带方法,不允许写任何方法,避免出现AppService过大的情况
  • DomainService封装所有跨实体的业务逻辑
  • 为避免出现AppService之间或者Domainservice之间相互调用,抽取出两者公共部分为一个Helper

聚合根和实体

  • 按聚合根分包,和包名一样的实体为聚合根
  • 可以识别的聚合根就写成聚合根,否则做成实体
  • 聚合根的一致性由自己保障
  • 跨实体的操作放到领域服务中

领域对象实体为充血模型,为一般业务对象,具备业务属性和业务行为

  • 实体都有自己的唯一标识通常
  • 实体可以不和表一一对应,通常操作单个表,可以操作多表

领域服务

某个操作过程或转换过程不是实体的职责时,我们便应该将该操作放在一个单独的接口中,即领域服务。

  • 用于实现某个领域的任务,不适合放在实体对象上时,就放在领域服务上
  • 放在实体的静态方法上有悖DDD
  • 避免在实体中调用资源库,数据通过AppService调用Repo传入
  • 如果有复杂逻辑,其中间的状态依赖前面步骤结果查询数据库作为后续逻辑的输入,通过把Repo传入到Domainservice的方式达到无状态效果

传输对象

  • DTO:接受Controller层发来的请求,返回结果
  • Entity :将DTO转换为要操作的领域层对象

无状态

  • 一次请求所需的全部信息,要么都包含在这个请求里,要么可以从外部获取到(比如说数据库),服务本身不存储任何信息
  • 有状态服务(stateful service)则相反,它会在自身保存一些数据,先后的请求是有关联的
  • 无状态服务的优势在于可以很方便地水平伸缩

简述

如果在线上环境,需要新建一个索引,会发生什么,会不会导致不可用,具体的创建步骤又是啥。

首先肯定会有一段时间导致表不可用的,主要就是看不可用的时间长短。

在不同的版本上主要有三种创建方式:Copy Table方式、Inplace方式、Online方式

Copy Table方式

早期版本,直接通过复制表实现

步骤

1、先为原表table创建临时表table_copy。

2、向临时表table_copy添加索引。

3、将原表table数据查询后插入到临时表table_copy,在将原表table改名为table_1。

4、在将临时表table_copy改名为table

不可用

在创建的过程中原表是可读的,但是在查询插入阶段是不可写的。

Inplace方式

MySQL5.5版本,没有复制临时表,减少了空间消耗,相对copy方式是一种进步。

步骤

1、读取主表索引列数据,并对数据排序

2、直接新建索引

不可用

创建过程中原表可读不可写。

Online方式

MySQL5.6.7版本,在原本inplace的基础上增添了可写的功能,相对inplace又是一种进步。

步骤

1、使用inplace方式创建索引,不用临时表。

2、在遍历聚簇索引,收集记录插入到新索引的过程中,记录修改保存到row log

3、聚簇索引遍历结束,并将所有记录插入之后,重放row log,是的记录一致。

不可用

只有在步骤3,重放row log的过程中锁表不可写。

什么是JVM类加载

指将编译后的class文件,读到内存中,首先将其放在运行时数据区的方法区内,然后再堆内创建class对象。class对象封装了类在方法区内的数据结构,并提供了访问方法区内的数据结构的接口。

image-20240409215152713

类加载器

自带加载器

自带加载器有三个:

1、启动类加载器:负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。

2、扩展类加载器:该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载DK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。

3、应用程序类加载器:该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

类加载的三种方式

  • 命令行启动应用时候由JVM初始化加载
  • 通过Class.forName()方法动态加载
  • 通过ClassLoader.loadClass()方法动态加载

双亲委派机制

1、一个类加载器收到类加载请求,不会首先自己去加载这个类,而是把请求委托到父类加载器去完成。

2、依次向上。

3、所有类加载请求都会被传递到顶层的启动类加载器中。

4、只有父加载器无法加载该类,才会由子类尝试加载。

image-20240409221650719

类的生命周期

生命周期主要包括7个部分:加载,验证,准备、解析、初始化、使用、卸载

类加载过程:加载,验证,准备、解析、初始化

其中 验证,准备、解析 这三个部分统称为连接

image-20240409223238427

加载,验证,准备、初始化 这四个部分是按顺序发生的,而解析则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。其他阶段是按顺序发生,不一定按顺序结束,通常是相互交叉的混合进行,在一个阶段执行的过程中调用另一个阶段。

加载:查找并加载类的二进制数据

验证:确保被加载类的正确性

准备:为类的静态变量分配内存,并将其初始化默认值

解析:把类中的符号引用转换为直接引用

初始化:到此才开始真正执行Java程序代码,执行类构造器方法的过程

简述

提到分布式,那么CAP原则是绕不过去的,那么什么是CAP原则呢。

CAP原则:在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)不可能都同时满足,最多只能同时满足两个。

img

详解

选项 描述
一致性(Consistency 指在分布式系统中,多个副本之间能够保持严格的数据一致性。
可用性(Availability) 指服务一直处于可用状态,每次请求都能获取非错响应
分区容错性(Partition tolerance) 指在分布式系统中,某个分区或副本出故障了,其他分区或副本能继续对外提供服务

为什么不能CAP兼容

1、在分布式系统中,分区容错性肯定是必须的,所以必须要有P。

2、添加C,就是添加一致性,那么就是要保证所有分区中的数据是一致的。假设有A、B、C三个分区,现在分区上的数据一致。

3、添加A,添加可用性,保证服务一直可用,我们看能不能达到这种效果。

​ A、B、C三个分区,分区上的数据一致。现在操作A分区中的数据发生更改,在保证一直性的情况下,得先同步更改B、C分区上得数据。更改数据需要时间,在同步更改的这段时间中,是不是就不能对外提供服务。 如果要先保证可用性,那是不是就不能先同步更改B、C分区上的数据。由此就可以看出CAP不能同时存在。

解决方案

虽然CAP不能同时存在,但是在分布式系统中一般AP、CP也可以根据场景的不同解决我们的问题了。

在可用性要求高的场景中,可以放弃一致性。这要求在数据不一致的情况下不影响我们的服务,比如NoSQL。

在一致性要求高的场景中,可以放弃可用性。这种场景是对数据有很高的要求,比如转账服务,可以牺牲一段可用时间,以保证各分区数据同步一致。

折中方案

在一致性要求比较高的场景中,同步各分区的时候,是不是需要同步到所有分区,如果同步一个或者大部分分区就可以的,那么这时候返回,是不是会提高可用性。这是一个在保证一致性的情况下提高可用性的办法,前提是同步到一个或大部分分区就可以保证我们的业务。

归根究底,一致性和可用性不能同时兼得,一个增加另一个必然减少。

架构图

四层结构

cola-arch.jpg

  1. 适配层(Adapter Layer):负责对前端展示(web,wireless,wap)的路由和适配,对于传统B/S系统而言,adapter就相当于MVC中的controller;

  2. 应用层(Application Layer):主要负责获取输入,组装上下文,参数校验,调用领域层做业务处理,如果需要的话,发送消息通知等。层次是开放的,应用层也可以绕过领域层,直接访问基础实施层;

  3. 领域层(Domain Layer):主要是封装了核心业务逻辑,并通过领域服务(Domain Service)和领域对象(Domain Entity)的方法对App层提供业务实体和业务逻辑计算。领域是应用的核心,不依赖任何其他层次;

  4. 基础实施层(Infrastructure Layer):主要负责技术细节问题的处理,比如数据库的CRUD、搜索引擎、文件系统、分布式服务的RPC等。此外,领域防腐的重任也落在这里,外部依赖需要通过gateway的转义处理,才能被上面的App层和Domain层使用。

    阅读全文 »

简述

ThreadLocal是什么,是java中用于实现线程内变量的工具类。允许每个线程都拥有自己的副本,从而实现线程隔离,解决多线程的共享对象的线程安全问题。

image-20240330210435879

ThreadLocal的使用

1
2
3
public static ThreadLocal<String> localVariable = new ThreadLocal<>(); // 新建
localVariable.set("他是沙雕"); // 设置值
String value = localVariable.get(); // 获取值

详述

实现原理

ThreadLocal的底层实现是一个特殊的map,可以理解往ThreadLocal设置一个xxx值,就是往这个特殊的map中设置一个key为当前线程,value为xxx的值。由于他的key只能为当前线程,所以在某个线程中set和get都不会影响其他线程,从而到达线程安全。下图的ThreadLocalMap就是拿个特殊的map。

image-20240330211735870

总体来说,java在运行的过程中会维护一个ThreadLocalMap,这个ThreadLocalMap的key为各个线程,value为各个线程想存放的东西,可以是String,可以是map,List等等。当时由于key为各个线程,所有每个线程只能由一个Entry ,也就是一个键值对(key-value),当然线程也可以不设置不存放。

image-20240330213034272

内存泄露

都知道ThreadLocal会内存泄漏,那么这个内存泄漏又是怎么回事。

都知道在java虚拟机中栈是私有的,而堆是共享的,所以ThreadLocalMap是存放在堆中的,而栈中存的只是一个引用,如下图。

image-20240330214507001

由于Entry extends WeakReference<ThreadLocal<?>>,所以可以看到,Entry中的key指向ThreadLocal是弱引用,也就是线程执行完之后,这个ThreadLocal就会被释放清理掉,但是由于ThreadLocalMap指向Entry是强引用,所以Entry不会被释放清理,如果这Entry一直不被清理释放,那么ThreadLocalMap的内存占用就会越来越大,以至于内存也漏。

所以为了防止这种问题的发生,在用完时,记得释放清理。

1
2
3
4
5
6
try {
threadLocal.set(value);
// 执行业务操作
} finally {
threadLocal.remove(); // 确保能够执行清理
}
0%