天天向上
2025-11-13 18:38:57 0 举报
AI智能生成
知识点
作者其他创作
大纲/内容
多线程
锁
Synchronized
与Reentrantlock
与Reentrantlock
实现
Sync的实现就是基于monitor 管程,monitor 对象中有owner,WaitSet,EntryList。线程会进入monitor 对象中,查看owner 是否有值,如果没有,则会写入自己的线程id。如果有,则证明已经有线程拿到了锁。就会去EntryList中等待,进入阻塞状态。持有锁的线程如果调用了wait 方法,则会进入WaitSet中,进入等待状态。等出了WaitSet,又会进入EntryList中阻塞
区别
同
Sync和Reentrantlock都是可重入锁
异
Reentrantlock可中断
Reentrantlock可锁超时
Reentrantlock可公平
Reentrantlock可多条件变量(避免虚假唤醒)
volatile
内存屏障
读屏障:
保证在该屏障之后,对共享变量读取,加载的是主存中最新数据
保证在该屏障之后,对共享变量读取,加载的是主存中最新数据
写屏障:
保证了在该屏障之前,对共享变量的改动都同步到主存中.不修改顺序
保证了在该屏障之前,对共享变量的改动都同步到主存中.不修改顺序
保证变量的内存可见性
禁止指令重排序
线程池
参数
核心线程数
任务队列
最大线程数
拒绝策略
AbortPolicy:丢弃任务并抛异常
DiscardPolicy:丢弃任务,但是不抛异常
DiscardOldestPolicy:丢弃队列最老的任务,然后把当前任务加入队列
CallerRunsPolicy:由调用线程处理该任务
线程工厂
超时时间
超时单位
痛点
1.设置核心参数不知道多少合适
2.上线后改配置,重启麻烦,不能动态调整
3.不能实时监控,类似黑盒
JMM
JMM(Java内存模型)
分为主内存和工作内存
有时候JIT优化代码,以后就没办法保证可见性
所以可以通过volatile修饰成员变量和静态成员变量,使变量更改时会通过总线嗅探技术,让线程可感知。重新前往主内存获取值。
因为线程在运行时会将变量值拷贝一份到工作内存。更改发生在主内存,线程不可感知,所以工作内存的变量依然是旧值。
因为线程在运行时会将变量值拷贝一份到工作内存。更改发生在主内存,线程不可感知,所以工作内存的变量依然是旧值。
不过synchronized也可以,只是实现相对更重。
在Java内存模型中,synchronized规定:线程在加锁时, 先清空工作内存→在主内存中拷贝最新变量的副本到工作内存 →执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁。
跨线程传递
ThreadLocal
传递范围
仅限于当前线程
传递方式
不传递
数据隔离
线程间数据隔离
实现原理
ThreadLocalMap
使用场景
单线程、线程内部
InheritableThreadLocal
传递范围
仅限于父子线程
传递方式
父线程到子线程
数据隔离
线程间数据隔离
实现原理
ThreadLocalMap+继承关系
使用场景
父子线程间
TransmittableThreadLocal
传递范围
可跨线程、跨线程池
传递方式
跟随线程池、手动传递
数据隔离
线程间数据隔离,但支持跨线程数据传递
实现原理
ThreadLocalMap+TtlRunnable
使用场景
异步、多线程
问题:并非标准的Java API,而是第三方库提供的,存在与其它库的兼容性问题,无形中增加了代码的复杂性和使用难度。
TransmissibleThreadLocal
传递范围
可跨线程、跨线程池
传递方式
跟随线程池、手动传递
数据隔离
线程间数据隔离,但支持跨线程数据传递
实现原理
ThreadLocalMap+Transmitter
使用场景
异步、多线程
@Async
默认线程池
在未指定线程池的情况下调用被标记为@Async的方法时,Spring会自动创建SimpleAsyncTaskExecutor线程池来执行该方法
使用
1.需要在配置类上增加@EnableAsync注解;
2.@Async注解可以标记一个异步执行的方法,也可以用来标记一个类表明该类的所有方法都是异步执行;
3.可以在@Async中自定义执行器。
面试题
在Java中,A线程启动了B线程,然后B线程启动了C线程,这个时候B线程执行结束了,C线程还没执行完。那么C线程会不会立刻关闭
不会,线程一旦启动就是独立的,创建者的生命周期不影响被创建者
线程池上下文传递
// 线程上下文传递(线程池外获取)
RequestAttributes context = RequestContextHolder.getRequestAttributes();
// 线程上下文传递(线程池内设置)
RequestContextHolder.setRequestAttributes(context, true);
ES
简单使用示例
1.获取client;
2.具体实现(查询条件主要封在QueryBuilder中,分页、排序、字段过滤、高亮主要封装在SourceBuilder中)
//1,构建QueryBuilder请求对象
QueryBuilder queryBuilder = QueryBuilders.matchQuery("title", "小米");
//2,构建SearchRequest请求对象,指定索引库
SearchRequest searchRequest = new SearchRequest("huizi");
//3,构建SearchSourceBuilder查询对象
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
//4,构建QueryBuilder对象指定查询方式和查询条件
//5,将QuseryBuilder对象设置到SearchSourceBuilder对象中
sourceBuilder.query(queryBuilder);
//字段过滤
sourceBuilder.fetchSource(new String[]{"title","price","band","category","id"},new String[]{"images"});
//排序
sourceBuilder.sort("price", SortOrder.DESC);
//分页
sourceBuilder.from(0);
sourceBuilder.size(2);
//5,将SearchSourceBuilder设置到SearchRequest中
searchRequest.source(sourceBuilder);
try {
//6,调用方法查询数据
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
//7,解析返回结果
SearchHit[] hits = searchResponse.getHits().getHits();
for (int i = 0; i < hits.length; i++) {
System.out.println("返回的结果: " + hits[i].getSourceAsString());
}
} catch (IOException e) {
e.printStackTrace();
}
2.具体实现(查询条件主要封在QueryBuilder中,分页、排序、字段过滤、高亮主要封装在SourceBuilder中)
//1,构建QueryBuilder请求对象
QueryBuilder queryBuilder = QueryBuilders.matchQuery("title", "小米");
//2,构建SearchRequest请求对象,指定索引库
SearchRequest searchRequest = new SearchRequest("huizi");
//3,构建SearchSourceBuilder查询对象
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
//4,构建QueryBuilder对象指定查询方式和查询条件
//5,将QuseryBuilder对象设置到SearchSourceBuilder对象中
sourceBuilder.query(queryBuilder);
//字段过滤
sourceBuilder.fetchSource(new String[]{"title","price","band","category","id"},new String[]{"images"});
//排序
sourceBuilder.sort("price", SortOrder.DESC);
//分页
sourceBuilder.from(0);
sourceBuilder.size(2);
//5,将SearchSourceBuilder设置到SearchRequest中
searchRequest.source(sourceBuilder);
try {
//6,调用方法查询数据
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
//7,解析返回结果
SearchHit[] hits = searchResponse.getHits().getHits();
for (int i = 0; i < hits.length; i++) {
System.out.println("返回的结果: " + hits[i].getSourceAsString());
}
} catch (IOException e) {
e.printStackTrace();
}
1.获取client;
2.具体实现(查询条件主要封在QueryBuilder中,分页、排序、字段过滤、高亮主要封装在SourceBuilder中)
//多索引查询
SearchRequest searchRequest = new SearchRequest(new String[]{"huizi","huizi2"});
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("title", "手机");
matchQueryBuilder.minimumShouldMatch("80%");
sourceBuilder.query(matchQueryBuilder);
searchRequest.source(sourceBuilder);
try {
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
//7,解析返回结果
SearchHit[] hits = searchResponse.getHits().getHits();
for (int i = 0; i < hits.length; i++) {
System.out.println("返回的结果: " + hits[i].getSourceAsString());
}
} catch (IOException e) {
e.printStackTrace();
}
2.具体实现(查询条件主要封在QueryBuilder中,分页、排序、字段过滤、高亮主要封装在SourceBuilder中)
//多索引查询
SearchRequest searchRequest = new SearchRequest(new String[]{"huizi","huizi2"});
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("title", "手机");
matchQueryBuilder.minimumShouldMatch("80%");
sourceBuilder.query(matchQueryBuilder);
searchRequest.source(sourceBuilder);
try {
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
//7,解析返回结果
SearchHit[] hits = searchResponse.getHits().getHits();
for (int i = 0; i < hits.length; i++) {
System.out.println("返回的结果: " + hits[i].getSourceAsString());
}
} catch (IOException e) {
e.printStackTrace();
}
1.获取client;
2.具体实现(查询条件主要封在QueryBuilder中,分页、排序、字段过滤、高亮主要封装在SourceBuilder中)
//批量查询
MultiSearchRequest request = new MultiSearchRequest();
SearchRequest firstSearchRequest = new SearchRequest(new String[]{"huizi","huizi2"});
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.matchQuery("title", "小米"));
firstSearchRequest.source(searchSourceBuilder);
request.add(firstSearchRequest);
SearchRequest secondSearchRequest = new SearchRequest(new String[]{"huizi","huizi2"});
searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.matchQuery("band", "大米"));
secondSearchRequest.source(searchSourceBuilder);
request.add(secondSearchRequest);
try {
MultiSearchResponse multiSearchResponse = client.msearch(request, RequestOptions.DEFAULT);
MultiSearchResponse.Item[] responses = multiSearchResponse.getResponses();
for (MultiSearchResponse.Item respons : responses) {
SearchHit[] hits = respons.getResponse().getHits().getHits();
for (int i = 0; i < hits.length; i++) {
System.out.println("返回的结果: " + hits[i].getSourceAsString());
}
}
} catch (IOException e) {
e.printStackTrace();
}
2.具体实现(查询条件主要封在QueryBuilder中,分页、排序、字段过滤、高亮主要封装在SourceBuilder中)
//批量查询
MultiSearchRequest request = new MultiSearchRequest();
SearchRequest firstSearchRequest = new SearchRequest(new String[]{"huizi","huizi2"});
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.matchQuery("title", "小米"));
firstSearchRequest.source(searchSourceBuilder);
request.add(firstSearchRequest);
SearchRequest secondSearchRequest = new SearchRequest(new String[]{"huizi","huizi2"});
searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.matchQuery("band", "大米"));
secondSearchRequest.source(searchSourceBuilder);
request.add(secondSearchRequest);
try {
MultiSearchResponse multiSearchResponse = client.msearch(request, RequestOptions.DEFAULT);
MultiSearchResponse.Item[] responses = multiSearchResponse.getResponses();
for (MultiSearchResponse.Item respons : responses) {
SearchHit[] hits = respons.getResponse().getHits().getHits();
for (int i = 0; i < hits.length; i++) {
System.out.println("返回的结果: " + hits[i].getSourceAsString());
}
}
} catch (IOException e) {
e.printStackTrace();
}
1.获取client;
2.具体实现(查询条件主要封在QueryBuilder中,分页、排序、字段过滤、高亮主要封装在SourceBuilder中)
//布尔组合
SearchRequest searchRequest = new SearchRequest("huizi");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
//名字-小米
MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("title", "小米").minimumShouldMatch("80%");
//品牌-小米
MatchQueryBuilder matchQueryBuilder2 = QueryBuilders.matchQuery("band", "小米").operator(Operator.AND);
//分类-化妆品
MatchQueryBuilder matchQueryBuilder3 = QueryBuilders.matchQuery("category", "化妆品");
//图片路径- jjj模糊
FuzzyQueryBuilder fuzzyQueryBuilder = QueryBuilders.fuzzyQuery("title", "小米");
//价格 必须 2699
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("price", "2699");
//must必须
boolQueryBuilder.must(matchQueryBuilder);
//must必须
boolQueryBuilder.must(termQueryBuilder);
//mustNot不能
// boolQueryBuilder.mustNot(matchQueryBuilder2);
//should可以
boolQueryBuilder.should(matchQueryBuilder3);
//结果中过滤
// boolQueryBuilder.filter(fuzzyQueryBuilder);
sourceBuilder.query(boolQueryBuilder);
searchRequest.source(sourceBuilder);
try {
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
SearchHit[] hits = searchResponse.getHits().getHits();
for (int i = 0; i < hits.length; i++) {
System.out.println("返回的结果: " + hits[i].getSourceAsString());
}
} catch (IOException e) {
e.printStackTrace();
}
2.具体实现(查询条件主要封在QueryBuilder中,分页、排序、字段过滤、高亮主要封装在SourceBuilder中)
//布尔组合
SearchRequest searchRequest = new SearchRequest("huizi");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
//名字-小米
MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("title", "小米").minimumShouldMatch("80%");
//品牌-小米
MatchQueryBuilder matchQueryBuilder2 = QueryBuilders.matchQuery("band", "小米").operator(Operator.AND);
//分类-化妆品
MatchQueryBuilder matchQueryBuilder3 = QueryBuilders.matchQuery("category", "化妆品");
//图片路径- jjj模糊
FuzzyQueryBuilder fuzzyQueryBuilder = QueryBuilders.fuzzyQuery("title", "小米");
//价格 必须 2699
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("price", "2699");
//must必须
boolQueryBuilder.must(matchQueryBuilder);
//must必须
boolQueryBuilder.must(termQueryBuilder);
//mustNot不能
// boolQueryBuilder.mustNot(matchQueryBuilder2);
//should可以
boolQueryBuilder.should(matchQueryBuilder3);
//结果中过滤
// boolQueryBuilder.filter(fuzzyQueryBuilder);
sourceBuilder.query(boolQueryBuilder);
searchRequest.source(sourceBuilder);
try {
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
SearchHit[] hits = searchResponse.getHits().getHits();
for (int i = 0; i < hits.length; i++) {
System.out.println("返回的结果: " + hits[i].getSourceAsString());
}
} catch (IOException e) {
e.printStackTrace();
}
(1)统计某个字段的数量
ValueCountBuilder vcb= AggregationBuilders.count("count_uid").field("uid");
(2)去重统计某个字段的数量(有少量误差)
CardinalityBuilder cb= AggregationBuilders.cardinality("distinct_count_uid").field("uid");
(3)聚合过滤
FilterAggregationBuilder fab= AggregationBuilders.filter("uid_filter").filter(QueryBuilders.queryStringQuery("uid:001"));
(4)按某个字段分组,相当于sql中的group by
TermsBuilder tb= AggregationBuilders.terms("group_name").field("name");
(5)求和
SumBuilder sumBuilder= AggregationBuilders.sum("sum_price").field("price");
(6)求平均
AvgBuilder ab= AggregationBuilders.avg("avg_price").field("price");
(7)求最大值
MaxBuilder mb= AggregationBuilders.max("max_price").field("price");
(8)求最小值
MinBuilder min= AggregationBuilders.min("min_price").field("price");
(9)按日期间隔分组
DateHistogramBuilder dhb= AggregationBuilders.dateHistogram("dh").field("date");
(10)获取聚合里面的结果
TopHitsBuilder thb= AggregationBuilders.topHits("top_result");
(11)嵌套的聚合
NestedBuilder nb= AggregationBuilders.nested("negsted_path").path("quests");
(12)反转嵌套
AggregationBuilders.reverseNested("res_negsted").path("kps ");
(13)拼接
.script 在后面拼接参数
searchSourceBuilder.aggregation(aggregationBuilder);
request.source(searchSourceBuilder);
ValueCountBuilder vcb= AggregationBuilders.count("count_uid").field("uid");
(2)去重统计某个字段的数量(有少量误差)
CardinalityBuilder cb= AggregationBuilders.cardinality("distinct_count_uid").field("uid");
(3)聚合过滤
FilterAggregationBuilder fab= AggregationBuilders.filter("uid_filter").filter(QueryBuilders.queryStringQuery("uid:001"));
(4)按某个字段分组,相当于sql中的group by
TermsBuilder tb= AggregationBuilders.terms("group_name").field("name");
(5)求和
SumBuilder sumBuilder= AggregationBuilders.sum("sum_price").field("price");
(6)求平均
AvgBuilder ab= AggregationBuilders.avg("avg_price").field("price");
(7)求最大值
MaxBuilder mb= AggregationBuilders.max("max_price").field("price");
(8)求最小值
MinBuilder min= AggregationBuilders.min("min_price").field("price");
(9)按日期间隔分组
DateHistogramBuilder dhb= AggregationBuilders.dateHistogram("dh").field("date");
(10)获取聚合里面的结果
TopHitsBuilder thb= AggregationBuilders.topHits("top_result");
(11)嵌套的聚合
NestedBuilder nb= AggregationBuilders.nested("negsted_path").path("quests");
(12)反转嵌套
AggregationBuilders.reverseNested("res_negsted").path("kps ");
(13)拼接
.script 在后面拼接参数
searchSourceBuilder.aggregation(aggregationBuilder);
request.source(searchSourceBuilder);
底层结构
倒排索引
通过分词策略,形成了词和文章的映射关系表,也称倒排表,这种词典 + 映射表即为倒排索引。有了倒排索引,就能实现 O(1) 时间复杂度的效率
在进行倒排索引的时候和该字段的type有关系
“倒排索引”叫这个名字,是因为它把“文档 → 词”的索引关系反过来了,变成了“词 → 文档”。这个结构对搜索效率非常关键,是搜索引擎的基础。
例子
原始文档
Doc1: "I love cats"
Doc2: "Cats are cute"
Doc3: "I love dogs"
Doc2: "Cats are cute"
Doc3: "I love dogs"
正常索引(正排索引)
Doc1 → ["I", "love", "cats"]
Doc2 → ["Cats", "are", "cute"]
Doc3 → ["I", "love", "dogs"]
Doc2 → ["Cats", "are", "cute"]
Doc3 → ["I", "love", "dogs"]
倒排索引
"I" → [Doc1, Doc3]
"love" → [Doc1, Doc3]
"cats" → [Doc1, Doc2]
"are" → [Doc2]
"cute" → [Doc2]
"dogs" → [Doc3]
"love" → [Doc1, Doc3]
"cats" → [Doc1, Doc2]
"are" → [Doc2]
"cute" → [Doc2]
"dogs" → [Doc3]
用法
请求字段
query
代表查询,搜索 类似于SQL的select关键字
叶子查询
match
全局查询,如果是多个词语,会进行分词查询。字母字母默认转换成全小写,进行匹配。
match_phrase
查询短语,会对短语进行分词,match_phrase的分词结果必须在text字段分词中都包含,而且顺序必须相同,而且必须都是连续的
term
单个词语查询,会精确匹配词语,会根据输入字段精确匹配。
terms
多个词语查询,精确匹配,满足多个词语中的任何一个都会返回
exists
类似于SQL的ISNULL,字段不为空的会返回出来。
range
类似于SQL的between and关键字,返回查询
ids
一次查询多个id,批量返回。
fuzzy
模糊查询
复合查询
must
返回的文档必须满足must子句的条件,并且参与计算分值.
must_ not
返回的文档必须不满足must_not定义的条件
should
返回的文档可能满足should子句的条件。在一个Bool查询中,如果没有must或者filter,有一个或者多个should子句,那么只要满足一个就可以返回。minimum_should_match参数定义了至少满足几个子句
filter
返回的文档必须满足filter子句的条件。不会参与计算分值,如果一个查询既有filter又有should,那么至少包含一个should子句,Filter过滤的结果会进行缓存,查询效率更高,建议使用filter。
aggs
代表聚合,类似于SQL的group by 关键字,对查询出来的数据进行聚合 求平均值最大值等
highlight
对搜索出来的结果中的指定字段进行高亮显示
sort
指定字段对查询结果进行排序显示,类比SQL的order by关键字
from和size
对查询结果分页,类似于SQL的limit关键字
post_filter
后置过滤器,在聚合查询结果之后,再对查询结果进行过滤
搜索结果
hits
total
匹配到的文档总数
hits数组
_index
_type
_id
_source
_score
衡量文档与查询的匹配程度。默认情况下,首先返回最相关的文档结果,就是说,返回的文档是按照 _score 降序排列的。如果没有指定任何查询,那么所有的文档具有相同的相关性,因此对所有的结果而言 1 是中性的 _score
max_score
与查询所匹配文档的 _score 的最大值。
took
执行整个搜索请求耗费了多少毫秒
shard
部分告诉我们在查询中参与分片的总数,以及这些分片成功了多少个失败了多少个。正常情况下我们不希望分片失败,但是分片失败是可能发生的。如果我们遭遇到一种灾难级别的故障,在这个故障中丢失了相同分片的原始数据和副本,那么对这个分片将没有可用副本来对搜索请求作出响应。假若这样,Elasticsearch 将报告这个分片是失败的,但是会继续返回剩余分片的结果。
timeout
查询是否超时。默认情况下,搜索请求不会超时。如果低响应时间比完成结果更重要,你可以指定 timeout 为 10 或者 10ms(10毫秒),或者 1s(1秒)。在请求超时之前,Elasticsearch 将会返回已经成功从每个分片获取的结果。
应当注意的是 timeout 不是停止执行查询,它仅仅是告知正在协调的节点返回到目前为止收集的结果并且关闭连接。在后台,其他的分片可能仍在执行查询即使是结果已经被发送了。
应当注意的是 timeout 不是停止执行查询,它仅仅是告知正在协调的节点返回到目前为止收集的结果并且关闭连接。在后台,其他的分片可能仍在执行查询即使是结果已经被发送了。
JVM
JIT(即时编译)
有时,循环太频繁,判断条件一直为false,JIT会激进优化从if(obj!=null)优化为if(false).这就属于:循环表达式外提(Loop Expression Hoisting)
-Djava.compiler=NONE 关闭JIT
JVM结构
GC算法
标记一清除算法(Mark-Sweep)老年代
缺点
- 标记清除算法的效率不算高
- 在进行 GC 的时候,需要停止整个应用程序,用户体验较差
- 这种方式清理出来的空闲内存是不连续的,产生内碎片,需要维护一个空闲列表
- 在进行 GC 的时候,需要停止整个应用程序,用户体验较差
- 这种方式清理出来的空闲内存是不连续的,产生内碎片,需要维护一个空闲列表
复制算法(copying)新生代
优点
- 没有标记和清除过程,实现简单,运行高效
- 复制过去以后保证空间的连续性,不会出现“碎片”问题。
- 复制过去以后保证空间的连续性,不会出现“碎片”问题。
缺点
- 此算法的缺点也是很明显的,就是需要两倍的内存空间。
- 对于 G1 这种分拆成为大量 region 的 GC,复制而不是移动,意味着 GC 需要维护 region 之间对象引用关系,不管是内存占用或者时间开销也不小
- 对于 G1 这种分拆成为大量 region 的 GC,复制而不是移动,意味着 GC 需要维护 region 之间对象引用关系,不管是内存占用或者时间开销也不小
标记-压缩算法(Mark-Compact)老年代
优点
- 消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM 只需要持有一个内存的起始地址即可。
- 消除了复制算法当中,内存减半的高额代价。
- 消除了复制算法当中,内存减半的高额代价。
缺点
- 从效率上来说,标记-整理算法要低于复制算法。
- 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
- 移动过程中,需要全程暂停用户应用程序。即:STW
- 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
- 移动过程中,需要全程暂停用户应用程序。即:STW
内存分配方式
空闲列表
指针碰撞
并发问题解决方案
TLAB
为每一个线程预先分配一块内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用CAS进行内存分配。
CAS
CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
调优思路
C2 编译器优化
逃逸分析
标量替换:用标量值代替聚合对象的属性值
栈上分配:对于未逃逸的对象分配对象在栈而不是堆
但栈上分配还不成熟,Hotspot虚拟机没有实现栈上分配.现在主要还是通过逃逸分析来实现锁消除和标量替换.来提升性能,降低内存占比
同步消除:清除同步操作,通常指 synchronized
新生代,老年代大小比例
进入老年代年龄
调整Eden 和幸存者区比例
禁用偏向锁
垃圾回收器
CMS
对内存的敏感度比较高
有时候使用CMS容易OOM,可以试试删掉CMS参数,使用默认1.8默认的
Parallel Scavenge+Parallel Old
1.8默认
设计模式
六大设计原则
1.开闭原则:对拓展开放,对修改关闭
2.单一职责原则:只做一件事
3.里氏替换原则:父类与子类的行为要保持与预期相符
4.迪米特法则:与外界耦合越少越好
5.接口隔离原则:将大接口拆分成多个以某个事务为边界的小接口
6.依赖倒置原则:面向接口编程
创建型模式
工厂模式
抽象工厂
生成器
原型
单例
结构型模式
装饰器模式
应用
各种IO流
示例
构建
使用(可使用任意数量装饰器)
现在让这个PlaceOrderCommand 能够打印日志,进行性能统计
Command cmd = new LoggerDecorator(
new PerformanceDecorator(
new PlaceOrderCommand()));
cmd.execute();
Command cmd = new LoggerDecorator(
new PerformanceDecorator(
new PlaceOrderCommand()));
cmd.execute();
如果PaymentCommand 只需要打印日志,装饰一次就可以了:
Command cmd = new LoggerDecorator(
new PaymentCommand());
cmd.execute();
Command cmd = new LoggerDecorator(
new PaymentCommand());
cmd.execute();
不足
存在耦合,一个处理日志/性能/事务的类为什么要实现业务接口(Command)呢?
如果别的业务模块,没有实现Command接口,但是也想利用日志/性能/事务等功能,就没办法
享元模式
含义
共享元素
应用
包装类、缓存、池化技术
适配器
桥接
组合
外观
代理
行为型模式
策略模式
模板方法
示例
说明
在父类(BaseCommand)中已经把那些“乱七八糟“的非功能代码都写好了, 只是留了一个口子(抽象方法doBusiness())让子类去实现。
优势
子类变的清爽, 只需要关注业务逻辑就可以了。
调用也很简单,例如:
BaseCommand cmd = ... 获得PlaceOrderCommand的实例...
cmd.execute();
调用也很简单,例如:
BaseCommand cmd = ... 获得PlaceOrderCommand的实例...
cmd.execute();
不足
父类会定义一切: 要执行哪些非功能代码, 以什么顺序执行等等
子类只能无条件接受,完全没有反抗余地。
如果有个子类, 根本不需要事务, 但是它也没有办法把事务代码去掉。
子类只能无条件接受,完全没有反抗余地。
如果有个子类, 根本不需要事务, 但是它也没有办法把事务代码去掉。
责任链
命令
解释器
迭代器
中介
备忘录
观察者
状态
访问者
计算机网络
持久连接
持久连接的特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。
三次握手
稳定通信,三次是最低保证
同时,为了保证数据有序,服务器与客户端要交换序列
同时,为了保证数据有序,服务器与客户端要交换序列
粘包
没规定好数据与数据之间字符串分隔符,如前4个字节是一个包,结果获取了前7个字节数据
自定义协议规定好分隔符或者固定长度,就可以避免
Http(类似公共协议)不会有粘包问题
也是为了效率,本来可以一个一个发,但效率低了;所以就批量发
案例
网络上有多条数据发送给服务端,数据之间使用 \n 进行分隔
但由于某种原因这些数据在接收时,被进行了重新组合,例如原始数据有3条为
Hello,world\n
I'm zhangsan\n
How are you?\n
变成了下面的两个 byteBuffer (黏包,半包)
Hello,world\nI'm zhangsan\nHo
w are you?\n
但由于某种原因这些数据在接收时,被进行了重新组合,例如原始数据有3条为
Hello,world\n
I'm zhangsan\n
How are you?\n
变成了下面的两个 byteBuffer (黏包,半包)
Hello,world\nI'm zhangsan\nHo
w are you?\n
半包
可能是因为缓冲区大小限制
协议
TCP/IP协议
应用层
对应协议
HTTP(超文本传输协议)
无状态协议
多个HTTP请求之间没有任何联系
FTP(文件传送协议)
Telnet(远程登录协议)
DNS(域名解析协议)
明文传输
解决什么问题
解决人类记忆域名,机器使用IP地址的问题
举例
问:“www.google.com 的地址是多少?” 答:“142.251.42.206”
SMTP(邮件传送协议)
POP3(邮局协议)
KCP
基于UDP+部分可靠性机制(KCP算法)
重传机制
TCP需要等待2倍RTO才会重传,KCP只需1.5倍RTO
乱序重排
TCP发现某个包漏了3次ACK包之后才会触发快速重传,KCP可以配置漏了2次ACK包之后才会触发快速重传
滑动窗口
拥塞控制
TCP发生丢包会认为网络环境变差,自动减少发包量,KCP也有类似机制,但还可以关掉拥塞控制
当网络环境变差的时候,TCP纷纷触发拥塞控制,相当于变相将网络资源让给了关掉拥塞控制的KCP
既保留UDP的快,又有TCP的可靠
比TCP快百分之三四十的传输速率
风险点
UDP在国内运营商网络环境下优先级比较低,遇到拥塞大概率会优先丢UDP包
解决方案
专为自家软件体系搭建虚拟专线网络,异地组网。但是技术和成本高
加更多中间层
前向纠错中间层
在一些数据包里冗余其他数据包的信息
这样就算丢了一些数据包,也能通过其他包去还原这部分信息
网络环境越差劲,我们就可以考虑冗余越多数据
角色:电商平台 OR 商家
应用层就像在特定城市特定大厦特定房间内的某一个用户,应用层之间的通讯就像两个不同用户之间发送的信,这个信是点对点的,从一个用户(某一主机内特定应用程序)到另外一个特定用户(另一主机内特定应用程序)。一个主机(大厦)内可能有很多应用程序(客户),我们如何区分它们呢,实际生活中我们用房间号,在电脑内部区分不同应用程序我们用端口号
传输层
对应协议
TCP:端口
UDP
角色:快递公司
用户写好了信,需要给信套上信封,并且写好发件人所在大厦,和收件人所在大厦,实际生活中的大厦完全可以类比为我们的计算机和服务器
网络层
对应协议
IP:IP地址
ICMP
ARP:地址解析协议
角色:
邮件准备好了,他首先会被送到本城市的快递公司,并且被打包,包裹上会写着源是重庆快递公司,目的是沈阳快递公司,但是重庆快递公司发现它不能直接发货到沈阳,需要通过北京快递公司进行中转。所以虽然目的是沈阳,但是他首先把这个包裹发给了北京。某个城市的快递公司就像IP协议,要抵达目的IP,需要查询路由表,如果发现目的地址不是直连就需要找下一跳。通过了解快递公司的工作,我们了解到IP协议是逐跳工作的。每一跳(路由器)根据目的IP地址查询下一跳,并且最终转发到目的地。
网络接口层
数据链路层
Link
角色:
重庆快递公司已经知道他需要把包裹发给北京快递公司了,现在他就把包裹送到重庆火车站,搭上去往北京的火车,然后在北京火车站卸货。然后送到北京快递公司,北京快递公司再判断下一跳为沈阳快递公司,并且选择适当的传输方式,例如:汽车,最后通过这种传输方式送到目的地沈阳快递公司。链路层协议就像包裹的运输方式,我们可以选择以太网(火车),也可以选择令牌环(汽车)。并且链路层协议是逐介质的,从一个网卡(重庆火车站)到另外一个网卡(北京火车站)。 所以你会发现一个数据包从源到目的,IP地址总是不变的(源是重庆快递公司,目的是沈阳快递公司),但是链路层协议却在不断变化,第一跳源是重庆火车站,目的是北京火车站,第二跳源是北京汽车站,目的是沈阳汽车站
物理层
只管发送数据(比特流)
Tips
frame:MAC地址(通过ARP协议得到)
TLS
HTTPS 和其他网络协议使用TLS 来进行加密 ,它是SSL 的现代化版本。
TLS 1.3 是TLS 协议的最新版本
SNI
属于TLS的一个扩展或者说特性
服务器名称指示
明文传输
在TLS握手中,明文告知服务器目标域名
解决什么问题
解决一个IP地址的服务器无法为多个HTTPS网站提供正确证书的问题
举例
说:“你好,我要开始加密握手了,我这次想访问的网站是 www.google.com”
VPC,Virtual Private Cloud
专有网络
用于隔离同一网段下机器.如隔离开发与生产机器
IP
分类(很少使用了,转而使用子网掩码)
A类地址
开头为0
7位网络号
24位主机号
可以有1600W+
B类地址
开头为10
14位网络号
16位主机号
C类地址
开头为110
21位网络号
8位主机号
可以有200+
D类地址
开头为1110
28位组播地址
E类地址
开头为1111
28位留待后用
网络号
停车场号
A类地址就像大型停车场
比较少,但是能停的车位特别多
B和C就像小型停车场
到处都是,但是能停的车位比较少
主机号
停车位号
子网掩码位
作用:表示IP地址的内部结构,区分网络号与主机号(因为具体结构各占多少不固定,用户可自行决定)
含义:分割网络号与主机号。可理解为IP前多少位不可变
比如掩码为16,那么11000000.10101000.00000000.00000000;后面16为可变
比如掩码为18,那么11000000.10101000.00000000.00000000;后面14为可变
IP为192.168.0.0,掩码位16.那么可用65534个IP(192.168.0.0为网络初始,192.168.255.255为广播)
可用范围:192.168.0.1~192.168.255.254
IP为192.168.31.170/20
掩码位20
前20个比特是网络号
后面12个比特位主机号
2的12次方,允许4096台机器
但还需要考虑到 网络初始 与 广播)
CIDR
Classless Inter-Domain Routing
无类别 域间 路由
局域网
A类私有IP
10.0.0.0--10.255.255.255
B类私有IP
172.16.0.0--172.31.255.255
C类私有IP
192.168.0.0--192.168.255.255
网络模型
用户空间和内核空间
背景
我们想要用户的应用来访问,计算机就必须要通过对外暴露的一些接口,才能访问到,从而间接的实现对内核的操控,但是内核本身上来说也是一个应用,所以他本身也需要一些内存,cpu等设备资源,用户应用本身也在消耗这些资源,如果不加任何限制,用户去操作随意的去操作我们的资源,就有可能导致一些冲突,甚至有可能导致我们的系统出现无法运行的问题,因此我们需要把用户和内核隔离开
进程的寻址空间
内核空间
用户空间
我们的应用程序也好,还是内核空间也好,都是没有办法直接去物理内存的,而是通过分配一些虚拟内存映射到物理内存中,我们的内核和应用程序去访问虚拟内存的时候,就需要一个虚拟地址,这个地址是一个无符号的整数
比如一个32位的操作系统,他的带宽就是32,他的虚拟地址就是2的32次方,也就是说他寻址的范围就是0~2的32次方
这片寻址空间对应的就是2的32个字节,就是4GB,其中会有3个GB分给用户空间,会有1GB给内核系统
在linux中,权限分成两个等级,0和3,用户空间只能执行受限的命令(Ring3),而且不能直接调用系统资源,必须通过内核提供的接口来访问内核空间可以执行特权命令(Ring0),调用一切系统资源
一般情况下用户的操作是运行在用户空间,而内核运行的数据是在内核空间的,而有的情况下,一个应用程序需要去调用一些特权资源,去调用一些内核空间的操作,所以此时他俩需要在用户态和内核态之间进行切换。
Linux系统为了提高IO效率,会在用户空间和内核空间都加入缓冲区
写数据时,要把用户缓冲数据拷贝到内核缓冲区,然后写入设备
读数据时,要从设备读取数据到内核缓冲区,然后拷贝到用户缓冲区
针对这个操作:用户在写读数据时,会去向内核态申请,想要读取内核的数据,而内核数据要去等待驱动程序从硬件上读取数据,当从磁盘上加载到数据之后,内核会将数据写入到内核的缓冲区中,然后再将数据拷贝到用户态的buffer中,然后再返回给应用程序
速度慢,就是这个原因,为了加速,我们希望read也好,还是wait for data也最好都不要等待,或者时间尽量的短
慢在了 等待 和 拷贝
I/O
I/O模型
1.0
服务端代码示例
阻塞、单进程
2.0
多进程、但创建线程耗费大量的资源(系统开销大)
当accept连接以后,对于这个新的socket , 不在主进程里处理, 而是新创建子进程来接管。 这样主进程就不会阻塞在receive 上, 可以继续接受新的连接了
还有改进的多线程
多路复用
定义:通过 I/O 事件分发,当内核数据准备好时,再以事件通知应用程序进行操作
3.0 : Select模型
图例
单线程
接受了客户端连接以后,不着急读取,将每个socket fd的编号(最多1024个)都发给OS,然后阻塞;
OS会在后台检查这些编号的socket, 如果发现这些socket可以读写,OS会把对应的socket 做个标记;唤醒去处理这些socket的数据,你处理完了,再把你的那些socket fd告诉OS,再次进入阻塞,如此循环往复。
4.0 : epoll模型
图例
单纯模型
阻塞IO(Blocking IO)
两个阶段都必须阻塞等待
阶段一(等待数据)
用户进程尝试读取数据(比如网卡数据)
此时数据尚未到达,内核需要等待数据
此时用户进程也处于阻塞状态
阶段二(拷贝数据)
数据到达并拷贝到内核缓冲区,代表已就绪
将内核数据拷贝到用户缓冲区
拷贝过程中,用户进程依然阻塞等待
拷贝完成,用户进程解除阻塞,处理数据
用户去读取数据时,会去先发起recvform一个命令,去尝试从内核上加载数据,如果内核没有数据,那么用户就会等待,此时内核会去从硬件上读取数据,内核读取数据之后,会把数据拷贝到用户态,并且返回ok
非阻塞IO(Nonblocking IO)
第一个阶段是非阻塞,第二个阶段是阻塞
阶段一(等待数据)
用户进程尝试读取数据(比如网卡数据)
此时数据尚未到达,内核需要等待数据
返回异常给用户进程
用户进程拿到error后,再次尝试读取
循环往复,直到数据就绪
阶段二(拷贝数据)
将内核数据拷贝到用户缓冲区
拷贝过程中,用户进程依然阻塞等待
拷贝完成,用户进程解除阻塞,处理数据
用户进程在第一个阶段是非阻塞,第二个阶段是阻塞状态。虽然是非阻塞,但性能并没有得到提高。相反 忙等(忙轮询)机制 会导致CPU空转,CPU使用率暴增
与IO多路复用结合,效果更好
IO多路复用(IO Multiplexing)
无论是阻塞IO还是非阻塞IO,用户应用在一阶段都需要调用recvfrom(数据拷贝函数)来获取数据,差别在于无数据时的处理方案
如果调用recvfrom(数据拷贝函数)时,恰好没有数据
阻塞IO会使CPU阻塞
非阻塞IO使CPU空转
都不能充分发挥CPU的作用
如果调用recvfrom(数据拷贝函数)时,恰好有数据,则用户进程可以直接进入第二阶段,读取并处理数据
单线程情况下,只能依次处理IO事件,如果正在处理的IO事件恰好未就绪(数据不可读或不可写),线程就会被阻塞,所有IO事件都必须等待,性能自然会很差
比如服务员给顾客点餐
顾客思考要吃什么(等待数据就绪)
顾客想好了,开始点餐(读取数据)
要提高效率有几种办法
方案一:增加更多服务员(多线程)
方案二:不排队,谁想好了吃什么(数据就绪了),服务员就给谁点餐(用户应用就去读取数据)
用户进程如何知道内核中数据是否就绪呢?
前提
文件描述符(File Descriptor):简称FD,是一个无符号整数,从0 开始的不断递增,用来关联Linux中的一个文件。在Linux中,一切皆文件,例如常规文件、视频、硬件设备等,当然也包括网络套接字(Socket)。
通过FD,我们的网络模型可以利用一个线程监听多个FD,并在某个FD可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。
第一个阶段是,第二个阶段是
阶段一
用户进程调用select,指定要监听的FD集合
监听FD对应的多个socket
任意一个或多个socket数据就绪则返回readable
此过程中用户进程阻塞
阶段二
用户进程找到就绪的socket
依次调用recvfrom(数据拷贝函数)读取数据
内核将数据拷贝到用户空间
用户进程处理数据
当用户去读取数据的时候,不再去直接调用recvfrom(数据拷贝函数)了,而是调用select的函数,select函数会将需要监听的数据交给内核,由内核去检查这些数据是否就绪了,如果说这个数据就绪了,就会通知应用程序数据就绪,然后来读取数据,再从内核中把数据拷贝给用户态,完成数据处理,如果N多个FD一个都没处理完,此时就进行等待。
用IO复用模式,可以确保去读数据的时候,数据是一定存在的,他的效率比原来的阻塞IO和非阻塞IO性能都要高
用IO复用模式,可以确保去读数据的时候,数据是一定存在的,他的效率比原来的阻塞IO和非阻塞IO性能都要高
利用单个线程来同时监听多个FD,并在某个FD可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源
监听FD的方式、通知的方式又有多种实现
select
Linux中最早的I/O多路复用实现方案
源码
nfds
遍历时,FD的上限
Linux下默认最大是1024个fd,从0-1023
问题
需要将整个fd set从用户空间拷贝到内核空间,select结束还要再次拷贝回用户空间
select无法得知具体是哪个fd就绪,需要遍历整个fdsetfd_set
监听的fd数量不能超过1024
流程
poll
poll模式对select模式做了简单改进,但性能提升不明显
流程
创建pollfd数组,向其中添加关注的fd信息,数组大小自定义
理论上讲,监听fd数量没上限
调用poll函数,将pollfd数组拷贝到内核空间,转链表存储,无上限
内核遍历fd,判断是否就绪
数据就绪或超时后,拷贝pollfd数组到用户空间,返回就绪fd数量n
用户进程判断n是否大于0,大于0则遍历pollfd数组,找到就绪的fd
源码
在调用poll函数时,会有多个pollfd结构体。这时只需要指定fd和events,不需要指定revents
revents
是内核在监听过程中,如果发现事件有就绪的情况,就会把就绪的事件类型放入revents;如果等到超时时间过了,还没发生任何事件,那就给0
与select对比
select模式中的fd_set大小固定为1024,而pollfd在内核中采用链表,理论上无上限
监听FD越多,每次遍历消耗时间也越久,性能反而会下降
epoll
epoll模式是对select和poll的巨大改进
提供了三个函数
拷贝前,会将就绪链表中断开,移除指针,然后再去拷贝
总结
select模式存在的三个问题
能监听的FD最大不超过1024
每次select都需要把所有要监听的FD都拷贝到内核空间(处理完以后还需要将所有FD从内核空间中拷贝回来)
两次拷贝,性能很差
每次都要遍历所有FD来判断就绪状态
poll模式的问题
poll利用链表解决了select中监听FD上限的问题,但依然要遍历所有FD,如果监听较多,性能会下降
epoll模式中如何解决这些问题的
基于epoll实例中的红黑树保存要监听的FD,理论上无上限,而且增删改查效率都非常高,性能不会随监听的FD数量增多而下降
每个FD只需要执行一次epoll_ctl添加到红黑树,以后每次epol_wait无需传递任何参数,无需重复拷贝FD到内核空间
内核会将就绪的FD直接拷贝到用户空间指定位置,用户进程无需遍历所以FD就能知道就绪的FD是谁。
利用ep_poll_callback机制来监听FD状态,无需遍历所有FD,因此性能不会随监听的FD数量增多而下降
其中select和pool只会通知用户进程有FD就绪,但不确定具体是哪个FD,需要用户进程逐个遍历FD来确认,所以性能也并不是那么好
epoll则会在通知用户进程FD就绪的同时,把已就绪的FD写到用户空间
事件通知机制
当FD有数据可读时,我们调用epoll_wait(或者select、poll)可以得到通知
事件通知的模式
LevelTriggered
简称LT,也叫做水平触发。只要某个FD中有数据可读,每次调用epoll_wait都会得到通知。直至数据处理完成,是Epoll的默认模式
采用LT模式并且还有数据,就会将FD再次添加到链表中
优点
实现简单
缺点
重复通知对性能有影响
惊群现象
EdgeTriggered
简称ET,也叫做边沿触发。只有在某个FD有状态变化时,调用epoll_wait才会被通知。只会被通知一次,不管数据是否处理完成
采用ET模式,移除指针就不管了
读取方式
执行一次epoll_ctl,手动添加
循环在步骤4,一次性读完。但是不能采用阻塞IO的模式
阻塞IO在读完数据以后,不是返回错误,而是等到下次有数据为止。导致进程被阻塞
举个栗子:
假设一个客户端socket对应的FD已经注册到了epoll实例中
客户端socket发送了2kb的数据
服务端调用epoll_wait,得到通知说FD就绪
服务端从FD读取了1kb数据
回到步骤3(再次调用epoll_wait,形成循环)
结论
ET模式避免了LT模式可能出现的惊群现象
ET模式最好结合非阻塞IO读取FD数据,相比LT会复杂一些
Web服务流程
服务器启动以后,服务端会去调用epoll_create,创建一个epoll实例,epoll实例中包含两个数据
1、红黑树(为空):rb_root 用来去记录需要被监听的FD
2、链表(为空):list_head,用来存放已经就绪的FD
创建好了之后,会去调用epoll_ctl函数,此函数会会将需要监听的数据添加到rb_root中去,并且对当前这些存在于红黑树的节点设置回调函数,当这些被监听的数据一旦准备完成,就会被调用,而调用的结果就是将红黑树的fd添加到list_head中去(但是此时并没有完成)
当第二步完成后,就会调用epoll_wait函数,这个函数会去校验是否有数据准备完毕(因为数据一旦准备就绪,就会被回调函数添加到list_head中),在等待了一段时间后(可以进行配置),如果等够了超时时间,则返回没有数据,如果有,则进一步判断当前是什么事件,如果是建立连接事件,则调用accept() 接受客户端socket,拿到建立连接的socket,然后建立起来连接,如果是其他事件,则把数据进行写出
信号驱动IO(Signal Driven IO)
与内核建立SIGIO的信号关联并设置回调,当内核有FD就绪时,会发出SIGIO信号通知用户,期间用户应用可以执行其它业务,无需阻塞等待
阶段一
用户进程调用sigaction,注册信号处理函数
内核返回成功,开始监听FD
用户进程不阻塞等待,可以执行其它业务
当内核数据就绪后,回调用户进程的SIGIO处理函数
阶段二
收到SIGIO回调信号
调用recvfrom(数据拷贝函数),读取
内核将数据拷贝到用户空间
用户进程处理数据
缺点
当有大量IO操作时,信号较多,SIGIO处理函数不能及时处理可能导致信号队列溢出
而且内核空间与用户空间的频繁信号交互性能也较低
异步IO(Asynchronous IO)
整个过程都是非阻塞的,用户进程调用完异步API后就可以去做其它事情,内核等待数据就绪并拷贝到用户空间后才会递交信号,通知用户进程。
不仅仅是用户态在试图读取数据后,不阻塞,而且当内核的数据准备完成后,也不会阻塞
由内核将所有数据处理完成后,由内核将数据写入到用户态中,然后才算完成,所以性能极高,不会有任何阻塞,全部都由内核完成
可以看到,异步IO模型中,用户进程在两个阶段都是非阻塞状态。
aio_read是想读哪个FD,读到哪里去
就像是领导安排任务,之后就不管了
问题
高并发场景下,内核积累的任务会越来越多,增加了内存的消耗,系统有崩溃的可能性
注意事项
需要做好并发访问的限流
对比
IO操作是同步还是异步,关键看数据在内核空间与用户空间的拷贝过程(数据读写的IO操作),也就是阶段二是同步还是异步
理解NIO
不知道大家有没有用过2010年左右(或许更早)2G时代的手机,可以运行那种基于J2ME的QQ,能聊天,看个空间,偷个菜(文字版)什么的。这种手机一般都有个缺点就是不能后台运行,一旦去做其他事情(玩游戏,看小说等),QQ就掉线了,就不能收到QQ消息了。如果想要实时接收到女神消息,就要一直保持打开着QQ,不能去做其他事情。这就类似于BIO,阻塞的。
后来QQ出了一个手机业务,叫超级QQ(每月10块呢),可以伪实时在线,同时更快的升级。之所以叫他伪实时在线,是因为它的实现方式是:当QQ收到消息时,腾讯会以短信的形式发到手机上,告诉你某某给你发消息了,请及时处理之类的(也可以直接回复短信,QQ上也会自动转发过去,不太相关暂时忽略)。此时再去登录QQ,就能立刻收到消息了。虽然手机同一时刻依然只能做一件事情,但是在没有QQ消息的时候也无需一直等待了,从而从容不迫去做别的事情。也就是非阻塞的了。
这个超级QQ的业务就像是NIO:人就是Selector,监听事件。短信就像是一个事件。QQ就像是Channel,建立沟通通道。人看到短信,根据短信内容,从而决定要不要打开QQ,处理消息
后来QQ出了一个手机业务,叫超级QQ(每月10块呢),可以伪实时在线,同时更快的升级。之所以叫他伪实时在线,是因为它的实现方式是:当QQ收到消息时,腾讯会以短信的形式发到手机上,告诉你某某给你发消息了,请及时处理之类的(也可以直接回复短信,QQ上也会自动转发过去,不太相关暂时忽略)。此时再去登录QQ,就能立刻收到消息了。虽然手机同一时刻依然只能做一件事情,但是在没有QQ消息的时候也无需一直等待了,从而从容不迫去做别的事情。也就是非阻塞的了。
这个超级QQ的业务就像是NIO:人就是Selector,监听事件。短信就像是一个事件。QQ就像是Channel,建立沟通通道。人看到短信,根据短信内容,从而决定要不要打开QQ,处理消息
线程模型
传统阻塞I/O服务模型
特点
采用阻塞 IO 模式获取输入的数据
每个连接都需要独立的线程完成数据的输入,业务处理,数据返回
缺点
当并发数很大,就会创建大量的线程,占用很大系统资源
连接创建后,如果当前线程暂时没有数据可读,该线程会阻塞在 Handler对象中的read 操作,导致上面的处理线程资源浪费
Reactor 模式
单 Reactor单线程
优点
模型简单,没有多线程、进程通信、竞争的问题,全部都在一个线程中完成
缺点
性能问题,只有一个线程,无法完全发挥多核 CPU的性能。Handler在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈
可靠性问题,线程意外终止,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障
使用场景
客户端的数量有限,业务处理非常快速,比如 Redis 在业务处理的时间复杂度 O(1) 的情况
生活案例
前台接待员、引导员和服务员是同一个人,全程为顾客服务
单 Reactor多线程
优点
可以充分的利用多核 cpu 的处理能力
缺点
多线程数据共享和访问比较复杂。Reactor 承担所有的事件的监听和响应,它是单线程运行,在高并发场景容易出现性能瓶颈。也就是说Reactor主线程承担了过多的事
生活案例
1个前台接待员身兼引导员,多个服务员,接待员只负责接待
主从Reactor多线程
优点
父线程与子线程的数据交互简单职责明确,父线程只需要接收新连接,子线程完成后续的业务处理
父线程与子线程的数据交互简单,Reactor 主线程只需要把新连接传给子线程,子线程无需返回数据
缺点
编程复杂度较高
应用实例
这种模型在许多项目中广泛使用,包括 Nginx 主从 Reactor 多进程模型,Memcached 主从多线程,Netty 主从多线程模型的支持
Netty 主要基于主从 Reactor 多线程模型做了一定的改进
生活案例
1个前台接待员、多个引导员,多个服务生
硬件
电脑
电脑视角
首先我要知道我的 IP 以及对方的 IP
通过子网掩码判断我们是否在同一个子网
在同一个子网就通过 arp 获取对方 mac 地址直接扔出去
不在同一个子网就通过 arp 获取默认网关的 mac 地址直接扔出去
集线器
物理层
仅仅是无脑将电信号转发到所有出口(广播),不做任何处理
视角
交换机
数据链路层
基于以太网设计
有MAC地址与端口对应表
可实现只发给目标 MAC 地址指向的那台电脑
最开始的状态和集线器差不多,表里没有关系
A发送数据包首先会到交换机,此时交换机会顺便记录下A的MAC地址
交换机将数据包转发到所有端口
之后只有目标机器会给相应,这是交换机就会记录下相应的机器MAC地址与端口
但是这样一直持续下去,MAC地址表会将变得无比巨大
是通过以太网内各节点之间不断通过交换机通信,不断完善起来的
这就可以引申出路由器
视角
我收到的数据包必须有目标 MAC 地址
通过 MAC 地址表查映射关系
查到了就按照映射关系从我的指定端口发出去
查不到就所有端口都发出去
路由器
网络层
网络层(IP协议)本身没有传输包的功能,包的实际传输是委托给数据链路层(以太网中的交换机)来实现的。
基于IP设计
作为一台独立的拥有 MAC 地址的设备,并且可以帮忙把数据包做一次转发
每个端口都有一个MAC地址
转发方案
事先规划好网络,再根据规划的网络烧录,定好的MAC地址
但这不现实
引申出新的规则:IP地址
可以根据目标IP地址,修改目标MAC地址
问题
A 给 C 发数据包,怎么知道是否要通过路由器转发呢?
子网
什么叫处于一个子网
子网掩码
将源 IP 与目的 IP 分别同这个子网掩码进行与运算,相等则是在一个子网,不相等就是在不同子网
如果源 IP 与目的 IP 处于一个子网,直接将包通过交换机发出去。
如果源 IP 与目的 IP 不处于一个子网,就交给路由器去处理。
A 如何知道,哪个设备是路由器?
在 A 上要设置默认网关(IP地址)
上一步 A 通过是否与 C 在同一个子网内,判断出自己应该把包发给路由器,那路由器的 IP 是多少呢?
其实说发给路由器不准确,应该说 A 会把包发给默认网关。对 A 来说,A 只能直接把包发给同处于一个子网下的某个 IP 上,所以发给路由器还是发给某个电脑,对 A 来说也不关心,只要这个设备有个 IP 地址就行。所以默认网关,就是 A 在自己电脑里配置的一个 IP 地址,以便在发给不同子网的机器时,发给这个 IP 地址。
路由器怎么知道,收到的这个数据包,该从自己的哪个端口出去,才能直接(或间接)地最终到达目的地 C 呢
路由表
路由器收到的数据包有目的 IP 也就是 C 的 IP 地址,需要转化成从自己的哪个端口出去,很容易想到,应该有个表,就像 MAC 地址表一样。
各种路由算法 + 人工配置逐步完善起来的
不同于 MAC 地址表的是,路由表并不是一对一这种明确关系
下一跳机制
刚才说的都是 IP 层,但发送数据包的数据链路层需要知道 MAC 地址,可是我只知道 IP 地址该怎么办呢?
arp
假如你(A)此时不知道你同伴 B 的 MAC 地址(现实中就是不知道的,刚刚我们只是假设已知),你只知道它的 IP 地址,你该怎么把数据包准确传给 B 呢?
在网络层,我需要把 IP 地址对应的 MAC 地址找到,也就是通过某种方式,找到 192.168.0.2 对应的 MAC 地址 BBBB。
这种方式就是 arp 协议,同时电脑 A 和 B 里面也会有一张 arp 缓存表,表中记录着 IP 与 MAC 地址的对应关系。
一开始的时候这个表是空的,电脑 A 为了知道电脑 B(192.168.0.2)的 MAC 地址,将会广播一条 arp 请求,B 收到请求后,带上自己的 MAC 地址给 A 一个响应。此时 A 便更新了自己的 arp 表。这样通过大家不断广播 arp 请求,最终所有电脑里面都将 arp 缓存表更新完整。
视角
我收到的数据包必须有目标 IP 地址
通过路由表查映射关系
查到了就按照映射关系从我的指定端口发出去(不在任何一个子网范围,走其路由器的默认网关也是查到了)
查不到则返回一个路由不可达的数据包
映射表
交换机中有 MAC 地址表用于映射 MAC 地址和它的端口
MAC 地址表是通过以太网内各节点之间不断通过交换机通信,不断完善起来的。
路由器中有路由表用于映射 IP 地址(段)和它的端口
路由表是各种路由算法 + 人工配置逐步完善起来的。
电脑和路由器中都有 arp 缓存表用于缓存 IP 和 MAC 地址的映射关系
arp 缓存表是不断通过 arp 协议的请求逐步完善起来的。
请求与响应头
Connection: keep-alive
1.客户端与服务端的连接保活技术(连接复用),可以减少 TCP 连接的重复建立和断开所造成的额外开销,减轻服务器端的负载,减少响应时间
2.Connection是个逐跳首部,不会被最终传递给目标,因此,如果中间有代理服务器,最终客户端会与代理服务器建立长连接而不是目标服务器
2.Connection是个逐跳首部,不会被最终传递给目标,因此,如果中间有代理服务器,最终客户端会与代理服务器建立长连接而不是目标服务器
管理客户端(如浏览器)与服务器之间的 TCP 连接是否在完成一次请求/响应后保持打开状态,以便后续请求复用该连接。这直接关系到网络性能和效率。
主要目的
优化性能,减少延迟
当客户端发送请求时带上 Connection: keep-alive(或在 HTTP/1.1 中默认隐含),它是在告诉服务器:“请在处理完这个请求后,不要关闭我们的 TCP 连接,我可能很快还要用它发送其他请求。”
如果服务器支持并同意保持连接,它也会在响应中包含 Connection: keep-alive(HTTP/1.1 响应中通常省略,因为这是默认的)。
在 HTTP/1.1 中,keep-alive 是 默认开启 的。客户端和服务器无需显式地在每个请求/响应中添加 Connection: keep-alive 头部来表示希望保持连接。
Content-Disposition
HTTP 响应头(有时也用在 HTTP 请求头 或 电子邮件 中)
控制内容是显示在浏览器内还是作为附件下载
告诉浏览器“收到这个内容后,你是应该直接打开它(inline),还是把它当作一个文件下载下来(attachment),并且我建议你下载时叫这个名字(filename)”
inline (默认行为)
指示客户端尝试在浏览器窗口内显示内容(如果它能处理该内容类型)
一个 image/jpeg 图片通常直接显示在页面中。
一个 text/html 页面直接渲染。
一个 application/pdf 文件,如果浏览器有 PDF 插件,可能会在浏览器标签页内打开。
attachment (最重要的用途)
指示客户端应该将内容视为附件,强制弹出“另存为”对话框让用户下载文件,而不是尝试在浏览器内显示它。这是实现文件下载功能的关键。
通常与 filename 参数一起使用,为下载的文件提供一个建议的名称:Content-Disposition: attachment; filename="report.pdf"
Cloudflare支持的端口
HTTP
80
8080
8880
2052
2082
2086
2095
8080
8880
2052
2082
2086
2095
HTTPS
443
2053
2083
2087
2096
8443
2053
2083
2087
2096
8443
1M带宽是指传输速率1Mbps(1兆比特每秒,bit per second)
1M带宽=128KB/s=128*8Kb/s=1024Kb/s=1Mb/s
WiFi
2.4GHz
穿墙效果好
5GHz
速率高
并非5Ghz穿墙能力更差。5GHz信号的波长要比2.4GHz信号的要短,而波长越短的电磁波穿透力就越强。但因为频率越高消耗在穿透上的能量越大,导致信号浪费,设备接受到的反而是反射衍射过来的信号。2.4Ghz下,衍射和反射比5Ghz要多,因此设备接受到的信号反而强。
网络问题
未连接到互联网
断网了
无法访问此网站
现象
www.google.com的响应时间过长
请求到不了服务器
XXX拒绝了我们的连接请求
请求能够到达服务器,但服务器拒绝访问
可能是服务器没启动
连接被重置
收到RST报文(ReSet,重置)
一般来说,当发现一个到达的报文段对于相关连接而言是不正确的时, TCP就会发送一个重置报文
场景
1.端口不存在
这种场景第一次握手时,服务器就不会返回正常的握手包
2.终止一条连接
四次挥手,通过发送FIN包关闭连接
会等排队等待发送的数据全部发出去后,才会被发送,以保证数据不会丢失
RST同样可以用来终止一条连接
不用等待排队数据发送,立即发给对方,等待发送的数据将被全部丢弃
3.时间等待错误
TCP有一系列的计时器用来完成超时重传以及连接状态的维护等工作,但如果超过定时器的时间,服务器已经清除了一条连接的信息,在这之后,客户端新的数据才姗姗来迟,那这时候,也会收到一个RST报文
三次握手完成之后,便立即发起了SSL握手,就不会超时
4.GFW
从中作梗,双向丢包
Wireshark
通过域名查询IP地址
dns.qry.name contains QQ.com
过滤
IP过滤
ip.src_host==192.168.100.20
过滤发起地址ip
ip.src == 192.168.1.23
过滤目标地址ip
ip.dst == 12.8.0.1
过滤源或目的地址
ip == 12.0.0.1
协议过滤
如果抓的包有很多种协议类型,可以输入 tcp 回车只看tcp 协议的包
IP过滤加协议过滤
ip.src_host==192.168.100.20 and http
端口过滤
tcp.port == 4980
tcp.port == 4542 or tcp.port == 4528
tcp.port in {80 443 8080}
底层握手(链接成功)
tcp.flags.ack == 0
底层握手(数据发送成功)
tcp.flags.syn == 1
HTTP 模式过滤
http.request.method == “GET”
http.request.method == “POST”
报文内容过滤
tcp.segment_data contains “202005190001”
过滤tcp 报文内容包括 202005190001 的报文
ip.addr 代表着 只有IP是xxx 就全部显现出来,不管是接收 还是发送
ICMP协议
type
报文类型,8代表请求,0代表应答
code
为0,表示为回显应答成功
checksum
表示认证这个ICMP报文是否被篡改过。校验完整性
checksum Status
表示校验的结果,Good代表没问题
identifier
表示标识符ID
Sequence Number
表示序列号
Data
具体发送到数据包,当然肯定是通过加密的。
网络加速
边缘节点加速
定义
边缘节点加速指的是通过部署在各地的边缘节点服务器(Edge Node)来为用户提供更近、更快的服务访问路径。
核心原理
用户访问内容时,不再从远程主服务器获取数据,而是从“离用户更近”的边缘节点获取。
这些边缘节点通常由 CDN(内容分发网络)提供。
例子
你在中国访问一个美国网站,原本需要跨越太平洋才能获取资源
如果这个网站使用了 CDN(如阿里云CDN、Cloudflare、Akamai 等),就可以从国内的某个 CDN 节点直接加载内容,大大减少延迟。
内容类型
静态内容(可缓存)
图片、JS、CSS、视频等
是否缓存
是
加速手段
靠边缘缓存服务器
是否属于统一平台
通常可集成在CDN/全站加速等平台中
动态加速
定义
动态加速指的是对动态内容(无法缓存)进行传输优化,通过智能路由、协议优化、链路选择等手段加快访问速度。
核心原理
不缓存,而是优化实时交互数据的传输路径。
典型应用
接口请求、API调用、数据库查询等不能预缓存的内容传输。
手段
TCP协议优化(如TCP握手优化)
智能选路(选最优链路)
重传机制优化
例子
一个APP请求接口 api.example.com/user/info,每次请求内容都不同(动态的),这就不能用CDN缓存。
此时可以通过“动态加速”服务(如阿里云全站加速 DCDN、腾讯云GA、AWS Global Accelerator)智能选择最快线路传输,提高速度和成功率。
内容类型
动态内容(不可缓存)
登录、支付、搜索、接口请求等
是否缓存
否
加速手段
靠智能路由/协议优化
边缘节点加速是让你“就近访问”缓存的内容,动态加速是让“不能缓存的内容”也走最优路径,两者属于不同的网络加速技术,常常配合使用
边缘节点服务器
就是部署在用户“附近”的服务器,专门用来缓存内容、处理请求,以实现“就近访问、快速响应”的目标。
它是 CDN、边缘计算、动态加速、IoT等技术的基础设施核心组成部分。
什么叫“边缘”?
“边缘”是相对“中心”来说的。
传统网站或服务通常部署在中心服务器(如北京、上海、美国AWS),所有用户访问时都得连到这个中心机房。
而边缘节点服务器就是部署在多个地理位置(如成都、广州、郑州、东京、硅谷等)的“小型中继服务器”
优点
更靠近用户(网络上更近,物理上也可能更近)
更快地响应请求
减少跨省/跨国访问所产生的延迟与丢包
能力
内容缓存
缓存网页、图片、视频、文件等静态资源
就近响应请求
用户请求直接命中边缘节点而不是回源
链路优化
动态请求通过边缘节点智能转发走最优链路
安全防护
提供 DDoS 防护、WAF、HTTPS 等安全能力
边缘计算
在节点上直接运行某些轻量级计算或服务(比如广告推荐、请求预处理)
类比
中心服务器
总公司总部仓库(如北京总部)
边缘节点服务器
各地分公司区域仓库(如成都分公司)
你订购商品时,商品从总部发货很慢,从离你最近的仓库先发货就快多了
总结
边缘节点服务器就是部署在离用户更近的服务器节点,它通过缓存内容或优化请求传输,来实现“加速、稳定、安全”的访问体验。
它是 CDN、边缘计算、动态加速的基础设施。
CDN
Content Delivery Network
是一种通过在全球范围内部署大量边缘节点服务器,将网站或应用内容分发到离用户最近的节点,从而加快访问速度、降低延迟、提高可用性与安全性的技术。
功能说明
缓存静态资源
把图片、JS、CSS、视频等缓存到边缘节点,减少回源请求
就近响应
用户访问资源时,从“最近的节点”提供服务
流量分担
分担源站压力,防止因访问量大而崩溃
抗攻击(如DDoS)
节点分布广,有防护能力,能防御部分网络攻击
HTTPS优化
提供免费的证书和加速的HTTPS连接
CDN 和边缘节点加速的关系
CDN 的核心就是通过边缘节点加速内容的访问。
所以“边缘节点加速”可以理解为“CDN 提供的主要能力之一”。
CDN 和动态加速的关系
传统 CDN 擅长加速静态内容,但对动态请求无能为力。为了解决这一问题,CDN厂商陆续推出了:
• 全站加速(Whole Site Acceleration)
• 动态加速(Dynamic Acceleration)
• 全球加速、GA、DCDN(Dynamic CDN)
这些功能都是在CDN网络基础上,使用智能路由、链路调度等技术,对动态请求也进行加速。
• 全站加速(Whole Site Acceleration)
• 动态加速(Dynamic Acceleration)
• 全球加速、GA、DCDN(Dynamic CDN)
这些功能都是在CDN网络基础上,使用智能路由、链路调度等技术,对动态请求也进行加速。
以一个电商网站为例
主页上的图片、商品信息页等走 CDN缓存 + 边缘节点加速;
登录、加入购物车、支付等操作请求走 动态加速(全站加速);
所有这些能力可能都在一个“CDN服务平台”上由同一个厂商(如阿里云CDN、腾讯云CDN、Cloudflare)提供。
DevOps
云原生
含义:使用任意编程语言开发的应用,称为原生应用.开发好原生应用以后,部署上云的过程,以及云上的一系列解决方案
CDN
一般只缓存静态数据
运营商黑洞
路由器设备有一个特殊接口叫null0,更形象点就是linux中的/dev/null,机房和运营商跑的是 BGP协议,如果机房发现某个ip流量异常,就会通过BGP宣告这个ip并指向下一跳地址为和运营商约定的地址,这个地址指向null0并进行广播,运营商收到后也会把被攻击的ip指向null0
在被攻击时,运营商为了保证这一网段下其他用户不受影响,就会拉入黑洞
DDOS
UDP洪水攻击
好像需要在udp的一个协议中伪造自身ip为我的服务器ip,使返回的数据被发送到我的服务器上,就是反射
可以写了一个简单的代码,用阿里云官方api监测到攻击,就自动解绑IP,释放IP,买新IP,绑定IP,最后域名解析到新IP
清洗
电脑使用
Mac使用
窗口相关
切换窗口:按下⌘+Tab
最大化窗口(⌃⌘+F):本窗口视觉上占满全部屏幕,存在感最大
正常窗口:本窗口视觉上和其他本程序或者其他程序的窗口共用桌面
最小化窗口(⌘+M):有两种设置:一种是本窗口在视觉上能见,但是最小,不占用桌面,挪动到Dock的右边,一种是“本窗口”视觉上不可见,最小化(隐藏)到Dock的程序图标中(这个隐藏和下面的⌘+H的主要区别就是⌘+M针对单独的一个窗口,⌘+H是隐藏程序的所有窗口)
隐藏程序的所有窗口(⌘+H):整个程序从视觉上消失,不显示在屏幕的任何地方,但只是看不见而已,其他一切照旧
关闭窗口(⌘+W):本窗口实际上被关闭,所有和本窗口相关的资源释放,如果文件有编辑会提示保存,但和本程序的其他窗口无关
关闭程序(⌘+Q):程序实际上被关闭,本程序所有的窗口关闭,所有资源释放
最大化窗口(⌃⌘+F):本窗口视觉上占满全部屏幕,存在感最大
正常窗口:本窗口视觉上和其他本程序或者其他程序的窗口共用桌面
最小化窗口(⌘+M):有两种设置:一种是本窗口在视觉上能见,但是最小,不占用桌面,挪动到Dock的右边,一种是“本窗口”视觉上不可见,最小化(隐藏)到Dock的程序图标中(这个隐藏和下面的⌘+H的主要区别就是⌘+M针对单独的一个窗口,⌘+H是隐藏程序的所有窗口)
隐藏程序的所有窗口(⌘+H):整个程序从视觉上消失,不显示在屏幕的任何地方,但只是看不见而已,其他一切照旧
关闭窗口(⌘+W):本窗口实际上被关闭,所有和本窗口相关的资源释放,如果文件有编辑会提示保存,但和本程序的其他窗口无关
关闭程序(⌘+Q):程序实际上被关闭,本程序所有的窗口关闭,所有资源释放
IDEA
编辑代码查看了调用类实现逻辑,然后可以使用后退快捷键,快速回到刚才待编辑的代码处:⌘ + ⌥ + ← / →(方向键)
查看历史文件,并且在弹出窗口内可以使用关键键快速查找:⌘ +E
统一命名:⇧ + F6
查看历史文件,并且在弹出窗口内可以使用关键键快速查找:⌘ +E
统一命名:⇧ + F6
键盘符号含义
⌘(command)、⎇(option)、⌃(control)、⎋(esc)、⇧(shift)、⌅(enter)
⇪(caps lock)、↩(return)、↖(home)、↘(end)、⇟(pagedown)、⇞(pageup)
⇪(caps lock)、↩(return)、↖(home)、↘(end)、⇟(pagedown)、⇞(pageup)
测试网络连接
telnet
MacOS 10.13后启用
nc
netcat
nc -vc 192.168.1.100 8080
Linux
查找指定名字文件或者jar包位置
find / -name "*xxx*.jar" 2>/dev/null
2>/dev/null 是为了忽略没有权限的错误信息。
找“正在运行”的某个 jar 包文件路径
ps -ef | grep java
服务器上面的日志文件,想从下往上看
less
less your.log
输入 G(大写)跳到最后一行
然后用方向键 ↑ 或 PgUp 往上翻
less +G your.log
less 配合搜索非常好用(输入 /关键字 回车,然后按 n 继续查找)
与vi相比,less只读,启动更快,更安全
实时查看最新日志
less
less +F your.log
可以实时
可以暂停查看
支持翻页,查旧日志
支持搜索
tail
tail -f your.log
-f:表示 follow,持续输出新增的内容
每有一行新的日志写入,都会实时显示出来
tail -n 200 -f your.log
想看最近200行
可以实时
不方便暂停查看
基本支持翻页,查旧日志
不支持搜索
查看 正在运行的 Java 项目监听了哪个端口
lsof
lsof -i -P -n | grep java
说明:
-i:列出网络连接
-P:显示数字端口(不转换为服务名)
-n:不解析域名
grep java:只筛选 Java 进程
示例
java 12345 root 123u IPv6 0xabcde 0t0 TCP *:8080 (LISTEN)
表示 PID = 12345 的 Java 进程正在监听 8080 端口
netstat
netstat -tunlp | grep java
配合 ps 查看 Java 启动命令
ps -ef | grep java
很多 Java 项目在启动命令里会写
--server.port=8081
-Dserver.port=9090
网络相关命令
telnet
用途
测试远程主机端口连通性或交互式访问(明文传输,不推荐用于生产环境)。
用法
telnet [主机] [端口]
示例
telnet example.com 80 # 测试HTTP端口
curl
用途
传输数据(支持HTTP/HTTPS/FTP等协议),适合API调试、下载小文件。
特点
无状态、默认输出到终端、支持丰富协议。
用法
curl [选项] [URL]
示例
curl -I https://example.com # 获取HTTP头
curl -O https://example.com/file.zip # 下载文件
wget
用途
递归下载文件/网站(支持HTTP/HTTPS/FTP)
特点
支持断点续传、后台下载、批量下载。
用法
wget [选项] [URL]
示例
wget -c https://example.com/file.zip # 断点续传
wget -r https://example.com # 递归下载整站
ping
用途
测试主机网络连通性(ICMP协议)。
用法
ping [主机/IP]
示例:
ping google.com # 持续发送ICMP包
ping -c 4 8.8.8.8 # 发送4次后停止
DNS查询工具
nslookup xxx.com
用途
DNS查询工具
查询DNS解析记录(A/MX/CNAME等)。
用法
nslookup [域名] [DNS服务器]
示例:
nslookup example.com
nslookup example.com 8.8.8.8 # 指定DNS服务器
dig
用途
DNS查询工具(比 nslookup 更详细)。
示例
dig example.com MX # 查询MX记录
查看路由信息
traceroute xxx.com
用途
追踪数据包路径(显示每一跳IP和延迟)。
原理
使用递增TTL触发ICMP超时响应
用法
traceroute [主机/IP]
示例:
traceroute google.com
tracepath xxx.com
用途
类似 traceroute,但无需root权限,显示路径MTU。
用法
tracepath [主机/IP]
示例:
tracepath example.com
mtr xxx.com
用途
结合 ping + traceroute,实时监控路径延迟和丢包率。
用法
mtr [主机/IP]
示例:
mtr -r -c 10 google.com # 生成10次测试报告
arp
查看/管理ARP缓存表
示例
arp -a # 显示ARP表
查看属于CentOS或者Ubuntu/Debian
cat /etc/os-release
不区分系统
VSCode
多行选择快捷键
Win/Linux
主要有:Shift + Alt + 鼠标拖动
Mac
Shift + Option + 鼠标拖动
软件使用
VS Code
一次选中多行 光标一次定位多行
1 . 鼠标点击开始位置(定位到行首时,鼠标就点击第一行的行首;定位到行尾时,鼠标就点击第一行的行尾;
2. 按住shift+alt 点击结束的位置(定位到行首时,鼠标就点击最后一行的行首;定位到行尾时,鼠标就点击最后一行的行尾;
3. 按键盘的home或者end键 可以快速定位到选中行的行首或者行尾 方便进行操作
2. 按住shift+alt 点击结束的位置(定位到行首时,鼠标就点击最后一行的行首;定位到行尾时,鼠标就点击最后一行的行尾;
3. 按键盘的home或者end键 可以快速定位到选中行的行首或者行尾 方便进行操作
代码规范
改善Java程序的151个建议
第一章、开发中通用的方法和准则
1、不要在常量和变量中出现易混淆的字母
如:long a=0l; --> long a=0L;字母l应该大写为L,醒目易懂
2、莫让常量蜕变成变量
public static final int t=new Random().nextInt();这样就会常量变变量
3、三元操作符的类型无比一致
nt i=80;
String s=String.valueOf(i<100?90:100);
String s1=String.valueOf(i<100?90:100.0);
System.out.print(s.equals(s1)); //false
String s=String.valueOf(i<100?90:100);
String s1=String.valueOf(i<100?90:100.0);
System.out.print(s.equals(s1)); //false
编译器会进行类型转换,会将s1的90转为90.0
4、避免带有变长参数的方法重载
public class MainTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println(PriceTool.calPrice(12, 1)); // 1
}
}
class PriceTool {
public static int calPrice(int price, int discount) {
return 1;
}
public static int calPrice(int price, int... discount) {
return 2;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println(PriceTool.calPrice(12, 1)); // 1
}
}
class PriceTool {
public static int calPrice(int price, int discount) {
return 1;
}
public static int calPrice(int price, int... discount) {
return 2;
}
}
编译器会从最简单的开始猜想,只要符合编译条件的即采用
5、别让null值和空值威胁到变长方法
null没有类型
6、覆写变长方法也循规蹈矩
7、警惕自增的陷阱
8、尽量不使用类似go to的旧语法
9、少用静态导入
java5开始引入 import static ,其目的是为了减少字符输入量,提高代码的可阅读性。
但滥用静态导入会使程序更难阅读,更难维护。静态导入后,代码中就不用再写类名了,但是我们知道类是“一类事物的描述”,缺少了类名的修饰,静态属性和静态方法的表象意义可以被无限放大,这会让阅读者很难弄清楚所谓何意
如:Math.PI,静态导入后只剩下PI
应遵循两个规则
不使用*通配符,除非是导入静态常量类(只包含常量的类或接口)
方法名是具有明确、清晰表象意义的工具类
10、不要在本类中覆盖静态导入的变量和方法
本地的方法和属性会被使用。因为编译器有最短路径原则,以确保本类中的属性、方法优先
如果要变更一个被静态导入的方法,最好的办法是在原始类中重构,而不是在本类中覆盖。
11、养成良好习惯,显式声明序列化UID
12、避免用序列化类在构造函数中为不变量赋值
因为反序列化时构造函数不会执行。JVM从数据流中获取一个object对象,然后根据数据流中的类文件描述信息查看,发现时final变量,需要重新计算,于是引用person类中的name值,而此时JVM又发现name竟然没有赋值,不能引用,于是不再初始化,保持原值状态。
13、避免为final变量复杂赋值
可维护的代码的20个建议
建议一:提前判断空值
建议二:保持变量不可变
建议三:使用类型提示和静态类型检查器
建议四:验证输入
永远记住:不要相信客户端传给你的任何输入参数
前端校验是为了用户体验
后端校验是为了数据安全
建议五:不要返回奇怪的异常
建议六:尽可能的复用已有的异常
建议七:处理异常要遵循“早抛晚捕”
建议八:合理重试
“退避”重试策略(retry backoff strategies)
固定间隔重试(Fixed Interval Retry)
这是最简单的重试策略,就是每次重试的时间都是固定的,例如重试3次,每次间隔20毫秒
如果失败的原因是系统过载,这种方法可能会加剧问题
线性退避(Linear Backoff)
每次重试间隔的时间都线性增加,如第一次是10毫秒,第二次是20毫秒,第三次是30毫秒
比固定间隔稍好,但是在遇到高延迟或高错误率的情况下仍可能不够高效
指数退避(Exponential Backoff)
每次重试间隔的时间都呈指数级增长,通常是平方级别增长(retry number)^2,例如第一次是2^0,即1秒,第二次是2^1,即2秒,第三次是2^2,即4秒,以此类推后续为8秒、16秒等
这是最常用的退避策略,尤其适用于高负载环境下,有效减少对系统和网络的冲击,更能提高成功的可能性
指数退避加抖动(Exponential Backoff with Jitter)
在指数退避的基础上再加入一些随机数值(抖动值)来进一步的分散重试请求
这种方式可以有效的避免多个客户端同时重试而造成的系统冲击(即惊群效应)
建议九:构建幂等系统
建议十:及时释放资源
建议十一:为日志分级
建议十二:保证日志原子性
建议十三:关注日志的性能
建议十四:不要记录敏感数据
日志中不应该包含用户的隐私数据,应该脱敏
建议十五:系统要有监控
监控能让充分掌控系统的状态,请求的时延,并发的数量,数据写入的大小等等
建议十六:分布式链路跟踪
建议十七:保证配置的简洁
建议十八:记录并校验配置
在程序启动时应该立即记录所有(非敏感的)配置,可以直接在控制台或者日志文件中看到这些配置,保证能直观的判断配置是否得到正确的配置
建议十九:为你的配置提供默认值
建议二十:将配置视为代码
配置即代码,配置应该和代码一样接受管理,尤其是生产环境的配置。错误的配置,可能会带来不可预估的灾难。因此配置的管理也应该和代码一样,接受版本控制、变更评审、测试、构建和发布。
分支名
主分支
master
main
测试分支
release预发布
test测试
开发分支
developer
dev
功能分支
feature/分支名称 (通常用于开发新功能)
feat
修复分支
bugfix/bug名(通常用于对分支bug修复,可能是一个长期的工作)
快速修复分支/生产bug修复
hotfix/版本号 (通常用于对主分支bug修复,或需要快速解决的紧急bug)
重构分支
refactor
要点
禁止直接修改主分支或开发分支,只能合并修改
所有的发布和tag必须在主分支上
测试
黑盒测试
定义
黑盒测试关注软件的外部行为,不考虑内部实现细节
方法
测试人员只关注输入和预期输出,就像在测试一个"黑盒子",不知道里面的具体结构
缺点
可能无法覆盖所有代码路径
例子
输入 2+2,检查输出是否为 4。
测试各种数学运算的正确性。
验证用户界面的功能是否符合需求。
特点
基于需求规格和用户手册进行测试
属于功能测试
确保软件满足用户需求
白盒测试(结构测试)
定义
白盒测试关注软件的内部逻辑和结构
方法
测试人员需要了解代码结构,并设计测试用例来覆盖不同的代码路径。
优点
有助于发现隐藏的bug和边界情况
可以全面测试内部逻辑
例子
确保所有的if-else分支都被测试到。
检查边界值,如最大和最小可接受的输入。
验证内部数据结构的正确使用。
特点
主要用于单元测试和集成测试
需要对代码有深入了解,基于代码结构和逻辑流程进行测试
代码覆盖率测试
确保代码质量和全面性
项目
自我介绍
面试官你好,我是XXX,来自于XXX。之前就职于XX科技有限公司,担任软件开发工程师一职。在职期间主要负责XX公司XX项目与研发。对线上问题处理,项目与数据库性能调优等问题都有自己的理解。对行业相关的研发设计流程也十分熟悉。因此决定面试咱们公司Java开发岗位。希望能获此机会,谢谢。
1.项目架构
2.项目中的难点
3.介绍下项目中比较复杂的实现
项目经历
开篇讲成绩,吸引住对方
交代清楚背景、渲染项目难度大
更能体现能力
搭建清晰合理的框架,使对方感觉到思路清晰有条理
各个要点,逐一说明
介绍细节与策略
提炼策略,体现有思考能决策,而非无脑执行
量化结果
上线计划
发布计划
准备
梳理要涉及哪些变更,影响面是哪些
有没有做到数据与接口兼容性
思考要不要做灰度,灰度策略是啥
确认上下游接口是否准备好
大概策略
先进行DDL(增加字段时不要加非空约束,否则会对之前有影响),建MQ,再发布代码
先发布服务提供方,再发布服务调用方
自底向上,数据存储层,微服务层,网关层,UI
注意点
不要轻易调整方法名,请求地址等
比如方法名不太优雅,调整后上线也可能故障,可能是别人的二方包,调用方会找不到该方法
涉及底层数据结构变更
能否平滑兼容
历史数据要不要迁移
应该在技术方案设计时就应该考虑清楚
功能应用场景
流量能否抗住
一致性要求高不高
性能爬坡、缓存预热
是否充分考虑回滚
切流
起线程池
同时去执行新逻辑与老逻辑,然后将处理结果做比较,及时发出告警
流量回放
老逻辑执行完以后将出参入参保存下来,可以通过MQ或者数据库;然后异步进行流量回放,再进行观察与告警
线上问题
生产环境cpu飙升
这类资源问题,第一时间要做的是两件事:
1. 问下其他人有无在这段时间做什么骚操作(查看代码提交记录)
2. 根据监控,快速排查各指标有无异常,比如某个http接口qps异常飙升。
当所有方便的手段用完之后,还是无法解决问题,就应该上机器用各种命令去查看,甚至是dump下jvm堆栈信息排查
1. 问下其他人有无在这段时间做什么骚操作(查看代码提交记录)
2. 根据监控,快速排查各指标有无异常,比如某个http接口qps异常飙升。
当所有方便的手段用完之后,还是无法解决问题,就应该上机器用各种命令去查看,甚至是dump下jvm堆栈信息排查
排查命令
首先是使用top命令定位系统中消耗较高的进程id
再使用jstack命令结合 | 管道符可以快速定位消耗较高的代码行。这是还可以使用grep命令过滤关键字
jstack PID | grep 'xxx'
arthas命令
thread -i 3000 -n 5
列出 3000ms 内最忙的 5 个线程栈
排查方法具体执行耗时
wget https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
trace com.test.flow.controller.FlowProcessController getProcessInfo
线下问题
启动耗时长
观察方法
1.利用BeanPostProcessor接口中postProcessBeforeInitialization与postProcessAfterInitialization方法在Bean初始化前后,统计初始化耗时
2.利用底层探针,例如arthas的trace命令统计每个方法耗时
3.手动debug
耗时原因
1.数据库初始化耗时->a.数据库连接池初始化;b.获取数据库元数据初始化
2.因为分库分表都需要将以上初始化
解决方案
a.数据库连接池初始化(设置Druid中asyncInit为true,进行懒加载)
b.获取数据库元数据初始化(将ColumnMetaData等数据变为JSON。存储在本地,直接读取文件;在工程中定义同样的包名类名,就可以把里面覆盖掉)
延伸
1.如果本地启动容器不想注册到注册中心,可以实现BeanPostProcessor接口,重写postProcessBeforeInitialization方法,将ProviderConfig中register字段set为false;
2.以上代码还应该进行环境隔离.利用@Conditional(TestEnvirChangCondition.class)指定环境;在TestEnvirChangCondition中实现Condition接口,重写matches方法.conditionContext.getEnvironment().getProperty(keyName,Boolean.class,defaultValue);
2.1建议使用 Spring 原生 Profiles 来区分集成测试环境
a. 在 DataSourceDefinitionProcessor 和 NotRegister 标注 @Profile("unittest")
b. 在具体测试类中标注 @ActiveProfiles("unittest")
2.以上代码还应该进行环境隔离.利用@Conditional(TestEnvirChangCondition.class)指定环境;在TestEnvirChangCondition中实现Condition接口,重写matches方法.conditionContext.getEnvironment().getProperty(keyName,Boolean.class,defaultValue);
2.1建议使用 Spring 原生 Profiles 来区分集成测试环境
a. 在 DataSourceDefinitionProcessor 和 NotRegister 标注 @Profile("unittest")
b. 在具体测试类中标注 @ActiveProfiles("unittest")
补充关于 Spring Bean 生命周期 SPI BeanPostProcessor 接口为什么不是一个很好的统计 Bean 耗时时间,主要原因在于一个 Bean 可能被多个 BeanPostProcessor 处理,所以需确保统计 BeanPostProcessor 的postProcessBeforeInitialization 必须是第一个调用,
同时,确保它的 postProcessAfterInitialization 是最后一个执行,显然这是矛盾的。
Spring 5.3 提供了 StartupStep API,它能够统计部分 Bean 和 IoC 容器耗时,但是还是不完整,估计作者也感受到了难度。其他类似需求相关实现开源代码
核心实现代码:https://github.com/microsphere-projects/microsphere-spring-projects/blob/main/microsphere-spring/microsphere-spring-context/src/main/java/io/github/microsphere/spring/context/event/
Bean 生命周期核心:BeanEventListener.java
Bean 耗时统计:BeanTimeStatistics.java
单元测试:BeanTimeStatisticsTest.java
同时,确保它的 postProcessAfterInitialization 是最后一个执行,显然这是矛盾的。
Spring 5.3 提供了 StartupStep API,它能够统计部分 Bean 和 IoC 容器耗时,但是还是不完整,估计作者也感受到了难度。其他类似需求相关实现开源代码
核心实现代码:https://github.com/microsphere-projects/microsphere-spring-projects/blob/main/microsphere-spring/microsphere-spring-context/src/main/java/io/github/microsphere/spring/context/event/
Bean 生命周期核心:BeanEventListener.java
Bean 耗时统计:BeanTimeStatistics.java
单元测试:BeanTimeStatisticsTest.java
如果要设计一个程序,跑在全公司所有机器上,并且要开端口.应如何确定这个端口
看要提供什么服务
统计哪些端口不能用,要不被人占用了,要么是保留端口
摸透公司安全相关策略
公司端口分配策略,如果有
一般都只能选择大端口,因为小端口一旦冲突就很麻烦,小端口很多通用软件占用(选好端口后就不能改了,别人就用不了了)
如果选的端口被人占用怎么办,是否要再加入保留端口(预先占用端口,避免分配)
告知选择的端口,有问题及时联系
逻辑删除字段利弊
潜在问题
如果在业务表中使用 is delete 字段来标记逻辑删除,査询时需要附加 is_delete=false 的条件,这会增加数据库的査询成本,尤其是数据量大的情况下,可能导致索引失效、查询性能下降。
业务表承担的是核心业务功能,通常会有很高的读写需求。如果删除的数据一直存在,业务表的体量会持续增大,不仅会影响查询效率,还会对数据存储和维护带来压力。
解决方案
宽表化归档数据
同一事务
归档表
需要考虑历史数据怎么查询问题
但应该考虑到删除的数据,不应该还能查出来。只能说出了问题可以用来恢复
也可以利用MQ,尝试异步方案
为了保证数据能够落地到归档表,并且独立事务,还做了额外操作的。不同的是采用了延迟归档。删除还是先逻辑删除,物理删除会放到后面操作。后续通过延迟的消息队列,落地到归档表(数据直接复制或者拿消息队列的数据写入)。归档表写入完数据后,才去物理删除数据。业务数据库和归档库在上述操作中没有事务的联系,异步的。
业务问题
支付
B扫C问题
扫完商品,确认生成订单,生成一个唯一的订单流水号,再利用订单号做幂等
基础的支付知识,超时了,这笔单就当做失败,丢队列里面发起撤销操作就好了,外加补偿撤销(因为队列是不可靠的)。
那么,为什么不是重试而是撤销回滚?
因为重试并不能保证下一次成功,也不知道重试多少次可以得到结果。实际的应用场景客人没办法在那里等待,需要最快的效率完成支付。
支付宝就有这个问题,海外的用户体验很差,比微信容易出错账。支付宝经常出现交易时长时间没有结果,支付宝返回订单失败状态,但是付款人实际成功,最终产生错账,这个bug到现在为止都没解决,时不时出一笔这种投诉。微信就没有这个问题。支付宝可能用的重试策略。
支付宝端:
a、可做幂等的情况下:
1、有订单ID:可以直接通过订单ID判断是否为重复扣款。
2、没有订单ID:可以通过扫码枪的唯一标识+IP+请求参数(如扣款金额+扣款账户,甚至商品清单等其他参数)+一定时间内(如3分钟内)判断是否为重复扣款。
b、不可做幂等的情况下:
1、可以增加其他通知渠道:比如微信在断网使用付款码的情况下,交易完成用户手机会收到一条扣款成功的短信。
2、支付宝服务端每日定时对账。
3、当新的扣款动作来时,是否可以关闭之前正在处理的订单。
用户端:
1、在被要求二次扫码前可以先看看自己的账单是否扣款成功。
2、可以先让后面排队的人先付款,等几分钟后在让商家确定自己的订单是否扣款成功。
3、在二次扫码前要求商家关闭之前的订单。
4、在发现重复扣款时,及时与支付宝客服和商家联系。
总之理论上是会发生重复扣款的现象,但自己在现实生活中经常被二次扫付款码,也并没有出现重复扣款的现象,那具体支付宝怎么做的咱也不知道,仅仅是揣测。
a、可做幂等的情况下:
1、有订单ID:可以直接通过订单ID判断是否为重复扣款。
2、没有订单ID:可以通过扫码枪的唯一标识+IP+请求参数(如扣款金额+扣款账户,甚至商品清单等其他参数)+一定时间内(如3分钟内)判断是否为重复扣款。
b、不可做幂等的情况下:
1、可以增加其他通知渠道:比如微信在断网使用付款码的情况下,交易完成用户手机会收到一条扣款成功的短信。
2、支付宝服务端每日定时对账。
3、当新的扣款动作来时,是否可以关闭之前正在处理的订单。
用户端:
1、在被要求二次扫码前可以先看看自己的账单是否扣款成功。
2、可以先让后面排队的人先付款,等几分钟后在让商家确定自己的订单是否扣款成功。
3、在二次扫码前要求商家关闭之前的订单。
4、在发现重复扣款时,及时与支付宝客服和商家联系。
总之理论上是会发生重复扣款的现象,但自己在现实生活中经常被二次扫付款码,也并没有出现重复扣款的现象,那具体支付宝怎么做的咱也不知道,仅仅是揣测。
这个是面试。实际中,几乎不会出现这种问题。在京东遇到过,重复支付的问题,第二天就原路回退了。一般情况都会在第二天进行对账,然后修复错误扣款,或者直接说平台方负责错误款项。
分离设计
读写分离
动静分离
静态数据
属于"无个性化"数据
种类
静态文件
HTML
CSS
JS图片
低频变动数据
字典数据
地区数据
组织架构
历史数据
动态数据
属于个性化/高频写数据
种类
个性化推荐
高频写数据
股市行情
5G信号数据
天气变化
优化
优化的关键前提
有效区分页面中的动静数据
静态页面处理需要几毫秒
动态页面处理需要几百毫秒
前后台分离
版本控制
合并指定分支
转移单个提交
git cherry-pick <commitHash>
转移多个提交
git cherry-pick <HashA> <HashB>
代码冲突
回到操作前
git cherry-pick --abort
不回到操作前
git cherry-pick --quit
git pull
git fetch +git merge
合并
merge
公共分支合并代码使用merge
rebase
私有分支需要拉去新代码可以用rebase
可以保留初始commit的创建时间
--committer-date-is-author-date
效果上,可以简单理解为:多次的Cherry pick
准确的说,rebase和merge是对代码产生的影响、结果相同。
merge会形成一个四边形,产生一个新的commit,就是一次新的提交,把develop分支所有变动内容带了过来;
rebase抛开commit id的变化,就相当于develop从没出现过一样,按顺序在master最前方新提交一遍;
补充一个命令cherry-pick,可以合并单次commit;
建议使用merge,保留原分支变更,master就是一次一次的merge,gitk命令可以很直观的看到,或平台网页端网络图那里更好看,什么时候合并了一个feture。rebase后看总图,会有很多重复提交;
merge会形成一个四边形,产生一个新的commit,就是一次新的提交,把develop分支所有变动内容带了过来;
rebase抛开commit id的变化,就相当于develop从没出现过一样,按顺序在master最前方新提交一遍;
补充一个命令cherry-pick,可以合并单次commit;
建议使用merge,保留原分支变更,master就是一次一次的merge,gitk命令可以很直观的看到,或平台网页端网络图那里更好看,什么时候合并了一个feture。rebase后看总图,会有很多重复提交;
一个新的需求,就按这个需求开一个新分支,当主分支有新增内容时(其他小伙伴合入代码了),且新增的内容对自己的新代码有影响,那么就rebase下主分支,将主分支的内容同步过来,并解决下自己的代码,比如冲突啥的,适配主分支的新代码。自己的需求开发完毕后,提MR,merge合入到主分支
回退
git revert -m 1 <合并提交的SHA>
git reset --hard HEAD~1
将会将当前分支的 HEAD 指针、暂存区和工作目录重置到前一个提交的状态,并丢弃所有未提交的更改。这样做相当于将当前分支向后移动一个提交。
--hard 参数表示重置模式为“硬重置”
HEAD~1是指向当前所在分支的最新提交的引用。~1 表示向上移动一个提交。因此,HEAD~1 表示当前分支的上一个提交。
Bug
文件导入异常
java.io.IOException: The temporary upload location [/tmp/tomcat/...] is not valid
SpringBoot项目启动后,系统默认会在 /tmp 目录下自动创建如下三个目录
hsperfdata_root,
tomcat.************.8080,(结尾是项目的端后)
tomcat-docbase.*********.8080
Multipart(form-data)的方式处理请求时,默认就是在第二个目录下创建临时文件的
CentOS7 定时清理临时文件目录的服务
/tmp目录的清理规则主要取决于/usr/lib/tmpfiles.d/tmp.conf文件的设定
#Clear tmp directories separately, to make them easier to override
v /tmp 1777 root root 10d # 清理/tmp下10天前的目录和文件
v /var/tmp 1777 root root 30d # 清理/var/tmp下30天前的目录和文件
v /tmp 1777 root root 10d # 清理/tmp下10天前的目录和文件
v /var/tmp 1777 root root 30d # 清理/var/tmp下30天前的目录和文件
systemctl status systemd-tmpfiles-clean
解决办法
重启服务,但是只能根据服务器的策略维持几天(如果这期间没有发生变化的文件或目录)
手动把缺失的目录建出来
更新SpringBoot的版本
2.1.4修复了
但2.4.5依旧存在这个问题
2.6和2.7都没有这个问题
手动设置Tomcat目录
启动脚本指定
DIR=/home/application
JAVATEMPDIR=${DIR}/temp
nohup java -jar application-1.0.0-1.jar -Xms10m -Xmx100m --server.port=9820 -java.tmp.dir=$JAVATEMPDIR 2>1&
JAVATEMPDIR=${DIR}/temp
nohup java -jar application-1.0.0-1.jar -Xms10m -Xmx100m --server.port=9820 -java.tmp.dir=$JAVATEMPDIR 2>1&
在SpringBoot的配置之中设定路径
spring.mvc.static-path-pattern=/upload/**
spring.http.multipart.max-file-size=10MB
#指定上传文件临时目录
spring.http.multipart.location=/opt/data/upload
spring.http.multipart.max-file-size=10MB
#指定上传文件临时目录
spring.http.multipart.location=/opt/data/upload
server.tomcat.basedir=/usr/local/tomcat
系统质量属性
性能
响应时间
并发处理能力
缓存策略
减少网络延迟
优化数据库查询
系统资源利用率
通过负载、测试、性能监控工具可以持续评估系统性能
可拓展性
垂直扩展
水平扩展
应采用无状态的应用开发,通过可伸缩的机制,完成对于系统性能拓展和稳定性需求
安全性
数据加密
访问控制
入侵检测
安全审计
遵循安全开发生命周期SDL原则,从设计到部署都要嵌入安全考量
定期对系统进行安全评估、渗透测试、修复漏洞
可维护性
设计清晰的代码结构和良好的文档
使用自动化构建
互操作性
采用标准化接口,比如Restful API,和标准化协议如HTTP,MQTT促进不同系统间的通信
支持多种不同的数据结构,JSON,xml
通过有效的标准化认证方式,如Oauth 2.0与JWT增强兼容性
灵活性
各组件之间采用松耦合,开放式架构,插件式设计
持久性
可用性
可靠性
易用性
移植性
成本效益
兼容性
测试性
Java基础
数据结构
Collection(单列集合)
List(存储有序,有索引,可重复)
ArrayList(底层实现是数组,线程不安全,查找修改速度快,增删慢)
初始化与扩容
ArrayList在创建时,其初始容量为0,即底层是一个空数组,而第一次往其中存入元素的时候,会进行第一次扩容,在这第一个扩容中,扩容为10,而之后的第二次,第三次,第N次扩容,均为前一次容量的1.5倍
痛点
扩容(创建时确定好大小,后面就避免扩容)。内存允许可以放21亿多的数据,调整源码还可以40亿。
数组赋值
查询是否存在是通过遍历
LinkedList(底层实现是链表,线程不安全,查找修改速度慢,增删可能也慢)
linkedList双向链表,没有初始化大小,也没有扩容的机制,就是一直在前面或者后面新增就好.
可以代替数组结构,但是没有数组效率高。如果频繁的增删,也不应该用链表(要在链表中删除某个元素,首先得找到它啊,链表的查找耗时)
比ArrayList更占内存(存储前后两个引用)
线程安全的List:Collections.synchronizedList(List< T> list)
写操作不多,但需要保证线程安全的场景
强一致性
用synchronized包装
CopyOnWriteArrayList
完全线程安全
适用场景
读多写少
允许短暂的数据不一致
弱一致性
"写时复制"策略
核心思想
当需要修改一个资源时,不直接修改原始资源,而是先复制一份副本,然后对副本进行修改,修改完成后再将原始资源指向新的副本。
好处是,在修改过程中,其他读取者仍然可以访问原始资源,而不会因为修改而阻塞或读取到不一致的数据。
这种策略用"空间换时间",用"内存开销换读取性能",在读多写少的场景下效果极佳
vector(底层实现是数组,线程安全,无论增删改查都慢)
线程安全
低效
Stack
线程安全
先进后出,类似喝酒吐了;入口和出口在同一边
Set(存储无序,无索引,不可重复)
HashSet(底层实现使用hash算法,乱序,不可重复)
特性
底层是HashMap
优势
查询是否存在不是遍历,高效
可以存放一个null
private static final Object PRESENT = new Object0;
HashMap::put(e, PRESENT);
LinkedHashSet
与HashSet不同之处在于采用链地址法存储,元素看起来有序,但插入删除速度稍逊于HashSet
数据去重并保持原始顺序
基于哈希表和双向链表的数据结构
继承自HashSet类,并且根据元素的hashCode值来决定元素的存储顺序,同时使用链表维护元素的插入顺序。
TreeSet(底层实现二叉树,一般用于排序)
默认是按照元素的自然顺序进行排序的,而不是按照添加的顺序。
TreeMultiset
Guava包的数据结构
默认是按照元素的自然顺序进行排序的,可重复。
HashMultiset
Guava包的数据结构
乱序,可重复
Queue
先进先出,类似喝酒没有吐;入口和出口在不同边
Map(双列集合)
HashMap(hash算法存储键)
特性
初始化与扩容
HashMap在创建时,其初始容量为0,而第一次往其中存入元素的时候,会进行第一次扩容(初始化),在这第一个扩容中,扩容为16,负载因子为0.75。
红黑树阈值为8,树化另一条件是map数组长度为64。须同时满足,解除树形化阈值6。之后扩容 2 * n 。
数组容量可以再构造方法中指定。但必须是2的幂次方(涉及位运算)
红黑树阈值为8,树化另一条件是map数组长度为64。须同时满足,解除树形化阈值6。之后扩容 2 * n 。
数组容量可以再构造方法中指定。但必须是2的幂次方(涉及位运算)
为什么数组扩容后,链表长度会减半?
因为我们确定桶位置是数组长度与hash进行&操作,长度扩为2倍,桶位置正好会改为2倍
因为我们确定桶位置是数组长度与hash进行&操作,长度扩为2倍,桶位置正好会改为2倍
LinkedHashMap(底层使用链表法存储键)
Key和Value都允许空
Key重复会覆盖、Value允许重复
有序
线程不安全
按照插入顺序进行访问
ConcurrentHashMap
1.8中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。
把之前的HashEntry改成了Node,但是作用不变,把值和next采用了volatile去修饰,保证了可见性
把之前的HashEntry改成了Node,但是作用不变,把值和next采用了volatile去修饰,保证了可见性
volatile的特性
1.保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的(实现可见性)
2.禁止进行指令重排序(实现有序性)
3.volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性
TreeMap(二叉树存储键)
特性
只允许空的 value,key 不能为空
key不可以重复,value允许重复
有序
线程不安全
传入的key进行了大小排序
HashTable
特性
HashTable的初始值是11
扩容 2 * n + 1
不允许null的Key和Value
直接使用给的初始值。
HashTable是线程安全的(方法上都有synchronized),同步机制决定了它无法追求运行速度上的极致。
在取余法的时候,使用位运算来提升效率已经意义不大了,更多的精力用在考虑解决hash冲突上。使用质数和奇数的取模运算,可以将Hash冲突的概率降低到最小。基本被淘汰
Properties
面试题
set集合和map集合的区别
同
1.存储的数据都是无序
异
1.Set为单列,Map为双列或者说键值对
2.Map的Key不能重复,value可以重复;Set值不允许重复
addAll方法不可放入null,会抛空指针
包装类自动拆箱存在风险
如方法返回值为boolean,Spring返回Boolean。直接返回Spring结果可能会为null。所以应该使用Boolean.TRUE.equals(result)判断
线程安全的数据结构
第一代
Vector
HashTable
第二代
Collections工具类
Collections.synchronizedList(new ArrayList<E>())
Collections.synchronizedMap(new HashMap<E>())
Collections.synchronizedSet(new HashSet<E>0)
第三代
JUC包中的组件
ConcurrentHashMap
synchronized (f)
CopyOnWriteArraySet
CopyOnWriteArrayList
String优化
1.在实际开发中,对于需要多次或大量拼接的操作,在不考虑线程安全问题时,我们就应该尽可能使用 StringBuilder 进行 append 操作。
如果提前知道需要拼接 String 的个数,就应该直接使用带参构造器指定 capacity,以减少扩容的次数
如果提前知道需要拼接 String 的个数,就应该直接使用带参构造器指定 capacity,以减少扩容的次数
2.对于程序中大量使用存在的字符串时,尤其存在很多已经重复的字符串时,使用 intern()方法能够节省内存空间
在sync互斥锁时,也可以利用intern()方法将锁的粒度降低。比如使用用户id字符串再intern()
面试题
Java是值传递还是引用传递
Java参数传递中,不管传递的是基本数据类型还是引用类型,都是值传递
基本数据类型
当传递基本数据类型,比如原始类型(int、long、char等)、包装类型(Integer、Long、String等),实参和形参都是存储在不同的栈帧内,修改形参的栈帧数据,不会影响实参的数据
引用类型
当传递的引用类型,形参和实参指向同一个地址的时候,修改形参地址的内容,会影响到实参。当形参和实参指向不同的地址的时候(比如重新new了对象),修改形参地址的内容,并不会影响到实参
排查OOM
手动生成
jmap -dump:format=b,file=/path/to/dumpfile.hprof <PID>
自动生成
-XX: +HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/jdk/dump
OOM后自动生成一份转储文件,文件保存到/usr/jdk/dumpm目录下
重写的返回类型可以不一样吗
在Java中重写的方法的返回类型可以不一样,但要满足一定条件
子类返回更具体类型
比如父类返回Number,子类返回Integer类型
当返回类型和父类方法完全无关,则不允许
Stream流
特点
惰性执行
中间操作(如map,filter等)仅记录操作逻辑,直到终端操作(如collect,foreach)才会触发实际计算,能够提升性能,尤其是在链式操作很多 的时候,避免中间结果反复创建
流水线像是描述你想干什么,而不是你怎么干。不像传统for循环,每步都得手动维护索引
不可变
每步操作都返回一个新的流,不会改变原始数据(在并发场景下很有优势,线程安全)
支持并行流,背后使用的forkjoin框架
为什么操作顺序不能乱
因为内部链式消费,一旦顺序错了,结果可能就完全变了。比如limit放在filter前后
MySQL
索引
性能分析工具
EXPLAIN
key
type☆
system,const, eq_ref,ref ,fulltext,ref_or_null,index_merge, unique_subquery, index_subquery,range,index,ALL
结果值从最好到最坏
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge >unique_subquery > index_subquery > range >index > ALL
SQL 性能优化的目标:至少要达到 range 级别,要求是 ref 级别,最好是 consts级别。
key_len☆
key_len:实际使用到的索引长度(即:字节数)
普通索引
通常约等于字段长度
比如字段类型为int
如果不允许为空,那么长度为4
如果允许为空,那么长度为5(多的一个是记录是否为null)
比如字段类型为varchar(100)
还要看字符编码,以下以UTF8举例
如果允许为空,那么长度为303(多的其中一个是记录是否为null,另外两个记录长度)
如果不允许为空,那么长度为302
通常恒定,如果变小可能是前缀索引或者模糊匹配
联合索引
命中一次key_len加一次长度。各字段key_len累计
越长代表匹配越精准,但不是性能越好,要结合rows和filtered看执行代价
可以判断用到了联合索引的前几列
rows☆
预估的需要读取的记录条数
rows 值越小,代表数据越有可能在一个页里面,这样io就会更小。
filtered
filtered 的值指返回结果的行占需要读到的行(rows 列的值)的百分比。
自己的理解: 比如读了100 rows. filtered 是10% 那么就说明还要对着100条进行过滤。
对于单表查询来说,这个filtered列的值没什么意义,更关注在连接查询中驱动表对应的执行计划记录的filtered值,它决定了被驱动表要执行的次数(即:rows * filtered)
extra☆
是用来说明一些额外信息,包含不适合在其他列中显示但十分重要的额外信息。通过这些额外信息来更准确的理解MySQL到底将如何执行给定的查询.
Using where
当我们使用全表扫描来执行对某个表的查询,并且该语句的WHERE子句中有针对该表的搜索条件时,在Extra列中会提示上述额外信息
当条件除了索引,还有其他条件,也会是这个提示
Using index
当查询列表以及搜索条件中只包含属于某个索引的列,也就是在可以使用覆盖索引的情况下,Extra列将会提示
Using index condition
有些搜索条件中虽然出现了索引列,但却不能使用到索引(索引条件下推)
Using filesort(重点优化)
有一些情况下对结果集中的记录进行排序是可以使用到索引的,在内存中或者磁盘上进行排序的方式统称为文件排序
优化:从业务层面看是否真的需要排序等操作,不用则去掉
Using temporary(重点优化)
借助临时表来完成一些功能,比如去重、排序之类的,比如我们在执行许多包含DISTINCT、GROUP BY、UNION等子句的查询过程中,如果不能有效利用索引来完成查询,MySQL很有可能寻求通过建立内部的临时表来执行查询.但是建立与维护临时表要付出很大成本
优化:从业务层面看是否真的需要临时表,需要的话则将临时表体量降到最低
trace
分析优化器执行计划
默认关闭
sys_scheme
索引相关
可以查看冗余索引
查看未使用过的索引
查询索引的使用情况
表相关
查询表的访问量
查看表的全表扫描情况
查询占用bufferpool较多的表
语句相关
监控SQL执行频率
监控使用了排序的SQL
监控使用了临时表或者磁盘临时表的SQL
IO相关
查看消耗磁盘IO的文件
Innodb相关
行锁阻塞情况
排查死锁
SHOW ENGINE INNODB STATUS
LATEST DETECTED DEADLOCK:
索引失效
失效场景
条件中有or
如果 OR 条件中,有一列没用到索引。那么整个 OR 都失效
这也是为什么尽量少用or的原因
即使其中有条件带索引也不会使用
要想使用or,又想让索引生效,只能将or条件中的每个列都加上索引
函数计算
where中索引列使用了函数
WHERE LEAF(name, 3) = 'Tom'
WHERE YEAR(create_time) = 2005
where中索引列有运算
WHERE age + 10 = 30
正确的做法
WHERE age = 30 - 10
类型转换
有类型转换时索引失效
字段类型为字符串,where查询时不加单引号,以熟悉形式查询
不等
使用不等于(!=、<>)
为了充分利用索引,有时候可以将>、<等价转为>=、<=的形式
或者将可能会有<、>的条件的字段尽量放在关联索引靠后位置
is null可以走索引,is not null无法使用索引
联合索引
复合索引没有用到左列字段(左前缀法则)
索引范围条件右边的列
表中数据量较少时,不会走索引,而是全表查询
小表扫描更快
使用like后面紧跟着%,如‘%XXX’
对于单列索引,尽量选择针对当前query过滤性更好的索引
在选择组合索引时,query过滤性最好的字段应该越靠前越好
在选择组合索引时,尽量选择能包含当前query中where子句中更多字段的索引
在选择组合索引时,如果某个字段可能出现范围查询,尽量将它往后放
在选择组合索引时,query过滤性最好的字段应该越靠前越好
在选择组合索引时,尽量选择能包含当前query中where子句中更多字段的索引
在选择组合索引时,如果某个字段可能出现范围查询,尽量将它往后放
设计索引原则
适合创建索引
1.字段的数值有唯一性的限制
2.频繁作为 WHERE 查询条件的字段
3.经常 GROUP BY 和 ORDER BY 的列
4.UPDATE、DELETE 的 WHERE 条件列
5.DISTINCT 字段需要创建索引
6.多表 JOIN 连接操作时,尽量不要超过 3 张,对WHERE 条件创建索引,对用于连接的字段创建索引,并且类型必须一致
7.使用列的类型小的创建索引
8.使用字符串前缀创建索引
9.区分度高(散列性高)的列适合作为索引
"过滤性好"(Selectivity,也常被称为“高基数” Cardinality)指的是一个字段中不重复的值很多。
高过滤性 (好)
像 user_id, email, id_card_number。
WHERE user_id = 123 几乎能一步定位到 1 条数据。
低过滤性 (差)
像 gender (性别), status (状态), is_deleted (是否删除)
WHERE gender = 'Male' 可能会返回表中 50% 的数据。
10.使用最频繁的列放到联合索引的左侧
11.在多个字段都要创建索引的情况下,联合索引优于单值索引
不适合创建索引
1. 在where中使用不到的字段,不要设置索引
2. 数据量小的表最好不要使用索引
3. 有大量重复数据的列上不要建立索引
4.避免对经常更新的表创建过多的索引
5.不建议用无序的值作为索引
6.删除不再使用或者很少使用的索引
7.不要定义冗余或重复的索引
底层结构
B+树
组成
段(逻辑概念)
由若干区组成
用于存储特定类型的数据(数据页或者索引页)
InnoDB在表空间中管理去分配的逻辑单元
组成
完整的区(1MB)
单位
1MB(64个页)
InnoDB以区为分配单位
若干零散的数据页(16KB)
存储空间被划分为七个部分
文件头(File Header)[38字节]
页头(Page Header)[56字节]
最大最小记录(Infimum+supremum)[26字节]
用户记录(User Records)[不确定]
空闲空间(Free Space)[不确定]
页目录(Page Directory)[不确定]
文件尾(File Tailer)[8字节]
InnoDB的最小存储单位
碎片区
直属表空间
类型
数据段
存放表中记录(数据行)的页
B+数的叶子节点
索引段
存放表中索引信息的页
B+数的非叶子节点
回滚段
存放undo log 的信息
空间段
用于管理空闲页的云信息
表空间
单位
.ibd文件
存储整个表或索引的数据
注意事项
1.根页面位置万年不动(根节点页号会被记录到某个地方,固定读取)
2.. 内节点中目录项记录的唯一性(会把主键值,添加到二级索引内节点的目录项记录)
3.一个页面最少存储2条记录
面试题
B+树与B树区别,为什么不用B
B+Tree:非叶子节点只存key,大大减少了非叶子节点的大小,那么每个节点就可以存放更多的记录,树更矮了,I/O操作更少了。所以B+Tree拥有更好的性能。
B+Tree不足
并发场景下,写操作导致页分裂(SMO,Split Merge Operation)的时候,刚好有并发读操作访问到错误的叶子节点,查错了节点,那么目标值肯定就搜索不到了,于是导致了错误查询;叶子节点的分裂,可能会导致父节点的分裂,这种调整最长可能级联到根节点
解决方案
在发生节点分裂时,把整颗 B+Tree 都锁了
数据的正确性得到了保证,但是性能就很低了,因为全局锁会影响了对所有页的访问
MySQL 在 5.7 版本后做了优化,但是整个 B+Tree 同时只能支持一个 SMO 操作的发生,高并发时大数据量插入导致多 SMO 的发生还是会被阻塞,影响性能
PolarDB MySQL引入了Blink Tree
通过high key判定是否是分裂中,是则通过兄弟节点指针二次查询
全文索引
用于全文本搜索,能够高效地处理文本查询
InnoDB和MyISAM存储引擎都支持全文索引
可以在CHAR、VARCHAR或TEXT类型的列上创建
全文索引的创建和维护可能会占用较多的存储空间,并且在频繁更新文本数据时会影响性能,因为每次更新都需要重新构建或调整 全文索引
索引种类
数据结构角度
B+树索引
适合于范围查询、全键值查询和最左前缀查询
哈希索引
适用于等值查询,但不支持范围查询
全文索引
专门用于全文本搜索,基于倒排索引实现
R-Tree索引
用于空间数据类型,如地理坐标,支持空间范围查询
物理存储角度
聚集索引
索引结构决定了数据的物理存储顺序。在InnoDB存储引擎中,主键索引就是聚集索引
非聚集索引
也称为二级索引或辅助索引
其数据存储与实际数据行分开,包含指向数据行的指针
字段特性角度
主键索引(PRIMARY KEY)
用于定义表的主键,每个表只能有一个主键,不允许重复和NULL值
唯一索引(UNIQUE)
保证索引列的值是唯一的,允许有零或一个NULL值
普通索引(INDEX 或 KEY)
基本的索引类型,无特殊限制
全文索引(FULLTEXT)
用于全文本搜索
空间索引(SPATIAL)
用于地理空间数据的索引,如GIS数据
字段个数角度
单列索引
在单个字段上创建的索引
联合索引
在多个字段上创建的索引,可以同时考虑多个字段的值
回表
回表指的是当使用非聚集索引(即二级索引)查询时,存储引擎首先根据非主键索引找到相关记录的位置,然后使用主键索引(或聚集索引)再次
查找具体的行数据
查找具体的行数据
这是因为非主键索引通常只包含索引列和主键列的信息,而不包含完整行数据。回表操作增加了额外的I/O开销,降低了查询性能
覆盖索引
覆盖索引指的是当一个索引包含了查询所需的所有列时,MYSQL可以不必访问表的数据行,直接从索引中获取数据,这样的索引被称为覆盖索引
覆盖索引避免了所谓的“回表”操作,减少了磁盘I/0次数,从而提高了查询性能
索引下推
在不使用索引下推的情况下,当使用非主键索引(即二级索引)查询时,存储引擎会先找到满足索引条件的行,然后再将这些行的主键返回给服务
器层,服务器层再根据主键进行回表操作,以获取完整的行数据并进一步筛选
器层,服务器层再根据主键进行回表操作,以获取完整的行数据并进一步筛选
而索引下推则允许存储引擎在扫描索引的过程中,直接过滤掉不满足査询条件的行,从而减少了服务器层需要处理的数据量,提高了查询效率
调优
调优手段
刷盘调优
exists与in
IO优化
只查询需要的字段,减少数据传输量
索引优化
查询字段全走索引,减少回表查询
降低锁粒度、降低锁持有时间(insert与update)
普通索引,联合索引(B+树,数据页,explain)
批量操作,单线程变多线程,分页查询
数据量大:分库分表,冷热分离
高性能分页
鉴于LIMIT offset,size的效率太低,结合之前一些实践,采用了一种改良的”滚动翻页”的实现方式
SELECT * FROM tableName WHERE id > #{lastBatchMaxId} [其他条件] ORDER BY id [ASC|DESC](这里一般选用ASC排序) LIMIT ${size}
重点感觉是id > #{lastBatchMaxId}
一般的分页方案使用的LIMIT offset,size需要先查询,后截断。而本方案每次查询都是最终的结果集
可广泛应用于各种批量查询、数据同步、数据导出以及数据迁移等等场景
limit深分页优化
游标法
理想情况
id是可以通过动态计算算出来
必须知道前一页id的位置在哪里
可以达到常数级别
延迟id关联法
场景
深度分页很多时候是直接跳过,而不是一页一页过去
先通过内连接子查询,只关联出来id
大概能够提升30%~70%
Cursor流式查询
内连接,左外连接,右外连接,全连接(union 与union all)
union all效率高于union
三大范式
列的原子性
行的唯一性
非主键值直接依赖主键列,不能间接依赖
分库分表
分库分表标准:
单表数据超过1000W(阿里推荐行数超过500W)
OR
单表数据文件(.ibd)超过20G(阿里推荐单表容量超过2GB)
OR
单表数据文件(.ibd)超过20G(阿里推荐单表容量超过2GB)
己知的生产环境中使用单表存储数据可以达到10亿条
分库分表方案
单库单表
原始方案
单库多表
有效缩小磁盘扫描范围
多库多表
提供数据库并行处理能力
面试题
1.怎么分?
2.为什么这么分?
3.是否有必要?
4.有考虑哪些地方?分布式事务,跨库关联,数据库成本
分库方案
范围分表(1~10、10~20、20~30)
适合场景
适合日志(只关心最近产生的)
问题
尾部热点、数据偏斜、资源浪费问题
Hash分表(id % 3 =X)
适合场景
总量基本一致,分散相对均匀;适合档案系统(根据档案编号提取档案)
问题
存在范围查询跨库问题
分库分表问题:
1.分布式事务
2.跨库Join问题(a.程序先查A表,再循环查B表;b.MyCat与Sharding支持两表跨库Join)
3.跨节点分页查询(单节点取n条,在程序中合并运算取Top N,或者走ES)
4.全局主键Id问题(雪花算法)
5.扩容问题
范围分表容易扩容,但存在尾部热点问题
Hash分表极难扩容,建议改为一致性Hash,但迁移难度较大
架构
主主(双向同步)
主从
目标
热备份、多活、故障切换、负载均衡、读写分离
问题
主从延迟
从库性能差、从库压力大、从库过多
解决方案
选择主从一样的机器、一主多从、从库3~5为宜
大事务、慢SQL
解决方案
1.减少批量处理
2.优化慢SQL语句
3.实时性要求高的业务强制走主库(或者加上事务注解,也可以强制走主库)
网络延迟
解决方案
升级带宽
低版本MySQL(低版本是单线程复制)
解决方案
换高版本MySQL
主从同步数据不一致(主从数据复制方式)
1.异步复制(客户端commit后不等从库返回就将结果返回客户端)
2.半同步复制(设置应答从库数量)
3.组复制(基于Paxos协议状态机复制,组内大多数同意)
主备(故障切换)
日志
redo log(重做日志、事务持久性)
undo log(回滚日志、事务原子性、保存相反的操作,进行insert会存delete)
bin log(二进制日志、备份数据)
relay log(中继日志)
底层实现
WAL(预先日志持久化):在持久化一个数据页之前,先将内存中相应的日志页持久化
SQL隔离级别
读未提交(Read Uncommitted),简称为RU
存在脏读
存在不可重复读
存在幻读
读已提交(Read Commited),简称为RC
Oracle默认隔离级别
大厂通常会将隔离级别改为RC
提升并发
可能会遇到幻读问题
脏读
已解决
存在不可重复读
存在幻读
可重复读(Repeatable Read),简称为RR
MySQL默认隔离级别
脏读
已解决
不可重复读
已解决
幻读
(MVCC)基本解决
串行化(serializable)
脏读
已解决
不可重复读
已解决
幻读
已解决
加锁读
无论哪种隔离级别,都不允许脏写的情况发生
并发问题
脏写
覆盖了他人的“草稿”(未提交的修改)
事务A 修改了 另一个 未提交 事务B 修改过 的数据
示例
1.事务A开启事务
2.事务B开启事务,将id=1的记录name值(王麻子)改为了李四
3.事务A将id=1的记录name值改为张三,并提交
4.事务B进行回滚,此时name值又变为王麻子
此时从事务A的角度来看,发生了脏写
脏读
读到了别人回滚前的临时数据
事务A 读取了 已经被事务B 更新 但还 没被提交 的数据;之后若事务B 回滚 ,事务A 读取 的内容就是 临时且无效 的
示例
1.事务A,B各开启了个事务
2.事务B开启事务,将id=1的记录name值(王麻子)改为了李四,还没提交
3.事务A去查询id=1的记录,如果读到name的值为李四,而事务B稍后进行了回滚,那么事务A相当于读到了一个不存在的数据
此时从事务A的角度来看,发生了脏读
RU会出现
不可重复读
读过的行被其他人UPDATE了
事务A 读取 了一个字段,然后事务B 更新 了该字段并提交,之后事务A 再次读取 同一字段, 值就不同 了
RC会出现
幻读
读过的范围被别人INSERT了
事务A从一个表 读取 了一个字段,然后事务B在该表中 插入并提交 了一些新纪录,之后事务A 再次读取 同一张表,就会多出几行
DELETE
删除变少,不算幻读(算 不可重复读)
RR会出现
多出来几行的叫幻影记录
MVCC(多版本并发控制)
MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读,而这个读指的就是快照读, 而非当前读。当前读实际上是一种加锁的操作,是悲观锁的实现。而MVCC本质是采用乐观锁思想的一种方式。
主要针对RC 和 RR 隔离级别
使用READ UNCOMMITTED隔离级别的事务,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了。
使用SERIALIZABLE隔离级别的事务,InnoDB规定使用加锁的方式来访问记录。
使用READ COMMITTED和REPEATABLE READ隔离级别的事务,都必须保证读到已经提交了的事务修改过的记录。假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,
核心问题就是需要判断一下版本链中的哪个版本是当前事务可见的,这是ReadView要解决的主要问题。
底层实现
显示事务(BEGIN...COMMIT)
会有trx_id
creator_trx_id为当前事务id
普通SELECT(autocommit=1)
没有trx_id
creator_trx_id为0
行格式中的隐藏字段
row_id
trx_id
每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id赋值给trx_id隐藏列
roll_pointer
回滚指针
每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
Undo Log版本链
多版本体现
Read View
并发控制,管理由Read View来
是事务A在使用MVCC机制进行快照读操作时产生的读视图。
当事务启动时,会生成数据库系统当前的一个快照,InnoDB为每个事务构造了一个数组,用来记录并维护系统当前活跃事务的ID(“活跃"指的就是,启动了但还没提交)。
组成
creator_trx_id
创建这个 Read View 的事务 ID。
说明:只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会为事务分配事务id,否则在一个只读事务中的事务id值都默认为 0 。
trx_ids
表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。
InnoDB全局事务系统(全实例范围),而非以表为单位
事务id也是在整个数据库中唯一且递增的
up_limit_id
活跃的事务中最小的事务 ID。
low_limit_id
表示生成ReadView时系统中应该分配给下一个事务的id值。low_limit_id 是系统最大的事务id值,这里要注意是系统中的事务id,需要区别于正在活跃的事务ID。
注意
low_limit_id并不是trx_ids中的最大值,事务id是递增分配的
比如,现在有id为 1 ,2 , 5 这三个事务,之后id为 5 的事务提交了。那么一个新的读事务在生成ReadView时,trx_ids就包括 1 和 2 ,up_limit_id的值就是 1 ,low_limit_id的值就是 6。
规则
如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
如果被访问版本的trx_id属性值小于ReadView中的up_limit_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。
如果被访问版本的trx_id属性值大于或等于ReadView中的low_limit_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
如果被访问版本的trx_id属性值在ReadView的up_limit_id和low_limit_id之间,那就需要判断一下trx_id属性值是不是在trx_ids列表中。
如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问。
如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
举例
假设:
• 当前 Read View 生成时系统中有活跃事务 {101, 103, 105}
• 所以:
• m_ids = [101, 103, 105]
• m_up_limit_id = 101
• m_low_limit_id = 106
• m_creator_trx_id = 0(普通读)
现在你读到一行数据:
• 该行的 trx_id = 102(由事务102更新过)
判断过程👇
1. trx_id == m_creator_trx_id → 102 == 0 ❌
2. trx_id < m_up_limit_id → 102 < 101 ❌
3. trx_id >= m_low_limit_id → 102 >= 106 ❌
4. trx_id in m_ids → 102 in [101,103,105] ❌
5. 既不在活跃集合,也不超过上限,说明事务102已提交 ✅
→ 版本可见。
获取时机
在隔离级别为读已提交(Read Committed)时
一个事务中的每一次 SELECT 查询都会重新获取一次Read View
注意,此时同样的查询语句都会重新获取一次 Read View,这时如果 Read View 不同,就可能产生不可重复读或者幻读的情况。
当隔离级别为可重复读时
就避免了不可重复读
这是因为一个事务只在第一次 SELECT 的时候会获取一次 Read View,而后面所有的 SELECT 都会复用这个 Read View
实现过程
1.首先获取事务自己的版本号,也就是事务 ID
2.获取ReadView
3.查询得到的数据,然后与ReadView事务版本进行比较
4.如果不匹配,就从undo Log中获取历史快照
5.最后返回符合规则的数据
未避免幻读场景
在可重复读的情况下InnoDB很大程度上避免幻读现象,但并不是完全解决。
快照读(普通 select 语句),是通过 MVCC 方式解决了幻读。
在MVCC情况下,事务A先开启事务,然后普通 select 一个不存在的值,比如id = 3;这时候是查不到的
然后事务b开启事务insert一个id等于3的记录。接着事务A去update id = 3的记录,再去select id = 3,就可以查出那条记录
当前读(select ... for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读。
另外一种场景是事务A先普通select,然后事务B插入一条数据并提交,这时事务A再来个当前读 select for update就也能查出刚才插入的记录
快照读
又叫一致性读,读取的是快照数据。
普通SELECT查询
MySQL里除了普通查询时快照读,其他都是当前读,比如update、insert、delete
当前读
执行前都会查询最新版本的数据,然后再做进一步的操作
当前读读取的是记录的最新版本(最新数据,而不是历史版本的数据),读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。加锁的 SELECT,或者对数据进行增删改都会进行当前读。
操作
SELECT * FROM user LOCK IN SHARE MODE
共享锁(S锁)
其他事务可以加共享锁,但不能加排他锁
其他事务可以读取但不能修改被锁定的行
多个事务可以同时持有同一行的共享锁
SELECT ... for update
排他锁(X锁)
其他事务不能对锁定的行加任何锁(包括共享锁和排他锁)
其他事务不能修改被锁定的行
其他事务的普通SELECT可以读取(取决于隔离级别),但不能加锁读取
update
排他锁(X锁)
insert
排他锁(X锁)
delete
排他锁(X锁)
应用场景
库存扣减
领取任务
抢红包/抢号段
保证一次只处理一个订单
锁
按照对数据操作类型
1.读锁/共享锁(S)
2.写锁/排他锁(X)
锁粒度划分
表级锁
1.表级S锁、X锁
2.意向锁
意向共享锁(Intention Shared Lock, lS)
意向共享锁不与任何行级锁冲突,它可以与其他事务的意向共享锁或意向排他锁共存
意向排他锁(Intention Exclusive Lock,lX)
意向排他锁也不与任何行级锁冲突,但它阻止其他事务获取该表的表级共享锁(S锁)或排他锁(X锁)
3.自增锁
4.MDL锁(元数据锁)
行级锁
1.记录锁
记录锁是有S锁和X锁之分的,称之为S型记录锁和X型记录锁。
S型记录锁
当一个事务获取了一条记录的S型记录锁后,其他事务也可以继续获取该记录的S型记录锁,但不可以继续获取X型记录锁;
X型记录锁
当一个事务获取了一条记录的X型记录锁后,其他事务既不可以继续获取该记录的S型记录锁,也不可以继续获取X型记录锁。
2.间隙锁
间隙锁锁定的是索引记录之间的“间隙”,也就是可能插入新行的位置。
在可重复读隔离级别下,当执行一个范围査询时,InnoDB会锁定满足査询条件的记录,同时还会锁定条件范围内的所有潜在间隙,以防止其他事
务在此间隙中插入新的行
3.临键锁
临键锁(Next-Key锁) = 记录锁 + 间隙锁,大致可以这样理解
事务的 update 语句中 where 是等值查询,并且 id 是唯一索引,所以只会对 id = 1 这条记录加锁,因此,事务 B 的更新操作并不会阻塞。
但是,在 update 语句的 where 条件没有使用索引,就会全表扫描,于是就会对所有记录加上 next-key 锁(记录锁 + 间隙锁),相当于把整个表锁住了。
作用
把“当前记录”以及“它前面的空档(间隙)”一起锁住。
4.插入意向锁
在插入操作开始之前,事务会在目标索引的适当位罟上申请插入意向锁。
这样做的目的是告诉其他事务,当前事务有意向在特定的索引间隙中插入
一行数据
一行数据
这个锁不会阻塞其他插入意向锁,
但是如果有人在这段间隙上加了“间隙锁”或“临键锁”,
那你这次插入就得等别人释放锁了
页级锁
对待锁态度
悲观锁
实现
添加同步锁,让线程串行执行
优点
简单粗暴
缺点
性能一般
适用场景
写入操作比较多
乐观锁
实现
版本法
CAS法
如果是库存,只要大于0就行;其他场景可能还是得判断数据是否有变化来判断实现.还可以考虑分批加锁,分段锁思想
优点
性能好
缺点
存在成功率低的问题
适用场景
读操作比较多,一定程度提高并发
按照加锁方式
1.隐式锁
2.显式锁
其他
1.全局锁
2.死锁
新增时死锁
排查
SHOW ENGINE INNODB STATUS;
从------------------------
LATEST DETECTED DEADLOCK
------------------------
开始复制,到
*** WE ROLL BACK TRANSACTION
或者日志结束的那一段。
修改时死锁
可以改为根据id修改
说明
Record/GAP/Next-Key 是锁的物理范围
S锁 / X锁 是锁的逻辑属性(共享 or 排他)
二者可以组合使用
结构
锁所在事务信息
不论是表锁还是行锁,都是在事务执行过程中生成的,哪个事务生成了这个锁结构,这里就记录这个事务的信息。
此锁所在的事务信息在内存结构中只是一个指针,通过指针可以找到内存中关于该事务的更多信息,比方说事务id等。
索引信息
对于行锁来说,需要记录一下加锁的记录是属于哪个索引的。这里也是一个指针。
表锁/行锁信息
表锁结构和行锁结构在这个位置的内容是不同的
表锁特有结构
记载着是对哪个表加的锁,还有其他的一些信息
表信息
其他信息
行锁特有结构
Space lD
记录所在表空间
Page Number
记录所在页号
n_bits
对于行锁来说,一条记录就对应着一个比特位,一个页面中包含很多记录,用不同的比特位来区分到底是哪一条记录加了锁。为此在行锁结构的末尾放置了一堆比特位,这个n_bits属性代表使用了多少比特位。
n_bits的值一般都比页面中记录条数多一些。主要是为了之后在页面中插入了新记录后 也不至于重新分配锁结构
type_mode
这是一个 32 位的数,被分成了lock_mode、lock_type和rec_lock_type三个部分
锁的模式(lock_mode)
占用低 4 位
可选的值
LOCK_IS(十进制的 0 ):表示共享意向锁,也就是IS锁。
LOCK_IX(十进制的 1 ):表示独占意向锁,也就是IX锁。
LOCK_S(十进制的 2 ):表示共享锁,也就是S锁。
LOCK_X(十进制的 3 ):表示独占锁,也就是X锁。
LOCK_AUTO_INC(十进制的 4 ):表示AUTO-INC锁。
在InnoDB存储引擎中,LOCK_IS,LOCK_IX,LOCK_AUTO_INC都算是表级锁的模式,LOCK_S和LOCK_X既可以算是表级锁的模式,也可以是行级锁的模式。
锁的类型(lock_type)
占用第 5 ~ 8 位,不过现阶段只有第 5 位和第 6 位被使用
LOCK_TABLE(十进制的 16 ),也就是当第 5 个比特位置为 1 时,表示表级锁。
LOCK_REC(十进制的 32 ),也就是当第 6 个比特位置为 1 时,表示行级锁。
行锁的具体类型(rec_lock_type)
使用其余的位来表示
只有在lock_type的值为LOCK_REC时,也就是只有在该锁为行级锁时,才会被细分为更多的类型
类型
LOCK_ORDINARY(十进制的 0 ):表示next-key锁。
LOCK_GAP(十进制的 512 ):也就是当第 10 个比特位置为 1 时,表示gap锁。
LOCK_REC_NOT_GAP(十进制的 1024 ):也就是当第 11 个比特位置为 1 时,表示正经记录锁。
LOCK_INSERT_INTENTION(十进制的 2048 ):也就是当第 12 个比特位置为 1 时,表示插入意向锁。其他的类型:还有一些不常用的类型我们就不多说了。
is_waiting属性呢?
基于内存空间的节省,所以把is_waiting属性放到了type_mode这个 32位的数字中
LOCK_WAIT(十进制的 256 ) :当第 9 个比特位置为 1 时,表示is_waiting为true,也就是当前事务尚未获取到锁,处在等待状态;
当这个比特位为 0 时,表示is_waiting为false,也就是当前事务获取锁成功。
其他信息
为了更好的管理系统运行过程中生成的各种锁结构而设计了各种哈希表和链表
一堆比特位
如果是行锁结构的话,在该结构末尾还放置了一堆比特位,比特位的数量是由上边提到的n_bits属性 表示的。
InnoDB数据页中的每条记录在记录头信息中都包含一个heap_no属性,伪记录Infimum的 heap_no值为 0 ,Supremum的heap_no值为 1 ,之后每插入一条记录,heap_no值就增 1 。锁结构最后的一堆比特位就对应着一个页面中的记录,一个比特位映射一个heap_no,即一个比特位映射到页内的一条记录。
监控
show status like 'innodb_row_lock%';
Innodb_row_lock_current_waits:当前正在等待锁定的数量;
Innodb_row_lock_time:从系统启动到现在锁定总时间长度;(等待总时长)
Innodb_row_lock_time_avg:每次等待所花平均时间;(等待平均时长)
Innodb_row_lock_time_max:从系统启动到现在等待最常的一次所花的时间;
Innodb_row_lock_waits:系统启动后到现在总共等待的次数;(等待总次数)
select * from performance_schema.INNODB_TRX
查询正在被锁阻塞的sql语句
select * from performance_schema.data_locks
查询所有锁的情况
select * from performance_schema.data_lock_waits
查询锁等待情况
MySQL并发事务访问相同记录
读-读情况
读-读情况,即并发事务相继读取相同的记录。读取操作本身不会对记录有任何影响,并不会引起什么问题,所以允许这种情况的发生。
写-写情况
写-写情况,即并发事务相继对相同的记录做出改动。
在这种情况下会发生脏写的问题,任何一种隔离级别都不允许这种问题的发生。所以在多个未提交事务相继对一条记录做改动时,需要让它们排队执行,这个排队的过程其实是通过锁来实现的。这个所谓的锁其实是一个内存中的结构,在事务执行前本来是没有锁的,也就是说一开始是没有锁结构和记录进行关联的
当一个事务想对这条记录做改动时,首先会看看内存中有没有与这条记录关联的锁结构,当没有的时候就会在内存中生成一个锁结构与之关联。比如,事务T1要对这条记录做改动,就需要生成一个锁结构与之关联
在锁结构里有很多信息,为了简化理解,只把两个比较重要的属性拿了出来:
trx信息:代表这个锁结构是哪个事务生成的。
is_waiting :代表当前事务是否在等待。
当事务T1改动了这条记录后,就生成了一个锁结构与该记录关联,因为之前没有别的事务为这条记录加锁,所以is_waiting属性就是false,我们把这个场景就称之为获取锁成功,或者加锁成功,然后就可以继续执行操作了。
在事务T1提交之前,另一个事务T2也想对该记录做改动,那么先看看有没有锁结构与这条记录关联,发现有一个锁结构与之关联后,然后也生成了一个锁结构与这条记录关联,不过锁结构的is_waiting属性值为true ,表示当前事务需要等待,我们把这个场景就称之为获取锁失败,或者加锁失败
在事务T1提交之后,就会把该事务生成的锁结构释放掉,然后看看还有没有别的事务在等待获取锁,发现了事务T2还在等待获取锁,所以把事务T2对应的锁结构的is_waiting属性设置为false,然后把该事务对应的线程唤醒,让它继续执行,此时事务T2就算获取到锁了。
几种说法
不加锁
意思就是不需要在内存中生成对应的锁结构,可以直接执行操作。
获取锁成功,或者加锁成功
意思就是在内存中生成了对应的锁结构,而且锁结构的is_waiting属性为false,也就是事务 可以继续执行操作。
获取锁失败,或者加锁失败,或者没有获取到锁
意思就是在内存中生成了对应的锁结构,不过锁结构的is_waiting属性为true,也就是事务 需要等待,不可以继续执行操作。
读-写或写-读情况
读-写或写-读,即一个事务进行读取操作,另一个进行改动操作。这种情况下可能发生脏读、不可重复读、幻读的问题。
各个数据库厂商对SQL标准的支持都可能不一样。比如MySQL在REPEATABLE READ隔离级别上就已经解决了幻读问题。
元数据
指关于数据库对象(如表、列、索引、视图、存储过程等)的信息(名字、数据类型、长度、精度等),这些信息描述了这些对象的结构和属性
面试题
exists与in
如查询的两表大小相当,那么用in和exists效率差别不大。
如两表中一小,一大,则子查询表大的用exists,子查询表小的用in。
not in 和not exists
如查询语句使用了not in 那么内外表都进行全表扫描,无法用到索引;
而not exists的子查询依然能用到表上的索引。所以无论哪个表大。用not exists都比not in要快。
还有取决于DBA
IN
看某个值是否存在集合里面,可以是静态列表,也可以是子查询
像是在好友列表里找人,看看他在不在列表里
先将子查询的结果集执行出来,缓存好,然后再和外层的每一行去比较
适合子查询数据量小,且外层表有索引的场景
EXISTS
判断一个子查询有没有结果,有结果就为true
像是在门口喊人,有人吗?有人就行,不关心那个人是谁
短路特性
EXISTS 在子查询中一旦找到符合条件的一条记录,就立刻返回 TRUE
外层表逐行驱动子查询,遇到结果立即停止
适合外层表数据量小且子查询有索引的场景
具体情况,还是需要MySQL选择的执行计划;MySQL可能会重写SQL语句
MySQL自增ID用完了该怎么办?
查看ID类型,如是int,把int改为bigint(-2的63次方~2的63次方-1);应该就不会用完了
数据库Hash索引与B+树索引区别
1.Hash索引不能进行范围查询
2.Hash索引不支持联合索引的最左索引
3.Hash索引不支持Order By排序
4.InnoDB不支持Hash索引
数据库内存占用高
排查思路
确定mysql具体的占用内存大小,通过命令: cat/proc/Mysql进程ID/status 查看。重点看VmRSS参数
查看当前配置的pool_size
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
查看performance schema内存占用量:show engine performance schema status;
排査MySQL为当前session会话分配的内存
查看session级别的buffer和cache占用内存大小。
show variables where variable name in ('binlog_cache_size','join_bufer_size', 'read_buffer_size','read_md_buffer_size', 'sort_bufer_size')
show variables where variable name in ('binlog_cache_size','join_bufer_size', 'read_buffer_size','read_md_buffer_size', 'sort_bufer_size')
查看当前活跃的连接数
SELECT * FROM information_schema.processlist WHERE command != 'Sleep'
SELECT * FROM information_schema.processlist WHERE command != 'Sleep'
排查当前临时表占用内存情况
查看tmp_table_size临时表配置的内存大小:线程级别参数,实际限制从 tmp_table_size 和 max_heap_table_size 两个变量的的值中取较小值。show variables where variable name in ('tmp_table_size",'max_heap_table_size”)
查看当前是否有临时表产生
show global status like '%tmp%'
show global status like '%tmp%'
解决思路
1.继续加大内存(如果参数调无可调时选择);
2.修改减小innodb_buffer_pool_size参数(牺牲一定innodb性能);
3.排查消耗内存的慢SQL,及时优化;
4.检査相关session参数是否设置合理,比如join_buffer_size、query_cache_size是否设置过大;
5.使用gdb回收内存碎片(生产环境谨慎操作)
gdb --batch --pid 'pidof mysqld' --ex'cal malloc trim(0)';
6.对MySQL进程配置jemalloc内存管理模块;
7.配置读写分离,将读操作应用到从库,减少对主库的影响;
数据库CPU占用过高
排查思路
1.确定mysql CPU占用
top -H -p <mysqld 进程 id>
pidstat -t-p <mysqld 进程 id>1 5
2.查看mysql 当前连接
show full processlist;
select *from information schema.processlist;
3.确定是否存在异常语句
是否有大量的未执行查询语句
是否有锁等情况
是否存在慢查询
是否有正在执行的 DML 语句
是否有执行了很长时间的 DDL语句
解决思路
1.优化 SQL,从逻辑上优化 SQL,降低 SQL 复杂度,降低 MySQL 执行成本
2.对 where、join、max()、min()、order by、group by 等子句用到的字段,创建相应的索引。
3.二级索引的正确使用。
4.参数优化
增加 tmp table size 大小
增加 max heap table size 大小
调整 key buffer size、table cache、innodb buffer pool size、innodb log file size 参数大小
5.检查 MySQL 连接数当前使用是否超过限制,如果超出限制,而且之前的连接没有得到释放,那新的连接肯定会连接不到,造成连接延迟,影响效率。
6.MySQL的 timeout 参数设置问题
VARCHAR(100) 和 VARCHAR(10)有什么区别
两者在存储相同内容时,实际数据占用空间相同;但前者因字段定义更大,在查询涉及排序操作时,可能导致更高的内存开销和性能下降,尤其当涉及文件排序(File Sort)时更容易触发磁盘排序。
但是仔细详细这其中存在设计问题,不应该用VARCHAR(100)字段排序呀
VARCHAR(100)中100说明只能最多存放100个字符,不是字节哈
要点
字段声明大小影响排序内存估算
MySQL 在排序时默认按最大长度计算,即使实际存储远小于定义值
排序性能受字段宽度制约
更大定义导致更多内存消耗,易超出 `sort_buffer` 内存上限,引发磁盘临时文件排序
文件排序模式差异
若字段总长超 `max_length_for_sort_data`(默认4096字节),MySQL 使用双路排序(先排序主键再回表),降低效率
单路 vs 双路排序 —— 小字段可走单路排序,全字段加载进内存高效;大字段需双路,增加 IO 开销
核心提醒
不是「能存多少」的问题,而是「会影响系统如何处理数据」——字段过大埋下隐性性能陷阱
排序方式
索引排序
filesort文件排序
内存排序
单路排序
双路排序
磁盘文件排序
VARCHAR类型的最大长度限制
理论上,最多能存储 65,535 个字节
但是还要考虑到其他额外字段信息
比如是否允许为NULL,如果允许,需要有一个字节存放是否为NULL。
VARCHAR本身还需要使用 1 到 2 个额外字节来存储数据的实际长度
存放字符数限制:取决于字符集
latin1这样单字节的字符集
知识点
MyBatisPlus按 in 的参数顺序排序
queryWrapper.last("ORDER BY FIELD(id," + Joiner.on(",").join(idList) + ")");
占比比较高的复杂SQL查询
CPU配置应该着重注意
数据量大,但是SQL都比较简单
大缓冲区、大容量就行
不注重强事务
可以考虑MyISAM引擎
但是MyISAM在备份方面不太行
支持事务,牺牲了很多性能
MyISAM全是二级索引
避坑
Update语句:给字段值+后缀
update A set B = B + "_fix" where id = 1000
这句SQL可能会异常:Error Code: 1292. Truncated incorrect DOUBLE value: '_fix' 0.016 sec
将字符串 "_fix" 附加到 request_id 字段时,而 MySQL 默认将 request_id 字段视为数值类型(如 INT、FLOAT、DOUBLE 等),因为它正在尝试执行数值加法操作(request_id + "_fix")。
在 SQL 中,你不能直接将字符串和数值相加,除非数值被显式地转换为字符串。在你的情况下,你需要确保 request_id 在与字符串 "_fix" 连接之前被转换为字符串
要实现给某个字段加字符串后缀,可以使用 SQL 的字符串连接函数 CONCAT() 。
update test_db.idempotent_request_tab set request_id = CONCAT(request_id, '_fix')
where id = 1000;
慢SQL排查
排查
开启MySQL慢查询日志
SHOW VARIABLES LIKE '%slow query log%';
如果slow query log的值为OFF,则表示未开启慢査询日志功能。
需要修改/etc/my.cnf文件,将如下两个参数设置为对应的值
slow query log = ON# 开启慢查询日志
long query time =1# 指定查询执行时间阈值(单位:秒)
long query time =1# 指定查询执行时间阈值(单位:秒)
重启MySQL服务。
分析MySQL慢查询日志
使用mysqldumpslow
mysqldumpslow [-a] [-d] [-g] [-s order-type] [-t] [log file ...]
explain SQL
优化步骤
0.先运行看看是否真的很慢,注意设置SQL NO CACHE
1.where条件单表查,锁定最小返回记录表,这句话的意思是把査询语句的where都应用到表中返回的记录数最小的表开始音起,单表每个字段分
别查询,看哪个字段的区分度最高
别查询,看哪个字段的区分度最高
2.explain查看执行计划,是否与1预期一致(从锁定记录较少的表开始查询)
3.order by limit 形式的sql语句让排序的表优先査
4.了解业务方使用场景
5.加索引时参照建索引的几大原则
6.观察结果,不符合预期继续从0分析
MySQL服务器数据处理能力
影响因素
1.硬件配置(CPU核心数、内存大小)
2.数据库表结构(字段数量、字段类型)
3.查询类型(全表扫描vs索引查询)
4. MySQL配置参数(如innodb_buffer_pool_size)
5.并发查询数量
基础配置分析
假设服务器配置为4核CPU,8GB内存。
在8GB总内存中,假设约6GB可用于查询操作。其余内存被MySQL自身进程和操作系统占用。
在理想情况下,这样的配置可能可以一次性处理约2-4GB的数据。但这只是一个粗略估计,实际处理能力还需要考虑其他因毒
示例场景分析
四字段表分析
1.假设条件
表字段:
字段1(ID):BIGINT(8字节)
字段2(单号):VARCHAR(50)(平均27字节,包括长度存储)
字段3(日期):DATETIME(20)(9字节)
字段4(状态):TINYINT(2)(1字节)
字段1(ID):BIGINT(8字节)
字段2(单号):VARCHAR(50)(平均27字节,包括长度存储)
字段3(日期):DATETIME(20)(9字节)
字段4(状态):TINYINT(2)(1字节)
行开销:20字节
可用内存:4GB
2.计算过程
1.每行数据大小=8+27+9+1+20= 65 字节
2.可用内存= 4GB =4,294,967.296 字节
3.理论最大行数=4,294,967,296/65≈66,076,420行
3.实际估算
考虑到实际环境因素,估算实际可处理行数为理论值的50-60%
实际估算行数≈33,000,000-39,600,000行
测试方法
1.创建与目标结构相同的测试表
2.逐步增加数据量
3.执行典型查询并监控性能
4.记录性能明显下降时的数据量
5.分析结果并与理论估算对比
性能优化建议
1.增加服务器内存
2.优化查询语句
3.合理设置索引
4.调整MySQL配置参数
5.控制并发查询数量
操作类型
DDL
Data Definition Language,数据定义语言
用于定义和管理数据库结构
如创建、修改、删除表、索引等
常见命令
CREATE(创建)
ALTER(修改)
新增列
SET FOREIGN_KEY_CHECKS=0;
ALTER TABLE `oa`.`employee_profile` ADD COLUMN `normal_users` varchar(500) NULL COMMENT '当天在职人员集合' AFTER `dimission_detail`;
SET FOREIGN_KEY_CHECKS=1;
ALTER TABLE `oa`.`employee_profile` ADD COLUMN `normal_users` varchar(500) NULL COMMENT '当天在职人员集合' AFTER `dimission_detail`;
SET FOREIGN_KEY_CHECKS=1;
修改列类型
SET FOREIGN_KEY_CHECKS=0;
ALTER TABLE `oa`.`employee_profile` MODIFY COLUMN `normal_users` longtext NULL COMMENT '当天在职人员集合' AFTER `dimission_detail`;
SET FOREIGN_KEY_CHECKS=1;
ALTER TABLE `oa`.`employee_profile` MODIFY COLUMN `normal_users` longtext NULL COMMENT '当天在职人员集合' AFTER `dimission_detail`;
SET FOREIGN_KEY_CHECKS=1;
SET FOREIGN_KEY_CHECKS=0;和SET FOREIGN_KEY_CHECKS=1;
是在关闭和开启MySQL的外键约束检查
DROP(删除)
删除整个表结构和数据
DROP TABLE table_name [RESTRICT|CASCADE];
TRUNCATE(清空表)
保留表结构,删除所有数据
TRUNCATE TABLE table_name;
MySQL中TRUNCATE不能回滚,PostgreSQL中可以
DML
Data Manipulation Language,数据操纵语言
用于对表中的数据进行操作
常见命令
INSERT(插入)
UPDATE(更新)
DELETE(删除)
DQL
Data Query Language,数据查询语言
用于查询数据库中的数据
核心命令是SELECT
可通过条件、排序、聚合等方式获取所需数据
DCL
Data Control Language,数据控制语言
用于管理数据库的访问权限和安全
常见命令
GRANT(授予权限)
REVOKE(收回权限)
TCL
Transaction Control Language,事务控制语言
用于管理数据库事务,确保数据操作的一致性
常见命令
COMMIT(提交事务)
ROLLBACK(回滚事务)
SAVEPOINT(设置保存点)
Spring
IOC
DI
基于注解的3种常规注入方式
基于属性注入(常用)
基于 setter 方法注入(较少见)
基于构造器注入(Spring 官方推荐)
优势
强制依赖清晰可见,避免 Null 依赖,符合不可变对象设计
避免循环依赖隐患
依赖完整性验证
AOP
切面执行顺序
执行顺序
环绕-->before/after-->afterThrow/afterReturn(afterThrow和afterReturn只会执行一个)
无异常
aroudBefore...
before...
add....
aroudAfter...
after...
afterReturn...
before...
add....
aroudAfter...
after...
afterReturn...
有异常无环绕
before...
after...
afterThrow...
after...
afterThrow...
有异常有环绕(与无异常情况执行顺序一致)
aroudBefore...
before...
aroudAfter...
after...
afterReturn...
before...
aroudAfter...
after...
afterReturn...
应用场景
用于日志打印
用于全局异常处理拦截
返回值统一处理
多数据源切换
Bean
Bean生命周期
1.Bean容器找到Spring配置文件中Bean的定义;
2.构造方法推断(执行无参构造方法,创建Bean)
3.Bean容器利用java 反射机制实例化Bean;
4.Bean容器为实例化的Bean设置属性值;
5.如果Bean实现了BeanNameAware接口,则执行setBeanName方法;
6.如果Bean实现了BeanClassLoaderAware接口,则执行setBeanClassLoader方法;
7.如果Bean实现了BeanFactoryAware接口,则执行setBeanFactory方法;
8.如果Bean实现了ApplicationContextAware接口,则执行setApplicationContext方法;
9.如果加载了BeanPostProcessor相关实现类,则执行postProcessBeforeInitialization方法;
10.如果Bean定义初始化方法(@PostConstruct注解执行、配置init-method、实现了InitializingBean接口),则执行定义的初始化方法;
11.如果加载了BeanPostProcessor相关实现类,则执行postProcessAfterInitialization方法;
12.当要销毁这个Bean时,如果自定义了销毁方法(@PreDestroy注解执行、配置destroy-method、实现了DisposableBean接口),则执行定义的销毁方法。
2.构造方法推断(执行无参构造方法,创建Bean)
3.Bean容器利用java 反射机制实例化Bean;
4.Bean容器为实例化的Bean设置属性值;
5.如果Bean实现了BeanNameAware接口,则执行setBeanName方法;
6.如果Bean实现了BeanClassLoaderAware接口,则执行setBeanClassLoader方法;
7.如果Bean实现了BeanFactoryAware接口,则执行setBeanFactory方法;
8.如果Bean实现了ApplicationContextAware接口,则执行setApplicationContext方法;
9.如果加载了BeanPostProcessor相关实现类,则执行postProcessBeforeInitialization方法;
10.如果Bean定义初始化方法(@PostConstruct注解执行、配置init-method、实现了InitializingBean接口),则执行定义的初始化方法;
11.如果加载了BeanPostProcessor相关实现类,则执行postProcessAfterInitialization方法;
12.当要销毁这个Bean时,如果自定义了销毁方法(@PreDestroy注解执行、配置destroy-method、实现了DisposableBean接口),则执行定义的销毁方法。
1.创建前准备
2.创建实例
3.依赖注入
4.容器缓存
5.销毁实例
2.创建实例
3.依赖注入
4.容器缓存
5.销毁实例
口诀
找(Bean定义)
构(构造方法推断)
例(实例化Bean))
属(设置属性)
扩(扩展接口)
初(初始化方法)
毁(销毁)
实例化
1.Bean容器找到Spring配置文件中Bean的定义;
2.构造方法推断(执行无参构造方法,创建Bean)
3.Bean容器利用java 反射机制实例化Bean;
属性赋值
目的
让Bean具备业务所需依赖
4.Bean容器为实例化的Bean设置属性值;
注入@Resource @Autowired @Value
回调Aware方法
目的
让Bean能访问到容器或者上下文资源
5.如果Bean实现了BeanNameAware接口,则执行setBeanName方法;
6.如果Bean实现了BeanClassLoaderAware接口,则执行setBeanClassLoader方法;
7.如果Bean实现了BeanFactoryAware接口,则执行setBeanFactory方法;
8.如果Bean实现了ApplicationContextAware接口,则执行setApplicationContext方法;
初始化前
9.如果加载了BeanPostProcessor相关实现类,则执行postProcessBeforeInitialization方法;
初始化
10.如果Bean定义初始化方法(@PostConstruct注解执行、配置init-method、实现了InitializingBean接口),则执行定义的初始化方法;
初始化后
11.如果加载了BeanPostProcessor相关实现类,则执行postProcessAfterInitialization方法;
使用
销毁
当要销毁这个Bean时,如果自定义了销毁方法(@PreDestroy注解执行、配置destroy-method、实现了DisposableBean接口),则执行定义的销毁方法。
Bean作用域
singleton
loC 容器中只有唯一的 bean 实例。Spring 中的 bean 默认都是单例的,是对单例设计模式的应用。
prototype
每次获取都会创建一个新的 bean 实例。也就是说,连续 getBean() 两次,得到的是不同的 Bean 实例。
request(仅 Web 应用可用)
每一次 HTTP 请求都会产生一个新的 bean(请求 bean),该 bean 仅在当前 HTTP request 内有效
session(仅 Web 应用可用)
每一次来自新 session 的 HTTP 请求都会产生一个新的 bean(会话 bean),该 bean 仅在当前 HTTP session 内有效。
application/global-session(仅 Web 应用可用)
每个 Web 应用在启动时创建一个 Bean(应用 Bean),该 bean 仅在当前应用启动时间内有效。
websocket(仅 Web 应用可用)
每一次 WebSocket 会话产生一个新的 bean。
配置Bean作用域
xml方式
注解方式
生命周期中组件的扩展
ApplicationContextAware接口
应用场景
a)获取其他Bean:
当一个Bean需要动态地获取其他Bean时,可以使用ApplicationContext.
applicationContext.getBeanOfType(FlowService.class)
继承ApplicationContextAware接口,然后再利用上下文通过类型获取所有同类型的类,放入Map中
b)获取环境变量:
可以通过ApplicationContext获取配置文件中的属性值。
c)发布事件:
ApplicationContext提供了发布事件的能力,实现该接口的Bean可以方便地发布自定义事件
d) 设置、获取资源:
可以使用ApplicationContext加载类路径或文件系统中的资源文件。
e)国际化支持:
ApplicationContext提供了获取国际化消息的方法。
过滤器和拦截器
执行顺序
监听器 > 过滤器 > 拦截器 > servlet执行 > 拦截器 > 过滤器 > 监听器
过滤器(Filter)
按照 web.xml 文件中配置的过滤器链执行过滤器
具体顺序
根据过滤器在 web.xml 中的注册顺序来确定的
基于 Servlet 规范实现的
作用
主要用于 Servlet 容器层面的请求处理,可以对 URL、HTTP 请求参数等进行拦截和处理
拦截器(Interceptor)
经过 Servlet 容器的过滤器链,请求就会被传递给 Spring MVC 的 DispatcherServlet。DispatcherServlet 会按照配置在 Spring 上下文中的拦截器链进行处理
Spring MVC 框架提供的一种处理机制
具体顺序
由拦截器在配置文件中声明的顺序决定的
作用
主要用于 Spring MVC 层面的请求处理,可以拦截和处理 Handler 方法的调用。
例如验证用户身份、记录访问日志等
实现HandlerInterceptor接口,就能添加拦截器
preHandle
在控制器方法执行前调用,如果返回true,则方法继续执行
如果返回false,则方法不会执行,拦截器会继续执行后续的postHandle和afterCompletion方法
postHandle
在控制器方法执行后,但在视图渲染前调用
afterCompletion
在整个请求完成,即视图渲染结束后调用。
实现WebMvcConfigurer接口的addInterceptors方法,就能使拦截器生效
循环依赖
通过三级缓存解决
解决在单例作用域下,通过setter/字段注入产生的循环依赖问题
三级:存储半成品Bean,未被引用的对象
二级:存储半成品,被其他Bean引用的Bean
一级:存储完整的Bean对象
二级:存储半成品,被其他Bean引用的Bean
一级:存储完整的Bean对象
过程
依靠引入"中间态(已经实例化还没初始化的半成品)"的概念
过程:
A实例化,未初始化。放入三级缓存中
A属性注入过程中,依赖B。发现B没有
B实例化,未初始化。放入三级缓存。
B属性注入过程中,发现依赖A。会依次从一级到三级找;从三级缓存中取出A注入,并且将A放入二级缓存,删掉三级中的A。
B初始化完成执行完其他流程得到一个完整的Bean,并且将B直接放入一级缓存中。
A继续完成B的属性注入,执行完其他流程形成一个完整的Bean,放入一级缓存
A实例化,未初始化。放入三级缓存中
A属性注入过程中,依赖B。发现B没有
B实例化,未初始化。放入三级缓存。
B属性注入过程中,发现依赖A。会依次从一级到三级找;从三级缓存中取出A注入,并且将A放入二级缓存,删掉三级中的A。
B初始化完成执行完其他流程得到一个完整的Bean,并且将B直接放入一级缓存中。
A继续完成B的属性注入,执行完其他流程形成一个完整的Bean,放入一级缓存
异常
1.scope=prototype 类型的循环依赖(原型多例导致)
2.无法解决构造函数注入(添加@Lazy解决)
3.被 @Async 增强的 Bean 的循环依赖
普通的 AOP 代理都是通过 AbstractAutoProxyCreator 来生成代理类的,其实现了 SmartInstantiationAwareBeanPostProcessor,而@Async 标记的类是通过 AbstractAdvisingBeanPostProcessor 来生成代理的,其没有实现
普通的 AOP 代理都是通过 AbstractAutoProxyCreator 来生成代理类的,其实现了 SmartInstantiationAwareBeanPostProcessor,而@Async 标记的类是通过 AbstractAdvisingBeanPostProcessor 来生成代理的,其没有实现
限制场景
构造函数注入导致的循环依赖
涉及非单例作用域(如prototype)的循环依赖
涉及到AOP代理或多个后置处理器(BeanPostProcessor)的循环依赖
存疑
解决方案
使用@Lazy注解
对于那些不需要在应用程序启动时立即初始化的Bean,可以使用@Lazy注解。这将使Bean在第一次被请求时才初始化,而不是在容器启动时。
构造函数注入
Spring官方推荐
@RequiredArgsConstructor
构造函数注入不能解决所有类型的循环依赖,尤其是当两个Bean在构造函数中互相依赖时,Spring将无法解决这种循环依赖,因为构造函数在
Bean初始化之前调用,此时Bean还未被创建。
Bean初始化之前调用,此时Bean还未被创建。
构造函数注入代码示例
@RequiredArgsConstructor在循环依赖时会启动失败,但这正是它的优势——早期暴露设计问题,促使你写出更健康的代码
重新设计架构、重构代码
Spring应用到的设计模式
工厂、模板、代理、单例
注解
项目启动注解
@SpringBootApplication(标记当前类为引导启动类(加载很多启动配置))
缓存注解
@EnableCaching
开启基于注解的缓存,使用 @EnableCaching 标注在 SpringBoot主启动类上
@Cacheable
定义
Spring会在其被调用后将其返回值缓存起来,以保证下次利用同样的参数来执行该方法时可以直接从缓存中获取结果,而不需要再次执行该方法
keyGenerator:key 生成器。 key 和 keyGenerator 二选一使用,官方说 key 和 keyGenerator 参数是互斥的,同时指定两个会导致异常。
keyGenerator:key 生成器。 key 和 keyGenerator 二选一使用,官方说 key 和 keyGenerator 参数是互斥的,同时指定两个会导致异常。
参数
value/cacheNames
缓存的名称,在 spring 配置文件中定义,必须指定至少一个
作为缓存的部分前缀使用
key
缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合
condition
缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存
unless
否定缓存。当 unless 指定的条件为 true ,方法的返回值就不会被缓存
sync
是否使用异步模式。默认是方法执行完,以同步的方式将方法返回的结果存在缓存中
@CachePut
与@Cacheable不同的是使用@CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。
@CacheEvict
标注在需要清除缓存元素的方法或类上。当标记在一个类上时表示其中所有的方法执行都会触发缓存的清除操作;allEntries是否需要清除缓存中的所有元素。当指定allEntries为true时,Spring Cache将忽略指定的key。需要清除所有的元素,这比单独清除元素更高效
value
key
condition
allEntries
allEntries是boolean类型,表示是否需要清除缓存中的所有元素。默认为false,表示不需要。当指定了allEntries为true时,Spring Cache将忽略指定的key。有的时候我们需要Cache一下清除所有的元素,这比一个一个清除元素更有效率。
beforeInvocation
清除操作默认是在对应方法成功执行之后触发的,即方法如果因为抛出异常而未能成功返回时也不会触发清除操作。使用beforeInvocation可以改变触发清除操作的时间,当我们指定该属性值为true时,Spring会在调用该方法之前清除缓存中的指定元素。
@Caching
可让我们在一个方法或者类上同时指定多个Spring Cache相关的注解。其拥有三个属性:cacheable、put和evict,分别用于指定@Cacheable、@CachePut和@CacheEvict
常用注解
引用类
@Autowired
@Inject
@Reference
@Resource
@Inject
@Reference
@Resource
声明类
@Bean
@Component
@Qualifier
@Primary
@Controller
@RestController
@Service
@Repository
@Component
@Qualifier
@Primary
@Controller
@RestController
@Service
@Repository
功能参数类
@RequestMaping
@RequstParam
@RequestBody
@PathVariable
@RequstParam
@RequestBody
@PathVariable
配置类
@Transactional
@Configuration
@Aspect
@ScanScope
@Value
@Configuration
@Aspect
@ScanScope
@Value
@Order 控制加载顺序,越小越先加载
@Scope(作用域,如:单例,原型,请求,会话等)
面试题
SpringBoot启动流程
1.主方法调用
2.创建SpringApplication实例
3.设置SpringApplication属性
4.初始化RunListeners
5.准备环境
6.创建ApplicationContext
7.注册Bean
8.准备ContextLoader
9.刷新ApplicationContext
10.调用Runners
11.发布ApplicationReadyEvent
12.启动嵌入式服务器
2.创建SpringApplication实例
3.设置SpringApplication属性
4.初始化RunListeners
5.准备环境
6.创建ApplicationContext
7.注册Bean
8.准备ContextLoader
9.刷新ApplicationContext
10.调用Runners
11.发布ApplicationReadyEvent
12.启动嵌入式服务器
1.在`main`方法中,我们调用`SpringApplication.run0`方法,这实际上是Spring Boot应用程序的启动入口。
2.`SpringApplication'类负青整个Spring Boot应用的启动过程。在`run0方法内部,首先会创建一个`SpringApplication'实例。
3.在创建`SpringApplication`对象后,会进行一系列的设置,包括读取命令行参数、设罟主类、设环境等。
4.SorinaApplication`会调用"initializeRunlisteners0方法,初始化监听器,这些监听器会监听Spring Boot应用的启动过程中的不同阶段。5.`SpringApplication`会调用`prepareEnvironment0方法,用来准备Spring Boot的运行环境,包括读取系统属性、配置文件等。
6.随后,'SpringApplication`会调用`createApplicationContext0方法,创建`ApplicationContext实例。默认情况下,如果是web应用,则创建`AnnotationConfigEmbeddedWebApplicationContext;非web应用则创建`AnnotationConfigApplicationContext.
7.创建完`ApplicationContext之后,`SpringApplication“会注册一些默认的Bean,例如`CommandLineRunner、ApplicationArguments等。
8.`SpringApplication`接下来会调用`prepareContextLoader0)方法,准备`ContextLoader,它负责加载配置类和初始化自动配置。9.`SpringApplication^会调用`refreshContext0方法,刷新ApplicationContext,在这个过程中,Spring loC容器开始加载和初始化Bean。
10.在`ApplicationContext刷新完毕后,`SpringApplication会执行`calRunners0方法,调用"ApplicationRunner和`CommandlineRunner接口的实现类,这些类可以在应用启动后执行自定义的逻辑。
11.最后,`SpringApplication会发布`ApplicationReadyEvent事件,表明Spring Boot应用已经启动完成并准备好接收请求,
12.如果是Web应用,嵌入式服务器(如Tomcat、Jetty或Undertow)会在`ApplicationContext'刷新完成后启动。
2.`SpringApplication'类负青整个Spring Boot应用的启动过程。在`run0方法内部,首先会创建一个`SpringApplication'实例。
3.在创建`SpringApplication`对象后,会进行一系列的设置,包括读取命令行参数、设罟主类、设环境等。
4.SorinaApplication`会调用"initializeRunlisteners0方法,初始化监听器,这些监听器会监听Spring Boot应用的启动过程中的不同阶段。5.`SpringApplication`会调用`prepareEnvironment0方法,用来准备Spring Boot的运行环境,包括读取系统属性、配置文件等。
6.随后,'SpringApplication`会调用`createApplicationContext0方法,创建`ApplicationContext实例。默认情况下,如果是web应用,则创建`AnnotationConfigEmbeddedWebApplicationContext;非web应用则创建`AnnotationConfigApplicationContext.
7.创建完`ApplicationContext之后,`SpringApplication“会注册一些默认的Bean,例如`CommandLineRunner、ApplicationArguments等。
8.`SpringApplication`接下来会调用`prepareContextLoader0)方法,准备`ContextLoader,它负责加载配置类和初始化自动配置。9.`SpringApplication^会调用`refreshContext0方法,刷新ApplicationContext,在这个过程中,Spring loC容器开始加载和初始化Bean。
10.在`ApplicationContext刷新完毕后,`SpringApplication会执行`calRunners0方法,调用"ApplicationRunner和`CommandlineRunner接口的实现类,这些类可以在应用启动后执行自定义的逻辑。
11.最后,`SpringApplication会发布`ApplicationReadyEvent事件,表明Spring Boot应用已经启动完成并准备好接收请求,
12.如果是Web应用,嵌入式服务器(如Tomcat、Jetty或Undertow)会在`ApplicationContext'刷新完成后启动。
SpringBoot的Starter加载
特点
本身是一个依赖聚合,包含了一系列的常用依赖
原理
SpringBoot的“约定大于配置”
自动配置Auto-Configuration机制
@SpringBootApplication(标记当前类为引导启动类(加载很多启动配置))
子注解
@EnableAutoConfiguration
最关键
@AutoConfigurationPackage --> 作用是将主配置类所在的包下面所有的组件都扫描到Spring容器中。
@Import(EnableAutoConfigurationImportSelector.class) --> 自动配置:通过源码查看其会加载源码中的META-INF/spring.facotries文件,这个文件中包含了很多配置类。通过加载这个文件里面的类信息,
然后这些类会去自动加载配置。
@Import(EnableAutoConfigurationImportSelector.class) --> 自动配置:通过源码查看其会加载源码中的META-INF/spring.facotries文件,这个文件中包含了很多配置类。通过加载这个文件里面的类信息,
然后这些类会去自动加载配置。
@SpringBootConfiguration(包含configuration注解,表示当前类为配置类)
@ComponentScan(扫描文件同级包以及子包中的Bean)
SpringBoot会扫描组件,加载自动配置,启动嵌入式服务器
Spring Boot starter 是一组预定义的 Maven 或 Gradle 依赖,它们将一组相关的库捆绑在一起,并自动进行配置。每个starter 都针对特定的功
能或库,简化了项目的依赖管理和配置过程。
加载过程
1.添加依赖:在项目中引入所需的starter依赖。
2.应用启动:SpringBoot应用启动,触发自动配置机制。
3.扫描classpath
查找所有jar包中的META-INF/spring.factories文件
Spring3 中使用META-NF/spring/org.springframework,boot.autoconfigure.AutoConfiguration.imports
4.加载配罟类:根据spring.factories文件中的声明,加载自动配置类。
5,条件判断:使用条件注解(如@ConditionalOnClass)决定哪些自动配置类生效。
6.创建和配置Bean:将符合条件的配置类中定义的Bean创建并添加到Spring容器。
常用的 Starter 示例
spring-boot-starter-web:提供了构建Web应用程序所需的一切,包括Spring Web MVc,Tomcat(作为默认的嵌入式Serviet容器),以及对Thymeleaf模板引擎的支持。
spring-boot-starter-data-jpa:用于集成JPA(Java Persistence AP!)和数据库访问。它包括了JPA的实现(如Hibemnate),事务管理,以及Spring Data JPA的扩展.
spring-boot-starter-security:为Spring Security提供了自动配置,使你能够轻松地为Web应用添加认证和授权功能。
spring-boot-starter-test:提供了一组测试相关的依赖,包括JUnit,Hamcrest、Mockito、Spring Test以及Tomcat或Jetty的嵌入式容器。
spring-boot-starter-aop:引入了Aspect]的依赖,支持面向切面编程。
spring-boot-starter-validation:基于JSR 303或JSR 349(Hibernate Validator)的验证框架,用于数据验证。
spring-boot-starter-amgp:用于与AMOP (Advanced Message Queuing Protocol)消息队列(如RabbitMO)集成,
spring-boot-starter-cache:支持常见的缓存抽象,如Ehcache、Caffeine和Redis.
spring-boot-starter-data-jpa:用于集成JPA(Java Persistence AP!)和数据库访问。它包括了JPA的实现(如Hibemnate),事务管理,以及Spring Data JPA的扩展.
spring-boot-starter-security:为Spring Security提供了自动配置,使你能够轻松地为Web应用添加认证和授权功能。
spring-boot-starter-test:提供了一组测试相关的依赖,包括JUnit,Hamcrest、Mockito、Spring Test以及Tomcat或Jetty的嵌入式容器。
spring-boot-starter-aop:引入了Aspect]的依赖,支持面向切面编程。
spring-boot-starter-validation:基于JSR 303或JSR 349(Hibernate Validator)的验证框架,用于数据验证。
spring-boot-starter-amgp:用于与AMOP (Advanced Message Queuing Protocol)消息队列(如RabbitMO)集成,
spring-boot-starter-cache:支持常见的缓存抽象,如Ehcache、Caffeine和Redis.
autowire和resource区别
a是spring提供,先根据类型匹配类型
r是Java提供,先根据bean名称匹配,可解耦
BeanFactory与ApplicationContext
BeanFactory和ApplicationContext是Spring的两大核心接口,都可以当做Spring的容器。其中ApplicationContext是BeanFactory的子接口
ApplicationContext,它是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入。 ApplicationContext启动后预载入所有的单实例Bean,通过预载入单实例bean ,确保当你需要的时候,你就不用等待,因为它们已经创建好了
相对于基本的BeanFactory,ApplicationContext 唯一的不足是占用内存空间。当应用程序配置Bean较多时,程序启动较慢
controller和service是在IOC同一个容器中吗?
Spring同一个
SpringBoot同一个
SpringMVC不一定(父子容器)
SpringBoot同一个
SpringMVC不一定(父子容器)
Spring源码需要有调整的地方,但它在jar包中应该怎么做
同包/同名类、javassist hotfix
SpringBoot是怎么找到并加载有@Service标注的类
扫描指定包路径下的所有类并加载,通过反射的方式得知该类是否被标注了@Service
super.save(),this.save(),save()区别
在使用MyBatis-Plus的Service的情况下,super.save()、this.save()和save()通常是等效的,因为Service类中这些方法都是通过继承关系来的。
super.save(): 调用父类(通常是ServiceImpl)的save()方法,执行对应的持久化操作。
this.save(): 调用当前类(你自己写的Service类)的save()方法,这个方法可以被你重写,以提供特定的业务逻辑。
this.save(): 调用当前类(你自己写的Service类)的save()方法,这个方法可以被你重写,以提供特定的业务逻辑。
在不重写这些方法的情况下,它们通常具有相同的实现。然而,这并不是一种良好的实践,因为你的Service类有可能会有特定的业务逻辑,如果你在以后的项目迭代中需要在保存操作中添加一些额外的逻辑,最好是重写save()方法,而不是直接调用基类的方法。
使用super.save()或者this.save()的目的是为了让代码更清晰,易读,以及为将来的扩展和修改提供更大的灵活性。
特殊的业务场景,需要在系统启动时执行某些任务
实现接口
CommandLineRunner
ApplicationRunner
目的
配置文件的加载
缓存预热
数据库的初始化等等操作
DCL(双重校验锁)
主要用于 单例和延迟加载 场景
解决对象初始化的线程安全
在多线程环境下,保证某个对象只被初始化一次,同时减少加锁带来的性能开销
说明
外面的check是为了性能考虑,加锁是消耗资源的,可以避免不必要的加锁,第二个是保证正确性的。当然,当你check的成本高于加锁成本或者并发很高,大部分都会有冲突的情况可以考虑不要最外面的check
比如并发情况下,对本地某个文件添加一行,你用jvm的synchronized锁,但是check依赖本地文件的读取是否写入这行,这种情况下check成本就远高于加锁成本了
这种复杂check的场景,check本身就很耗时间
外层check的通过的概率如果很高的话,也是可以去掉check的,比如两台机器定时整点同时写db一条数据,两台机器本地时间误差很小的情况下,很大概率在同一时间检查是没有这条数据的,这种可以考虑去掉
分布式
Gateway
面试题
与Nginx相比,Gateway优势
如果只是做负载均衡、转发、限流等通用逻辑,那么Nginx就够了(LB)
优势是:可以在网关侧做很多逻辑。如登录验证、风控(业务网关)
Nacos
问题
出现端口被占用或没开放端口
原因
Nacos2.0版本相比1.X新增了gRPC的通信方式,因此需要增加2个端口。新增端口是在配置的主端口(server.port)基础上,进行一定偏移量自动生成。
端口
9848
偏移量1000
客户端gRPC请求服务端端口,用于客户端向服务端发起连接和请求
9849
偏移量1001
服务端gRPC请求服务端端口,用于服务间同步等
自定义ID生成策略
方式一:声明为 Bean 供 Spring 扫描注入
@Component
public class CustomIdGenerator implements IdentifierGenerator {
@Override
public Long nextId(Object entity) {
//可以将当前传入的class全类名来作为bizKey,或者提取参数来生成bizKey进行分布式Id调用生成.
String bizKey = entity.getClass().getName();
//根据bizKey调用分布式ID生成
long id = ....;
//返回生成的id值即可.
return id;
}
}
public class CustomIdGenerator implements IdentifierGenerator {
@Override
public Long nextId(Object entity) {
//可以将当前传入的class全类名来作为bizKey,或者提取参数来生成bizKey进行分布式Id调用生成.
String bizKey = entity.getClass().getName();
//根据bizKey调用分布式ID生成
long id = ....;
//返回生成的id值即可.
return id;
}
}
方式二:使用配置类
@Bean
public IdentifierGenerator idGenerator() {
return new CustomIdGenerator();
}
public IdentifierGenerator idGenerator() {
return new CustomIdGenerator();
}
方式三:通过 MybatisPlusPropertiesCustomizer 自定义
@Bean
public MybatisPlusPropertiesCustomizer plusPropertiesCustomizer() {
return plusProperties -> plusProperties.getGlobalConfig().setIdentifierGenerator(new CustomIdGenerator());
}
public MybatisPlusPropertiesCustomizer plusPropertiesCustomizer() {
return plusProperties -> plusProperties.getGlobalConfig().setIdentifierGenerator(new CustomIdGenerator());
}
特性
唯一性
高可用
高性能
单调递增,趋势递增性
安全性
Actuator
实现了应用的监控与管理
Spring MVC
MVC
一种软件架构设计模式
全称是Model-View-Controler(模型-视图-控制器)
核心思想
将应用程序的业务逻辑、用户界面(UI)和控制逻辑解耦(分离),分派给三个不同的角色,从而提高代码的可维护性、可复用性和可扩展性。
Model(模型)
职责
代表应用程序的数据和业务逻辑(业务规则、状态)。它负责数据的获取、存储、处理和验证,
特点
不关心数据如何显示(View)或如何被操作控制(Controller)。
当模型状态发生变化时,通常会通知观察者(通常是视图,在观察者式下)
例子
一个用户对象(包含姓名、邮箱等属性)、一个处理订单计算的服务类、一个与数据库交互的0DAO(数据访问对象)类。
说明
Service层的逻辑
实现业务规则,属于Model的核心部分
Service返回的对象
承载Model的数据
DAO/Repository
数据访问层,属于Model的底层支持
View (视图)
职责
负责将模型数据展示给用户。它决定了用户看到的内容和格式。
特点
从模型获取需要显示的数据
不包含业务逻辑。
可以存在多个不同的视图来展示同一个模型(例如,同一个数据既可以在网页上显示,也可以在手日
机App上显示,或者在Excel表格中显示)。当模型数据改变时,视图通常会收到通知并更新自己的显示。
例子
一个显示用户信息的网页(HTML)、一个商品列表的UI界面、一个报表页面。
Controller(控制器)
职责
作为 Model和 View 之间的中介,接收用户的输入(事件/请求),调用相应的 Model来处理业务逻辑,然后选择并更新对应的 View。
特点
响应用户交互(点击按钮、提交表单、页面请求等)
解读用户输入(比如表单数据、URL参数)。
调用模型对象执行操作或获取数据。
根据模型处理的结果,决定将哪个视图呈现给用户
例子
处理用户登录请求的控制器方法:接收用户名密码 ->调用模型(用户服务)验证 ->根据验证结果决定跳转到主页(成功)或返回登录页显示错误信息(失败)。
Spring MVC是Spring团队对MVC的实现
Servlet
Java EE规范中定义的一个规范(接口)
怎么响应用户请求
HttpServlet
DispatcherServlet 是 Spring MVC 的核心入口,本质上就是一个 HttpServlet
是Servlet接口的一个抽象实现类,专门用于处理HTTP协议的请求。
传统开发模式
每个接口都需要对应一个servlet对象,然后从HttpRequest中解析参数,处理后,再将响应写入到HttpResponse
有很多重复代码
Spring MVC实现方式
通过前端控制器设计模式和注解驱动的编程模型,实现了高度简化和集中化的请求处理机制
关键组件
DispatcherServlet(前端控制器)
统一入口
接收所有HTTP请求(在 web.xml 中配置)
中央调度
协调各组件工作(唯一需要配置的 Servlet)
替代了传统模式中每个接口对应一个 Servlet 的方式
HandlerMapping(处理器映射)
根据 URL 找到对应的控制器方法
注解驱动
@RequestMapping, @GetMapping, @PostMapping 等
假如客户端请求/api/users/{id}
HandlerMapping 并非返回UserApiController 对象和涉及到它的拦截器,而是返回HandlerExecutionChain对象
HandlerExecutionChain handler = mapping.getHandler(request);
HandlerExecutionChain(处理器执行链)
负责流程控制,管理整个生命周期
类比
计算机主板(整合CPU、内存等)
职责
执行链容器:管理处理器和拦截器的执行流程
包含
处理器
HandlerMethod
负责方法执行,只关注方法调用环节
类比
CPU(具体执行计算的部件)
职责
处理器核心:封装实际要执行的控制器方法
包含
目标方法
UserApiController.getUser()
目标 bean
UserApiController 实例
方法参数信息
方法注解信息
HandlerMethodArgumentResolver (参数解析器)
HandlerMethod 自动完成的动作
1. 将路径变量 "123" 转换为 Long 类型
2. 将转换后的值 123L 绑定到方法参数
3. 调用 userApiController.getUser(123L)
注意
@PathVariable在HandlerMapping阶段处理,而@RequestBody是在HandlerAdapter阶段处理
@RequestParam
处理跨越了 HandlerMapping 和 HandlerAdapter 两个阶段
HandlerMapping 阶段 (初步检查和信息收集)
目的
这是一个早期失败(fail-fast)机制。如果连必需的请求参数都没有,那么这个方法根本就不是处理这个请求的合适候选者,直接按“找不到处理器”处理更高效,避免进入后续更复杂的处理流程(如读取请求体)。它确保了只有参数条件满足的方法才会被选为处理器。
HandlerAdapter 阶段 (核心解析、类型转换和绑定)
但它的核心解析和绑定发生在 HandlerAdapter 阶段
具体是 RequestMappingHandlerAdapter 调用 HandlerMethodArgumentResolver 时
@Valid(或 @Validated)注解
校验处理发生在 HandlerAdapter 阶段
依赖前提
参数绑定/数据转换必须先完成(例如 @RequestBody 反序列化成功)
拦截器列表
包含的拦截器
全局注册的拦截器(所有请求都经过)
LoggingInterceptor(全局)
匹配路径的拦截器(如配置了 /api/** 路径)
AuthInterceptor(匹配 /api/** 路径)
自定义映射策略添加的拦截器
HandlerExecutionChain 包含 HandlerMethod
HandlerAdapter(处理器适配器)
实际调用控制器方法
自动处理参数绑定和返回值转换
其中的参数与返回值Response都不是HTTP协议规定的那种格式,所以需要Adapter将Controller输出转变为一个JSON,写到HttpResponse里面去
RequestMappingHandlerAdapter
核心职责
将HTTP请求体(Request Body)转换成控制器方法参数所期望的Java对象。
如果方法或者类上面标注了@ResponseBody注解,那么就会拿到RequestResponseBodyMethodProcessor
通过它来实现将Controller的输出变为一个JSON
Controller方法标注 @ResponseBody 后,其输出的原始格式就是你在代码中 return 语句后面写的那个东西在内存中的表示形式(一个对象实例、一个集合对象、一个字符串对象、一个基本类型值等)。
@ResponseBody 注解本身并不改变Controller方法返回的数据本身。它只是告诉Spring框架:“不要把这个返回值当作视图名去解析,也不要把它放进模型里。请把这个返回值对象本身作为HTTP响应的主体内容,并使用配置好的 HttpMessageConverter(如 MappingJackson2HttpMessageConverter)把它转换成客户端需要的格式(如JSON)。”
存在多个 HandlerMethodReturnValueHandler 实现
在 Spring MVC 中,当存在多个 HandlerMethodReturnValueHandler 实现时,Spring 通过一套优先级匹配机制来确定使用哪个处理器。
处理器注册与排序
Spring 将所有可用的 HandlerMethodReturnValueHandler 实现注册到一个 处理器链(HandlerMethodReturnValueHandlerComposite) 中。这个处理器链内部维护着一个有序列表(通常按优先级排序)。
默认情况下,Spring Boot 会自动注册约 15 个内置处理器(如处理 @ResponseBody 的 RequestResponseBodyMethodProcessor,处理视图名的 ViewNameMethodReturnValueHandler 等)
匹配过程:supportsReturnType()
当 Controller 方法执行完毕后,Spring 会遍历处理器链中的每一个处理器,调用其 supportsReturnType() 方法,检查是否支持当前返回类型。第一个匹配成功的处理器将被使用。
在 Spring MVC 中,HandlerAdapter 实现类(如 RequestMappingHandlerAdapter)的 afterPropertiesSet() 方法中调用 setReturnValueHandlers() 是设置该适配器使用的返回值处理器列表。
重要的类
DispatcherServlet
主要职责
文件上传解析(MultipartResolver)
通过HandlerMapping查找处理器(Controller方法)
通过HandlerAdapter执行处理器方法
处理视图渲染(ViewResolver)
处理异常(HandlerExceptionResolver)
RequestMappingHandlerMapping
处理器映射器
属于HandlerMapping的实现
作用
建立请求与处理方法的映射关系
核心流程
扫描@Controller类,解析类和方法上的@RequestMapping,@GetMapping等注解
生成RequestMappingInfo对象(包含URL模式,HTTP方法等匹配条件)
注册HandlerMethod到映射表,请求时匹配最合适的处理器
RequestMappingHandlerAdapter
作用
适配并且执行HandlerMethod
核心功能
调用HandlerMethodArgumentResolver解析方法参数
执行反射调用方法
通过HandlerMethodReturnValueHandler处理返回值(如JSON转换,视图渲染)
支持数据绑定,验证及类型转换
请求流程
1.首先用户发送请求-->Dispatcherservet
前端控制器(Dispatcherservet)收到请求后自己不进行处理,而是委托给其他的解析器进行处理,作为统一访问点,进行全局的流程控制
2. DispatcherServlet-->HandlerMapping
HandlerMapping 将会把请求映射为 HandlerExecutionChain 对象(包含一个Handler 处理器(页面控制器)对象、多个Handlernterceptor 拦载器)对象,通过这种策略模式,很容易添加新的映射策略
3. DispatcherServet-->HandlerAdapter
HandlerAdapter 将会把处理器包装为适配器,从而支持多种类型的处理器,即适配器设计模式的应用,从而很容易支持很多类型的处理器:
4.HandlerAdapter-->处理器功能处理方法的调用
HandlerAdapter 将会根据适配的结果调用真正的处理器的功能处理方法,完成功能处理:并返回一个ModelAndView 对象(包含模型数据、逻辑视图名):
5.ModelAndView 的逻辑视图名-->ViewResoiver
ViewResoiver 将把逻辑视图名解析为具体的View,通过这种策略模式,很容易再换其他视图技术;
6.View-->渲染
View 会根据传进来的Model 模型数据进行渲染,此处的Model 实际是一个Map 数据结构,因此很容易支持其他视图技术
7.返回控制权给DispatcherServlet
由DispatcherServlet 返回响应给用户,到此一个流程结束。
核心优势
零配置参数绑定
自动转换请求参数到方法参数(基本类型/对象)
支持 JSON/XML 等格式的自动序列化/反序列化
声明式编程模型
统一异常处理
Event事件
什么是Event机制
在Spring Boot中,事件(Event)机制是一种允许组件之间进行解耦通信的机制。通过发布事件,其他对该事件感兴趣的组件可以订阅并响应该
事件。Spring的事件机制基于 ApplicationEvent 类和 Applicationlistener 接口,提供了强大的事件处理能力。
事件。Spring的事件机制基于 ApplicationEvent 类和 Applicationlistener 接口,提供了强大的事件处理能力。
如何使用Event机制
定义事件
首先,你需要定义自己的事件类,通常继承自ApplicationEvent类。
发布事件
在需要发布事件的地方,使用ApplicationEventPublisher接囗来发布你定义的事件。通常,ApplicationContext实现了ApplicationEventPublisher接口,所以你可以从Spring容器中注入ApplicationEventPublisher。
监听事件
要监听事件,你需要创建一个事件监听器,实现ApplicationListener接口,或者使用@EventListener注解
4.自动装配事件监听器
如果使用@Eventlistener注解,Spring会自动检测并装配事件监听器。但是,如果你实现Applicationlistener接口,你需要确保监听器被Spring管理,通常是通过@Component或其他类似的注解。
使用@EnableAsync支持异步事件处理
如果你想异步处理事件,可以在配置类上添加@EnableAsync注解,并确保你的事件处理器方法也标注了@Async注解。
Spring Boot 中的内置事件
ApplicationStartingEvent:在运行开始时发送
ApplicationEnvironmentPreparedEvent:在环境已知时发送,但在创建上下文之前。
ApplicationPreparedEvent:在刷新之前发送,已知上下文,但尚未刷新。
ApplicationStartedEvent:在上下文已刷新且所有应用和命令行运行器已被调用之后发送
ApplicationReadyEvent:在应用准备好服务请求时发送
ApplicationFailedEvent:在启动过程中发生异常时发送
事务
ACID
Atomicity原子性
定义
一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节
事务在执行过程中发生错误,会被恢复(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
实现原子性的关键,是当事务回滚时能够撤销所有已经成功执行的sql语句
实现方式
MySQL
undo log 回滚日志
记录事务修改数据前的旧版本数据副本
PostgresQL
WAL (Write-Ahead Logging) 日志
在事务提交前,将所有变更写入WAL日志文件,事务失败时可通过WAL日志回滚操作。
SQLite
WAL (Write-Ahead Logging) 日志
事务的更改首先写入WAL日志文件,
事务提交时将更改应用到数据库文件,失败时丢弃未提交的更改。
事务提交时将更改应用到数据库文件,失败时丢弃未提交的更改。
Rollback Journal
在事务开始前,将要修改的数据页的副本写入回滚日志,事务失败时恢复数据到事务开始前的状态
MongoDB
单文档操作的原子性
确保单个文档的写操作是原子的
多文档事务
从 MongoD8 4.0 开始支持副本集中的多文档事务,从 4.2 开始支持分片集群中的多文档事务,确保事务内的所有操作要么全部提交,要么全部回滚。
Consistency一致性
执行事务前后,数据保持一致
一致性是事务追求的最终目标
事务执行前后,数据库都必须从一个一致的状态,变成另一个一致的状态。
数据库层面的保障
事务执行完之后,不能破坏数据库的完整性约束(如主键唯一、外键引用、字段约束、金额不为负数等)
即使中途出错、回滚,数据库也应该保持一致
前面提到的原子性、持久性和隔离性,其实都是为了保证数据库状态的一致性
应用层面保障
在业务中,比如购买操作只扣除用户的余额,不减库存,肯定无法保证状态的一致。
实现方式
MySQL
外键约束、事务日志(Binary Log)和检查约束:确保数据一致性。
PostgresQL
SQLite
MongoDB
Isolation隔离性
数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致(并发访问数据库时,一个事务不被其他事务所干扰)
锁 和 MVCC 保证隔离性
实现方式
MySQL
事务隔离级别:READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ(默认)和 SERIALIZABLE.
多版本并发控制(MVCC):实现快照读和当前读,
PostgresQL
事务隔离级别:READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ和 SERIALIZABLE.
多版本并发控制(MVCC):在事务开始时获取数据库的快照,使用行级锁(Row-Level Locking)防止并发冲突。
SQLite
事务隔离级别:READ UNCOMMITTED和SERIALIZABLE。
数据库级别的锁和 MVCC:使用共享锁和排他锁管理事务的并发访问
MongoDB
文档级别的锁:更新文档时使用文档级锁。
使用锁机制和MVCC
Durability持久性
事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失
实现方式
MySQL
依靠 redo log 实现持久性
PostgresQL
使用WAL确保持久性
SQLite
当事务提交时,所有更改被写入磁盘,或记录在WAL日志中
MongoDB
MongoDB的事务一旦提交,更改即变为永久性。
事务传播特性
REQUIRED:支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择(默认)。[前后调用都坐在同一辆车上,要炸全都完了]
REQUIRES_NEW:新建事务,如果当前存在事务,把当前事务挂起。[前后调用坐两辆车,前车炸不影响]
SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行。[别人有作业就抄,没有就算了]
NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。[不写作业,如果别人写了,就给他锁起来]
MANDATORY:支持当前事务,如果当前没有事务,就抛出异常。[别人有作业就抄,没有就举报给老师]
强制性的
NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。[不写作业,如果别人写了,就举报给老师]
NESTED:支持当前事务,如果当前事务存在,则执行一个嵌套事务,如果当前没有事务,就新建一个事务。[写了部分作业,当能抄的时候做个标记,如果别人的作业有问题,退回到标记点]
嵌套
嵌套事务的方法抛异常,但父事务捕获,则回滚到保存点,父事务继续
嵌套事务的方法抛异常,父事务不捕获,则整个父事务都回滚
保存点
只要你在一个已存在事务中调用了标注为 Propagation.NESTED 的方法:
Spring 的事务管理器(比如 DataSourceTransactionManager)会自动创建一个 Savepoint;
然后在嵌套方法执行完后根据情况:
正常执行 → 释放 Savepoint;
出现异常 → 回滚到 Savepoint。
可以手动设置,但一般不需要
区别
NESTED和REQUIRED_NEW的区别
REQUIRED_NEW是新建一个事务并且新开始的这个事务与原有事务无关,原有事务回滚,不会影响新开启的事务
而NESTED则是当前存在事务时会开启一个嵌套事务,在NESTED情况下,父事务回滚时,子事务也会回滚,子事务发生异常回滚时,父事务是否回滚取决于异常是否继续向上传递。
NESTED和REQUIRED的区别
REQUIRED情况下,调用方存在事务时,则被调用方和调用方使用同一个事务,那么被调用方出现异常时,由于共用一个事务,所以无论是否catch异常,事务都会回滚
而在NESTED情况下,被调用方发生异常时,调用方可以catch其异常,这样只有子事务回滚,父事务不会回滚
事务隔离级别
ISOLATION_DEFAULT(默认同数据库)
默认值,表示使用底层数据库的默认隔离级别。
大部分数据库通常是ISOLATION_READ_COMMITTED
ISOLATION_READ_UNCOMMITTED(读未提交)
该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。
该级别不能防止脏读,不可重复读和幻读,因此很少使用该隔离级别。比如PostgreSQL实际上并没有此级别
ISOLATION_READ_COMMITTED(读已提交)
该隔离级别表示一个事务只能读取另一个事务已经提交的数据。
该级别可以防止脏读,这也是大多数情况下的推荐值
ISOLATION_REPEATABLE_READ(可重复读)
该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。
该级别可以防止脏读和不可重复读
ISOLATION_SERIALIZABLE(串行化)
所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
但是这将严重影响程序的性能。
通常情况下也不会用到该级别
优先级
当Spring中设置的隔离级别与数据库设置不一致时,以Spring设置的隔离级别为准。
长事务
声明式事务
@Transactional
编程式事务
TransactionTemplate的execute方法
优点
1.避免由于Spring aop问题,导致事务失效的问题
2.能够更小粒度的控制事务的范围,更直观
定义
长事务指的是执行时间较长的数据库事务,通常持续时间超过几秒甚至几分钟
特征
可能涉及大量数据的读写
通常包含复杂的业务逻辑或多个操作步骤
可能跨越多个业务流程
产生原因
复杂的数据处理逻辑
大量数据的批处理操作
与外部系统的交互
网络延迟或I/O瓶颈
不恰当的事务边界设计
潜在问题
锁定资源时间长,影响并发性能
增加死锁风险
可能导致数据库连接池耗尽
影响系统的响应时间和吞吐量
在分布式系统中可能导致数据不一致
处理策略
1)事务拆分
将长事务拆分为多个短事务
2)异步处理
将耗时操作放入队列异步处理
3)批处理优化
对大量数据操作进行批处理优化
4)提高硬件性能
增加服务器资源或优化数据库配置
5)使用补偿事务
实现业务层面的回滚机制
6)乐观锁
替代长时间的悲观锁定
7)重新设计
重新评估业务流程,可能的话简化操作
最佳实践
尽量避免长事务,保持事务简短和聚焦
仔细设计事务边界,确保只包含必要的操作
使用合适的隔离级别
定期监控和分析长事务,找出优化机会
在应用层实现超时机制,避免事务无限期挂起
监控和诊断
使用数据库监控工具识别长事务
分析事务日志,找出耗时较长的操作
使用性能分析工具定位瓶颈
事务失效
1.访问权限问题(需要public)
2.方法用final、static修饰(AOP无法重写)
static
static 方法属于 类级别,不属于实例。
而 Spring 的事务代理是针对 Bean 实例方法调用的,代理对象根本“拦截不到” static 方法的调用。
所以 static 方法上的 @Transactional 完全不会生效,Spring 甚至不会报错,只是安静地忽略。
final
Spring 事务通常使用 CGLIB 代理(子类代理) 来生成代理对象。
CGLIB 是通过 继承目标类 并 重写目标方法 来实现“拦截”的。
CGLIB 是通过 继承目标类 并 重写目标方法 来实现“拦截”的。
但如果方法被 final 修饰,就不能被子类重写,这样事务增强逻辑就无法织入
所以 final 方法上加 @Transactional 也不会生效。
3.方法内部调用(走this.绕过了代理对象,而不是走的重写方法,可以新写个service或自己注入自己,或使用AopContext)
4.未被spring管理(如忘了加@Service注解)
5.多线程调用(不同数据库连接)
事务注解基于ThreadLocal存储上下文,如果线程一变,事务就断了
6.表不支持事务(执行引擎是MyISAM)
7.未开启事务
SpringBoot
如果是springboot项目,那么你很幸运。因为springboot通过DataSourceTransactionManagerAutoConfiguration类,已经默默的帮你开启了事务。
你所要做的事情很简单,只需要配置spring.datasource相关参数即可。
你所要做的事情很简单,只需要配置spring.datasource相关参数即可。
Spring
如果是传统的spring项目,则需要在applicationContext.xml文件中,手动配置事务相关参数。如果忘了配置,事务肯定是不会生效的。
事务不回滚
1.错误的传播特性
2.自己吞了异常(手动try...catch)
3.rollbackFor没配对,(默认只回滚RuntimeException(运行时异常)和Error(错误),抛出IOException时,Spring置若罔闻)
4.自定义了回滚异常
@Transactional(rollbackFor = BusinessException.class)
抛了SqlException、DuplicateKeyException等异常。而BusinessException是我们自定义的异常,报错的异常不属于BusinessException,所以事务也不会回滚。
5.嵌套事务没有try/catch,导致回滚多了
因为doOtherThing方法出现了异常,没有手动捕获,会继续往上抛,到外层add方法的代理方法中捕获了异常。所以,这种情况是直接回滚了整个事务,不只回滚单个保存点。
怎么样才能只回滚保存点呢?
可以将内部嵌套事务放在try/catch中,并且不继续往上抛异常。
这样就能保证,如果内部嵌套事务中出现异常,只回滚内部事务,而不影响外部事务。
面试点
两个服务,跨应用调用写入,连的同一个数据库,还存在分布式事务吗?
调用方写入数据后,再调用因为Feign能感知到调用失败,所以会抛异常,这种情况就能回滚;调用后调用方才写入数据报错,被调方无法感知到,所以数据不会回滚
在这种情况下,严格来说,仍然存在分布式事务的问题,尽管风险可能比连接不同数据库的情况要小。原因如下:
1.服务隔离: 尽管两个服务连接同一个数据库,但它们仍是独立的应用程序,可能运行在不同的进程或服务器上。
2.网络因素: 跨应用调用涉及网络通信,可能出现网络延迟、中断等问题。
3.并发控制: 两个服务可能同时访问相同的数据,需要考虑并发控制问题。
4.失败恢复: 如果其中一个服务在事务过程中失败,需要有机制来保证数据一致性。
1.服务隔离: 尽管两个服务连接同一个数据库,但它们仍是独立的应用程序,可能运行在不同的进程或服务器上。
2.网络因素: 跨应用调用涉及网络通信,可能出现网络延迟、中断等问题。
3.并发控制: 两个服务可能同时访问相同的数据,需要考虑并发控制问题。
4.失败恢复: 如果其中一个服务在事务过程中失败,需要有机制来保证数据一致性。
同一个应用连接两个数据库,存在分布式事务吗
应该需要引入本地事务
可能存在,但不一定。这取决于您的具体应用场景和设计:
a)如果应用需要保证对两个数据库的操作在逻辑上是一个原子操作(要么全部成功,要么全部失败),那么就需要使用分布式事务;
b)如果应用对两个数据库的操作是相互独立的,不需要保证强一致性,那么就不需要使用分布式事务。
a)如果应用需要保证对两个数据库的操作在逻辑上是一个原子操作(要么全部成功,要么全部失败),那么就需要使用分布式事务;
b)如果应用对两个数据库的操作是相互独立的,不需要保证强一致性,那么就不需要使用分布式事务。
底层实现
Spring 的事务是基于 ThreadLocal + 代理机制 实现的。
事务信息绑定在线程上
事务是线程级局部变量,不是跨线程共享资源
当一个方法被 @Transactional 标注时,Spring 在进入该方法时会:
1. 通过 AOP 代理创建一个事务;
2. 将这个事务信息(Connection、事务状态等)放入当前线程的 ThreadLocal;
3. 方法执行完毕后,提交或回滚;
4. 最后清理掉 ThreadLocal。
Spring的事务是通过数据库连接来实现的。当前线程中保存了一个map,key是数据源,value是数据库连接。
我们说的同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。
代理方式
Java的动态代理
触发条件
目标类实现了接口
如果类没有实现接口,就算强制指定也会报错
实现原理
通过 java.lang.reflect.Proxy
特点
代理接口方法
基础机制
反射 + 接口代理
性能
调用速度略慢(反射)
代理范围
只能代理接口方法
Spring默认选择
CGLib
触发条件
目标类没有接口
还可以强制指定
@EnableTransactionManagement(proxyTargetClass = false)
proxyTargetClass = false → 强制使用 JDK 动态代理
proxyTargetClass = true → 强制使用 CGLIB 代理
实现原理
Spring 会使用 CGLIB(Code Generation Library),
在运行时生成一个 OrderService 的子类,
这个子类重写所有非 final 的方法,在里面包上一层增强逻辑。
在运行时生成一个 OrderService 的子类,
这个子类重写所有非 final 的方法,在里面包上一层增强逻辑。
特点
代理类本身的方法
基础机制
字节码生成 + 子类继承
性能
稍快(直接调用)
代理范围
可代理类所有非final方法
分布式事务
CAP原则
含义
一致性(Consistency):每次读操作都能保证返回的是最新数据;
可用性(Availablity):任何一个没有发生故障的节点,会在合理的时间内返回一个正常的结果;
分区容忍性(Partition-torlerance):当节点间出现网络分区,照样可以提供服务。
实践
CP是强一致性,AP是可用性。
zk是要求强一致性,也就是主从机器数据需要一致,如果主机崩了,此时就需要停下来进行选举,选举时间需要至少30s以上,在这期间系统是不可用的,服务不能注册与查询。这对于大中厂来说是不可接受的
相比之下,ap就没有主从之分,一台机器可以复制另外一台机器的数据,即使是其中一台机器宕机,其他也可以继续提供服务
相比之下,ap就没有主从之分,一台机器可以复制另外一台机器的数据,即使是其中一台机器宕机,其他也可以继续提供服务
延伸出
BASE理论
即使无法做到强一致性,但是可以采用适当的采取弱一致性,即最终一致性
Basically Available(基本可用)
Soft state(软状态)
Eventually consistent(最终一致性)
Raft算法(共识算法)
节点状态
1.随从(启动都是随从)
2.候选者(如果节点没有监听到领导发消息,则就会变成候选者)
3.领导(候选者会给其他所有节点发送投票,当该节点收到大多数投票,则会成为领导。"领导选举过程"->系统中所有改变都需要通过领导)
领导选举
选举超时时间(控制选举过程)
1.选举超时(150~300ms)是随从想要成为候选者中间的时间(节点自旋时间)
2.在自旋时间过后,还没有领导发命令,可能是没领导。就可以变成候选者,让其他节点选自己当领导
3.自旋时间结束,成为候选人。发起一轮新选举。先投自己1票.
4.然后给别人发投票请求,让别人投票
5.如果接受请求的节点还没有投票。则可以投给发起者(投票后会重置自旋时间)
6.当发起者成为领导后,会给随从发送追加日志的消息
心跳超时时间
1.追加日志消息会以指定间隔时间发送出去(维护心跳连接)
2.其他节点收到消息以后,又会重置自旋时间
3.这一轮选举会一直维持到有一个随从停止接收心跳,成为候选者(如领导宕机)
4.如果两个节点都成为候选者,将会出现投票分离的情况
5.进行多次选举,直到选出领导
日志复制
案例
客户端想让分布式系统中SET 5
1.首先客户端给领导发送SET 5的命令,领导收到后不提交
2.每一个改变的命令都会被添加成"节点日志",并在下一个心跳时发送追加日志给其他所有节点.
3.复制日志到其他节点,当其他节点收到并写好后会回复收到日志
4.当领导收到大多数节点回复后,就会提交SET 5,并在下一个心跳时间响应给客户端,接着下一个心跳时间再通知其他节点提交
5.这样系统状态达到一致性
Raft在面临网络分区的时候保持一致性
1.如ABC、DE,分别处于两个机房。并有网络连接
2.在两机房出现网络故障后,会分别选领导。
3.当客户端分别要改变两个机房数据时,老领导所在的机房可能会出现没办法超过大多数的情况,所以一直保存不成功。
4.另一个机房,新选出的领导在选举过后如果超过大多数,则可以保存更改请求
5.在网络恢复以后没超过半数的机房,会回退所有未提交的数据。复制多数领导的日志
具体解决方案
本地消息表
尽量让本地事务去保证,然后利用手动任务 + 定时任务;后续再通过本地消息表进行补偿性操作;达到最终一致的效果
实现思路
可靠投递模型(先本地落盘,后异步发送)
1.①insert本地消息表;
②定时任务OR人工
③调远程commit(成功后delete消息)
性能与可靠性高,弊端为实时性
乐观锁思想
①先去远程调commit;
②失败后insert本地消息表
需要注意远程调之前还是之后,调之前开启事务,基本上没办法去做回滚
悲观锁思想
①事务开启之前先insert一行数据;
②调用远程
③如果远程调用成功,删除之前那行记录
外网或不可靠网络建议:第三种
第二种解决方案在投递消息没有成功,未写入信息。机器重启或宕机时,消息丢失的情况
第三种可靠性更高,但是弊端是事务会更大(本地事务,中间嵌套了远程调用,事务会更大。对数据库资源消耗也会更大,不太适合高并发)
第三种可靠性更高,但是弊端是事务会更大(本地事务,中间嵌套了远程调用,事务会更大。对数据库资源消耗也会更大,不太适合高并发)
业界主流分布式思维解决方案,都不保证它不重新投递
第一代
代表模型
2PC(XA)、3PC
XA
1.XA协议较简单,数据库原生支持,使用成本低
2.XA协议性能不理想,全程同步阻塞
协调者是单点
一旦挂了,所有节点都得等
类似于所有分行必须等总行“点头”才能真正放款,一个分行慢,全系统都得等
其实XA也是2PC的一种数据库级实现,但在微服务中,2PC通常是 应用自己实现的版本
3PC
为了改进 2PC 的「协调者宕机导致阻塞」问题,
3PC 加入了一个中间阶段,进一步拆分流程
3PC 加入了一个中间阶段,进一步拆分流程
阶段1:CanCommit —— 问问大家能不能执行?
阶段2:PreCommit —— 所有分支先准备
阶段3:DoCommit —— 真正提交
实现极其复杂
网络分区或者消息延迟仍可能导致不一致
落地少
可能造成资源浪费
处理不好还可能造成死锁
实现
关键特征
数据库主导
同步阻塞
一致性类型
强一致性
第二代
代表模型
TCC事务补偿型(柔性事务)
实现
Seata
使用
@GlobalTransactional
增加一张Seata指定的Undo Log表
角色
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚
写好Try、Confirm、Cancel相关逻辑,框架会在适当的时候调用:
一阶段prepare:调用Try逻辑;
二阶段commit:调用Confirm逻辑;
二阶段rollback:调用Cancel逻辑;
一阶段prepare:调用Try逻辑;
二阶段commit:调用Confirm逻辑;
二阶段rollback:调用Cancel逻辑;
系统侵入性比较高
使用注解,指定二三阶段调用的方法
Saga模型
核心思想
把全局事务拆分成多个本地事务,失败时执行反向补偿操作
由协调器驱动
适合场景
电商订单
库存
积分
关键特征
应用主导
异步补偿
一致性类型
最终一致性
第三代
代表模型
最大努力通知型(柔性事务、可大并发)
类似微信,支付宝推送消息
带间隔退避策略
适合场景
银行回调
支付网关通知
可靠消息 + 最终一致性(柔性事务、可大并发)
适合场景
下单后发货
消息驱动架构
关键特征
消息驱动
异步重试
一致性类型
最终一致性
问题
幂等性
同一个请求请求了多次,但结果要保证与只请求一次的结果相同
幂等很少用乐观锁
TCC模型 和 Saga模型
悬挂
Cancel 比 Try 先执行了 —— 补偿“悬”在空中,因为主业务都还没开始。
举个例子
系统执行时,网络抖了一下
1. 事务协调者发出 Try 请求给 A 系统(冻结 100 元)
2. 但这个请求网络丢包了,A 系统根本没收到。
3. 事务协调者以为 Try 失败,就发出 Cancel 请求。
4. A 系统收到 Cancel,尝试“解冻”100元。
问题来了
A 根本没冻结过钱(Try 没执行过),
却执行了 Cancel(取消操作),
这就叫「悬挂」:Cancel 操作悬空执行,没有 Try 对应的上下文
却执行了 Cancel(取消操作),
这就叫「悬挂」:Cancel 操作悬空执行,没有 Try 对应的上下文
现实比喻
就像你原本打算下单买手机(Try),但网络延迟,订单根本没生成。
结果客服来电话说「你的订单取消啦」(Cancel),
而你一脸懵逼:「我根本没下单啊!」
常见解决方案
幂等&防悬挂机制
1. 每次 Try 前,记录一条事务状态(比如标记为“TRYING”);
2. Cancel 执行前检查是否存在对应 Try 记录:
• 没有 → 不执行(防悬挂)
• 有但已完成 → 忽略(防重复)
3. 利用 全局事务ID(XID) 作为唯一标识。
补偿(反)操作已完成,业务逻辑(正)请求到了(此时应该忽略)
空补偿
补偿(反)操作比业务逻辑(正)早到
Cancel 执行了,但 Try 其实已经执行失败,
所以 Cancel 对应的业务逻辑是“空的”补偿操作。
所以 Cancel 对应的业务逻辑是“空的”补偿操作。
举个例子
还是上面的转账场景:
1. Try 请求到了 A 系统;
2. A 系统在执行时发生异常(比如数据库锁住了),冻结失败;
3. 事务协调者判断这次事务失败,于是发出 Cancel;
4. Cancel 来了,但 A 系统发现:根本没有冻结资金记录(因为 Try 根本没成功),
所以执行了个“空操作”。
—— 这就叫「空补偿」。
1. Try 请求到了 A 系统;
2. A 系统在执行时发生异常(比如数据库锁住了),冻结失败;
3. 事务协调者判断这次事务失败,于是发出 Cancel;
4. Cancel 来了,但 A 系统发现:根本没有冻结资金记录(因为 Try 根本没成功),
所以执行了个“空操作”。
—— 这就叫「空补偿」。
生活比喻
就像你下单买手机(Try),付款时卡顿失败;
系统误以为你下单了,又发出“退款操作”(Cancel)。
但其实你根本没付钱,退款也就退不了任何钱。
这就是「空补偿」。
常见解决方案
和悬挂类似,依然要用事务状态表
1. Try 阶段成功后才记录状态为「TRY_SUCCESS」;
2. Cancel 阶段执行前检查:
如果无 Try 记录 → 拒绝执行(避免空补偿);
如果 Try 状态为「TRY_FAIL」 → 直接忽略。
本质问题都是 分布式系统里消息乱序、幂等和状态不一致
事件的事务注解
@EventListener
可以理解为无论里面外面炸,都会回滚
@TransactionalEventListener
可以理解为事件里面炸,外面不管也不回滚;外面炸,里外都会回滚
Redis
数据类型
基本类型
string (字符串)
格式
字符串
int整型
float浮点型
特点
保存登录的用户信息,可以使用String结构,以JSON字符串来保存,比较直观
简单的数据结构,例如验证码
list (列表)
简介
Redis中的List类型与Java中的LinkedList类似,可以看做是一个双向链表结构
既可以支持正向检索和也可以支持反向检索
特征
有序
元素可以重复
插入和删除快
查询速度一般
应用场景
对于顺序有要求
朋友圈点赞列表
用户评论列表
如果数据会变化,并且需要分页(需要滚动分页查询,记录lastId)。则List就不太适合,应考虑ZSET
面试题
如何利用list结构模拟一个栈
入口和出口在同一边
入栈使用LPUSH,出栈使用LPOP,或入栈使用RPUSH,出栈使用RPOP
如何利用list结构模拟一个队列
入口和出口在不同边
入队使用LPUSH,出队使用RPOP,或入队使用RPUSH,出队使用LPOP
如何利用list结构模拟一个阻塞队列
入口和出口在不同边
出队时采用BLPOP或BRPOP
hash (字典)
特点
Hash结构可以将对象中的每个字段独立存储,可以针对单个字段做CRUD,并且内存占用更少,使用更灵活
偏复杂的数据,比如对象
set (集合)
简介
Redis的Set结构与lava中的HashSet类似,可以看做是一个value为null的HashMap。因为也是一个hash表,因此具备与HashSet类似的特征
特征
无序
元素不可重复
查找快
支持交集,并集,差集等功能
应用场景
用户标签,生成随机数,抽奖
好友列表,共同好友,关注列表
优惠券购买记录信息
SortedSet/ZSet(有序集合)
简介
Redis的SortedSet是一个可排序的set集合,与Java中的TreeSet有些类似,但底层数据结构却差别很大。
SortedSet中的每一个元素都带有一个score属性,可以基于Score属性对元素排序
特征
可排序
元素不重复
查询速度快
应用场景
排行榜,社交需求,比如用户点赞
底层实现
ZipList
跳表
键值对少于128个 且 每个元素长度不超过64字节,使用ZipList,否则跳表
特殊类型
GEO
Geolocation的简写形式,代表地理坐标
底层使用ZSET
BitMap
HyperLog
根据概率推算
string结构实现
会过滤重复元素
数据量小还是可以精确统计
数据量大,会有小于0.81%的误差
命令
通用
KEYS
查看符合模板的所有key
注意
不建议为生产环境上使用,会造成阻塞所有请求
DEL
删除一个或多个指定的key
EXISTS
判断key是否存在
EXPIRE
给一个key设置有效期,有效期到期时该key会被自动删除
TTL
查看一个key的剩余有效期
-1
永久有效
-2
有效期过期,被删除
object encoding KEY
查看编码
String类型常见命令
存取命令
SET
添加或者修改已经存在的一个String类型的键值对
GET
根据key获取String类型的value
MSET
批量添加多个String类型的键值对
MGET
根据多个key获取多个String类型的value
自增/自减命令
INCR
让一个整型的key自增1
DECR
让一个整型的key自减1
INCRBY
让一个整型的key自增并指定步长,例如:incrby num 2 让num值自增2
设置负数,也能实现DECR自减的效果
INCRBYFLOAT
让一个浮点类型的数字自增并指定步长
组合命令
SETNX
添加一个String类型的键值对,前提是这个key不存在,否则不执行
SETEX
添加一个String类型的键值对,并指定有效期
List类型常见命令
存取命令
LPUSH key element ...
向列表左侧插入一个或多个元素
LPOP key
移除并返回列表左侧的第一个元素,没有则返回nil
RPUSH key element ...
向列表右侧插入一个或多个元素
RPOP key
移除并返回列表右侧的第一个元素
LRANGE key star end
返回一段角标范围内的所有元素
BLPOP和BRPOP
与LPOP和RPOP类似,只不过在没有元素时等待指定时间,而不是直接返回nil
B代表block阻塞
Hash类型常见命令
存取命令
HSET key field value
添加或者修改hash类型key的field的值
HGET key field
获取一个hash类型key的field的value
HMSET
批量添加多个hash类型key的field的值
HMGET
批量获取多个hash类型key的field的值
HGETALL
获取一个hash类型的key中的所有的field和value
HKETS
获取一个hash类型的key中的所有的field
HVALS
获取一个hash类型的key中的所有的value
自增命令
HINCRBY
让一个hash类型key的字段值自增并指定步长
组合命令
HSETNX
添加一个hash类型的key的field值,前提是这个field不存在,否则不执行
删除命令
HDEL key field[field...]
在hash类型中删除一个或多个指定字段
Set类型常见命令
存取命令
SADD key member ...
向set中添加一个或多个元素
SREM key member ...
移除set中的指定元素
SCARD key
返回set中元素的个数
SISMEMBER key member
判断一个元素是否存在于set中
SMEMBERS
获取set中的所有元素
集合间操作命令
SINTER key1 key2 ...
求key1与key2的交集
SDIFF key1 key2 ...
求key1与key2的差集
SUNION key1 key2 ...
求key1和key2的并集
SortedSet/ZSet类型常见命令
存取命令
ZADD key score member
添加一个或多个元素到sorted set,如果已经存在则更新其score值
ZREM key member
删除sorted set中的一个指定元素
ZSCORE key member
获取sorted set中的指定元素的score值
ZRANK key member
获取sorted set 中的指定元素的排名
ZCARD key
获取sorted set中的元素个数
ZCOUNT key min max
统计score值在给定范围内的所有元素的个数
ZINCRBY key increment member
让sorted set中的指定元素自增,步长为指定的increment值
ZRANGE key min max
按照score排序后,获取指定排名范围内的元素
ZRANGEBYSCORE key min max
按照score排序后,获取指定score范围内的元素
集合间操作命令
ZDIFF
求差集
ZINTER
求交集
ZUNION
求并集
注意:所有的排名默认都是升序,如果要降序则在命令的Z后面添加REV(reverse,反转)即可
GEO数据类型命令
GEOADD
添加一个地理空间信息,包含:经度(longitude)、纬度(latitude)、值(member)
GEODIST
计算指定的两个点之间的距离并返回
GEOHASH
将指定member的坐标转为hash字符串形式并返回
GEOPOS
返回指定member的坐标
GEORADIUS
指定圆心、半径,找到该圆内包含的所有member,并按照与圆心之间的距离排序后返回。6.2以后已废弃
GEOSEARCH
在指定范围内搜索member,并按照与指定点之间的距离排序后返回。范围可以是圆形或矩形。6.2.新功能
GEOSEARCHSTORE
与GEOSEARCH功能一致,不过可以把结果存储到一个指定的key。 6.2.新功能
BitMap数据类型命令
SETBIT
向指定位置(offset)存入一个0或1
GETBIT
获取指定位置(offset)的bit值
BITCOUNT
统计BitMap中值为1的bit位的数量
BITFIELD
操作(查询、修改、自增)BitMap中bit数组中的指定位置(offset)的值
BITFIELD_RO
获取BitMap中bit数组,并以十进制形式返回
BITOP
将多个BitMap的结果做位运算(与 、或、异或)
BITPOS
查找bit数组中指定范围内第一个0或1出现的位置
HyperLog数据类型命令
PFADD
PFCOUNT
PFMERGE
可以合并每天的结果,统计该月的数据
key的结构
Redis的key允许有多个单词形成层级结构,多个单词之间用":"隔开,格式如下->项目名:业务名:类型:id
面试题
项目中如何用,用过哪些数据类型
主从同步
全量同步
场景
主从第一次建立连接时
判断依据
Replication Id
简称replid,是数据集的标记,id一致则说明是同一数据集。每一个master都有唯一的replid,slave则会继承master节点的replid
确定拷贝哪个master的数据
offset
偏移量,随着记录在repl_baklog(环形数组)中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。如果slave的offset小于master的offset,说明slave数据落后于master,需要更新
确定拷贝了多少,进度怎么样
slave节点断开时间太久,repl_baklog中的offset已经被覆盖时
同步阶段
第一阶段
slave:执行replicaof命令,建立连接
slave:请求数据同步
master:判断是否是第一次同步
master:是第一次,返回master的数据版本信息
slave:保存版本信息
第二阶段
master:执行bgsave,生成RDB
master:记录RDB期间的所有命令
master:发送RDB文件
slave:清空本地数据,加载RDB文件
第三阶段
master:发送repl_backlog中的命令
slave:执行接收到的命令
增量同步
场景
除了第一次做全量同步,其它大多数时候slave与master都是做增量同步
只更新slave与master存在差异的部分数据
同步阶段
第一阶段
slave:重启
slave:psync replid offset
master:判断请求replied是否一致
master:不是第一次,回复 continue
第二阶段
master:去repl_baklog中获取offset后的数据
master:发送offset后的命令
slave:执行命令
注意点
repl_baklog大小有上限,写满后会覆盖最早的数据。如果slave断开时间过久,导致尚未备份的数据被覆盖,则无法基于log做增量同步,只能再次全量同步
优化点
在master中配置repl-diskless-sync yes启用无磁盘复制,避免全量同步时的磁盘IO
Redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘IO
适当提高repl_baklog的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步
限制一个master上的slave节点数量,如果实在是太多slave,则可以采用主-从-从链式结构,减少master压力
单线程还是多线程
如果仅仅聊Redis的核心业务部分(命令处理),答案是单线程
如果是聊整个Redis,那么答案就是多线程
版本迭代
Redis v4.0:引入多线程异步处理一些耗时较旧的任务,例如异步删除命令unlink
Redis v6.0:在核心网络模型中引入 多线程,进一步提高对于多核CPU的利用率
单线程为什么快
1.完全基于内存操作,读写效率高(持久化是fork子进程与Linux系统,页缓存技术)
它的性能瓶颈是网络延迟而不是执行速度,因此多线程并不会带来巨大的性能提升
2.单线程操作,避免了频繁的上下文切换。频繁切换会影响系统性能
3.合理高效的数据结构
4.采用了非阻塞IO多路复用机制。采用epoll模型
引入多线程会面临线程安全问题,必然要引入线程锁这样的安全手段,实现复杂度增高,而且性能也会大打折扣
如何保证缓存与数据库的操作的同时成功或失败
单体项目
将缓存与数据库操作放在一个事务
分布式项目
利用TCC等分布式事务方案
缓存雪崩
现象
缓存Key同一时间大量失效或者Redis服务宕机,大量请求直接达到数据库,DB瘫痪
解决方案
1.随机失效时间
2.平均分布在集群不同节点
3.不设置失效时间
4.跑定时任务,定时刷新
5.给缓存业务添加降级限流策略
提前做好容错处理(比如:快速失败,拒绝服务)
6.给业务添加多级缓存
NG缓存、Redis缓存、JVM缓存
缓存穿透
现象
利用缓存与DB都没有的数据,如id=-1,直接穿透缓存打到数据库,一般属于恶意攻击
解决方案
1.参数合法性校验
2.无论查出什么都缓存(缓存空对象,空字符串)
优点
实现简单,维护方便
缺点
额外的内存消耗
子主题
3.使用布隆过滤器
优点
内存占用较少,没有多余的key
缺点
实现较复杂
存在误判可能
4.拉黑相关IP
缓存击穿
现象
一个被高并发访问并且缓存重建业务较复杂的key突然失效了,大量该Key请求打到数据库
解决方案
1.分布式锁,单体用互斥锁
优点
没有额外的内存消耗
保证一致性
实现简单
缺点
线程需要等待,性能受影响
可能有死锁风险
2.永不失效(逻辑过期)
增加一个expire字段,配合合适的内存空间淘汰策略
实现细节
线程A
1.查询缓存,发现逻辑时间已过期
2.获取互斥锁成功
3.开启新线程B生成新缓存
4.返回过期数据
线程B
1.查询数据库重建缓存数据
2.写入缓存,重置逻辑过期时间
3.释放锁
线程C
1.查询缓存,发现逻辑时间已过期
2.获取互斥锁失败
3.返回过期数据
优点
线程无需等待,性能较好
缺点
不保证一致性
有额外内存消耗
实现复杂
缓存预热,缓存降级
删除缓存还是更新缓存
删除缓存(推荐)
更新数据库时让缓存失效,查询时再更新缓存
更新缓存
每次更新数据库都更新缓存,无效写操作较多
双写一致性
含义
什么是双写?
同一份数据,需要写数据库、写缓存。
双写很难保证强一致性,可以保证最终一致性。
要做到强一致性就需要将所有读写请求用队列串行化,但是性能非常差,降低系统的QPS。
没有完美的方案,用到缓存就会存在不一致的情况,需要根据具体业务权衡得失,选择合适业务的方案。
双写很难保证强一致性,可以保证最终一致性。
要做到强一致性就需要将所有读写请求用队列串行化,但是性能非常差,降低系统的QPS。
没有完美的方案,用到缓存就会存在不一致的情况,需要根据具体业务权衡得失,选择合适业务的方案。
解决方案
先更新数据库,再删除缓存(推荐方案)
此方案数据不一致的几率比较低,并且实现简单。
造成数据不一致的情况:在缓存刚好失效(可能时间到期)时,有线程查询数据库得到旧值,另外一个线程更新数据库并删除缓存后,前面持有旧值的线程将数据存入缓存,造成数据不一致。
造成数据不一致的情况:在缓存刚好失效(可能时间到期)时,有线程查询数据库得到旧值,另外一个线程更新数据库并删除缓存后,前面持有旧值的线程将数据存入缓存,造成数据不一致。
先删除缓存,再更新数据库
造成数据不一致的情况:删除缓存后,有线程将旧的数据重写回缓存,造成数据不一致。
先更新数据库,再更新缓存
造成数据不一致的情况:多个线程更新缓存时,由于网络问题导致更新顺序错乱,造成数据不一致。
延时双删
先删除缓存,再更新数据库,休眠一段时间,再删除缓存。
在高并发场景会影响性能,也极大降低了数据不一致的可能性。
在高并发场景会影响性能,也极大降低了数据不一致的可能性。
异步更新
使用Canal中间件订阅数据库的binlog,来对缓存更新。
造成数据不一致的情况:消费binlog也有一定时间的延迟,还是可能出现数据不一致。
造成数据不一致的情况:消费binlog也有一定时间的延迟,还是可能出现数据不一致。
写缓存失败的情况
上述方案都可能出现更新或删除缓存失败的情况,可以另起线程重试,或加入消息队列重试。
慢查询
定义
在Redis执行时,耗时超过某个阈值的命令
slowlog-log-slower-than
慢查询阈值
单位是微秒。默认是10000,建议1000
慢查询会被放入慢查询日志中,日志的长度有上限,可以通过配置指定
slowlog-max-len
慢查询日志(本质是一个队列)的长度。默认是128,建议1000
不能过大,导致过多内存占用
slowlog len
查询慢查询日志长度
slowlog get [n]
读取n条慢查询日志
第一行
日志编号
第二行
日志加入时的时间戳
第三行
慢查询耗时
第四行
慢查询命令
第五行
客户端的IP和端口
第六行
客户端的名称
slowlog reset
清空慢查询列表
怎么快速计算redis里面某些前缀的key数量
scan慢慢扫
部署方式
单机
问题
数据丢失问题
故障恢复问题
并发能力问题
存储能力问题
高可用
主从复制
哨兵模式
集群模式
Redis Cluster采用的是类一致性哈希算法实现节点选择的
Cluster会分成了16384(2的14次方) 个Slot(槽位),哈希槽类似于数据分区,每个键值对都会根据它的 key,被映射到一个哈希槽中
具体执行过程分为两大步
1.根据键值对的 key,按照 CRC16 算法计算一个 16 bit 的值。
2.再用 16bit 值对 16384(2的14次方) 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽。
每个Redis节点负责处理一部分槽位
分布式锁
1.setnx + expire(分开执行)
使用SETNX命令来抢占锁,如果成功,再使用EXPIRE命令给锁设置一个过期时间。
这种方案的缺点是SETNX和EXPIRE不是原子操作,可能导致锁无法释放。
2.setnx + value值是过期时间
使用SETNX命令来抢占锁,如果失败,再获取锁的过期时间,如果已经过期,就用GETSET命令更新锁的过期时间。
这种方案的缺点是需要客户端的时间同步,而且可能导致锁的过期时间被覆盖或者被其他客户端删除。
3.set的扩展命令(set ex px nx)
原理
利用setnx的互斥性;利用ex避免死锁;释放锁时判
断线程标示
断线程标示
缺陷
不可重入、无法重试、锁超时失效
EX表示过期时间
PX表示毫秒单位
NX表示只有当键不存在时才设置值
4.set ex px nx + 校验唯一随机值,再删除(避免误删)
增加了一个唯一随机值作为锁的持有者标识,只有持有者才能释放锁。
这种方案的优点是增加了安全性,避免了其他客户端误删或者覆盖锁。
5.Redisson
可重入锁原理
利用Redis中Hash结构,key为锁,field为线程id,value为上锁次数,记录线程标识和重入次数;
锁一次就+1,释放锁就-1,为零则删除锁
watchDog延续锁时间
利用信号量控制锁重试等待
缺陷
Redis宕机引起锁失效问题
6.Redisson + RedLock
Redlock是Redis作者提出的一种基于多个Redis节点实现分布式锁的算法,可以保证在任何时刻只有一个客户端持有锁,并且能够容忍一定数量的节点故障。
这种方案的优点是高可用性和高可靠性。
7.Redisson + MultiLock
原理
多个独立的Redis节点,必须在所有节点都获取重入锁,才算获取锁成动
缺陷
运维成本高、实现复杂
持久化
RDB
RDB 快照(Redis Database Backup file):将某一个时刻的内存数据,以二进制的方式写入磁盘。
物理层面
比如页号xx、偏移量ywy写入了'zzz'数据
触发场景
执行save命令
save命令会导致主进程执行RDB,这个过程中其它所有命令都会被阻塞。只有在数据迁移时可能用到。
执行bgsave命令
这个命令执行后会开启独立进程完成RDB,主进程可以持续处理用户请求,不受影响。
Redis停机时
Redis停机时会执行一次save命令
触发RDB条件时
redis.conf配置文件中配置信息
save 在多少秒内 至少有多少个key被修改,就执行bgsave
禁用
save ""
AOF
AOF 日志(Append Only File,文件追加方式):记录所有的操作命令,并以文本的形式追加到文件中。
逻辑层面
比如对某个数据进行了add语句操作
默认关闭
命令日志文件
刷盘策略
always
表示每执行一次写命令,立即记录到AOF文件
刷盘时机
同步刷盘
优点
可靠性高,几乎不丢数据
缺点
性能影响大
everysec
写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案
刷盘时机
每秒刷盘
优点
性能适中
缺点
最多丢失1秒数据
no
写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
刷盘时机
操作系统控制
优点
性能最好
缺点
可靠性较差,可能丢失大量数据
重写AOF文件
bgrewriteaof
bg re write aof
会在触发阈值时自动去重写AOF文件
redis.conf配置
auto-aof-rewrite-percentage 100
AOF文件比上次文件 增长超过多少百分比则触发重写
auto-aof-rewrite-min-size 64mb
AOF文件体积最小多大以上才触发重写
写后日志
1.由于Redis在写入日志之前,不对命令进行语法检查,所以只记录执行成功的命令,避免出现记录错误命令的情况
2.而且在命令执行后再写日志不会阻塞当前的写操作。
混合
混合持久化方式:Redis 4.0 新增了混合持久化的方式,集成了 RDB 和 AOF 的优点。
在重写AOF文件的时候生成快照RDB,把当前数据以RDB的方式进行存储,替代原本AOF文件。新增加的指令还是使用AOF追加到文件后面。
在重写AOF文件的时候生成快照RDB,把当前数据以RDB的方式进行存储,替代原本AOF文件。新增加的指令还是使用AOF追加到文件后面。
配置
用来做缓存的Redis实例尽量不要开启持久化功能
安全性要求较高的业务,如分布式锁、库存、订单流水,另外单独redis实例,再去做持久化
建议关闭RDB持久化功能,使用AOF持久化
持久化频率不高,可能间隔60秒,甚至更长。这期间命令可能会丢失。数据安全性不能保证
如果缩短时间间隔,即使底层用的Copy-On-Write机制,但在内存比较大的情况下,耗时也会比较久,又会增加大量磁盘IO;性能影响大
利用脚本定期在slave节点做RDB,实现数据永久备份
设置合理的rewrite阈值,避免频繁的bgrewrite
百分比
文件大小
Background rewrite
配置no-appendfsync-on-rewrite = yes,禁止在rewrite期间做aof,避免因AOF引起的阻塞
追求性能:yes
追求数据安全性:no
部署相关
Redis实例的物理机要预留足够内存,应对fork和rewrite
fork极限情况下,内存可能会翻倍
单个Redis实例内存上限不要太大,例如4G或8G。可以加快fork的速度、减少主从同步、数据迁移压力
建议单机多实例
不要与CPU密集型应用部署在一起
fork和rewrite消耗比较高
比如ES也是CPU密集型
不要与高硬盘负载应用一起部署。例如:数据库、消息队列
客户端
Jedis
以Redis命令作为方法名称,学习成本低。简单实用,但是Jedis实例是线程不安全的,多线程环境下需要基于连接池来使用
Lettuce(Spring默认兼容)
Lettuce是基于Netty实现的,支持同步,异步和响应式编程方式。并且是线程安全的。支持Redis的哨兵模式、集群模式和管道模式。
Redisson
Redisson是一个基于Redis实现的分布式、可伸缩的Java数据结构集合。包含了诸如Map、Queue、Lock、Semaphore、AtomicLong等强大功能
单机
Spring Data Redis
兼容Jedis,Lettuce
使用步骤
1.引入spring-boot-starter-data-redis依赖
2.在application.yml配置Redis信息
3.注入RedisTemplate
注意点
Redis的key和value序列化
序列化方案
方案一:
1.自定义RedisTemplate
2. 修改RedisTemplate的序列化器为GenericJackson2JsonRedisSerializer
1.自定义RedisTemplate
2. 修改RedisTemplate的序列化器为GenericJackson2JsonRedisSerializer
方案二:
1,使用StringRedisTemplate
2.写入Redis时,手动把对象序列化为JSON
3.读取Redis时,手动把读取到的JSON反序列化为对象
1,使用StringRedisTemplate
2.写入Redis时,手动把对象序列化为JSON
3.读取Redis时,手动把读取到的JSON反序列化为对象
MQ
List结构
基于List结构模拟消息队列
优点
利用Redis存储,不受限于JVM内存上限
基于Redis的持久化机制,据安全性有保证
可以满足消息有序性
缺点
无法避免消息丢失(POP命令,取出并销毁。当取了消息但还没处理就宕机,则消息丢失)
只支持单消费者
PubSub(发布订阅)
基本的点对点消息模型(天生阻塞)
命令
SUBSCRIBE channel[channel] : 订阅一个或多个频道
PUBLISH channel msg : 向一个频道发送消息
PSUBSCRIBE pattern[pattern]: 订阅与pattern格式匹配的所有频道(开头的P就是pattern)
优点
采用发布订阅模型,支持多生产、多消费
缺点
不支持数据持久化
无法避免消息丢失
消息堆积有上限,超出时数据丢失
Stream
比较完善的消息队列模型
命令
XADD users * name jack age 21
创建名为 users的队列,并向其中发送一个消息,内容是: {name=jack,age=21},并且使用Redis自动生成消息ID
XREAD COUNT 1 STREAMS users 0 | $
从users队列读取一条消息(消息永久存在)
0代表从第一条消息开始
$代表从最新的消息开始
XREAD COUNT 1 BLOCK 0 STREAMS users $
一直阻塞读,还可以写具体毫秒时间
特点
消息可回溯
一个消息可以被多个消费者读取
可以阻塞读取
有消息漏读的风险(缺点)
消费者组
命令
XGROUP CREATE key groupName ID [MKSTREAM]
key:队列名称
groupName:消费者组名称
ID:起始ID标示,$代表队列中最后一个消息,0则代表队列中第一个消息
MKSTREAM:队列不存在时自动创建队列
XGROUP DESTORY key groupName
删除指定的消费者组
XGROUP CREATECONSUMER key groupname
consumername
consumername
给指定的消费者组添加消费者
XGROUP DELCONSUMER key groupname consumername
删除消费者组中的指定消费者
XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS
key [key ...] ID [ID ...]
key [key ...] ID [ID ...]
参数
group:消费组名称
consumer:消费者名称,如果消费者不存在,会自动创建一个消费者
count:本次查询的最大数量
BLOCK miuliseconds:当没有消息时最长等待时间
NOACK:无需手动ACK,获取到消息后自动确认
STREAMS key:指定队列名称
ID:获取消息的起始ID
">”:从下一个未消费的消息开始(正常情况使用)
其它:根据指定id从pending-list中获取已消费但未确认的消息,例如0,是从pending-list中的第一个
消息开始
消息开始
特点
消息可回溯
可以多消费者争抢消息,加快消费速度
可以阻塞读取
没有消息漏读的风险
有消息确认机制,保证消息至少被消费一次
XACK key groupName ID [ID]
确认消息
key:队列名称
groupName:消费者组名称
ID:消息ID
XPENDING key groupName
查看pending list中的数据
最佳实践
优雅的key结构
约定
遵循基本格式:[业务名称]:[数据名]:[id]
长度不超过44字节
在低于4.0版本,限制为39字节
原本为embstr格式,超过后为raw模式
不包含特殊字符
好处
可读性强
避免key冲突
方便管理
更节省内存
key是string类型,底层编码包含int、embstr和raw三种。embstr在小于44字节使用,采用连续内存空间,内存占用更小。当字节数大于44字节时,会转为raw模式存储,在raw模式下,内存空间不是连续的,而是采用一个指针指向了另外一段内存空间,在这段空间里存储SDS内容,这样空间不连续,访问的时候性能也就会收到影响,还有可能产生内存碎片
拒绝BigKey
判定条件
Key本身的数据量过大
一个String类型的Key,它的值为5 MB
Key中的成员数过多
一个ZSET类型的Key,它的成员数量为10,000个
Key中成员的数据量过大
一个Hash类型的Key,它的成员数量虽然只有1,000个,但这些成员的Value(值)总大小为100 MB
推荐值
单个key的value小于10KB
对于集合类型的key,建议元素数量小于1000
危害
网络阻塞
对BigKey执行读请求时,少量的QPS就可能导致带宽使用率被占满,导致Redis实例,乃至所在物理机变慢
比如一个key占到5M,如并发20,就需要100M带宽
数据倾斜
BigKey所在的Redis实例内存使用率远超其他实例,无法使数据分片的内存资源达到均衡
Redis阻塞
对元素较多的hash、list、zset等做运算会耗时较旧,使主线程(单线程)被阻塞
CPU压力
对BigKey的数据序列化和反序列化会导致CPU的使用率飙升,影响Redis实例和本机其它应用
排查BigKey命令
MEMORY USAGE key
不推荐使用此命令,占用CPU使用率
String类型
STRLEN key
集合类型
LLEN key
看个数
redis-cli -a 密码 --bigkeys
不够完整、只会给每种数据类型的最大的.但有可能第二大的也是
scan扫描
自己编程,利用scan扫描Redis中的所有key,利用strlen、hlen等命令判断key的长度(此处不建议使用MEMORY USAGE)
scan 命令调用完后每次会返回2个元素,第一个是下一次迭代的光标,第一次光标会设置为0,当最后一次scan 返回的光标等于0时,表示整个scan遍历结束了,第二个返回的是List,一个匹配的key的数组
第三方工具
如 Redis-Rdb-Tools 分析RDB快照文件,全面分析内存使用情况
https://github.com/sripathikrishnan/redis-rdb-tools
网络监控
自定义工具,监控进出Redis的网络数据,超出预警值时主动告警
一般阿里云搭建的云服务器就有相关监控页面
如何删除BigKey
问题
BigKey内存占用较多,即便时删除这样的key也需要耗费很长时间,导致Redis主线程阻塞,引发一系列问题
解决方案
redis 3.0 及以下版本
如果是集合类型,则遍历BigKey的元素,先逐个删除子元素,最后删除BigKey
Redis 4.0以后
提供了异步删除的命令:unlink
优化措施
大key治理
更换缓存对象序列化方法,由原来的JSON序列化调整为Protostuff序列化方式。
治理效果:缓存对象大小由1.5M减少到了0.5M。
使用压缩算法
在存储缓存对象时,再使用压缩算法(如gzip)对数据进行压缩,注意设置压缩阈值,超过一定阈值后再进行压缩,以减少占用的内存空间和网络传输的数据量。
压缩效果:500k压缩到了17k。
恰当的数据类型
示例
存储一个User对象,我们有多种存储方式
json字符串
key
user:1
value
{"name": "Jack", "age": 21}
优点
实现简单粗暴
缺点
数据耦合,不够灵活
字段打散
key
user:1:name
user:1:age
value
Jack
21
优点
可以灵活访问对象任意字段
缺点
占用空间大、没办法做统一控制
hash(推荐)
key
user:1
field
name
age
value
jack
21
优点
底层使用ziplist,空间占用小,可以灵活访问对象的任意字段
缺点
代码相对复杂
假如有hash类型的key,其中有100万对field和value,field是自增id,这个key存在什么问题?如何优化?
存在的问题
hash的entry数量超过500时,会使用哈希表而不是ZipList,内存占用较多
可以通过hash-max-ziplist-entries配置entry上限。但是如果entry过多就会导致BigKey问题
解决方案
拆分为string类型
存在的问题
string结构底层没有太多内存优化,内存占用较多
想要批量获取这些数据比较麻烦
拆分为小的hash,将 id / 100 作为key, 将id % 100 作为field,这样每100个元素为一个Hash
批处理优化
单机
解决方案
原生的M操作
缺点
MSET虽然可以批处理,但是却只能操作部分数据类型,因此如果有对复杂数据类型的批处理需要,建议使用Pipeline
Pipeline批处理
注意事项
批处理时不建议一次传输太多的命令,否则单次命令占用带宽过多。会导致网络阻塞
Pipeline的多个命令之间不具备原子性
相比之下,M命令是原子操作
集群
如MSET或Pipeline这样的批处理需要在一次请求中携带多条命令,而此时如果Redis是一个集群,那批处理命令的多个key必须落在一个插槽中,否则就会导致执行失败。
原因
因为多条命令计算出来的插槽值不能保证是同一个;应该保存在不同节点上,但是批处理是在一次连接中将所有命令执行完。如果保存在多个节点上就需要多个连接,这就不是批处理了
解决方案
串行命令
实现思路
for循环遍历,依次执行每个命令
耗时
N次网络耗时+N次命令耗时
优点
实现简单
缺点
耗时非常久
串行slot
实现思路
在客户端计算每个key的slot,将slot一致分为-组,每组都利用Pipeline批处理。串行执行各组命令
耗时
m次网络耗时 +N次命令耗时;m=key的slot个数
优点
耗时较短
缺点
实现稍复杂
slot越多,耗时越久
并行slot(推荐)
实现思路
在客户端计算每个key的slot,将slot一致分为-组,每组都利用Pipeline批处理。并行执行各组命令
耗时
1次网络耗时 +N次命令耗时
优点
耗时非常短
缺点
实现复杂
Spring Boot 封装实现了Lettuce的并行slot
stringRedisTemplate
hash_tag(不推荐)
实现思路
将所有key设置相同的hash_tag,则所有key的slot一定相同
耗时
1次网络耗时 +N次命令耗时
优点
耗时非常短、实现简单
缺点
容易出现数据倾斜
集群
集群虽然具备高可用特性,能实现自动故障恢复,但是如果使用不当,也会存在一些问题
集群完整性问题
在Redis的默认配置中,如果发现任意一个插槽不可用,出于数据完整性考虑,使整个集群都会停止对外服务
可以设想一下,如果有几个slot不能使用,那么此时整个集群都不能用了,我们在开发中,其实最重要的是可用性,所以需要把如下配置修改成no,即有slot不能使用时,我们的redis集群还是可以对外提供服务
cluster-require-full-coverage no
配置以后,如果set数据在健康的插槽还是可以正常写入,但如果数据落在异常插槽,会写入失败
集群带宽问题
集群节点之间会不断的互相Ping来确定集群中其它节点的状态。(因为没有哨兵节点)
Ping携带的信息
插槽信息
集群状态信息
集群中节点越多,集群状态信息数据量也越大,10个节点的相关信息可能达到1kb,此时每次集群互通需要的带宽会非常高,这样会导致集群中大量的带宽都会被ping信息所占用,这是一个非常可怕的问题,所以我们需要去解决这样的问题
解决方案
避免大集群,集群节点数不要太多,最好少于1000,如果业务庞大,则建立多个集群。
避免在单个物理机中运行太多Redis实例
部署10可能就差不多
配置合适的cluster-node-timeout值
可以将Ping的时间设置cluster-node-timeout的一半
cluster-node-timeout是默认客观下线时间
数据倾斜问题
BigKey或者集群批处理使用了相同的hash_tag,可能会导致数据倾斜。使得部分节点负担过重,部分过轻
客户端访问集群性能问题
节点选择
读写判断
插槽判断
命令的集群兼容性问题
有关这个问题咱们已经探讨过了,当我们使用批处理的命令时,redis要求我们的key必须落在相同的slot上,然后大量的key同时操作时,是无法完成的,所以客户端必须要对这样的数据进行处理,这些方案我们之前已经探讨过了,所以不再这个地方赘述了。
lua和事务问题
lua和事务都是要保证原子性问题,如果你的key不在一个节点,那么是无法保证lua的执行和事务的特性的
在集群模式是没有办法执行lua和事务!!!
到底是集群还是主从
单体Redis(主从Redis)已经能达到万级别的QPS,并且也具备很强的高可用特性。如果主从能满足业务需求的情况下,所以如果不是在万不得已的情况下,尽量不搭建Redis集群
命令及安全配置
Redis会绑定在0.0.0.0:6379,这样将会将Redis服务暴露到公网上,而Redis如果没有做身份认证,会出现严重的安全漏洞
漏洞重现方式:https://cloud.tencent.com/developer/article/1039000
漏洞出现的核心的原因有以下几点
Redis未设置密码
利用了Redis的config set命令动态修改Redis配置
使用了Root账号权限启动Redis
为了避免这样的漏洞,这里给出一些建议
① Redis一定要设置密码
② 禁止线上使用下面命令:keys、flushall、flushdb、config set等命令。可以利用rename-command禁用。
rename-command
相当于给命令取了个UUID的别名
③ bind:限制网卡,禁止外网网卡访问
④ 开启防火墙
⑤不要使用Root账户启动Redis
⑥ 尽量不是有默认的端口
内存安全和配置
当Redis内存不足时,可能导致Key频繁被删除、响应时间变长、QPS不稳定等问题。当内存使用率达到90%以上时就需要我们警惕,并快速定位到内存占用的原因。
类型
数据内存(排查重点)
是Redis最主要的部分,存储Redis的键值信息。主要问题是BigKey问题、内存碎片问题
进程内存
Redis主进程本身运行肯定需要占用内存,如代码、常量池等等;这部分内存大约几兆,在大多数生产环境中与Redis数据占用的内存相比可以忽略。
缓冲区内存(排查重点)
一般包括客户端缓冲区、AOF缓冲区、复制缓冲区等。客户端缓冲区又包括输入缓冲区和输出缓冲区两种。这部分内存占用波动较大,不当使用BigKey,可能导致内存溢出。
类型
复制缓冲区
主从复制的repl backlog buf,如果太小可能导致频繁的全量复制,影响性能。通过repl-backlogsize来设置,默认1mb
AOF缓冲区
AOF刷盘之前的缓存区域,AOF执行rewrite的缓冲区。无法设置容量上限
客户端缓冲区
输入缓冲区
输入缓冲区最大1G且不能设置
输出缓冲区
输出缓冲区可以设置
client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
class
客户端类型
normal:普通客户端
replica:主从复制客户端
pubsub:PubSub客户端
hard limit
缓冲区上限在超过limit后断开客户端
<soft limit> <soft seconds>
缓冲区上限,在超过soft limit 并且持续了 soft seconds秒后断开客户端
默认值
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
Redis处理完命令以后,返回结果时先存放的地方
info clients
可以看到连接了多少个客户端,输入与输出缓冲区占用
client list
可以看到id,地址,输入,输出缓冲区占用omem,连接时间
查看内存命令
info memory
查看内存分配的情况
memory xxx
查看key的主要占用情况
memory stats参数说明
peak.allocated
Redis进程自启动以来消耗内存的峰值
total.allocated
Redis使用其分配器分配的总字节数,即当前的总内存使用量。
startup.allocated
Redis启动时消耗的初始内存
replication.backlog
复制积压缓冲区的大小
clients.slaves
主从复制中所有从节点的读写缓冲区大小
clients.normal
除从节点外,所有其他客户端的读写缓冲区大小
aof.buffer
AOF持久化使用的缓存和A0F重写时产生的缓存
db.0
业务数据库的数量。
overhead.hashtable.main
当前数据库的hash链表开销内存总和,即元数据内存。
overhead.hashtable.expires
用于存储key的过期时间所消耗的内存。
overhead.total
数值=startup.allocated+replication.backlog+clients.slaves+clients.normal+aof.buffer+db.X
keys.count
当前Redis实例的key总数
keys.bytes-per-key
当前Redis实例每个key的平均大小
计算公式
(total.allocated-startup.allocated)/keys.count
dataset.bytes
纯业务数据占用的内存大小。
dataset.percentage
纯业务数据占用的内存比例
计算公式
dataset.bytes*100/(total.allocated-startup.allocated)
peak.percentage
当前总内存与历史峰值的比例
计算公式
tota1.allocated*180/peak.allocated
fragmentation
内存的碎片率
原理
数据结构
动态字符串SDS
Redis中保存的Key是字符串,value往往是字符串或者字符串的集合。字符串是Redis中最常用的一种数据结构。
Redis没有直接使用C语言中的字符串,因为C语言字符串存在很多问题
C语言中的字符串,本质是字符数组
获取字符串长度的需要通过运算
非二进制安全
比如字符串中途有\0这种特殊字符,可能就读到一般就中断了
不可修改
放在了内存中的常量池
Redis构建了一种新的字符串结构,称为简单动态字符串(Simple Dynamic String)
例子
set name 小明
那么Redis将在底层创建两个SDS,其中一个是包含“name”的SDS,另一个是包含“小明”的SDS。
Redis是C语言实现的,其中SDS是一个结构体(Java中的类)
源码
struct _attribute_((_packed_)) sdshdr8 {
uint8_t len; /* buf已保存的字符串字节数,不包含结束标示,属于header*/
uint8_t alloc; /* buf申请的总的字节数,不包含结束标示,属于header*/
unsigned char flags; /*不同SDS的头类型,用来控制SDS的头大小,属于header*/
char buf[];
}
uint8_t len; /* buf已保存的字符串字节数,不包含结束标示,属于header*/
uint8_t alloc; /* buf申请的总的字节数,不包含结束标示,属于header*/
unsigned char flags; /*不同SDS的头类型,用来控制SDS的头大小,属于header*/
char buf[];
}
其中flags值
#define SDS TYPE 5 0
#define SDS TYPE 8 1
#define SDS TYPE 16 2
#define SDS TYPE 32 3
#define SDS TyPE 64 4
#define SDS TYPE 8 1
#define SDS TYPE 16 2
#define SDS TYPE 32 3
#define SDS TyPE 64 4
例如,一个包含字符串“name”的sds结构
2(len)|2(alloc)|1(flags)|h|i|\0
SDS之所以叫做动态字符串,是因为它具备动态扩容的能力
例如一个内容为“hi”的SDS
4(len)|4(alloc)|1(flags)|n|a|m|e|\0
如果新字符串小于1M,则新空间为扩展后字符串长度的两倍+1;
如果新字符串大于1M,则新空间为扩展后字符串长度+1M+1。称为内存预分配。
如果新字符串大于1M,则新空间为扩展后字符串长度+1M+1。称为内存预分配。
假如我们要给SDS追加一段字符串“,Tom”,这里首先会申请新内存空间
6(len)|12(alloc)|1(flags)|h|i|,|T|o|m|\0
优点
获取字符串长度的时间复杂度为O(1)
支持动态扩容
减少内存分配次数
二进制安全
IntSet
IntSet是Redis中set集合的一种实现方式,基于整数数组来实现,并且具备长度可变、有序等特征
结构
typedef struct intset {
uint32_t encoding; /*编码方式,支持存放16位、32位、64位整数*/
uint32_t length; /* 元素个数*/
int8_t contents[];/* 整数数组,保存集合数据*/
} intset;
uint32_t encoding; /*编码方式,支持存放16位、32位、64位整数*/
uint32_t length; /* 元素个数*/
int8_t contents[];/* 整数数组,保存集合数据*/
} intset;
int8:int整数,8代表8个比特位(一个字节 -128~127)
contents:只是一个指针,指向了起始元素的地址
encoding包含三种模式,表示存储的整数大小不同
/* Note that these encodings are ordered, so:
* INTSET ENC INT16 < INTSET ENC INT32 < INTSET ENC INT64. */
#define INTSET ENC INT16 (sizeof(int16 t))/* 2字节整数,范围类似java的short*/
#define INTSET ENC INT32 (sizeof(int32 t)/* 4字节整数,范围类似java的int */
#define INTSET ENC INT64 (sizeof(int64 t))/* 8字节整数,范围类似java的long */
* INTSET ENC INT16 < INTSET ENC INT32 < INTSET ENC INT64. */
#define INTSET ENC INT16 (sizeof(int16 t))/* 2字节整数,范围类似java的short*/
#define INTSET ENC INT32 (sizeof(int32 t)/* 4字节整数,范围类似java的int */
#define INTSET ENC INT64 (sizeof(int64 t))/* 8字节整数,范围类似java的long */
虽然intset结构将contents属性声明为int8_t类型的数组,但实际上contents数组并不保存任何int8_t类型的值,contents数组的真正类型取决于encoding属性的值
为了方便查找,Redis会将intset中所有的整数按照升序依次保存在contents数组中
Header
encoding:INTSET ENC INT16
length:3
int16 t contents[]
5
0x001
10
0x003
20
0x005
现在,数组中每个数字都在int16_t的范围内,因此采用的编码方式是INTSET_ENC_INT16,每部分占用的字节大小为:
encoding:4字节
length:4字节
contents:2字节 * 3 = 6字节
encoding:4字节
length:4字节
contents:2字节 * 3 = 6字节
寻址公式
startPtr +(sizeof(int16)*index)
startPtr起始地址
index
角标
升级
我们向该其中添加一个数字:50000,这个数字超出了int16_t的范围,intset会自动升级编码方式到合适的大小(会去判断哪种编码方式合适)
流程
升级编码为INTSET_ENC_INT32, 每个整数占4字节,并按照新的编码方式及元素个数扩容数组
倒序依次将数组中的元素拷贝到扩容后的正确位置
倒序是为了防止元素被覆盖
将待添加的元素放入数组末尾
最后,将inset的encoding属性改为INTSET_ENC_INT32,将length属性改为4
源码
总结
Intset可以看做是特殊的整数数组
推荐在数据量不大的情况下使用
特点
Redis会确保Intset中的元素唯一、有序
具备类型升级机制,可以节省内存空间
底层采用二分查找方式来查询,保证有序性
Dict(HashTable)
Redis是一个键值型(Key-Value Pair)的数据库,我们可以根据键实现快速的增删改查。而键与值的映射关系正是通过Dict来实现的
内存中分散,不连续
容易造成内存碎片
指针本身也需要8个字节,浪费空间
组成
字典(Dict)
源码
typedef struct dict {
dictType *type; // dict类型,内置不同的hash函数
void *privdata; // 私有数据,在做特殊hash运算时用
dictht ht[2]; // 一个Dict包含两个哈希表,其中一个是当前数据,另一个一般是空,rehash时使用
long rehashidx; // rehash的进度,-1表示未进行
unsigned long iterators; // rehash是否暂停,1则暂停,0则继续
} dict;
dictType *type; // dict类型,内置不同的hash函数
void *privdata; // 私有数据,在做特殊hash运算时用
dictht ht[2]; // 一个Dict包含两个哈希表,其中一个是当前数据,另一个一般是空,rehash时使用
long rehashidx; // rehash的进度,-1表示未进行
unsigned long iterators; // rehash是否暂停,1则暂停,0则继续
} dict;
哈希表(DictHashTable)
源码
typedef struct dictht {
// entry数组
// 数组中保存的是指向entry的指针
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小的掩码,总等于size-1
unsigned long sizemask;
// entry个数
unsigned long used;
} dictht;
// entry数组
// 数组中保存的是指向entry的指针
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小的掩码,总等于size-1
unsigned long sizemask;
// entry个数
unsigned long used;
} dictht;
*table是dictEntry类型的指针
**table是存放指向dictEntry类型指针的数组
size是2的N次方,table知道了起始位置的指针,通过size得知当前数组有多大
used告知我们当前有多少数据
可以理解为底层就是个数组
哈希节点(DictEntry)
源码
typedef struct dictEntry {
void *key;// 键
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;// 值
// 下一个Entry的指针
struct dictEntry *next;
} dictEntry;
void *key;// 键
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;// 值
// 下一个Entry的指针
struct dictEntry *next;
} dictEntry;
v值是联合体,可以是里面的任何一种,但不能同时成立
next可以形成单向链表
生活中的例子
想象一个图书馆的书架(table 数组),每个格子(数组位置)都贴着一张标签(指针),标签上写着某本书(dictEntry)的具体位置。如果有新书要放在同一个格子,就像把新书放在书架上时,不是插到最后,而是放在最前面,形成一个链条。
采用了头插法,避免了遍历全部数据
当我们向Dict添加键值对时,Redis首先根据key计算出hash值(h),然后利用 h & sizemask来计算元素应该存储到数组中的哪个索引位置。我们存储k1=v1,假设k1的哈希值h =1,则1&3 =1,因此k1=v1要存储到数组角标1位置。
为什么是& sizemask
size是2的N次方
sizemask是& size-1
比如size是16,sizemask就是15(1111),按位&,有0为0,和1做&运算结果是本身。结果必然落在0~15之间。
早期Java中的HashMap中是取余,相比之下&运算效率高3~10倍
扩缩容
Dict中的HashTable就是数组结合单向链表的实现,当集合中元素较多时,必然导致哈希冲突增多,链表过长,则查询效率会大大降低。
触发哈希表扩容条件
Dict在每次新增键值对时都会检查负载因子(LoadFactor = used/size)
哈希表的 LoadFactor >= 1,并且服务器没有执行 BGSAVE 或者 BGREWRITEAOF 等后台进程;
此类后台进程对CPU使用很高,还会有大量IO读写,所以可能会影响性能
哈希表的 LoadFactor > 5 ;
Dict除了扩容以外,每次删除元素时,也会对负载因子做检查,当loadFactor < 0.1 时,会做哈希表收缩
源码
扩容源码
static int _dictExpandIfNeeded(dict *d)
{
// 如果正在rehash,则返回ok
if (dictIsRehashing(d)) return DICT_OK;
// 如果哈希表为空,则初始化哈希表为默认大小:4
if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);
// 当负载因子(used/size)达到1以上,并且当前没有进行bgrewrite等子进程操作
// 或者负载因子超过5,则进行 dictExpand ,也就是扩容
if (d->ht[0].used >= d->ht[0].size &&
(dict_can_resize ||
d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
{
// 扩容大小为used +1,底层会对扩容大小做判断,实际上找的是第一个大于等于 used+1 的 2^n
return dictExpand(d, d->ht[0].used*2);
}
return DICT_OK;
}
{
// 如果正在rehash,则返回ok
if (dictIsRehashing(d)) return DICT_OK;
// 如果哈希表为空,则初始化哈希表为默认大小:4
if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);
// 当负载因子(used/size)达到1以上,并且当前没有进行bgrewrite等子进程操作
// 或者负载因子超过5,则进行 dictExpand ,也就是扩容
if (d->ht[0].used >= d->ht[0].size &&
(dict_can_resize ||
d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
{
// 扩容大小为used +1,底层会对扩容大小做判断,实际上找的是第一个大于等于 used+1 的 2^n
return dictExpand(d, d->ht[0].used*2);
}
return DICT_OK;
}
执行 BGSAVE 或者 BGREWRITEAOF 等后台进程时,会将dict_can_resize会置为0(false),执行完以后置为1
dict_force_resize_ratio为5
收缩源码
// t hash.c # hashTypeDeleted()
if (dictDelete((dict*)o->ptr, field) == C_OK) {
deleted = 1;
// 删除成功后,检查是否需要重置Dict大小,如果需要则调用dictResize重置
/* Always check if the dictionary needs a resize after a delete. */
if (htNeedsResize(o->ptr)) dictResize(o->ptr);
}
if (dictDelete((dict*)o->ptr, field) == C_OK) {
deleted = 1;
// 删除成功后,检查是否需要重置Dict大小,如果需要则调用dictResize重置
/* Always check if the dictionary needs a resize after a delete. */
if (htNeedsResize(o->ptr)) dictResize(o->ptr);
}
//server.c文件
int htNeedsResize(dict *dict) {
long long size, used;
// 哈希表大小
size = dictSlots(dict);
// entry数量
used = dictSize(dict);
// size > 4(哈希表初识大小) 并且 负载因子低于0.1
return (size > DICT_HT_INITIAL_SIZE && (used*100/size < HASHTABLE_MIN_FILL));
}
int htNeedsResize(dict *dict) {
long long size, used;
// 哈希表大小
size = dictSlots(dict);
// entry数量
used = dictSize(dict);
// size > 4(哈希表初识大小) 并且 负载因子低于0.1
return (size > DICT_HT_INITIAL_SIZE && (used*100/size < HASHTABLE_MIN_FILL));
}
//dict.c文件
int dictResize(dict *d)
{
unsigned long minimal;
// 如果正在做bgsave或bgrewriteof或rehash,则返回错误
if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
// 获取used,也就是entry个数
minimal = d->ht[0].used;
// 如果used小于4,则重置为4
if (minimal < DICT_HT_INITIAL_SIZE)
minimal = DICT_HT_INITIAL_SIZE;
// 重置大小为minimal,其实是第一个大于等于minimal的2^n
return dictExpand(d, minimal);
}
int dictResize(dict *d)
{
unsigned long minimal;
// 如果正在做bgsave或bgrewriteof或rehash,则返回错误
if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
// 获取used,也就是entry个数
minimal = d->ht[0].used;
// 如果used小于4,则重置为4
if (minimal < DICT_HT_INITIAL_SIZE)
minimal = DICT_HT_INITIAL_SIZE;
// 重置大小为minimal,其实是第一个大于等于minimal的2^n
return dictExpand(d, minimal);
}
rehash
无论扩容还是收缩,必定会创建新的哈希表,导致哈希表的size和sizemask变化,而key的查询与sizemask有关。因此必须对哈希表中的每一个key重新计算索引,插入新的哈希表,这个过程称为rehash
过程
计算新hash表的realeSize,值取决于当前要做的是扩容还是收缩
如果是扩容,则新size为第一个大于等于dict.ht[0].used + 1的2^n
如果是收缩,则新size为第一个大于等于dict.ht[0].used的2^n (不得小于4)
按照新的realeSize申请内存空间,创建dictht,并赋值给dict.ht[1]
设置dict.rehashidx = 0,标示开始rehash
(简化版)将dict.ht[0]中的每一个dictEntry都rehash到dict.ht[1]
(实际渐进式rehash版本)每次执行新增、查询、修改、删除操作时,都检查一下dict.rehashidx是否大于-1,如果是则将dict.ht[0].table[rehashidx]的entry链表rehash到dict.ht[1],并且将rehashidx++。直到dict.ht[0]的所有数据都rehash到dict.ht[1]
将dict.ht[1]赋值给dict.ht[0],给dict.ht[1]初始化为空哈希表,释放原来的dict.ht[0]的内存
将rehashidx赋值为-1,代表rehash结束
在rehash过程中,新增操作,则直接写入ht[1],查询、修改和删除则会在dict.ht[0]和dict.ht[1]依次查找并执行。这样可以确保ht[0]的数据只减不增,随着rehash最终为空
渐进式rehash
Dict的rehash并不是一次性完成的。试想一下,如果Dict中包含数百万的entry,要在一次rehash完成,极有可能导致主线程阻塞。因此Dict的rehash是分多次、渐进式的完成
总结
结构
类似java的HashTable、HashMap,底层是数组加链表来解决哈希冲突
Dict包含两个哈希表,ht[0]平常用,ht[1]用来rehash
伸缩
当LoadFactor大于5或者LoadFactor大于1并且没有子进程任务时,Dict扩容
当LoadFactor小于0.1时,Dict收缩
扩容大小为第一个大于等于used + 1的2^n
收缩大小为第一个大于等于used 的2^n
Dict采用渐进式rehash,每次访问Dict时执行一次rehash
rehash时ht[0]只减不增,新增操作只在ht[1]执行,其它操作在两个哈希表
ZipList
是一种特殊的“双端链表” ,由一系列特殊编码的连续内存块组成。可以在任意一端进行压入/弹出操作, 并且该操作的时间复杂度为 O(1)
结构
属性
zlbytes
类型
uint32_t
长度
4字节
用途
记录整个压缩列表占用的内存字节数
zltail
类型
uint32_t
长度
4字节
用途
记录压缩列表表尾节点距离压缩列表的起始地址有多少字节,通过这个偏移量,可以确定表尾节点的地址。
zllen
类型
uint16_t
长度
2字节
用途
记录了压缩列表包含的节点数量。 最大值为UINT16_MAX (65534),如果超过这个值,此处会记录为65535,但节点的真实数量需要遍历整个压缩列表才能计算得出。
entry
类型
列表节点
长度
不定
用途
压缩列表包含的各个节点,节点的长度由节点保存的内容决定。
为了节省内存,避免存1和存200万使用相同的节点大小
说明
ZipList 中的Entry并不像普通链表那样记录前后节点的指针,因为记录两个指针要占用16个字节,浪费内存
结构
previous_entry_length
前一节点的长度,占1个或5个字节。
如果前一节点的长度小于254字节,则采用1个字节来保存这个长度值
如果前一节点的长度大于254字节,则采用5个字节来保存这个长度值,第一个字节为0xfe,后四个字节才是真实长度数据
encoding
编码属性,记录content的数据类型(字符串还是整数)以及长度,占用1个、2个或5个字节
分类
字符串
以“00”开头
编码
|00pppppp|
编码长度
1 bytes
字符串大小
<= 63 bytes
以“01”开头
编码
|01pppppp|qqqqqqqq|
编码长度
2 bytes
字符串大小
<= 16383 bytes
以“10”开头
编码
|10000000|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt|
编码长度
5 bytes
字符串大小
<= 4294967295 bytes
例如,我们要保存字符串:“ab”和 “bc”
整数
encoding是以“11”开始,则证明content是整数,且encoding固定只占用1个字节
以“11”开头
编码
编码长度
1
整数类型
int16_t(2 bytes)
编码(11010000)
编码长度
1
整数类型
int32_t(4 bytes)
编码(11100000)
编码长度
1
整数类型
int64_t(8 bytes)
编码(11110000)
编码长度
1
整数类型
24位有符整数(3 bytes)
编码(11111110)
编码长度
1
整数类型
8位有符整数(1 bytes)
特殊编码(1111xxxx)
编码长度
1
整数类型
直接在xxxx位置保存数值,范围从0001~1101,减1后结果为实际值(0~12)
例子
一个ZipList中包含整数值:"2"和"5"
2
11110011
既有encoding,又有contents
contents
负责保存节点的数据,可以是字符串或整数
注意事项
ZipList中所有存储长度的数值均采用小端字节序,即低位字节在前,高位字节在后。例如:数值0x1234,采用小端字节序后实际存储值为:0x3412
zlend
类型
uint8_t
长度
1字节
用途
特殊值 0xFF (十进制 255 ),用于标记压缩列表的末端。
问题
ZipList的连锁更新问题
ZipList的每个Entry都包含previous_entry_length来记录上一个节点的大小,长度是1个或5个字节:
如果前一节点的长度小于254字节,则采用1个字节来保存这个长度值
如果前一节点的长度大于等于254字节,则采用5个字节来保存这个长度值,第一个字节为0xfe,后四个字节才是真实长度数据
如果前一节点的长度小于254字节,则采用1个字节来保存这个长度值
如果前一节点的长度大于等于254字节,则采用5个字节来保存这个长度值,第一个字节为0xfe,后四个字节才是真实长度数据
现在,假设我们有N个连续的、长度为250~253字节之间的entry,因此entry的previous_entry_length属性用1个字节即可表示
如果现在新增一个大于等于254字节的a节点数据,原先头节点b的previous_entry_length为0(占一个字节),现在需要更新b的previous_entry_length。现在新增的节点需要5个字节来保存这个长度值。这就导致b节点从250字节长到了254及以上,b节点后面的c节点以此类推。
这种频繁内存申请,销毁,数据迁移的动作,涉及到内核态的切换,很大程度影响性能
这种特殊情况下产生的连续多次空间扩展操作称之为连锁更新(Cascade Update)。新增、删除都可能导致连锁更新的发生。
问题根源
记录了前一个节点大小
解决方案
listpack
特性
压缩列表的可以看做一种连续内存空间的"双向链表"
列表的节点之间不是通过指针连接,而是记录上一节点和本节点长度来寻址,内存占用较低
如果列表数据过多,导致链表过长,可能影响查询性能
增或删较大数据时有可能发生连续更新问题
QuickList
ZipList问题
ZipList虽然节省内存,但申请内存必须是连续空间,如果内存占用较多,申请内存效率很低。怎么办?
答:为了缓解这个问题,我们必须限制ZipList的长度和entry大小。
但是我们要存储大量数据,超出了ZipList最佳的上限该怎么办?
答:我们可以创建多个ZipList来分片存储数据。
数据拆分后比较分散,不方便管理和查找,这多个ZipList如何建立联系?
答:Redis在3.2版本引入了新的数据结构QuickList,它是一个双端链表,只不过链表中的每个节点都是一个ZipList。
结构示意图
源码
配置项
list-max-ziplist-size
为了避免QuickList中的每个ZipList中entry过多,Redis提供的配置
如果值为正,则代表ZipList的允许的entry个数的最大值
如果值为负,则代表ZipList的最大内存大小
-1:每个ZipList的内存占用不能超过4kb
-2(默认值):每个ZipList的内存占用不能超过8kb
-3:每个ZipList的内存占用不能超过16kb
-4:每个ZipList的内存占用不能超过32kb
-5:每个ZipList的内存占用不能超过64kb
-2(默认值):每个ZipList的内存占用不能超过8kb
-3:每个ZipList的内存占用不能超过16kb
-4:每个ZipList的内存占用不能超过32kb
-5:每个ZipList的内存占用不能超过64kb
list-compress-depth
除了控制ZipList的大小,QuickList还可以对节点的ZipList做压缩
因为链表一般都是从首尾访问较多,所以首尾是不压缩的。这个参数是控制首尾不压缩的节点个数
0(默认值):特殊值,代表不压缩
1:标示QuickList的首尾各有1个节点不压缩,中间节点压缩
2:标示QuickList的首尾各有2个节点不压缩,中间节点压缩
以此类推
1:标示QuickList的首尾各有1个节点不压缩,中间节点压缩
2:标示QuickList的首尾各有2个节点不压缩,中间节点压缩
以此类推
总结
是一个节点为ZipList的双端链表
节点采用ZipList,解决了传统链表的内存占用问题
控制了ZipList大小,解决连续内存空间申请效率问题
中间节点可以压缩,进一步节省了内存
SkipList(跳表)
定义
SkipList首先是多重链表,或者是多级指针
与传统链表相比有几点差异
元素按照升序排列存储
节点可能包含多个指针,指针跨度不同
传统链表指针跨度为1
底层链表保存所有数据,每一层链表都是下一层的子集
平均时间复杂度为O(logn),最差是O(n)
示意图
内存图
score值是得分,排序用的,允许重复
ele是保存的数据,SDS字符串
Redis中跳表比普通跳表多了backward后退指针
总结
跳跃表是一个双向链表,每个节点都包含score和ele值
节点按照score值排序,score值一样则按照ele字典排序
每个节点都可以包含多层指针,层数是1到32之间的随机数
不同层指针到下一个节点的跨度不同,层级越高,跨度越大
增删改查效率与红黑树基本一致,实现却更简单
跳表与红黑树比较
红黑树性能更好
范围查找性能相当
也不会更节约内存
跳表更容易实现
跳表更容易实现无锁并发,红黑树很难做到
但这点与Redis关系不大,Redis的单线程
RedisObject
Redis中的任意数据类型的键和值都会被封装为一个RedisObject,也叫做Redis对象
源码
编码方式
OBJ_ENCODING_RAW
raw编码动态字符串
OBJ_ENCODING_INT
long类型的整数的字符串
OBJ_ENCODING_HT
hash表(字典dict)
OBJ_ENCODING_ZIPMAP
已废弃
OBJ_ENCODING_LINKEDLIST
双端链表
OBJ_ENCODING_ZIPLIST
压缩列表
OBJ_ENCODING_INTSET
整数集合
OBJ_ENCODING_SKIPLIST
跳表
OBJ_ENCODING_EMBSTR
embstr的动态字符串
OBJ_ENCODING_QUICKLIST
快速列表
OBJ_ENCODING_STREAM
Stream流
一个Redis对象头信息占用空间就要16字节(不包含指针指向的空间)
type占用4比特位
encoding占用4比特位
lru占用24比特位
refcount占用4字节
ptr占用8字节
假设有10个字符串对象,就需要10个对象来存储,浪费了空间。更加推荐利用集合来存储,只占用一个对象头信息
五种数据结构
OBJ_STRING
int、embstr、raw
OBJ_LIST
LinkedList和ZipList(3.2以前)、QuickList(3.2以后)
OBJ_SET
intset、HT
OBJ_ZSET
ZipList、HT、SkipList
OBJ_HASH
ZipList、HT
Redis中会根据存储的数据类型不同,选择不同的编码方式
什么是redisObject
从Redis的使用者的角度来看,⼀个Redis节点包含多个database(非cluster模式下默认是16个,cluster模式下只能是1个),而一个database维护了从key space到object space的映射关系。这个映射关系的key是string类型,⽽value可以是多种数据类型,比如:
string, list, hash、set、sorted set等。我们可以看到,key的类型固定是string,而value可能的类型是多个。
⽽从Redis内部实现的⾓度来看,database内的这个映射关系是用⼀个dict来维护的。dict的key固定用⼀种数据结构来表达就够了,这就是动态字符串sds。而value则比较复杂,为了在同⼀个dict内能够存储不同类型的value,这就需要⼀个通⽤的数据结构,这个通用的数据结构就是robj,全名是redisObject。
⽽从Redis内部实现的⾓度来看,database内的这个映射关系是用⼀个dict来维护的。dict的key固定用⼀种数据结构来表达就够了,这就是动态字符串sds。而value则比较复杂,为了在同⼀个dict内能够存储不同类型的value,这就需要⼀个通⽤的数据结构,这个通用的数据结构就是robj,全名是redisObject。
五种数据类型
String
Redis中最常见的数据存储类型
基本编码方式是RAW,基于简单动态字符串(SDS)实现,存储上限为512mb
需要调用两次内存分配函数
还需要通过指针进行寻址
如果存储的SDS长度小于44字节,则会采用EMBSTR编码,此时object head与SDS是一段连续空间。申请内存时
只需要调用一次内存分配函数,效率更高
只需要调用一次内存分配函数,效率更高
为什么限制于44字节呢?
一个Redis对象头信息占用空间就要16字节
SDS头占用3字节,结尾\0占用1个字节
内容占用44字节
一共64字节
一个内存分片大小,不会产生内存碎片
如果存储的字符串是整数值,并且大小在LONG_MAX范围内,则会采用INT编码:直接将数据保存在RedisObject的ptr指针位置(刚好8字节),不再需要SDS了。
将字符串转为二进制位形式的数字存储
内存结构
List
可以从首、尾操作列表中的元素
哪一个数据结构能满足上述特征?
LinkedList :普通链表,可以从双端访问,内存占用较高,内存碎片较多
ZipList :压缩列表,可以从双端访问,内存占用低,存储上限低
QuickList:LinkedList + ZipList,可以从双端访问,内存占用较低,包含多个ZipList,存储上限高
Redis的List结构类似一个双端链表,可以从首、尾操作列表中的元素
在3.2版本之前,Redis采用ZipList和LinkedList来实现List,当元素数量小于512并且元素大小小于64字节时采用ZipList编码,超过则采用LinkedList编码
在3.2版本之后,Redis统一采用QuickList来实现List:
内存结构
Set
Redis中的单列集合
特点
不保证有序性
保证元素唯一
求交集、并集、差集
Set对查询元素的效率要求非常高,思考一下,什么样的数据结构可以满足?
HashTable,也就是Redis中的Dict,不过Dict是双列集合(可以存键、值对)
Set是Redis中的集合,不一定确保元素有序,可以满足元素唯一、查询效率要求极高。
为了查询效率和唯一性,set采用HT编码(Dict)。Dict中的key用来存储元素,value统一为null
当存储的所有数据都是整数,并且元素数量不超过set-max-intset-entries(512)时,Set会采用IntSet编码,以节省内存
起初为IntSet编码,一旦后续不满足条件,也会转为Dict编码
源码
源码
内存结构
ZSet
也就是SortedSet,其中每一个元素都需要指定一个score值和member值
score值和member值就是键值映射关系
member是key,score是value
特点
可以根据score值排序后
值越小,排序越靠前
member必须唯一
如果不唯一,后添加member的score值就会覆盖先添加的score值
可以根据member查询分数
因此,zset底层数据结构必须满足键值存储、键必须唯一、可排序这几个需求。之前学习的哪种编码结构可以满足?
SkipList:可以排序,并且可以同时存储score和ele值(member)
满足
键值存储、可排序
不满足
键必须唯一、可以根据member查询分数
如果强行根据member查询分数,那么就是将跳表当成普通链表,一个一个遍历去找member,性能低下
HT(Dict):可以键值存储,并且可以根据key找value
满足
键值存储
键必须唯一、可以根据member(key)查询分数(value)
不满足
可排序
结合使用
进阶
当元素数量不多时,HT和SkipList的优势不明显,而且更耗内存。因此zset还会采用ZipList结构来节省内存
需同时满足两个条件
元素数量小于zset_max_ziplist_entries,默认值128
每个元素都小于zset_max_ziplist_value字节,默认值64
ziplist本身没有排序功能,而且没有键值对的概念,因此需要有zset通过编码实现
ZipList是连续内存,因此score和element是紧挨在一起的两个entry, element在前,score在后
score越小越接近队首,score越大越接近队尾,按照score值升序排列
源码
内存结构
Hash
Hash结构与Redis中的Zset比较
相同
都是键值存储
都需求根据键获取值
键必须唯一
不同
zset的键是member,值是score;hash的键和值都是任意值
zset要根据score排序;hash则无需排序
使用
底层采用的编码与Zset基本一致,只需要把排序有关的SkipList去掉
Hash结构默认采用ZipList编码,用以节省内存。 ZipList中相邻的两个entry 分别保存field和value
当数据量较大时,Hash结构会转为HT编码,也就是Dict,触发条件有两个
ZipList中的元素数量超过了hash-max-ziplist-entries(默认512)
相比ZSet默认的128,Hash的阈值是512。因为Hash不用排序,直接存就可以。执行效率也能接受
不建议超过1000
ZipList中的任意entry大小超过了hash-max-ziplist-value(默认64字节)
源码
网络模型
源码
模型图
单线程
详细
大概
多线程
当我们的客户端想要去连接我们服务器,会去先到IO多路复用模型去进行排队,会有一个连接应答处理器,他会去接受读请求,然后又把读请求注册到具体模型中去,此时这些建立起来的连接,如果是客户端请求处理器去进行执行命令时,他会去把数据读取出来,然后把数据放入到client中, clinet去解析当前的命令转化为redis认识的命令,接下来就开始处理这些命令,从redis中的command中找到这些命令,然后就真正的去操作对应的数据了,当数据操作完成后,会去找到命令回复处理器,再由他将数据写出。
真正限制Redis性能的就是命令解析这一部分和结果缓冲区往外写这部分,只有这两块多线程可以提升处理速度,减少网络IO带来的影响。虽然说单词响应请求的时间没有减少,但吞吐量提升了。
通信协议
Redis是一个CS架构的软件,通信一般分两步(不包括pipeline和PubSub)
客户端(client)向服务端(server)发送一条命令
服务端解析并执行命令,返回响应结果给客户端
因此客户端发送命令的格式、服务端响应结果的格式必须有一个规范,这个规范就是通信协议。
RESP协议
Redis Serialization Protocol
版本
Redis 1.2版本引入了RESP协议
Redis 6.0版本中,从RESP2升级到了RESP3协议,增加了更多数据类型并且支持6.0的新特性–客户端缓存
Redis 2.0版本中成为与Redis服务端通信的标准,称为RESP2
默认使用的依然是RESP2协议
在RESP中,通过首字节的字符来区分不同数据类型,常用的数据类型包括5种
单行字符串
首字节是 ‘+’ ,后面跟上单行字符串,以CRLF( “\r\n” )结尾。例如返回"OK": “+OK\r\n”
二进制不安全
错误(Errors)
首字节是 ‘-’ ,与单行字符串格式一样,只是字符串是异常信息,例如:“-Error message\r\n”
数值
首字节是 ‘:’ ,后面跟上数字格式的字符串,以CRLF结尾。例如:“:10\r\n”
多行字符串
首字节是 ‘$’ ,表示二进制安全的字符串,最大支持512MB
$5\r\nhello\r\n
$5字符串占用字节大小
hello
真正的字符串数据
第一个\r\n代表记录长度的部分结束
如果大小为0,则代表空字符串
“$0\r\n\r\n”
如果大小为-1,则代表不存在
“$-1\r\n”
数组
首字节是 ‘*’,后面跟上数组元素个数,再跟上元素,元素数据类型不限
*3\r\n
$3\r\nset\r\n
$4\r\nname\r\n
$6\r\n张三\r\n
$3\r\nset\r\n
$4\r\nname\r\n
$6\r\n张三\r\n
*3\r\n
数组元素个数
其他的为数组元素
*3\r\n
:10\r\n
$5\r\nhello\r\n
*2\r\n$3\r\nage\r\n:10\r\n
:10\r\n
$5\r\nhello\r\n
*2\r\n$3\r\nage\r\n:10\r\n
有数组嵌套
后面有两个元素
age
10
模拟Redis客户端
public class Main {
static Socket s;
static PrintWriter writer;
static BufferedReader reader;
public static void main(String[] args) {
try {
// 1.建立连接
String host = "192.168.150.101";
int port = 6379;
s = new Socket(host, port);
// 2.获取输出流、输入流
writer = new PrintWriter(new OutputStreamWriter(s.getOutputStream(), StandardCharsets.UTF_8));
reader = new BufferedReader(new InputStreamReader(s.getInputStream(), StandardCharsets.UTF_8));
// 3.发出请求
// 3.1.获取授权 auth 123321
sendRequest("auth", "123321");
Object obj = handleResponse();
System.out.println("obj = " + obj);
// 3.2.set name 虎哥
sendRequest("set", "name", "虎哥");
// 4.解析响应
obj = handleResponse();
System.out.println("obj = " + obj);
// 3.2.set name 虎哥
sendRequest("get", "name");
// 4.解析响应
obj = handleResponse();
System.out.println("obj = " + obj);
// 3.2.set name 虎哥
sendRequest("mget", "name", "num", "msg");
// 4.解析响应
obj = handleResponse();
System.out.println("obj = " + obj);
} catch (IOException e) {
e.printStackTrace();
} finally {
// 5.释放连接
try {
if (reader != null) reader.close();
if (writer != null) writer.close();
if (s != null) s.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private static Object handleResponse() throws IOException {
// 读取首字节
int prefix = reader.read();
// 判断数据类型标示
switch (prefix) {
case '+': // 单行字符串,直接读一行
return reader.readLine();
case '-': // 异常,也读一行
throw new RuntimeException(reader.readLine());
case ':': // 数字
return Long.parseLong(reader.readLine());
case '$': // 多行字符串
// 先读长度
int len = Integer.parseInt(reader.readLine());
if (len == -1) {
return null;
}
if (len == 0) {
return "";
}
// 再读数据,读len个字节。我们假设没有特殊字符,所以读一行(简化)
return reader.readLine();
case '*':
return readBulkString();
default:
throw new RuntimeException("错误的数据格式!");
}
}
private static Object readBulkString() throws IOException {
// 获取数组大小
int len = Integer.parseInt(reader.readLine());
if (len <= 0) {
return null;
}
// 定义集合,接收多个元素
List<Object> list = new ArrayList<>(len);
// 遍历,依次读取每个元素
for (int i = 0; i < len; i++) {
list.add(handleResponse());
}
return list;
}
// set name 虎哥
private static void sendRequest(String ... args) {
writer.println("*" + args.length);
for (String arg : args) {
writer.println("$" + arg.getBytes(StandardCharsets.UTF_8).length);
writer.println(arg);
}
writer.flush();
}
}
static Socket s;
static PrintWriter writer;
static BufferedReader reader;
public static void main(String[] args) {
try {
// 1.建立连接
String host = "192.168.150.101";
int port = 6379;
s = new Socket(host, port);
// 2.获取输出流、输入流
writer = new PrintWriter(new OutputStreamWriter(s.getOutputStream(), StandardCharsets.UTF_8));
reader = new BufferedReader(new InputStreamReader(s.getInputStream(), StandardCharsets.UTF_8));
// 3.发出请求
// 3.1.获取授权 auth 123321
sendRequest("auth", "123321");
Object obj = handleResponse();
System.out.println("obj = " + obj);
// 3.2.set name 虎哥
sendRequest("set", "name", "虎哥");
// 4.解析响应
obj = handleResponse();
System.out.println("obj = " + obj);
// 3.2.set name 虎哥
sendRequest("get", "name");
// 4.解析响应
obj = handleResponse();
System.out.println("obj = " + obj);
// 3.2.set name 虎哥
sendRequest("mget", "name", "num", "msg");
// 4.解析响应
obj = handleResponse();
System.out.println("obj = " + obj);
} catch (IOException e) {
e.printStackTrace();
} finally {
// 5.释放连接
try {
if (reader != null) reader.close();
if (writer != null) writer.close();
if (s != null) s.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private static Object handleResponse() throws IOException {
// 读取首字节
int prefix = reader.read();
// 判断数据类型标示
switch (prefix) {
case '+': // 单行字符串,直接读一行
return reader.readLine();
case '-': // 异常,也读一行
throw new RuntimeException(reader.readLine());
case ':': // 数字
return Long.parseLong(reader.readLine());
case '$': // 多行字符串
// 先读长度
int len = Integer.parseInt(reader.readLine());
if (len == -1) {
return null;
}
if (len == 0) {
return "";
}
// 再读数据,读len个字节。我们假设没有特殊字符,所以读一行(简化)
return reader.readLine();
case '*':
return readBulkString();
default:
throw new RuntimeException("错误的数据格式!");
}
}
private static Object readBulkString() throws IOException {
// 获取数组大小
int len = Integer.parseInt(reader.readLine());
if (len <= 0) {
return null;
}
// 定义集合,接收多个元素
List<Object> list = new ArrayList<>(len);
// 遍历,依次读取每个元素
for (int i = 0; i < len; i++) {
list.add(handleResponse());
}
return list;
}
// set name 虎哥
private static void sendRequest(String ... args) {
writer.println("*" + args.length);
for (String arg : args) {
writer.println("$" + arg.getBytes(StandardCharsets.UTF_8).length);
writer.println(arg);
}
writer.flush();
}
}
内存策略
Redis之所以性能强,最主要的原因就是基于内存存储。然而单节点的Redis其内存大小不宜过大,会影响持久化或主从同步性能。
可以通过修改配置文件来设置Redis的最大内存
# 格式:
# maxmemory <bytes>
#例如:
maxmemory 1gb
过期策略
在学习Redis缓存的时候我们说过,可以通过expire命令给Redis的key设置TTL(存活时间)
可以发现,当key的TTL到期以后,再次访问name返回的是nil,说明这个key已经不存在了,对应的内存也得到释放。从而起到内存回收的目的
有两个问题需要我们思考
Redis是如何知道一个key是否过期呢
Redis本身是一个典型的key-value内存存储数据库,因此所有的key、value都保存在之前学习过的Dict结构中。不过在其database结构体中,有两个Dict:一个用来记录key-value;另一个用来记录key-TTL。
利用两个Dict分别记录key-value对及key-ttl对
如果想知道key有没有过期,直接在后者查。查询到对应的TTL,加以判断
是不是TTL到期就立即删除了呢
惰性删除
顾明思议并不是在TTL到期后就立刻删除,而是在访问一个key的时候(增删改查时),检查该key的存活时间,如果已经过期才执行删除。
问题
如果存在大量不被人访问的过期key,那么就不会被删除。浪费了内存。所以就需要周期删除
主线程执行
周期删除
顾明思议是通过一个定时任务,周期性的抽样部分过期的key,然后执行删除
执行周期
Redis服务初始化函数initServer()中设置定时任务,按照server.hz的频率来执行过期key清理,模式为SLOW
执行频率受server.hz影响,默认为10,即每秒执行10次,每个执行周期100ms。
执行清理耗时不超过一次执行周期的25%.默认slow模式耗时不超过25ms
逐个遍历db,逐个遍历db中的bucket,抽取20个key判断是否过期
如果没达到时间上限(25ms)并且过期key比例大于10%,再进行一次抽样,否则结束
Redis的每个事件循环前会调用beforeSleep()函数,执行过期key清理,模式为FAST
过期key比例小于10%不执行
执行频率受beforeSleep()调用频率影响,但两次FAST模式间隔不低于2ms
执行清理耗时不超过1ms
逐个遍历db,逐个遍历db中的bucket,抽取20个key判断是否过期
如果没达到时间上限(1ms)并且过期key比例大于10%,再进行一次抽样,否则结束
疑似 由主线程在事件循环中 定期执行
开始以为是 由定时任务的线程执行
有空看源码确定
总结
RedisKey的TTL记录方式:在RedisDB中通过一个Dict记录每个Key的TTL时间
过期key的删除策略
惰性清理:每次查找key时判断是否过期,如果过期则删除
定期清理:定期抽样部分key,判断是否过期,如果过期则删除。
定期清理的两种模式
SLOW模式执行频率默认为10,每次不超过25ms
FAST模式执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms
淘汰策略
当Redis内存使用达到设置的上限时,主动挑选部分key删除以释放更多内存的流程。
系统配置
#
# The default is:
#
# maxmemory-policy noenviction
# The default is:
#
# maxmemory-policy noenviction
Redis会在处理客户端命令的方法processCommand()中尝试做内存淘汰
源码
LFU的访问次数之所以叫做逻辑访问次数
是因为并不是每次key被访问都计数,而是通过运算
算法
生成0~1之间的随机数R
计算 (旧次数 * lfu_log_factor + 1),记录为P
如果没有旧次数,就为0
如果 R < P ,则计数器 + 1,且最大不超过255
除了第一次一定+1,后面再访问就不一定会累加了,一定概率
后面累加的概率会随着访问次数,越来越低
访问次数会随时间衰减,距离上一次访问时间每隔 lfu_decay_time 分钟(默认1),计数器 -1
策略
no-enviction:禁止驱逐数据,也就是当内存不足以容纳新入数据时,新写入操作就会报错,请求可以继续进行,线上任务也不能持续进行,采用no-enviction策略可以保证数据不被丢失,这也是系统默认的一种淘汰策略
volatile-ttl:除了淘汰机制采用LRU,策略基本上与volatile-lru相似,从设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰,ttl值越小越优先被淘汰
volatile-lru:从设置过期时间的数据集(server.db[i].expires)中挑选出最近最少使用的数据淘汰。没有设置过期时间的key不会被淘汰,这样就可以在增加内存空间的同时保证需要持久化的数据不会丢失
allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰,该策略要淘汰的key面向的是全体key集合,而非过期的key集合
allkeys-random:从数据集(server.db[i].dict)中选择任意数据淘汰
也就是直接从db->dict中随机挑选
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰。当内存达到限制无法写入非过期时间的数据集时,可以通过该淘汰策略在主键空间中随机移除某个key
也就是从db->expires中随机挑选
allkeys-lfu: 对全体key,基于LFU算法进行淘汰
volatile-lfu: 对设置了TTL的key,基于LFI算法进行淘汰
比较容易混淆的有两个
LRU(Least Recently Used),最少最近使用
用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。
最近使用的时间最小,就应该被淘汰。(最久未使用)
LFU(Least Frequently Used),最少频率使用。
会统计每个key的访问频率,值越小淘汰优先级越高。
策略选择
在Redis中,数据有一部分访问频率较高,其余部分访问频率较低,或者无法预测数据的使用频率时,设置allkeys-lru是比较合适的
如果所有数据访问概率大致相等时,可以选择allkeys-random
如果研发者需要通过设置不同的ttl来判断数据过期的先后顺序,此时可以选择volatile-ttl策略
如果希望一些数据能长期被保存,而一些数据可以被淘汰掉时,选择volatile-lru或volatile-random都是比较不错的
由于设置expire会消耗额外的内存,如果计划避免Redis内存在此项上的浪费,可以选用allkeys-lru 策略,这样就可以不再设置过期时间,高效利用内存了
执行步骤
MQ
应用场景
解耦
削峰
异步
如何不重复消费
幂等性
生产者
生产者发送每条数据的时候,里面加一个全局唯一的 id,类似订单 id 之类的东西,然后你这里消费到了之后,先根据这个 id 去比如 Redis 里查一下,之前消费过吗?如果没有消费过,你就处理,然后这个 id 写 Redis。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可
消费者
状态判断法
消费者消费数据后把消费数据记录在 redis 中,下次消费时先到 redis 中查看是否存在该消息,存在则表示消息已经消费过,直接丢弃消息。
业务判断法
通常数据消费后都需要插入到数据库中,使用数据库的唯一性约束防止重复消费。每次消费直接尝试插入数据,如果提示唯一性字段重复,则直接丢失消息。一般都是通过这个业务判断的方法就可以简单高效地避免消息的重复处理了。
如何不丢失消息
生产者
Confirm机制
一旦消息投递到队列,队列则会向生产者发送一个通知,如果设置了消息持久化到磁盘,则会等待消息持久化到磁盘之后再发送通知。生产者在发送完消息后不会等待回应,所以confirm机制性能相对比事务机制高。还会伴有超时机制。
队列
消息持久化
RabbitMQ 的消息默认存放在内存上面,如果不特别声明设置,消息不会持久化保存到硬盘上面的,如果节点重启或者意外死掉,消息就会丢失。
要想做到消息持久化,必须满足以下三个条件,缺一不可。
要想做到消息持久化,必须满足以下三个条件,缺一不可。
Exchange 设置持久化
Queue 设置持久化
Message持久化发送:发送消息设置发送模式deliveryMode=2,代表持久化消息
消费者
ACK确认机制
消费端消费完成后通知服务端,服务端才把消息删除。不自动,改手动
消息积压问题
原因
发送快了
可能是没有收到确认,重复发送
消费慢了
解决方案
临时
临时扩容
消息有序性
RabbitMQ
保证同一组消息发送到同一队列,消费者固定消费一个队列
RabbitMQ同一个队列中是有序的(根据消息发送的顺序)只要保证是同一个消费者,消费就没问题,接收到的顺序也是按照发送的顺序
RabbitMQ同一个队列中是有序的(根据消息发送的顺序)只要保证是同一个消费者,消费就没问题,接收到的顺序也是按照发送的顺序
kafka
保证严格的有序性:只能使用一个分区来接收消息,类似于MQ中的队列。
也使用局部有序的方式,因为kafka主要是使用多线程进行消费。
在线程消费之前,创建几个本地队列缓存,将需要按照顺序处理的消息放在同一个队列里,再由线程消费。(比如同一个订单号的消息)
也使用局部有序的方式,因为kafka主要是使用多线程进行消费。
在线程消费之前,创建几个本地队列缓存,将需要按照顺序处理的消息放在同一个队列里,再由线程消费。(比如同一个订单号的消息)
rocketMQ
同一个订单的 binlog 进入到同一个 MessageQueue 中就可以了。
因为同一个 MessageQueue 内的消息是一定有序的,
一个 MessageQueue 中的消息只能交给一个消费者
来进行处理,所以消费者消费的时候就一定会是有序的。(和RabbitMQ差不多)
因为同一个 MessageQueue 内的消息是一定有序的,
一个 MessageQueue 中的消息只能交给一个消费者
来进行处理,所以消费者消费的时候就一定会是有序的。(和RabbitMQ差不多)
其他
无中间件的情况下可以使用数据库对消息进行存储,然后利用时间或者其他id等特性排序,然后再依次处理
Kafka
名词解释
Producer
消息生产者,就是向 Kafka broker 发消息的客户端。
Consumer
消息消费者,向 Kafka broker 取消息的客户端。
Consumer Group(CG):消费者组
由多个 consumer 组成。消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费;消费者组之间互不影响。
所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。
Broker
一台 Kafka 服务器就是一个 broker。一个集群由多个 broker 组成。一个broker 可以容纳多个 topic。
Topic
可以理解为一个队列,生产者和消费者面向的都是一个 topic。
Partition
为了实现扩展性,一个非常大的 topic 可以分布到多个 broker(即服务器)上,一个 topic 可以分为多个 partition,每个 partition 是一个有序的队列。
Replica:副本。一个 topic 的每个分区都有若干个副本,一个 Leader 和若干个Follower。
一个 topic 的每个分区都有若干个副本,一个 Leader 和若干个Follower。
Leader
每个分区多个副本的“主”,生产者发送数据的对象,以及消费者消费数据的对象都是 Leader。
Follower
每个分区多个副本中的“从”,实时从 Leader 中同步数据,保持和Leader 数据的同步。
Leader 发生故障时,某个 Follower 会成为新的 Leader。
面试题
生产者原理
为什么需要额外实现序列化器
JDK自带的序列化器太重
数据乱序怎么解决
Kafka单分区内的数据有序,原因是In Flight Requests,默认每个broker缓存五个请求,当出现乱序时会自动排序。
在存储日志的时候,它的索引是按照什么方法存储的?
日志是按照稀疏索引的方式存储的,每往log文件写入4kb数据,就会往index文件写入一条索引。且保存的是相对的offset,避免占用过多的空间。
如何高效的读写数据?
顺序读写
磁盘分为顺序读写与随机读写,基于磁盘的随机读写确实很慢,但磁盘的顺序读写性能却很高,kafka 这里采用的就是顺序读写。
Page Cache
为了优化读写性能,Kafka 利用了操作系统本身的 Page Cache。数据直接写入page cache定时刷新脏页到磁盘即可。消费者拉取消息时,如果数据在page cache中,甚至能不需要去读磁盘io。读操作可直接在 Page Cache 内进行。如果消费和生产速度相当,甚至不需要通过物理磁盘(直接通过 Page Cache)交换数据。
零拷贝原理是什么?
Kafka使用了零拷贝技术,也就是直接将数据从内核空间的读缓冲区直接拷贝到内核空间的 socket 缓冲区,然后再写入到 NIC 缓冲区,避免了在内核空间和用户空间之间穿梭
什么是用户态? 什么是内核态?
用户态(User Mode):当进程在执行用户自己的代码时,则称其处于用户运行态。
内核态(Kernel Mode):当一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核运行态,此时处理器处于特权级最高的内核代码中执行。
为什么分内核态和用户态?
假设没有内核态和用户态之分,程序就可以随意读写硬件资源了,比如随意读写和分配内存,这样如果程序员一不小心将不适当的内容写到了不该写的地方,很可能就会导致系统崩溃。
而有了用户态和内核态的区分之后,程序在执行某个操作时会进行一系列的验证和检验之后,确认没问题之后才可以正常的操作资源,这样就不会担心一不小心就把系统搞坏的情况了,也就是有了内核态和用户态的区分之后可以让程序更加安全的运行,但同时两种形态的切换会导致一定的性能开销。
消费者初始化流程是什么样的?
通过对GroupId进行Hash得到那台服务器的coordinator ,coordinator负责选出消费组中的Leader ,并且协调信息。真正存储消费记录的是 _consumer_offsets_partition 。
如何做到精确一次性消费?
开启事务 ,以及幂等性
生产者端 -> 集群
集群 -> 消费者
消费者-> 框架(数据库)
为什么kafka不做读写分离?
读写分离是指生产者发送到Leader副本,消费者从Follower副本读取
1.延时问题:数据从leader副本到follow副本是需要过程的,从网络>主节点内存 ->主节点磁盘 -> 网络 -> 从节点内存 -> 从节点磁盘,比较耗时不适合对实时性要求高的应用。
2.负载均衡:读写分离,很大一部分原因是怕同一个节点负载过大。但是kafka通过分区的负载均衡,天然的就均衡了各个broker的压力。
如何保证Kafka消息可靠性?
生产端:ack设置为-1,保证消息同步到follower副本。发送消息方式设置为同步或者异步,做好失败回滚措施
broker端:页缓存pagecache设置直接刷盘模式,确保不会有消息在页缓存中的时候宕机。
消费端:关闭消息自动提交,改为手动提交。避免消息没消费完就提交了offset导致消息丢失
总得来说,要保证严格的可靠性,就会失去很大的可用性,这是一个平衡的过程。
消息堆积怎么办?
我们都知道,消息的消费速度取决于消费者的速度,在消费速度不变的情况下,增加分组内消费者的个数,能倍速的提高消费速度。而消费者的个数又受限于分区个数,消费者个数超过分区数后,再提高消费者个数就没有意义。
为了能够再提高临时的速度,我们还可以设置临时topic在临时主题中,去加大分区数,将所有原消费者直接将消息再次投递到临时topic中,进行更大规模消费群的消费。这是一个取巧的方案,适合解决临时大量消息的堆积。
为了能够再提高临时的速度,我们还可以设置临时topic在临时主题中,去加大分区数,将所有原消费者直接将消息再次投递到临时topic中,进行更大规模消费群的消费。这是一个取巧的方案,适合解决临时大量消息的堆积。
分区数越多,吞吐量就会越高吗?
在一定条件下,分区数的数量是和吞吐量成正比的,分区数和性能也是成正比的。但是超过了限度后,不升反降。
客户端/服务器端需要使用的内存就越多
文件句柄的开销
越多的分区可能增加端对端的延迟
降低高可用性
命令
主题命令行操作
查看当前服务器中的所有 topic
kafka-topics.sh --bootstrap-server 47.106.86.64:9092 --list
创建 first topic
kafka-topics.sh --bootstrap-server 47.106.86.64:9092 --create --partitions 1 --replication-factor 3 --topic first
选项说明
--topic 定义 topic 名
--replication-factor 定义副本数
--partitions 定义分区数
查看 first 主题的详情
kafka-topics.sh --bootstrap-server 47.106.86.64:9092 --describe --topic first
修改分区数
(注意:分区数只能增加,不能减少)
kafka-topics.sh --bootstrap-server 47.106.86.64:9092 --alter --topic first --partitions 3
删除 topic(需要配置信息)
kafka-topics.sh --bootstrap-server 47.106.86.64:9092 --delete --topic first
生产者命令行操作
查看操作生产者命令参数
kafka-console-producer.sh --bootstrap-server 47.106.86.64:9092 --topic first
--bootstrap-server <String: server toconnect to> 连接的 Kafka Broker 主机名称和端口号。
--topic <String: topic> 操作的 topic 名称。
发送消息
消费者命令行操作
查看操作消费者命令参数
kafka-console-consumer.sh
–bootstrap-server <String: server toconnect to> 连接的 Kafka Broker 主机名称和端口号。
–topic <String: topic> 操作的 topic 名称。
–from-beginning 从头开始消费。
–group <String: consumer group id> 指定消费者组名称。
消费消息
消费 first 主题中的数据(消费增量数据)
kafka-console-consumer.sh --bootstrap-server 47.106.86.64:9092 --topic first
把主题中所有的数据都读取出来(包括历史数据)。
kafka-console-consumer.sh --bootstrap-server 47.106.86.64:9092 --topic first --from-beginning
kafka-console-consumer.sh --bootstrap-server 47.106.86.64:9092 --from-beginning --topic first
Kafka 生产者
发送原理
在消息发送的过程中,涉及到两个线程,main线程和sender线程
main线程是消息的生产线程
sender线程是jvm单例的线程,专门用于消息的发送
在jvm的内存中开辟了一块缓存空间叫RecordAccumulator(消息累加器),用于将多条消息合并成一个批次,然后由sender线程发送给kafka集群。
消息在生产过程会调用send方法然后经过拦截器经过序列化器,再经过分区器确定消息发送在具体topic下的哪个分区,然后发送到对应的消息累加器中。并且每个队列和主题分区都具有一一映射关系。消息在累加器中,进行合并,达到了对应的size(batch.size)或者等待超过对应的等待时间(linger.ms),都会触发sender线程的发送。
消息累加器是多个双端队列
sender线程有一个请求池,默认缓存五个请求( max.in.flight.requests.per.connection ),发送消息后,会等待服务端的ack,如果没收到ack就会重试默认重试int最大值( retries )。如果ack成功就会删除累加器中的消息批次,并相应到生产端。
当双端队列中的DQueue满足 batch.size 或者 linger.ms 条件时触发sender线程。
注意:消息发送失败会自动重试,不需要我们在回调函数中手动重试。
流程图
生产者分区
分区的好处
存储的角度
合理使用存储资源,实现负载均衡
将海量数据存储在多台服务器上
计算的角度
提高并行计算的可行性
提高生产者往集群发送的并行度
增加了消费者,消费数据的并行度
发送消息分区策略
默认的分区器 DefaultPartitioner
1) 指定分区;
指明partition的情况下,直接将指明的值作为partition值;例如partition=0,所有数据写入分区0
2)指定key,计算hash得分区;
没有指明partition值但有key的情况下,将key的hash值与topic的partition数进行取余得到partition值;
例如:key1的hash值=5,key2的hash值=6,topic的partition数=2,那么key1对应的value1写入1号分区,key2对应的value2写入0号分区
面试题
假如我们想将订单表的所有数据发送到Kafka的某一个分区(单分区)
key设置为订单表的表名就可以实现
3)指定随机粘性分区;
(3)既没有parition值又没有key值的情况下,Kafka采用Sticky Parition(黏性分区器),会随机选择一个分区,并尽可能一直使用该分区,待该分区的batch已满或者已完成(时间到),Kafka再随机一个分区进行使用(和上一次的分区不同)。
例如:第一次随机选择0号分区,等0号分区当前批次满了(默认16k)或者linger.ms设置的时间到,Kafka再随机一个分区进行使用(如果还是0会继续随机)。
自定义分区器
需求
例如我们实现一个分区器实现,发送过来的数据中如果包含 Hi,就发往 0 号分区,不包含 Hi,就发往 1 号分区。
实现步骤
(1)定义类实现 Partitioner 接口。
(2)重写 partition()方法。
(3)使用分区器的方法,在生产者的配置中添加分区器参数。
生产经验
生产者提高吞吐量
batch.size
批次大小
默认16k
linger.ms
等待时间,默认是0
修改为5-100ms
Batch.size 与 linger.ms 配合使用,根据生成数据的大小指定。
compression.type
压缩snappy
buffer.memory
在异步发送并且分区很多的情况下,32M的数据量容易被满足,进程交互加大,可以适当提高到64M。
RecordAccumlator
为了提高生产者的吞吐量,我们通过累加器将多条消息合并成一批统一发送。在broker中将消息批量存入。减少多次的网络IO。
消息累加器默认32m,如果生产者的发送速率大于sender发送的速率,消息就会堆满累加器。生产者就会阻塞,或者报错,报错取决于阻塞时间的配置。
累加器的存储形式为ConcurrentMap<TopicPartition, Deque<ProducerBatch>>,可以看出来就是一个分区对应一个双端队列,队列中存储的是ProducerBatch一般大小是16k根据batch.size配置,新的消息会append到ProducerBatch中,满16k就会创建新的ProducerBatch,并且触发sender线程进行发送。
如果消息量非常大,生成了大量的ProducerBatch,在发送后,又需要JVM通过GC回收这些ProducerBatch就变得非常影响性能,所以kafka通过 BufferPool作为内存池来管理ProducerBatch的创建和回收,需要申请一个新的ProducerBatch空间时,调用 free.allocate(size, maxTimeToBlock)找内存池申请空间。
如果单条消息大于16k,那么就不会复用内存池了,会生成一个更大的ProducerBatch专门存放大消息,发送完后GC回收该内存空间。
消息累加器默认32m,如果生产者的发送速率大于sender发送的速率,消息就会堆满累加器。生产者就会阻塞,或者报错,报错取决于阻塞时间的配置。
累加器的存储形式为ConcurrentMap<TopicPartition, Deque<ProducerBatch>>,可以看出来就是一个分区对应一个双端队列,队列中存储的是ProducerBatch一般大小是16k根据batch.size配置,新的消息会append到ProducerBatch中,满16k就会创建新的ProducerBatch,并且触发sender线程进行发送。
如果消息量非常大,生成了大量的ProducerBatch,在发送后,又需要JVM通过GC回收这些ProducerBatch就变得非常影响性能,所以kafka通过 BufferPool作为内存池来管理ProducerBatch的创建和回收,需要申请一个新的ProducerBatch空间时,调用 free.allocate(size, maxTimeToBlock)找内存池申请空间。
如果单条消息大于16k,那么就不会复用内存池了,会生成一个更大的ProducerBatch专门存放大消息,发送完后GC回收该内存空间。
数据可靠性
消息确认机制-ACK
producer提供了三种消息确认的模式,通过配置acks来实现
acks为0时, 表示生产者将数据发送出去就不管了,不等待任何返回。
这种情况下数据传输效率最高,但是数据可靠性最低,当 server挂掉的时候就会丢数据;
acks为1时(默认),表示数据发送到Kafka后,经过leader成功接收消息的的确认,才算发送成功,
如果leader宕机了,就会丢失数据。
acks为-1/all时,表示生产者需要等待ISR中的所有follower都确认接收到数据后才算发送完成,这样数据不会丢失,因此可靠性最高,性能最低。
思考
Leader收到数据,所有Follower都开始同步数据,但有一个Follower,因为某种故障,迟迟不能与Leader进行同步,那这个问题怎么解决呢?
Leader维护了一个动态的in-sync replica set(ISR),意为和Leader保持同步的Follower+Leader集合(leader:0,isr:0,1.2)。
如果Follower长时间未向Leader发送通信请求或同步数据,则该Follower将被踢出ISR。该时间阈值由replica.lag.time.max.ms参数设定,默认30s。例如2超时,(leader:0,isr:0,1)。
这样就不用等长期联系不上或者已经故障的节点。
数据可靠性分析
如果分区副本设置为1个,或者ISR里应答的最小副本数量min.insync.replicas 默认为1)设置为1,和ack=1的效果是一样的,仍然有丢数的风险(leader:0,isr:0)
AR = ISR + ORS
正常情况下,如果所有的follower副本都应该与leader副本保持一定程度的同步,则AR = ISR,OSR = null。
ISR 表示在指定时间内和leader保存数据同步的集合;
ORS表示不能在指定的时间内和leader保持数据同步集合,称为OSR(Out-Sync Relipca set)。
数据完全可靠条件 = ACK级别设置为-1 + 分区副本大于等于2 + ISR里应答的最小副本数量大于等于2
总结
acks=0,生产者发送过来数据就不管了,可靠性差,效率高:
acks=1,生产者发送过来数据Leader应答,可靠性中等,效率中等;
acks=-1,生产者发送过来数据Leader和ISR队列里面所有Follwer应答,可靠性高,效率低;
生产环境
acks=0很少使用;
acks=1,一般用于传输普通日志,允许丢个别数据;
acks=-1,,一般用于传输和钱相关的数据,对可靠性要求比较高的场景。
数据重复分析
acks=-1
生产者发送过来的数据,Leader和ISR队列里面的所有节点收齐数据后应答。
有可能会出现重复数据
解决方案
消息id唯一
数据去重
幂等性原理
在一般的MQ模型中,常有以下的消息通信概念
至少一次(At Least Once)
ACK级别设置为-1 + 分区副本大于等于2 + ISR里应答的最小副本数量>=2
可以保证数据不丢失,但是不能保证数据不重复。
最多一次(At Most Once)
ACK级别设置为0 。
可以保证数据不重复,但是不能保证数据不丢失。•
精确一次(Exactly Once)
对于一些重要的信息,比如和钱相关的数据,要求数据既不能重复也不能丢失
至少一次 + 幂等性 。 Kafka 0.11版本引入一项重大特性:幂等性和事务。
幂等性
指Producer不论向Broker发送多少次重复数据,Broker端都只会持久化一条,保证了不重复
简单地说就是对接口的多次调用所产生的结果和调用一次是一致的。生产者在进行重试的时候有可能会重复写入消息,而使用Kafka 的幂等性功能之后就可以避免这种情况。(不产生重复数据)
重复数据的判断标准
具有<PID, Partition, SeqNumber>相同主键的消息提交时,Broker只会持久化一条。
其中ProducerId(PID)是Kafka每次重启都会分配一个新的;
Partition 表示分区号;
Sequence Number 序列化号,是单调自增的。
broker中会在内存维护一个pid+分区对应的序列号。如果收到的序列号正好比内存序列号大一,才存储消息,如果小于内存序列号,意味着消息重复,那么会丢弃消息,并应答。如果远大于内存序列号,意味着消息丢失,会抛出异常。
所以幂等解决的是sender到broker间,由于网络波动可能造成的重发问题。用幂等来标识唯一消息。
并且幂等性只能保证的是在单分区单会话(正常启动一次)内不重复。
如何使用幂等性
只需要显式地将生产者客户端参数enable.idempotence设置为true即可(这个参数的默认值为true)
并且还需要确保生产者客户端的retries、acks、max.in.filght.request.per.connection参数不被配置错,默认值就是对的。
消息事务
说明
开启事务,必须开启幂等性,底层基于幂等性
由于幂等性不能跨分区运作,为了保证同时发的多条消息,要么全成功,要么全失败。kafka引入了事务的概念
开启事务需要producer设置transactional.id的值并同时开启幂等性。
通过事务协调器,来实现事务
执行流程
代码
API
消息顺序
消息在单分区内有序(有条件的)
为了保证消息的顺序性,需要做到如下几点
如果未开启幂等性(或者在1.x版本之前的老版本),需要 max.in.flight.requests.per.connection 设置为1。(缓冲队列最多放置1个请求)
如果开启幂等性,需要 max.in.flight.requests.per.connection 设置为小于5。
因为broker端会缓存producer主题分区下的五个request,保证最近5个request是有序的。
如果Request3在失败重试后才发往到集群中,必然会导致乱序,但是集群会重新按照序列号进行排序(最对一次排序5个)。
多分区内无序(如果对多分区进行排序,造成分区无法工作需要等待排序,浪费性能)
核心参数
bootstrap.servers
生产者连接集群所需的broker地址清单。例如
hadoop102:9092,hadoop103:9092,hadoop104:9092,可以设置1个或者多个,中间用逗号隔开。
hadoop102:9092,hadoop103:9092,hadoop104:9092,可以设置1个或者多个,中间用逗号隔开。
注意这里并非需要所有的broker地址,因为生产者从给定的broker里查找到其他 broker 信息。
key.serializer 和 value.serializer
指定发送消息的key和value 的序列化类型。一定要写全类名。
buffer.memory
RecordAccumulator 缓冲区总大小,默认32m。
batch.size
缓冲区一批数据最大值,默认16k。适当增加该值,可以提高吞吐量,但是如果该值设置太大,会导致数据传输延迟增加。
linger.ms
如果数据迟迟未达到batch.size,sender等待linger.time
之后就会发送数据。单位ms,默认值是0ms,表示没有延迟。生产环境建议该值大小为5-100ms 之间。
acks
0:生产者发送过来的数据,不需要等数据落盘应答,
1:生产者发送过来的数据,Leader 收到数据后应答。
-1(all):生产者发送过来的数据,Leader+和isr队列里面的所有节点收齐数据后应答。默认值是-1,-1和all是等价的。
max.in.flight.requests.per.connection
允许最多没有返回ack的次数,默认为5,开启幂等性要保证该值是1-5的数字
需要与enable.idempotence配合使用,否则还想解决乱序,那么次数就只能设置为1
enable.idempotence
是否开启幂等性,默认true,开启幂等性。
retries
当消息发送出现错误的时候,系统会重发消息。retries表
示重试次数。默认是 int 最大值,2147483647.如果设置了重试,还想保证消息的有序性,需要设置MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION=1
否则在重试此失败消息的时候,其他的消息可能发送成功了。
retry.backoff.ms
两次重试之间的时间间隔,默认是100ms。
compression.type
生产者发送的所有数据的压缩方式。默认是none,也就是不压缩。
支持压缩类型:none、gzip、snappy、1z4 和 zstd
支持压缩类型:none、gzip、snappy、1z4 和 zstd
Kafka Broker
Broker 翻译成“代理”或“经纪人”比较贴切,但最准确的理解应该是 Kafka 集群中的一个工作服务器节点。
它是数据存储、消息传递处理和集群协调工作的核心执行单元
流程图
架构
架构图
在kafka集群中,可能会有一台broker,也可能会有多台broker。每个 broker 上存储着多个不同 Topic 的分区副本
每个Topic通常被划分为多个分区,每个分区还会有多个副本(Replica),其中有一个副本被选为 Leader,负责处理读写请求,其他副本为 Follower,负责同步数据。以实现并行处理和负载分担,同时为了保持可靠性。
kafka会尽量将分区副本均衡地分布在所有broker上,避免副本集中在某些节点上。
组成
broker
服务器
topic
主题
partition
分区,分而治之
分区之间没有主从关系,是同一级别
replica
副本,可靠性
leader
follower
生产者和消费者只针对leader操作
ZK存储
Zookeeper在Kafka中扮演了重要的角色,kafka使用zookeeper进行元数据管理
保存broker注册信息,包括主题(Topic)、分区(Partition)信息等,选择分区leader。
疑问
一个broker 一般就是一台服务器吗?会不会一个broker 在多台服务器或者多个broker 在一台服务器上呢
生产环境 / 标准实践
严格坚持 一个 Broker 进程对应一台独立服务器。这是确保 Kafka 集群性能、稳定性、可扩展性和真正高可用性的基石。
单机多 Broker
仅在本地开发、测试、学习环境中使用。
资源争抢严重(尤其是磁盘 I/O),性能很差且不稳定。
不具备高可用性,单点故障会宕掉所有本机 Broker。
绝不应该用于生产环境或任何对性能和可靠性有要求的场景。
一个 Broker 跨多机
技术上完全不可能。一个 Broker 就是一个部署在单机单进程内的完整服务实例。
概念
AR
Assigned Replicas
某个分区配置的所有副本(Leader + Follower)集合
ISR
In-Sync Replicas
当前与 Leader 保持同步的副本集合
健康的
是AR的动态子集
Broker选举Leader
kafka中涉及多处选举机制,容易搞混
broker(控制器)选leader
在kafka集群中由很多的broker(也叫做控制器),但是他们之间需要选举出一个leader,其他的都是follower。
broker的leader有很重要的作用
创建、删除主题、增加分区并分配leader分区;
集群broker管理,包括新增、关闭和故障处理;
分区重分配(auto.leader.rebalance.enable=true,后面会介绍),分区leader选举。
分区多副本选leader
消费者选Leader
每个broker都有唯一的brokerId,他们在启动后会去竞争注册zookeeper上的Controller结点,谁先抢到,谁就是broker leader。而其他broker会监听该结点事件,以便后续leader下线后触发重新选举。
生产经验
节点服役和退役
服役新节点
执行负载均衡操作
启动一台新的KafKa服务端(加入原有的Zookeeper集群)
查看原有的 分区信息 describe
指定需要均衡的主题
$ vim topics-to-move.json
生成负载均衡计划(只是生成计划)
bin/kafka-reassign-partitions.sh --bootstrap-server 47.106.86.64:9092 --topics-to-move-json-file topics-to-move.json --broker-list "0,1,2,3" --generate
创建副本存储计划
所有副本存储在 broker0、broker1、broker2、broker3 中
vim increase-replication-factor.json
执行副本计划
kafka-reassign-partitions.sh --bootstrap-server 47.106.86.64:9092 --reassignment-json-file increase-replication-factor.json --execute
验证计划
kafka-reassign-partitions.sh --bootstrap-server 47.106.86.64:9092 --reassignment-json-file increase-replication-factor.json --verify
退役旧节点
执行负载均衡操作
先按照退役一台节点,生成执行计划,然后按照服役时操作流程执行负载均衡。
指定需要均衡的主题
$ vim topics-to-move.json
在服役新节点时已经创建,所以可以略过
创建执行计划
kafka-reassign-partitions.sh --bootstrap-server 47.106.86.64:9092 --topics-to-move-json-file topics-to-move.json --broker-list "0,1,2" --generate
创建副本存储计划
所有副本存储在 broker0、broker1、broker2、broker3 中
vim increase-replication-factor.json
复制执行 创建执行计划 那一步后系统返回的数据。Proposed 下面那行的数据
执行副本计划
kafka-reassign-partitions.sh --bootstrap-server 47.106.86.64:9092 --reassignment-json-file increase-replication-factor.json --execute
验证计划
kafka-reassign-partitions.sh --bootstrap-server 47.106.86.64:9092 --reassignment-json-file increase-replication-factor.json --verify
执行停止命令
kafka-service-stop.sh
kafka副本Replica
作用
提高数据可靠性
默认副本1个,生产环境通常配置为2个,个别公司设置为3个
太多副本会增加磁盘存储空间,增加网络上数据传输,降低效率
保证数据可靠性
分类
Leader
每个分区的多个副本中的"主副本",生产者以及消费者只与 Leader 交互。
生产者只会把数据发往Leader,然后Follower找Leader进行同步数据
Follower
每个分区的多个副本中的"从副本"
负责实时从 Leader 中同步数据,保持和 Leader 数据的同步。Leader 发生故障时,从 Follower 副本中重新选举新的 Leader 副本对外提供服务。
名词概念
AR
Assigned Replicas
含义
分区中的所有 Replica 统称
指分配给某个特定分区的所有副本的集合。这些副本分布在不同的 Broker 上。
作用
定义了该分区数据理论上应该有多少个备份。由管理员在创建 Topic 时通过 replication-factor 参数指定。
AR = ISR +OSR
ISR
In-Sync Replicas
含义
与 Leader 副本保持同步的Follower集合。
如果Follower长时间未向Leader发送通信请求或同步数据,则该Follower将被踢出ISR
该时间阈值由replica.lag.time.max.ms参数决定,默认30s
这是一个 AR 的子集。
作用
这是 Kafka 实现高可用和数据一致性的核心机制。
只有 ISR 中的副本才有资格在 Leader 宕机时被选举为新的 Leader。
生产者发送的消息只有在被 ISR 中的所有副本成功复制后(取决于 acks 配置),才被认为是“已提交”的。
所有与 Leader 副本保持一定程度同步的Replica(包括 Leader 副本在内)组成 ISR
OSR
Out-of-Sync Replicas
含义
未能与 Leader 副本保持同步的副本集合(延迟过多的)。
这也是 AR 的一个子集。
作用
表示那些暂时失效、落后或无法从 Leader 同步数据的副本
OSR与 Leader 副本同步滞后过多的 Replica 组成了
LEO
Log End Offset
含义
日志末端位移。
标识每个副本(无论是 Leader 还是 Follower)的本地日志文件中下一条待写入消息的位移 (Offset)。
每个副本都有内部的LEO,代表当前队列消息的最后一条偏移量offset + 1。
HW
High Watermark
含义
高水位标记。
高水位,代表所有ISR中的LEO最低的那个LEO
消费者可见的最大消息的offset是HW减一
作用
这是分区级别的概念(所有副本共享同一个 HW 值,由 Leader 管理和传播)。它定义了已提交消息的边界。
关键点
消费者只能消费到 HW 之前的消息 (offset < HW)。 HW 之后的消息对消费者是不可见的,即使它们已经被写入 Leader 或某些 Follower 的日志中。
Leader选举流程
Kafka 集群中有一个 broker 的 Controller 会被选举为 Controller Leader (4.2.2) ,负责管理集群Broker 的上下线,所有 topic 的分区副本分配和 Leader 选举等工作。
Broker中Controller 的信息同步工作是依赖于 Zookeeper 的 ./broker/topic 目录下的信息
结论先行: 如果leader副本下线, 会在ISR队列中存活为前提,按照Replicas队列中前面优先的原则。
实践
创建一个新的 topic,4 个分区,4 个副本
kafka-topics.sh --bootstrap-server 47.106.86.64:9092 --create --topic atguigu1 --partitions 4 --replication-factor 4
查看 Leader 分布情况
kafka-topics.sh --bootstrap-server 47.106.86.64:9092 --describe --topic atguigu1
停止掉 hadoop105 的 kafka 进程,并查看 Leader 分区情况
kafka-server-stop.sh
kafka-topics.sh --bootstrap-server 47.106.86.64:9092 --describe --topic atguigu1
停止掉 hadoop104 的 kafka 进程,并查看 Leader 分区情况
kafka-server-stop.sh
kafka-topics.sh --bootstrap-server 47.106.86.64:9092 --describe --topic atguigu1
Leader和Follower故障处理细节
Follower故障
如果follower落后leader过多,follower就会被移除ISR队列
体现在落后时间 repca.lag.time.max.ms ,或者落后偏移量repca.lag.max.messages(由于kafka生成速度不好界定,后面取消了该参数)
等待该队列LEO追上HW,才会重新加入ISR中。
Leader故障流程
旧Leader先被从ISR队列中踢出,然后从ISR中选出一个新的Leader来;
此时为了保证多个副本之间的数据一致性,其他的follower会先将各自的log文件中高于HW的部分截取掉,然后从新的leader同步数据(由此可知这只能保证副本之间数据一致性,并不能保证数据不丢失或者不重复)
体现了设置ACK-all的重要性。
分区副本分配
如果 kafka 服务器只有 4 个节点,那么设置 kafka 的分区数大于服务器台数,在 kafka底层如何分配存储副本呢?
创建 16 分区,3 个副本
创建一个新的 topic,名称为 second。
查看分区和副本情况。
生产经验
手动调整分区副本
手动调整分区副本存储的步骤
创建一个新的 topic,名称为 three。
kafka-topics.sh --bootstrap-server 47.106.86.64:9092 --create --partitions 4 --replication-factor 2 --topic three
创建副本存储计划(所有副本都指定存储在 broker0、broker1 中)
输入如下内容
执行副本存储计划。
验证副本存储计划。
分区自动调整
一般情况下,我们的分区都是平衡散落在broker的,随着一些broker故障,会慢慢出现leader集中在其中少部门几台broker上的情况,造成集群负载不均衡,这时候就需要分区平衡。
为了解决上述问题kafka出现了自动平衡的机制。kafka提供了下面几个参数进行控制
auto.leader.rebalance.enable
自动leader parition平衡,默认是true;
leader.imbalance.per.broker.percentage
每个broker允许的不平衡的leader的比率,默认是10%,如果超过这个值,控制器将会触发leader的平衡
leader.imbalance.check.interval.seconds
检查leader负载是否平衡的时间间隔,默认是300秒
但是在生产环境中是不开启这个自动平衡,因为触发leader partition的自动平衡会损耗性能
或者可以将触发自动平衡的参数leader.imbalance.per.broker.percentage的值调大点,避免频繁触发。
增加副本因子
在生产环境当中,由于某个主题的重要等级需要提升,我们考虑增加副本。
副本数的增加需要先制定计划,然后根据计划执行。
不能通过命令行的方法添加副本。
副本数的增加需要先制定计划,然后根据计划执行。
不能通过命令行的方法添加副本。
操作
创建 topic
手动增加副本存储
创建副本存储计划(所有副本都指定存储在 broker0、broker1、broker2 中)。
执行副本存储计划。
kafka-reassign-partitions.sh --bootstrap-server 47.106.86.64:9092 --reassignment-json-file increase-replication-factor.json --execute
文件存储
文件存储机制
在Kafka中主题(Topic)是一个逻辑上的概念,分区(partition)是物理上存在的
每个partition对应一个log文件
属于逻辑概念
该log文件中存储的就是Producer生产的数据。Producer生产的数据会被不断追加到该log文件末端。
说明历史的数据不进行修改,提升读写速度
Segment
为防止log文件过大导致数据定位效率低下,Kafka采用了分片和索引机制,将每个partition分为多个Segment
每个Segment默认1G( log.segment.bytes )
构成
".log"文件
日志文件
".index"文件
偏移量索引文件
index为稀疏索引,大约每往log文件写入4kb数据,会往index文件写入一条索引。
参数log.index.interval.bytes。默认4kb。
构成
offset
相对Offset
Index文件中保存的offset为相对offset,这样能确保offset的值所占空间不会过大。因此能将offset的值控制在固定大小
position
查找步骤
1.根据目标offset定位Segment文件
2.找到小于等于目标offset的最大offset对应的索引项
3.定位到log文件
4.向下遍历找到目标Record
".timeindex"文件
时间戳索引文件
数据默认保存7天,通过".timeindex"文件判断日志保存了多久的功能
其他文件
这些文件位于一个文件夹下
该文件命名规则为:topic名称+分区号。如:first-0
index和log文件是以当前segment的第一条消息的offset命名
需要通过特定工具打开才能看到文件信息
kafka-run-class.sh kafka.tools.DumpLogSegments --files ./00000000000000000000.index
kafka-run-class.sh kafka.tools.DumpLogSegments --files ./00000000000000000000.log
因为存的是序列化过后的数据
文件清理策略
Kafka将消息存储在磁盘中,为了控制磁盘占用空间的不断增加就需要对消息做一定的清理操作。Kafka 中每一个分区副本都对应一个Log,而Log又可以分为多个日志分段,这样也便于日志的清理操作。
日志清理策略
日志删除(delete)
按照一定的保留策略直接删除不符合条件的日志分段。
kafka中默认的日志保存时间为7天
可以通过调整如下参数修改保存时间
基于时间策略
log.retention.hours:最低优先级小时,默认7天
log.retention.minutes:分钟
log.retention.ms:最高优先级毫秒
log.retention.check.interval.ms:负责设置检查周期,默认5分钟
file.delete.delay.ms:延迟执行删除时间
log.retention.bytes:当设置为-1时表示运行保留日志最大值(相当于关闭);当设置为1G时,表示日志文件最大值
日志删除任务会周期检查当前日志文件中是否有保留时间超过设定的阈值来寻找可删除的日志段文件集合;这里需要注意log.retention参数的优先级:log.retention.ms > log.retention.minutes > log.retention.hours,默认只会配置log.retention.hours参数,值为168即为7天
删除过期的日志段文件,并不是简单的根据日志段文件的修改时间计算,而是要根据该日志段中最大的时间戳来计算的,首先要查询该日志分段所对应的时间戳索引文件,查找该时间戳索引文件的最后一条索引数据,如果时间戳大于0就取值,否则才会使用最近修改时间。
避免文件中有部分过期了,有部分没过期;只有最新一条也过期了才删除
在删除的时候先从Log对象所维护的日志段的跳跃表中移除要删除的日志段,用来确保已经没有线程来读取这些日志段;接着将日志段所对应的所有文件,包括索引文件都添加上**.deleted的后缀;最后交给一个以delete-file命名的延迟任务来删除这些以.deleted为后缀的文件,默认是1分钟执行一次,可以通过file.delete.delay.ms**来配置
基于日志大小策略
默认关闭
日志删除任务会周期性检查当前日志大小是否超过设定的阈值(log.retention.bytes,默认是-1,表示无穷大)
如果超过设置的所有日志总大小阈值,删除最早的segment
基于日志起始偏移量
该策略判断依据是日志段的下一个日志段的起始偏移量 baseOffset是否小于等于 logStartOffset,如果是,则可以删除此日志分段。这里说一下logStartOffset,一般情况下,日志文件的起始偏移量 logStartOffset等于第一个日志分段的 baseOffset,但这并不是绝对的,logStartOffset的值可以通过 DeleteRecordsRequest请求、使用 kafka-delete-records.sh 脚本、日志的清理和截断等操作进行修改
日志压缩(compact)
针对每个消息的key进行整合,对于有相同key的不同value值,只保留最后一个版本。
压缩
压缩前
K1可以依次保存有V1,V2,V3等
压缩后
K1只保存最新值V3
压缩后的ofset可能是不连续的,比如上面例子中V1,V2对应的offset就没了,当从这些offset消费消息时,将会拿到比这个ofset大的ofset对应的消息;假如V2对应的offset为6,准备消费offset为6时,实际上会拿到offset为7的消息,并从这个位置开始消费。
这种策略只适合特殊场景,比如消息的key是用户ID,value是用户的资料,通过这种压缩策略,整个消息集里就保存了所有用户最新的资料。
可以通过修改broker端参数 log.cleanup.policy 来进行配置
Kafka高效读写数据
快速读写的原因
kafka是分布式集群,采用分区方式,并行操作
读取数据采用稀疏索引,可以快速定位消费数据
顺序写磁盘
Kafka 的 producer 生产数据,要写入到 log 文件中,写的过程是一直追加到文件末端,为顺序写。
官网有数据表明,同样的磁盘,顺序写能到 600M/s,而随机写只有 100K/s。
这与磁盘的机械机构有关,顺序写之所以快,是因为其省去了大量磁头寻址的时间。
但现在不少情况都是用SSD,可能提升不是很大
页缓存和零拷贝
零拷贝
零拷贝并不是不需要拷贝,而是减少不必要的拷贝次数
通常使用在IO读写过程中。常规应用程序IO过程会经过四次拷贝
数据从磁盘经过DMA(直接存储器访问)到内核的Read Buffer;
内核态的Read Buffer到用户态应用层的Buffer
用户态的Buffer到内核态的Socket Buffer
Socket Buffer到网卡的NIC Buffer
从上面的流程可以知道内核态和用户态之间的拷贝相当于执行两次无用的操作,之间切换也会花费很多资源;当数据从磁盘经过DMA 拷贝到内核缓存(页缓存)后,为了减少CPU拷贝的性能损耗,操作系统会将该内核缓存与用户层进行共享,减少一次CPU copy过程,同时用户层的读写也会直接访问该共享存储,本身由用户层到Socket缓存的数据拷贝过程也变成了从内核到内核的CPU拷贝过程,更加的快速
Kafka零拷贝
Kafka的数据加工处理操作交由Kafka生产者和Kafka消费者处理。Kaka Broker应用层不关心也不操作处理存储的数据,所以就不用
走应用层,传输效率高。
走应用层,传输效率高。
页缓存
操作系统PageCache页缓存
Kafka重度依赖底层操作系统提供的PageCache功能
当上层有写操作时,操作系统只是将数据写入PageCache。当读操作发生时,先从PageCache中查找,如果找不到,再去磁盘中读取。
实际上PageCache是把尽可能多的空闲内存都当做了磁盘缓存来使用。
甚至如果我们的消息存在页缓存PageCache中,还避免了硬盘到内核的拷贝过程,更加一步提升了消息的吞吐量。 (大概就理解成传输的数据只保存在内核空间,不需要再拷贝到用户态的应用层)
核心参数
replica.lag.time.max.ms
ISR 中,如果Follower 长时间未向 Leader 发送通信请求或同步数据,则该Follower将被踢出ISR。
该时间阈值,默认 30s。
auto.leader.rebalance.enable
默认是 true。自动 Leader Partition 平衡。
建议关闭。
leader.imbalance.per.broker.percentage
每个 broker 允许的不平衡的leader的比率。如果每个broker超过了这个值,控制器会触发 leader 的平衡。
默认是 10%。
leader.imbalance.check.interval.seconds
检查leader 负载是否平衡的间隔时间。
默认值 300 秒。
log.segment.bytes
Kafka中log 日志是分成一块块存储的,此配置是指log日志划分成块的大小
默认值 1G
log.index.interval.bytes
kafka 里面每当写入了 4kb 大小的日志(.log),然后就往index 文件里面记录一个索引
默认 4kb
log.retention.hours
Kafka 中数据保存的时间,默认7天
log.retention.minutes
Kafka 中数据保存的时间,分钟级别,默认关闭。
log.retention.ms
Kafka 中数据保存的时间,毫秒级别,默认关闭。
log.retention.check.interval.ms
检查数据是否保存超时的间隔,默认是5分钟
log.retention.bytes
超过设置的所有日志总大小,删除最早的 segment。
默认等于-1,表示无穷大。
log.cleanup.policy
表示所有数据启用删除策略:如果设置值为compact,表示所有数据启用压缩策略。
默认是 delete
num.io.threads
负责写磁盘的线程数。整个参数值要占总核数的 50%。
默认是8。
num.replica.fetchers
副本拉取线程数,这个参数占总核数的 50%的 1/3
默认是1。
num.network.threads
数据传输线程数,这个参数占总核数的 50%的 2/3
默认是3。
log.flush.interval.messages
强制页缓存刷写到磁盘的条数,默认是long 的最大值,9223372036854775807。
一般不建议修改,交给系统自己管理。
log.flush.interval.ms
每隔多久,刷数据到磁盘,默认是null。
一般不建议修改,交给系统自己管理
auto.create.topics.enable
如果设置为true(默认值是 true),那么当生产者向一个未创建的主题发送消息时,会自动创建一个分区数为um.partitions(默认值为1)、副本因子为 default.replication.factor(默认值为 1)的主题。除此之外,当一个消费者开始从未知主题中读取消息时,或者当任意一个客户端向未知主题发送元数据请求时,都会自动创建一个相应主题。
默认值是 true
这种创建主题的方式是非预期的,增加了主题管理和维护的难度。
生产环境建议将该参数设置为 false。
Kafka Consumer
消费者和消费者相互独立
不存在说消费者A将消息消费走了,消费者B就读不到了
互不影响
消费方式
PULL
消费者主动向服务端拉取消息
Kafka采用的方式
由于推模式很难考虑到每个客户端不同的消费速率,导致消费者无法消费消息而宕机,因此kafka采用的是pull的模式
缺点
如果服务端没有消息,消费端就会一直空轮询。
为了避免过多不必要的空轮询,kafka做了改进,如果没消息服务端就会暂时保持该请求,在一段时间内有消息再回应给客户端。
PUSH
服务端主动推送消息给消费者
缺点
因为由Broker决定发送速率,很难考虑到每个客户端不同的消费速率,可能导致消费者无法消费消息而宕机
可能消费者A消费速度可以达到50m/s,消费者B消费速度只能达到10m/s
这种情况下,该配合哪个消费速率推送好呢
消费的offset位置
老版本
Zookeeper
新版本
每个消费者的offset有消费者提交到系统主题保存
_consumer_offsets
kafka底层数据持久化在磁盘上的
消费者组
为了提高自己的消费能力
消费某一个主题或者某一分区的数据
注意事项
每一个分区的数据,在同一个消费组下,只能由一个消费者消费
一个分区不能两个消费者过来消费,消费乱套了。容易产生重复数据
可以将消费者组当成一个独立的消费者,是一个整体,分工明确
消费者组原理
Consumer Group(CG):消费者组,由多个consumer组成。形成一个消费者组的条件,是所有消费者的groupid相同。
消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费。
消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。
如果向消费组中添加更多的消费者,超过主题分区数量,则有一部分消费者就会闲置,不会接收任何消息。
消费者组初始化流程
coordinator
辅助实现消费者组的初始化和分区的分配。
应该是GroupCoordinator
coordinator节点选择
groupid的hashcode值%50
groupid
手动指定,属于必填参数
50
存储消费者的offset会放在__consumer offsets主题里面,其对应的分区数量就是50
如果修改其分区数,那么这个50也会跟着变
例如
groupid的hashcode值=1,1%50=1,那么 consumer ofsets主题的1号分区,在哪个broker上,就选择这个节点的coordinator作为这个消费者组的老大。
消费者组下的所有的消费者提交offset的时候就往这个分区去提交offset。
流程
1,每个consumer都主动向coordinator发送JoinGroup(加入消费组)请求
2,随机从消费者当中,选出一个consumer作为leader
3,把要消费的topic情况发送给leader消费者
4,leader会负责制定消费方案
5,把消费方案发给coordinator
6,coordinator就把消费方案下发给各个consumer
7,每个消费者都会和coordinator保持心跳(默认3s),一旦超时(session.timeout.ms=45s),该消费者会被移除,并触发再平衡;或者消费者处理消息的时间过长(max.poll.interal.ms=5分钟),也会触发再平衡
消费者组详细消费流程
KafkaConsumer 的协调者(通常是 ConsumerCoordinator)在成功加入组并完成分区分配后,会触发 Fetcher 实例,然后由 Fetcher 获取相关配置后,再通过 ConsumerNetworkClient 来实际发送 FETCH 请求。
fetch.min.bytes
每批次最小抓取大小,默认1字节
fetch.max.wait.ms
一批数据最小值未达到的超时时间,默认500ms
fetch.max.bytes
每批次最大抓取大小,默认50m
消费者组要想进行工作,就需要创建一个消费者网络连接客户端(ConsumerNetworkClient),用于和kafka集群进行交互
调用sendFetches()方法,发送消费请求
准备完成以后,调用send方法,发送请求
发送完请求过后,会通过回调方法onSuccess方法把对应的结果拉取过来。放在一个消息队列中
消费者调用FetchedRecords方法拉取数据
Max.poll.records
一次拉取数据返回消息的最大条数,默认500条
parseRecord(反序列化)
因为生产端将数据序列化过
Interceptors(拦截器)
生产端也有拦截器
处理数据
组件
KafkaConsumer (主入口)
这是用户直接调用的客户端对象。你调用 consumer.poll(...) 时,它内部会协调所有其他组件的工作。
ConsumerCoordinator (协调者)
负责消费者组的协调工作,包括发现GroupCoordinator、加入组、心跳维持、偏移量提交与获取等。
关键动作
在消费者启动或重平衡后,当它成功加入消费者组并从GroupLeader(也可能是它自己)那里获取到最终的分区分配结果(Assignment)后,它会将这个分配信息告知给 KafkaConsumer,进而更新 Fetcher 和订阅状态。
ConsumerCoordinator像是公司的外部联系人,负责和对方(GroupCoordinator)沟通,而GroupCoordinator就像是对方公司的对接部门
Fetcher (数据抓取器)
这是发送 FETCH 请求的真正核心。它的唯一职责就是从broker拉取消息。
它提供了一个非常重要的方法:sendFetches()。
检查哪些分区需要被拉取(基于当前的分区分配)。
为这些分区构建 FETCH 请求。
通过 ConsumerNetworkClient 将这些请求异步地发送出去,但并不会立刻等待结果。
在后续的 poll() 调用中,Fetcher 才会通过 ConsumerNetworkClient 去获取之前发送的请求的响应结果。
ConsumerNetworkClient (网络客户端)
它是一个异步网络I/O的封装器和调度器。它本身不包含业务逻辑(比如不知道该拉取哪个分区)
职责
管理到所有broker的连接。
将高层(如 Fetcher)的请求放入队列。
在适当的时机(例如 poll() 被调用时)真正地执行网络I/O(发送待发送的请求,接收已到达的响应)。
处理重试、超时等网络问题。
你可以把客户端看作一个“邮差”,Fetcher 是“写信的人”,ConsumerNetworkClient 负责把信寄出去并把回信带回来。
生产经验
分区平衡以及再平衡
一个consumer group中有多个consumer组成,一个topic有多个partition组成,现在的问题是,到底由哪个consumer来消费哪个
partition的数据。
partition的数据。
Kafka有四种主流的分区分配策略
Range
Range 是对每个 topic 而言的。
首先对同一个 topic 里面的分区按照序号进行排序,并对消费者按照字母顺序进行排序
假如现在有7个分区,3个消费者,排序后的分区将会是0,1,2,3,4,5,6;消费者排序完之后将会是C0,C1,C2.
消费者内部会进行编号
通过 partitions数/consumer数 来决定每个消费者应该消费几个分区。如果除不尽,那么前面几个消费者将会多消费1个分区。
案例
例如,7/3=2余1,除不尽,那么 消费者 C0 便会多消费1个分区。
8/3=2余2,除不尽,那么C0和C1分别多消费一个。
容易产生数据倾斜!
注意:如果只是针对1个topic 而言,C0消费者多消费1个分区影响不是很大。但是如果有N 多个 topic,那么针对每个 topic,消费者C0都将多消费1个分区,topic越多,C0消费的分区会比其他消费者明显多消费 N个分区。
分区分配再平衡
如果某个消费者宕机,其负责的任务会整体转由其他一个消费者处理
(1)停止掉 0 号消费者,快速重新发送消息观察结果(45s 以内,越快越好)。
1 号消费者:消费到 3、4 号分区数据。
2 号消费者:消费到 5、6 号分区数据。
0 号消费者的任务会整体被分配到 1 号消费者或者 2 号消费者。 (被整体分配)
1 号消费者:消费到 3、4 号分区数据。
2 号消费者:消费到 5、6 号分区数据。
0 号消费者的任务会整体被分配到 1 号消费者或者 2 号消费者。 (被整体分配)
说明:0 号消费者挂掉后,消费者组需要按照超时时间 45s 来判断它是否退出,所以需要等待,时间到了 45s 后,判断它真的退出就会把任务分配给其他 broker 执行。
(2)再次重新发送消息观察结果(45s 以后)。
1 号消费者:消费到 0、1、2、3 号分区数据。
2 号消费者:消费到 4、5、6 号分区数据。
1 号消费者:消费到 0、1、2、3 号分区数据。
2 号消费者:消费到 4、5、6 号分区数据。
说明:消费者 0 已经被踢出消费者组,所以重新按照 range 方式分配。
RoundRobin
RoundRobin 针对集群中所有Topic而言
RoundRobin 轮询分区策略,是把所有的 partition 和所有的consumer 都列出来,然后按照 hashcode 进行排序,最后通过轮询算法来分配 partition 给到各个消费者。
分区分配再平衡
(1)停止掉 0 号消费者,快速重新发送消息观看结果(45s 以内,越快越好)。
1 号消费者:消费到 2、5 号分区数据
2 号消费者:消费到 4、1 号分区数据
0 号消费者的任务会按照 RoundRobin 的方式,把数据轮询分成 0 、6 和 3 号分区数据,分别由 1 号消费者或者 2 号消费者消费。(采用轮训)
1 号消费者:消费到 2、5 号分区数据
2 号消费者:消费到 4、1 号分区数据
0 号消费者的任务会按照 RoundRobin 的方式,把数据轮询分成 0 、6 和 3 号分区数据,分别由 1 号消费者或者 2 号消费者消费。(采用轮训)
说明:0 号消费者挂掉后,消费者组需要按照超时时间 45s 来判断它是否退出,所以需要等待,时间到了 45s 后,判断它真的退出就会把任务分配给其他 broker 执行。
(2)再次重新发送消息观看结果(45s 以后)。
1 号消费者:消费到 0、2、4、6 号分区数据
2 号消费者:消费到 1、3、5 号分区数据
1 号消费者:消费到 0、2、4、6 号分区数据
2 号消费者:消费到 1、3、5 号分区数据
说明:消费者 0 已经被踢出消费者组,所以重新按照 RoundRobin 方式分配。
在Consumer配置中设置
Sticky
粘性分区定义
可以理解为分配的结果带有“粘性的”。
即在执行一次新的分配之前,考虑上一次分配的结果,尽量少的调整分配的变动,可以节省大量的开销。
粘性分区是Kaka从0.11x版本开始引入这种分配策略,首先会尽量均衡的放置分区到消费者上面,在出现同一消费者组内消费者出现问题的时候,会尽量保持原有分配的分区不变化。
核心目标
追求两次分配间的最大相似性,其次才是均匀
尽可能保留原有分配,只重新分配“失效”的分区
CooperativeSticky
合作者粘性
3.0新增的
默认策略是Range+CooperativeSticky
可以通过配置参数partition.assignment.strategy,修改分区的分配策略。
Kafka可以同时使用多个分区分配策略
消费者事务
如果想完成Consumer端的精准一次性消费,那么需要Kafka消费端将消费过程和提交offset过程做原子绑定。此时我们需要将Kafka的offset保存到支持事务的自定义介质(比如MySQL)
要想完全精确,除了消费者采用事务,生产者与集群也得采用幂等性 + 事务,同时还要做到"至少一次"(ACK=-1),副本数大于等于2,另外ISR队列里面副本数也得大于等于2
数据积压(提高吞吐量)
1) 如果是Kafka消费能力不足
可以考虑增加Topic的分区数,并且同时提升消费组的消费者数量,消费者数=分区数。(两者缺一不可)
增加消费者的CPU的核数
2) 如果是下游的数据处理不及时
提高每批次拉取的数量。批次拉取数据过少(拉取数据/处理时间<生产速度),使处理的数据小于生产的数据,也会造成数据积压。
从一次最多拉取500条,调整为一次最多拉取1000条
max.poll.records
一次 poll 拉取数据返回消息的最大条数,默认是 500 条
还需注意每批次拉取上限为50M,如果1000条大小超过了,也需要一并调整
fetch.max.bytes
默认Default: 52428800(50 m)。
消费者获取服务器端一批消息最大的字节数。
如果服务器端一批次的数据大于该值(50m)仍然可以拉取回来这批数据,因此,这不是一个绝对最大值。一批次的大小受 message.max.bytes (broker config)ormax.message.bytes (topic config)影响。
offset位移
offset 的默认维护位置
Kafka 0.9 版本之前consumer默认将offset保存在Zookeeper中
弊端
每个consumer都要和zk同步offset,会带来大量网络通信消耗
从0.9版本之后consumer默认保存在Kafka一个内置的topic中,该topic为_consumer_offsets
_consumer_offsets 主题里面采用 key 和 value 的方式存储数据
key 是 group.id+topic+分区号,value就是当前offset的值。
假如group.id为test,topic为good,那么key就为[test,good,0]。其中的0是指0号分区
每隔一段时间,kafka内部会对这个topic进行compact,也就是每个group.id+topic+分区号就保留最新数据。
为什么 _consumer_offsets 默认有 50 个分区
高写入吞吐量和并行性的需求
想象一下一个大型 Kafka 集群
可能有成千上万个消费者组。
每个消费者组可能有多个消费者实例。
每个消费者都会定期(例如每秒)向 _consumer_offsets 提交位移。
如果这个主题只有 1 个分区,那么所有的写入请求都会落到集群中的某一台 Broker 上,这个 Broker 很快就会成为单点性能瓶颈,导致位移提交延迟增高,甚至影响整个集群的稳定性。
将分区数设置为 50,意味着写入负载可以被分散到最多 50 个不同的 Broker 上。Kafka 通过对 消费者组名(Group ID) 进行哈希计算,来决定该组的位移信息存储在哪个分区
不同的消费者组会均匀地(理想情况下)分布到不同的分区上。
实现了高并发写入,避免了单点瓶颈。
压缩效率与数据清理
_consumer_offsets 主题的日志清理策略是 compact(压缩),而不是普通的基于时间的删除。它只保留每个键(Key)的最新版本。对于这个主题,Key 就是 group.id + topic + partition。
压缩过程需要扫描和清理日志。如果只有一个巨大的分区,压缩过程会非常耗时且影响性能。将数据分散到 50 个分区中,压缩任务可以并行进行,每个分区的数据量更小,清理更快,对集群的影响也更小。
为未来扩展预留空间
50 是一个“足够大”的默认值,可以满足绝大多数中小型集群的需求,甚至为大型集群提供了足够的缓冲。Kafka 的设计者选择了一个“一次设定,基本无需更改”的保守值。对于 99% 的用户来说,他们永远不需要去调整这个数字。如果设置得太小(比如 5),当集群规模增长时,管理员就需要手动去扩展这个内部主题的分区数,而操作内部主题是有风险的。
一般情况下, 当集群中第一次有消费者消费消息时会自动创建主题_ consumer_ offsets, 不过它的副本因子还受offsets.topic .replication.factor参数的约束,这个参数的默认值为3 (下载安装的包中此值可能为1),分区数可以通过offsets.topic.num.partitions参数设置,默认为50。
在配置文件 config/consumer.properties 中添加配置 exclude.internal.topics=false,默认是 true,表示不能消费系统主题。为了查看该系统主题数据,所以该参数修改为 false。
查看命令
消费者提交offset的方式
自动提交
为了使我们能够专注于自己的业务逻辑,Kafka提供了自动提交offset的功能。
参数
enable.auto.commit
是否开启自动提交offset功能
默认是true
auto.commit.interval.ms
自动提交offset的时间间隔,默认是5s
手动提交
虽然自动提交offset十分简单便利,但由于其是基于时间提交的,开发人员难以把握offset提交的时机。因此Kafka还提供了手动提交offset的API
手动提交offset的方法
commitSync(同步提交)
必须等待offset提交完毕,再去消费下一批数据。 阻塞线程,一直到提交到成功,会进行失败重试
commitAsync(异步提交)
发送完提交offset请求后,就开始消费下一批数据了。没有失败重试机制,会提交失败
两者的相同点是,都会将本次提交的一批数据最高的偏移量提交
不同点是,同步提交阻塞当前线程,一直到提交成功,并且会自动失败重试(由不可控因素导致,也会出现提交失败);而异步提交则没有失败重试机制,故有可能提交失败。
自动提交有可能出现消息消费失败,但是却提交了offset的情况,导致消息丢失。为了能够实现消息消费offset的精确控制,更推荐手动提交。
指定offset消费位置
在kafka中当消费者查找不到所记录(消费者组第一次消费 或者 服务器上不再存在当前偏移量时,例如该数据已被删除)的消费位移时,会根据auto.offset.reset的配置,决定从何处消费。
auto.offset.reset = earliest | latest | none 默认是 latest。
earliest
自动将偏移量重置为最早的偏移量,--from-beginning
latest(默认值)
自动将偏移量重置为最新偏移量
none
如果未找到消费者组的先前偏移量,则向消费者抛出异常。
Kafka中的消费位移是存储在一个内部主题中的, 而我们可以使用**seek()**方法可以突破这一限制:消费位移可以保存在任意的存储介质中, 例如数据库、 文件系统等。以数据库为例, 我们将消费位移保存在其中的一个表中, 在下次消费的时候可以读取存储在数据表中的消费位移并通过seek()方法指向这个具体的位置 。
指定时间消费
原理就是查到时间对应的offset再去指定位移消费,为了确保同步到分区信息,我们还需要确保能获取到分区,再去查询分区时间
通过时间获取offset的API
Map<TopicPartition, OffsetAndTimestamp> topicPartitionOffsetAndTimestampMap = kafkaConsumer.offsetsForTimes(topicPartitionLongHashMap);
漏消费和重复消费
重复消费
已经消费了数据,但是 offset 没提交。
场景
自动提交offset引起。
1) Consumer每5s提交offset
2) 如果提交offset后的2s,consumer挂了
3) 再次重启consumer,则从上一次提交的offset处(offset为2)继续消费,导致重复消费
漏消费
先提交 offset 后消费,有可能会造成数据的漏消费。
场景
设置offset为手动提交,当ofset被提交时,数据还在内存中未落盘,此时刚好消费者线程被kill掉,那么offset已经提交,但是数据未处理,导致这部分内存中的数据丢失。
消费者API
流程图
流程图
独立消费者
订阅主题
订阅分区
消费者组
流程图
核心参数
bootstrap.servers
向Kafka 集群建立初始连接用到的 host/port 列表。
key.deserializer 和 value.deserializer
指定接收消息的key和value 的反序列化类型
一定要写全类名。
group.id
标记消费者所属的消费者组。
enable.auto.commit
默认值为 true,消费者会自动周期性地向服务器提交偏移量
auto.commit.interval.ms
如果设置了 enable.auto.commit 的值为true,则该值定义了消费者偏移量向 Kafka 提交的频率
默认 5s
auto.offset.reset
当Kafka中没有初始偏移量或当前偏移量在服务器中不存在(如,数据被删除了),该如何处理?
earliest:自动重置偏移量到最早的偏移量。
latest:默认,自动重置偏移量为最新的偏移量。
none:如果消费组原来的(previous)偏移量不存在,则向消费者抛异常。
anything:向消费者抛异常
offsets.topic.num.partitions
_consumer_offsets 的分区数,默认是 50 个分区。不建议修改。
heartbeat.interval.ms
Kafka消费者和 coordinator 之间的心跳时间,默认 3s.
该条目的值必须小于 session.timeout.ms,也不应该高于session.timeout.ms 的 1/3。不建议修改。
session.timeout.ms
Kafka消费者和coordinator 之间连接超时时间,默认 45s。超过该值,该消费者被移除,消费者组执行再平衡。
max.poll.interval.ms
消费者处理消息的最大时长,默认是5分钟。超过该值,该消费者被移除,消费者组执行再平衡,
fetch.min.bytes
默认1个字节。消费者获取服务器端一批消息最小的字节数。
fetch.max.wait.ms
默认 500ms。如果没有从服务器端获取到一批数据的最小字节数。该时间到,仍然会返回数据
fetch.max.bytes
消费者获取服务器端一批消息最大的字节数。如果服务器端一批次的数据大于该值(50m)仍然可以拉取回来这批数据,因此,这不是一个绝对最大值。
一批次的大小受message.max.bytes( broker config) or max.message.bytes (topic config) 影响。
默认 Default: 52428800(50 m)
max.poll.records
一次 pull 拉取数据返回消息的最大条数
默认是 500条
消费者再平衡
heartbeat.interval.ms
Kafka消费者和 coordinator 之间的心跳时间,默认 3s
该条目的值必须小于 session.timeout.ms,也不应该高于session.timeout.ms的1/3.
session.timeout.ms
Kafka消费者和coordinator 之间连接超时时间,默认45s超过该值,该消费者被移除,消费者组执行再平衡,
max.poll.interval.ms
消费者处理消息的最大时长,默认是5分钟。超过该值,该消费者被移除,消费者组执行再平衡。
partition.assignment.strategy
消费者分区分配策略,默认策略是Range + CooperativeSticky。Kafka可以同时使用多个分区分配策略。
可以选择的策略包括:Range、IRoundRobin 、Sticky、CooperativeSticky
生产调优手册
Kafka 硬件 配置选择
场景说明
100万日活,每人每天100条日志,每天总共的日志条数是100万*100条=1亿条。
1亿/24 小时/60分/60秒 =1150 条/每秒钟。
每条日志大小:0.5k-2k(取1k)。
1150条/每秒钟*1k≈1m/s。
高峰期流量每秒钟:1150条*20倍(20倍到40倍左右)=23000条。每秒多少数据量:20MB/s。
1亿/24 小时/60分/60秒 =1150 条/每秒钟。
每条日志大小:0.5k-2k(取1k)。
1150条/每秒钟*1k≈1m/s。
高峰期流量每秒钟:1150条*20倍(20倍到40倍左右)=23000条。每秒多少数据量:20MB/s。
服务器台数选择
服务器台数=2 * (生产者峰值生产速率 * 副本 / 100) + 1
=2*(20m/s*2/100)+1
=3台
磁盘选择
kata底层主要是顺序写,固态硬盘(随机读写优势大)和机械硬盘的顺序写速度差不多。
建议选择普通的机械硬盘
每天总数据量:1亿条 * 1k ≈ 100G
100G * 2个副本 * 保存时间 3天/ 0.7(30%的余量) ≈ 1T
建议三台服务器硬盘总大小,大于等于1T。
内存选择
组成
堆内存(kafka内部配置)
Kafka 堆内存建议生产环境每个节点:10g ~ 15g
在 kafka-server-start.sh 中修改
查看 Kafka 进程号
根据 Kafka 进程号,查看 Kafka 的 GC 情况
jstat -gc 2321 1s 10
参数说明
S0C
第一个幸存区的大小;
S1C
第二个幸存区的大小
S0U
第一个幸存区的使用大小;S1U:第二个幸存区的使用大小
EC
伊甸园区的大小
OC
老年代大小
MC
方法区大小
MU:方法区使用大小
CCSC
压缩类空间大小
YGC
年轻代垃圾回收次数:
YGCT
年轻代垃圾回收消耗时间
FGC
老年代垃圾回收次数
FGCT
老年代垃圾回收消耗时间
EU
伊甸园区的使用大小
OU
老年代使用大小
CCSU
压缩类空间使用大小
GCT
垃圾回收消耗总时间
根据 Kafka 进程号,查看 Kafka 的堆内存
页缓存(服务器内存)
页缓存是 Linux 系统服务器的内存
我们只需要保证 1 个 segment(默认1g)中25%的数据在内存中就好
每个节点 页缓存大小 = (分区数*1g*25%) / 节点数。
例如10个分区,页缓存大小= ( 10 * 1g * 25%) / 3 ≈ 1g
建议服务器内存大于等于:10G + 1G = 11G
建议服务器内存大于等于:10G + 1G = 11G
其中3是三台服务器
CPU 选择
num.io.threads = 8
负责写磁盘的线程数,整个参数值要占总核数的50%。
num.replica.fetchers=1
副本拉取线程数,这个参数占总核数的 50%的 1/3.
num.network.threads=3
数据传输线程数,这个参数占总核数的50%的 2/3。
还有众多线程(比如各种定时任务),只是这三个最耗时
3秒心跳
超时
超过45秒
超过5分钟
等等定时任务
如果服务器是32 core
24 core
用于上面的三个线程使用
分别改为12,4,8 core
8 core
用于其他线程使用
网络选择
网络带宽 = 峰值吞吐量20MB/S选择千兆网卡即可。
100Mbps单位是bit;10M/s单位是byte;1byte = 8bit,100Mbps/8 = 12.5M/s。
一般百兆的网卡(100Mbps)、千兆的网卡(1000Mbps)、万兆的网卡(10000Mbps)
Kafka 总体
如何提升吞吐量
提升生产吞吐量
(1)buffer.memory:发送消息的缓冲区大小,默认值是 32m,可以增加到64m。
(2)batch.size:默认是16k。如果 batch 设置太小,会导致频繁网络请求,吞吐量下降;如果 batch 太大,会导致一条消息需要等待很久才能被发送出去,增加网络延时。
(3)linger.ms,这个值默认是0,意思就是消息必须立即被发送。一般设置一个5-100毫秒。如果 1inger.ms 设置的太小,会导致频繁网络请求,吞吐量下降;如果 linger.ms 太长,会导致一条消息需要等待很久才能被发送出去,增加网络延时。
(4)compression.type:默认是 none,不压缩,但是也可以使用lz4压缩,效率还是不错的,压缩之后可以减小数据量,提升吞吐量,但是会加大 producer 端的 CPU 开销(应该根据压测效果决定)。
增加分区
消费者提高吞吐量
(1)调整 fetch.max.bytes 大小,默认是 50m。
(2)调整max.poll.records 大小,默认是 500 条。
增加下游消费者处理能力
数据精准一次
生产者角度
acks 设置为-1(acks=-1)
幂等性(enable.idempotence=true)+事务
broker服务端角度
分区副本大于等于2(--replication-factor 2)
ISR 里应答的最小副本数量大于等于2(min.insync.replicas=2)
消费者
事务 + 手动提交 offset(enable.auto.commit = false)
消费者输出的目的地必须支持事务(MySOL、Kafka)
合理设置分区数
(1)创建一个只有1个分区的 topic。
(2)测试这个topic的producer 吞吐量和 consumer 吞吐量。
(3)假设他们的值分别是Tp和Tc,单位可以是 MB/S。
(4)然后假设总的目标吞吐量是Tt,那么分区数=Tt/min(Tp,Tc)
例如:producer吞吐量=20m/s;consumer吐量=50m/s,期望吞吐量100m/s
分区数=100/20=5分区
分区数一般设置为:3-10个
分区数不是越多越好,也不是越少越好,需要搭建完集群,进行压测,再灵活调整分区个数
单条日志大于 1m
message.max.bytes
broker端接收每个批次消息最大值。
默认 1m
如果大于,有可能会卡死
max.request.size
生产者发往 broker 每个请求消息最大值。针对topic级别设置消息体的大小
默认 1m
replica.fetch.max.bytes
副本同步数据,每个批次消息最大值。
默认1m
fetch.max.bytes
消费者获取服务器端一批消息最大的字节数。
如果服务器端一批次的数据大于该值(50m)仍然可以拉取回来这批数据,因此,这不是一个绝对最大值。
一批次的大小受message.max.bytes( broker config) or max.message.bytes (topic config) 影响。
默认 Default: 52428800(50 m)
服务器挂了
正常处理办法
(1)先尝试重新启动一下,如果能启动正常,那直接解决。
如果启动报错,根据报错排查
(2)如果重启不行,考虑增加内存、增加CPU、网络带宽。
3)如果将 kafka整个节点误删除,如果副本数大于等于2,可以按照服役新节点的方式重新服役一个新节点,并执行负载均衡。
集群压力测试
集群压力测试
Kafka 压测
用 Kafka 官方自带的脚本,对 Kafka 进行压测。
生产者压测:kafka-producer-perf-test.sh
消费者压测:kafka-consumer-perf-test.sh
Kafka Producer 压力测试
创建一个 test topic,设置为 3 个分区 3 个副本
bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --create --replication-factor 3 --partitions 3 --topic test
在/opt/module/kafka/bin 目录下面有这两个文件。我们来测试一下
bin/kafka-producer-perf-test.sh --topic test --record-size 1024 --num-records 1000000 --throughput 10000 --producer-props bootstrap.servers=hadoop102:9092,hadoop103:9092,hadoop104:9092 batch.size=16384 linger.ms=0
参数说明
record-size是一条信息有多大,单位是字节,本次测试设置为1k。
num-records是总共发送多少条信息,本次测试设置为100万条
throughput 是每秒多少条信息,设成-1,表示不限流,尽可能快的生产数据,可测出生产者最大吞吐量。本次实验设置为每秒钟1万条。
producer-props 后面可以配置生产者相关参数,batch.size 配置为16K
调整 batch.size 大小
batch.size 默认值是 16k。本次实验 batch.size 设置为32k
bin/kafka-producer-perf-test.sh --topic test --record-size 1024 --num-records 1000000 --throughput 10000 --producer-props bootstrap.servers=hadoop102:9092,hadoop103:9092,hadoop104:9092 batch.size=32768 linger.ms=0
batch.size 默认值是 16k。本次实验 batch.size 设置为 4k
bin/kafka-producer-perf-test.sh -- topic test --record-size 1024 --num-records 1000000 --throughput 10000 --producer-props bootstrap.servers=hadoop102:9092,hadoop103:9092,hadoop104:9092 batch.size=4096 linger.ms=0
调整 linger.ms 时间
linger.ms 默认是 0ms。本次实验 linger.ms 设置为 50ms.
bin/kafka-producer-perf-test.sh -- topic test --record-size 1024 --num-records 1000000 --throughput 10000 --producer-props bootstrap.servers=hadoop102:9092,hadoop103:9092,hadoop104:9092 batch.size=4096 linger.ms=50
调整压缩方式
默认的压缩方式是 none。本次实验 compression.type 设置为 snappy。
bin/kafka-producer-perf-test.sh --topic test --record-size 1024 --num-records 1000000 --throughput 10000 --producer-props bootstrap.servers=hadoop102:9092,hadoop103:9092,hadoop104:9092 batch.size=4096 linger.ms=50 compression.type=snappy
默认的压缩方式是 none。本次实验compression.type 设置为 zstd。
bin/kafka-producer-perf-test.sh --topic test --record-size 1024 --num-records 1000000 --throughput 10000 --producer-props bootstrap.servers=hadoop102:9092,hadoop103:9092,hadoop104:9092 batch.size=4096 linger.ms=50 compression.type=zstd
默认的压缩方式是 none。本次实验compression.type 设置为 gzip。
bin/kafka-producer-perf-test.sh --topic test --record-size 1024 --num-records 1000000 --throughput 10000 --producer-props bootstrap.servers=hadoop102:9092,hadoop103:9092,hadoop104:9092 batch.size=4096 linger.ms=50 compression.type=gzip
默认的压缩方式是 none。本次实验compression.type 设置为 lz4。
bin/kafka-producer-perf-test.sh --topic test --record-size 1024 --num-records 1000000 --throughput 10000 --producer-props bootstrap.servers=hadoop102:9092,hadoop103:9092,hadoop104:9092 batch.size=4096 linger.ms=50 compression.type=lz4
调整缓存大小
默认生产者端缓存大小 32m。本次实验 buffermemory 设置为 64m。
bin/kafka-producer-perf-test.sh -- topic test --record-size 1024 --num-records 1000000 --throughput 10000 --producer-props bootstrap.servers=hadoop102:9092,hadoop103:9092,hadoop104:9092 batch.size=4096 linger.ms=50 buffer.memory=67108864
Kafka Consumer 压力测试
修改/opt/module/kafka/config/consumer.properties 文件中的一次拉取条数为 500
max.poll.records=500
其实默认就是500条,这儿只是为了显式体现,也方便后面测试修改
消费100万条日志进行压测
bin/kafka-consumer-perf-test.sh --bootstrap-server hadoop102:9092,hadoop103:9092,hadoop104:9092 --topic test --messages -1000000 --consumer.config config/consumer.properties
配置文件只能写到文件中,而不能像生产者压测那样使用--producer-props调整配置
参数说明
--bootstrap-server 指定Kafka集群地址
--topic 指定topic 的名称
--messages 总共要消费的消息个数。本次实验100万条
一次拉取条数为 2000
调整 fetch.max.bytes 大小为 100m
源码
生产者源码
初始化
生产者sender线程初始化
形象比喻
想象一个大型超市(Kafka 集群),每个收银台(或商品储存区)都是一个 Broker。
生产者是送货的卡车,把商品(消息)运送到超市的各个区域(Broker)。
消费者是购物者,去到不同的收银台(Broker)结账(消费消息)。
每个收银台(Broker)负责自己区域商品的进销存管理。如果一个收银台坏了(Broker 宕机),附近的收银台可以分担它的工作(副本接管),超市整体(集群)还能继续运营。
增加更多的收银台(Broker)可以让超市服务更多的顾客(更高的吞吐量)或者存放更多商品(更大的存储)。
编程概念
面向对象
封装
继承
多态
Prototype
Mixin
Traits
Duck Typing
函数式编程
高阶函数
闭包
惰性求值
递归
不可变状态
无副作用
元编程
Java的动态代理,CGLib
Lisp的宏(代码即数据)
并发模型
Java线程
Python协程
Go的Go routine
Erlang的Actor
注意点
同步与互斥
锁
死锁
软件事物内存
虚拟机和垃圾回收
集大成者:JVM垃圾回收
静态类型,动态类型,类型推导
静态类型的语言在编译器就能确定类型编译器能帮忙发现错误,做些优化但是会增加代码量
动态类型是在运行期确定类型,非常灵活但是运行期才能发现错误。所谓动态一时爽,重构火葬场。
Java10引入了类型推导
抽象语法树(AST)
指针
理解指针,对理解计算机的底层运作大有好处
错误处理 (异常),泛型同步异步,序列化
重构
重构、是不改变原来代码逻辑的提前下,通过前人总结的经验,利用各种设计模式,把垃圾代码整理一个新的垃圾
你看着挺干净的屋子你妈又收拾了一遍
重构前:1+87689+1-87689=2
重构后:1+1=2
重构后:1+1=2
Web认证与授权
Cookie
Cookie是服务器发送到用户浏览器并保存在用户本地设备上的小型文本文件。它包含有关用户的信息,以及与网站的交互信息。例如登录状态、语言选择、购物车信息等。
Session
与Cookie类似,Session也是用于维持用户的会话状态的技术。但与Cookie不同的是,Session是存储在服务器端的。当用户访问一个网站时,服务器可以为该用户创建一个Session,并将相关信息存储在其中。这样,服务器就可以跟踪用户的状态并在用户再次访问时恢复该状态。
Session 的痛点
实际在生产上,为了保障高可用,一般服务器至少需要两台机器,通过负载均衡的方式来决定到底请求该打到哪台机器上。A服务器生成了session,其余服务器会找不到session,导致请求失败(如无法添加购物车)
解决方案
session 复制
A 生成 session 后复制到 B, C,这样每台机器都有一份 session
缺点
同一样的一份 session 保存了多份,数据冗余
如果节点少还好,但如果节点多的话,特别是像阿里,微信这种由于 DAU 上亿,可能需要部署成千上万台机器,这样节点增多复制造成的性能消耗也会很大。
session 粘连
让每个客户端请求只打到固定的一台机器上,比如浏览器登录请求打到 A 机器后,后续所有的添加购物车请求也都打到 A 机器上,Nginx 的 sticky 模块可以支持这种方式,支持按 ip 或 cookie 粘连等等
缺点
对应的机器挂了怎么办?
session 共享
目前各大公司普遍采用的方案,将 session 保存在 redis,memcached 等中间件中,请求到来时,各个机器去这些中间件取一下 session 即可。
缺点
每个请求都要去 redis 取一下 session,多了一次内部连接,消耗了一点性能,另外为了保证 redis 的高可用,必须做集群
Token
无状态
Token是一种独立于服务器的安全凭证,不依赖于会话状态。它通常被用于API身份验证和访问控制。当用户第一次登录后,服务器利用用户名、密码和密钥生成一个token并将此token返回给客户端。以后,客户端只需带上这个token前来请求数据即可,无需再次带上用户名和密码。与Session相比,Token具有更好的安全性,特别是在分布式环境下 。
一般是放在 header 的 Authorization 自定义头里,不是放在 Cookie 里的,这主要是为了解决跨域不能共享 Cookie 的问题
在移动端原生请求是没有 cookie 之说的,而 sessionid 依赖于 cookie,sessionid 就不能用 cookie 来传了,如果用 token 的话,由于它是随着 header 的 authoriize 传过来的,也就不存在此问题,换句话说token 天生支持移动平台,可扩展性好
缺点
token 太长
不太安全
它太长放在 cookie 里可能导致 cookie 超限,那就只好放在 local storage 里,这样会造成安全隐患,因为 local storage 这类的本地存储是可以被 JS 直接读取的,另外由上文也提到,token 一旦生成无法让其失效,必须等到其过期才行,这样的话如果服务端检测到了一个安全威胁,也无法使相关的 token 失效。
跨域问题
浏览器出于安全考虑,
对 同源请求 放行,
对 异源请求 限制这些限制规则统称为同源策略。
因此限制造成的开发问题,称之为跨域(异源)问题
对 同源请求 放行,
对 异源请求 限制这些限制规则统称为同源策略。
因此限制造成的开发问题,称之为跨域(异源)问题
什么是同源
在URL地址里面,源 = 协议 + 域名 + 端口
不同源例子
例1
http://a.com:81/a
https://a.com: 81/ a
协议不一样
例2
http://a. com:81/a
http://www.a.com:81/a
域名不一样
例3
http://a.com:81/a
http://a.com:82/a
端口不一样
同源例子
例1
http://a. com:81/a
http://a. com:81/a/b
有了同源策略,用户才能放心点开每一个链接,而不用担心安全问题
对标签发出的跨域请求 轻微限制
link
script
img
video
audio
对AJAX发出的跨域请求 严厉限制
跨域发生的时机
AJAX(老的XHR/新的fetch)通过浏览器(浏览器知道是跨域请求,但还是会发出)将请求发出
服务器处理好结果以后,给出正常响应
浏览器进行校验(因为知道是跨域,所以会进行校验,如果是正常的就直接返回了JS)
通过
交付
未通过
引发错误,跨域问题
1. 我们的前端服务器请求后端服务器发生了跨域问题(错误)
2. 当发生跨域时,服务器无法收到请求(错误)
3. 即便是发生跨域也不一定引发跨域问题(正确)
4. 跨域仅发生在AJAX过程中(错误)
2. 当发生跨域时,服务器无法收到请求(错误)
3. 即便是发生跨域也不一定引发跨域问题(正确)
4. 跨域仅发生在AJAX过程中(错误)
解决方案
CORS规则
最正统的解决方案
Cross-Origin Resource Sharing
CORS是一套机制,用于浏览器校验跨域请求
基本理念
只要服务器明确表示允许,则校验通过
服务器明确拒绝或没有表示,则校验不通过
使用前提
必须确保服务器是自己人
必须需要服务器端配合
请求分类
简单请求
请求方法
GET
HEAD
POST
头部字段满足CORS安全规范,详见 W3C
简单理解
不改请求头部,就满足安全规范;改了就不满足
请求头的Content-Type
text/plain
multipart/form-data
application/x-www-form-urlencoded
解决
浏览器发现跨域了,请求会自动带上当前页面源:Origin: http://my. com
从哪个源发生了这个跨域请求
响应
Access-Control-Allow-Origin: http://my.com
浏览器发现Origin与Access-Control-Allow-Origin一致,就校验通过
Access-Control-Allow-Origin: *
预检请求
预检请求OPTIONS(第一步不会真实发送这个请求,而是先询问)
请求
Origin: http://my. com
Access-Control-Request-Method: POST
请求方法
Access-Control-Request-Headers: a, b, content-type
本次请求改动了哪些请求头
响应
Access-Control-Allow-Origin: http://my.com
允许的源
Access-Control-Allow-Methods: POST
允许的请求方式
Access-Control-Allow-Headers: a, b, content-type
请求头允许包含的数据
Access-Control-Max-Age: 86400
86400秒以内都不用再问,再问也是回答上述结果
JSONP
JSON with Padding
JSONP是解决跨域问题的古老方案
同源策略中,对标签的跨域请求限制较小。
JSONP利用了这一点。
具体实现
创建script元素,发送跨域请求
<script src ="跨域地址">
服务器响应结果是一个函数调用
发送不了Post请求,只能发Get请求
代理
能不能更改服务器
能
浏览器支持CORS吗
支持
CORS
不支持
JSONP
不能
代理
单点登录
指在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
CSRF 攻击
在于对于同样域名的每个请求来说,它的 cookie 都会被自动带上,这个是浏览器的机制决定的,所以很多人据此认定 cookie 不安全。
后门
是为了绕过正常的认证,未授权访问,或者执行一些隐秘的通道。通常带有恶意的目的
开发者模式
也称为调试模式
是提供给开发者或者测试人员用于诊断,测试,优化程序的合法功能
CLI
Command-Line Interface
命令行界面
定点数
小数点位置固定的数字
小数点
固定不动
优点
精确、计算快
缺点
范围和精度不可兼得
核心思想
定点数就是小数点位置固定的数字。我们提前约定好,比如用32位二进制数,其中前16位表示整数部分,后16位表示小数部分。那么它能表示的范围和精度在“造尺子”的那一刻就固定了。它擅长处理范围固定、要求精确计算的场景(比如财务计算)。
浮点数
科学计数法
格式
光速 = 2.998 × 10⁸ m/s
电子质量 = 9.109 × 10⁻³¹ kg
符号(Sign)
表示正负
有效数字(Significand/Mantissa)
例子里的 2.998或 9.109。它决定了这个数的精度(能有多精确)
指数(Exponent)
例子里的 8或 -31。它决定了这个数的范围(能有多大或多小)
A × 10ⁿ
A 叫做 系数 或 尾数。它必须是一个 大于等于1且小于10 的数(即 1 ≤ |A| < 10)。它负责记录这个数的“细节”和“精度”。
10ⁿ 叫做 指数部分。n 是一个整数(可以是正数、负数或零)。它负责告诉别人这个数有多大或多小,决定了数量的“级”。
× 就是乘号。
小数点
浮动(由指数决定)
优点
表示范围巨大
缺点
有精度损失
核心思想
浮点数用科学计数法的方式,用“指数”来动态调整小数点的位置,从而在有限的位数内,既能表示巨大的天体距离,又能表示微小的原子直径。它是一种“用精度换范围”的权衡策略。
FP32
1 + 8 + 23
FP64
1 + 11 + 52
FP16
1 + 5 + 10
BF16
1 + 8 + 7
FP8
8-bit Floating Point
E4M3
指数位少,尾数位多
结构:1个符号位 + 4个指数位 + 3个尾数位
特点
动态范围较小:只有4位指数,能表示的数值范围比E5M2要窄。
精度相对较高:因为有3位尾数,它的精度比E5M2要高一些,能更精细地表示一个范围内的数。
好比:一把量程较小的尺子(只能从1厘米量到10米),但刻度更密集(有厘米刻度)。
E5M2
指数位多,尾数位少
结构:1个符号位 + 5个指数位 + 2个尾数位
特点
动态范围大:因为有5位指数,它能表示的数值范围非常广(从极小的数到极大的数)。
精度低:因为只有2位尾数,它的精度非常低。它能表示的数是“跳跃式”的,相邻两个可表示的数之间差距较大。
好比:一把量程很大的尺子(能从1厘米量到1公里),但刻度非常稀疏(只有米尺,没有厘米和毫米刻度)
UE8M0
结构
U(Unsigned):无符号位,仅支持非负数(0或正数),省去符号位占用。
E8(8-bit Exponent):8位指数位,决定数值的数量级范围(即“放大缩小”的尺度)。
M0(0-bit Mantissa):尾数位为0,数值被固定为 2的指数幂(例如:2⁻¹⁰⁰, 2⁰, 2¹⁰⁰)
主要用于存放缩放因子
关键突破
通过取消尾数位,UE8M0 将数值简化为纯指数形式,使复杂的浮点乘法简化为指数加法(如:2ᴬ × 2ᴮ = 2ᴬ⁺ᴮ),大幅提升计算效率
真正的难点是计算单元架构对各种精度的支持
设计一个n位浮点数谁都会,但是显卡最开始是只支持float32然后是double64,随后才逐渐支持FP16,BF16,int8,FP8。能不能用某个精度就是看架构。目前训练上BF16已经很成熟了,推理上更是百花齐放。
至于int8和UE8M0的区别:int8是等距离的分布,UE8M0是指数距离的分布,范围更大;BF16之所以能用在训练上,就是因为范围比fp16大,不用scale就能训练
至于int8和UE8M0的区别:int8是等距离的分布,UE8M0是指数距离的分布,范围更大;BF16之所以能用在训练上,就是因为范围比fp16大,不用scale就能训练
INT8:表示均匀分布的离散值。每个相邻数值的差是固定的(步长为1)。它的动态范围非常有限(-128 到 127)。
UE8M0/FP8/BF16/FP32:表示指数分布的离散值。相邻数值之间的比值大致固定(是2的幂次关系),但绝对差值随数值增大而增大。这带来了巨大的动态范围(许多个数量级)。
FP16:范围(~5.96×10⁻⁸ to 65504)较小,在训练过程中非常容易发生梯度下溢出(gradient underflow),即梯度值太小,被舍入成0,导致训练停滞。
BF16:范围(~1.18×10⁻³⁸ to 3.39×10³⁸)与FP32相当,完美避免了梯度下溢出的问题,保证了训练的稳定性。虽然它的精度(8位尾数)比FP16(10位尾数)低,但神经网络对范围的敏感性远高于对精度的敏感性。
场景题
第三方接口超时
代理法
解决方案
增加一个代理,向业务调用方屏蔽究竟是“本地实时”还是“异步远程”去获取返回结果
业务场景
通过OpenID实时获取微信用户基本信息
通过手机号实时获取钉钉用户基本信息
流程
本地实时流程
业务调用方调用内部service;
内部service调用异步代理service;
异步代理service通过OpenID在本地拿取数据;
异步代理service将数据返回内部service;
内部service返回结果给业务调用方;
远程异步流程
异步代理service定期跨公网调用微信服务;
微信服务返回数据;
刷新本地数据;
优点
公网抖动,第三方接口超时,不影响内部接口调用。
缺点
本地返回的不是最新数据(很多业务可以接受数据延时)
主备切换法
业务场景
调用第三方短信网关,或者电子合同等
解决方案
同时使用(或者备份)多个第三方服务供应商
流程
业务调用方调用内部service;
内部service调用第一个三方接口;
超时后,调用第二个备份服务,未来都直接调用备份服务,直到超时的服务恢复
内部service返回结果给业务调用方;
优点
公网抖动,第三方接口超时,不影响内部接口调用(初期少数几个请求会超时)
缺点
不是所有公网调用都能够像短息网关,电子合同服务一样有备份接口的,像微信、支付宝等就只此一家。
异步调用法
业务场景
本地结果,向第三方服务同步数据
例如用户在58到家平台下单,58到家平台需要通知平台商家为用户提供服务
解决方案
本地调用成功就返回成功,再异步调用第三方接口同步数据(和异步代理有微小差别)。
流程
本地流程
业务调用方调用内部service;
内部service写本地数据;
内部service返回结果给业务调用方成功;
异步流程
异步service定期将本地数据取出(或者通知也行,实时性好);
异步调用第三方接口同步数据;
优点
公网抖动,第三方接口超时,不影响内部接口调用。
缺点
不是所有业务场景都可以异步同步数据。
算法
深度遍历(DFS)和广度遍历(BFS)
深度遍历(DFS)
- 你选择一条路径一直走到底
- 遇到死胡同就返回到上一个路口,选择另一条没走过的路继续走
- 就像解迷宫时,你会一直往前走,直到走不通才回头
- 遇到死胡同就返回到上一个路口,选择另一条没走过的路继续走
- 就像解迷宫时,你会一直往前走,直到走不通才回头
生活中的例子
深度遍历就像你在翻一本书,你会从第一章看到最后,一章一章往下读
实现
通常用递归实现
性能考虑
使用递归时注意栈溢出风险,可以考虑使用迭代方式
应用场景
适合:寻找最远的节点,或者需要回溯的场景
适用场景
- 目标数据在树的较深层
- 树的分支不平衡(某些分支特别深)
- 内存有限制(因为DFS占用空间与树高度成正比)
广度遍历(BFS)
- 你站在入口,先把所有直接相连的路口都看一遍
- 然后再从这些路口往前走一步,看看它们能通向哪里
- 就像水波纹扩散一样,一层一层地向外探索
生活中的例子
就像你在图书馆找书,你会先看看每个大分类,然后再细看感兴趣的分类
实现
通常用队列实现
性能考虑
需要额外的队列空间,处理大数据时要注意内存使用
应用场景
适合:寻找最短路径,或者需要按层次处理的场景
适用场景
- 目标数据在树的较浅层
- 树比较平衡
- 需要按层级查找数据
实际项目中的优化建议
如果树是二叉搜索树,使用二分查找最快
如果经常查询,可以建立索引表
如果是多线程环境,可以并行搜索
- 普通查找场景:优先使用DFS(代码简单,内存占用小)
- 需要按层级查找:使用BFS
- 高性能要求:建立索引或使用缓存
- 特殊结构(如二叉搜索树):使用对应的专用算法
总的来说,具体选择要根据:
1. 数据分布特点
2. 内存限制
3. 性能要求
4. 树的结构特点
AI
概念
智能
本质上就是通过搜集信息,对于不同的情景作出针对性的输出反应
发展流派/方法论
符号主义
从数学的形式化推理体系中得到灵感,主张智能可以用符号的逻辑推理来模拟
起初人们试图找到精确函数,来解决一切原理,但是后来遇到了瓶颈
专家系统
局限性
完全是在复制人类经验,能力上限就是专家的水平,无法作到比人更好
从设计完成这套系统开始,他就永远静止不变的水平。不能像人一样,随着经验和时间增加,水平不断提升
联结主义
事先弄个非常复杂的函数,然后根据计算出的预测值和真实值的误差,不断调整里面的未知参数。
复杂函数(模型定义) + 输入数据 → 预测值 → 与真实值比较得误差(损失) → 反向传播计算梯度(误差如何分摊给每个参数) → 优化器利用梯度更新参数 → (重复迭代) → 找到最优参数 → 模型学会预测/表征数据 → 应用于新数据
神经网络
表示函数
感知机
单层
MLP
多层感知机
经典神经网络结构
感知机这种最早期的神经网络,它的设计很大程度上借鉴甚至是脱胎于逻辑推理。其思路同样是组合不同的特征条件来进行推理。这里的每个神经元,也就像符号逻辑当中的一个一个命题的字母一样。只不过它是用数值计算的方式来模拟逻辑。而数值计算本身不局限于有限且明确的符号推理,因而在更广泛的领域(图像识别,环境感知,控制)具有更强大的潜力
CNN
卷积神经网络
用于图像数据处理
不需要和前一层所有神经元全都稠密的连接,而只需要和局部的几个神经元连接。而且每个神经元和前一层的连接参数结构又都类似
可以减少参数和运算量,提升神经网络性能
RNN
用于序列数据处理
循环神经网络
模型架构
稠密模型(Dense Models)
核心特点
每一次前向计算(处理一个输入)时,整个网络(或当前层)的几乎每一个参数都会被用到。
最常见的结构就是标准的 Transformer 架构
稀疏模型(Sparse Models)
核心特点
每一次前向计算(处理一个输入)时,只有网络中的一小部分参数会被激活和使用。
工作原理
引入“条件计算”或“专家模型”的概念。
模型内部包含多个相对独立的子网络(称为“专家”)。
对于一个给定的输入,一个路由机制(通常也是一个小的神经网络)会判断这个输入更适合哪些“专家”来处理。
只有被选中的少数专家(及其对应的参数)会被激活并进行计算。其他专家处于“休眠”状态。
混合专家模型Mixture of Experts, MoE
Google 的 Switch Transformer, GLaM
DeepSeek 的 DeepSeek-MoE
Mistral 的 Mixtral 8x7B
Meta 的 Llama MoE 等。
技术手段
机器学习(Machine Learning,ML)
模型结构
黑箱的来源
损失函数
怎么奖励一个机器
损失函数最小化为目标
掌握规律,损失函数就会很小
以定量的方式,度量一组系数所对应的多项式到底拟合的好不好
把函数预测的数值和实际数据点的数值误差平方加到一起
梯度
梯度下降
梯度消失
训练过程
机器怎么建立条件反射
调整参数的过程
反向传播
通过反向传播训练参数
预训练
Pre-training
实现训练好一个基础模型的方式
后训练
Post-training
微调
基于预训练的模型,继续训练,让模型学会具体任务的方式
监督微调
精调
fine tuning
强化学习
RLHF
基于人类反馈的强化学习
让模型说的话,更合人类心意的方式
Deep Seek的GRPO实现方案
推理
参数调整好以后,根据函数的输入,计算输出结果的过程
人工智能
让机器表现出人的智能行为
Artificial Intelligence,AI
子领域
机器学习
让机器学习数据中的规律,进行预测
Machine Learning,ML
子领域
深度学习
使用深度神经网络来进行学习
Deep Learning,DL
GAN
生成式对抗网络
维度灾难
模型
函数
万物都被函数所描述
参数
模型权重
模型里面的参数
相当于是旋钮
训练得到
负责知识,语音理解,推理等核心能力
决定模型"能做什么"
偏置(阈值)
bias
当神经元完成所有加权和输入汇总(即计算出加权和)后,会在将其传入激活函数前,先加上一个固定数值
为了使预测尽可能准确
提示词
Prompt
包含System Prompt
可以指导大模型回答流程与风格
控制行为风格,角色设定,语气限制,任务边界等
决定模型"怎么做"
不错的提示词
你收到我的任务后,请你先根据你的分析和理解复述一遍,等我确认后你再执行
当我请你讲解一个知识点时,不要只告诉我定义和公式,而是像在讲故事一样,尽量生动形象,详细介绍,将一个冰冷的知识点变得"有血有肉",方便理解:
• 为什么会有人一开始想到要研究它?当时要解决的痛点是什么?
• 如果让我去想象这个公式/定理,我该如何在脑海里“看见”它的图景或意义?
• 科学家或数学家在探索的过程中曾经绕过哪些弯路,才最终走到现在的优美结论?
• 学习过程中常见的误区或容易掉进去的坑有哪些?我该如何避免?
• 为什么会有人一开始想到要研究它?当时要解决的痛点是什么?
• 如果让我去想象这个公式/定理,我该如何在脑海里“看见”它的图景或意义?
• 科学家或数学家在探索的过程中曾经绕过哪些弯路,才最终走到现在的优美结论?
• 学习过程中常见的误区或容易掉进去的坑有哪些?我该如何避免?
我需要你扮演一个10年经验的Java资深专家。通过我给出的场景,你可以根据你资深经验提供一个中学生水平都能理解,最适合的解决方案或者解答。我的问题是:
这是一个我的工程代码,我现在需要你深度分析这个工程的源码架构,然后给我绘制三张工程代码架构相关的图,具体工作步骤如下:
1、分析每一个头文件、源文件,分析它们的功能
2、分析出里面的关键类、关键函数
3、从主函数main开始出发,分析这些关键函数之间的调用关系,并把这些调用关系用JSON的方式记录起来,一层层递进。
4、最后分析上一步生成的JSON文件,生成Mermaid画图格式的文件,用来描述项目关键函数调用关系图。图中的每一个节点有两部分,上面部分是标题,下面是函数名。如果是类成员函数,标题就是所在的类名。如果是单独的函数,标题就是所在的文件名,比如main.c之类的。然后节点与节点之间有箭头连线,箭头被指向的节点表示被调用者。
5、根据分析的结果画一张接口数据流转图,同样使用Mermaid画图格式。
6、根据分析的结果画一张模块层级图,同样使用Mermaid画图格式。
请在当前目录下创建者三个Mermaid画图格式的文件。
最后生成的结果需要复制到支持mermaid格式的在线网站渲染哦,比如https://www.processon.com/mermaid,还有https://mermaid.live/
请你在每次回答我提出的问题或陈述时,都按照以下三个步骤来组织你的回答:
1.【首要共识】首先,基于我提供的信息,给出一个直接、优质的回答或分析。
2.【破壁视角】其次,必须为我提供一个与上述回答可能相悖的、对立的或来自完全不同角度的关键观点、理论或事实。请说明这个视角的合理性与价值。
3.【认知检查】最后,向我提出一个反思性问题,旨在挑战我可能存在的潜在假设、思维定式或信息盲区。
请确认你已理解此指令。我们开始。
避免我们跟ai对话时,走向信息茧房的神级prompt
大模型
模型里的参数特别大
不能像人一样"边对话边学习",每次生成都是基于固定权重的"推理",而不是更新
任何能力提升都需要"重新训练",或者"人工干预后微调"
大语言模型
用于自然语言处理的模型
LLM
Large Language Model
涌现
当模型参数量足够大的时候,对话能力有了质的提升。产生了一定程度的推理能力,这种量变引起质变,而突然出现的,之前没有的能力的现象
完全开源模型
开放了权重与训练代码的模型
mistral
私有化部署
模型下载到本地,进行使用的过程
部署软件
Ollama
方便开发者本地运行大模型的工具
不少命令和Docker类似
http://localhost:11434/api/chat
对外暴露端口为11434
vllm
LM Studio
Cherry Studio
生成式AI
基于输入内容,自动生成新内容
token
可以翻译为词元,但是没必要
分割成最小粒度的词
上下文
对话时,所有给到大模型的信息
随机性
大模型就是个大函数,函数是死的,根据前面的词,输出的下一个词是固定的。但是我们可以一定程度地调整输出的随机性,让下一个词的生成并不是总前面概率最高的那个词
温度
控制随机性的参数
Top-K
控制范围从概率最高的K值词中选择
幻觉
随机性太高
容易胡说八道
随机性太低
过于保守,也可能说错
从语言中说得通,在事实上虚假的情况
联网
大模型回答问题前,先去网上查找相关信息。然后再将互联网信息与问题共同发给大模型,然后进行回答
检索增强生成
RAG
Retrieval-Augmented Generation
有些数据网络上查不到,或者企业的数据不方便公开放在互联网上,希望大模型在这些私有的数据库中查找答案。这种方式就是RAG
和联网的思路一样,也是先查资料,再回答问题
知识库
RAG中涉及的私有数据库
词嵌入
Embedding
把文字转换成词向量的方式
向量
向量是数学和物理学中表示大小和方向的量
几何表示
有向线段,向量可以用一条带箭头的线段表示,线段的长度表示大小,箭头的方向表示方向。
代数表示
坐标表示,在直角坐标系中,向量可以用一组坐标来表示。
向量检索
对比词向量之间的相似度,以在知识库中找到相关问题答案的方式
向量模型
用于把文档分割后的片段向量化或者查询时把用户输入的内容向量化
向量数据库
为了让模型与知识库中的语义进行匹配,知识通常会以向量的形式存储在向量数据库
向量数据库使用流程
借助于向量模型,把文档知识数据向量化后存储到向量数据库
将专业的数据存储在文档中
再借助文本分割器,将大的文档切割成一个一个小的文本片段
将小的文本片段,借助专门的向量模型(擅长文本向量化)把文本片段转化为向量
再将每一个向量与对应的文本片段一块存储到向量数据库
用户输入的内容,借助于向量模型转化为向量后,与数据库中的向量通过计算余弦相似度的方式,找出相似度比较高的文本片段
最后把用户提交的问题和找出的文本片段,共同组成要提交给大模型的问题,发送给大模型
Qdrant
Pinecone
PGVector(PostgreSQL)
Milvus
RedisSearch(Redis)
可以Storing Metadata
不能Filtering by Metadata
不能Removing Embeddings
其余三个都满足
Chroma
Elasticsearch
在 VectorStore 中,查询与传统关系数据库不同。它们执行相
似性搜索,而不是精确匹配。
向量余弦相似度
用于表示坐标系中两个点之间的距离远近
向量余弦相似度越大,两点之间的距离越小
两个向量的余弦相似度越高,说明向量对应的文本相似度越高
cosθ = 向量的内积除以每一个向量模的乘积
两个向量内积
两个向量对应坐标乘积的和
向量模长
当前向量所有坐标的平方和,然后再开方
内容创作领域
PGC
Professional Generated Content
传统的,由专业机构,如权威专家,影视公司,媒体机构创作的内容
UGC
由普通用户创作的内容
AIGC
由AI创作的内容
AGI
通用人工智能
Artificial General Intelligence
多模态
处理多种模式内容(如处理文本,图像,视频)的能力
智能体
Agent
按照工作流封装大模型和一整套工具集,用于自动完成某一类复杂任务的程序
多智能体
多个智能体互相协作,完成更复杂任务的程序
工作流(大模型应用开发)
多次使用大模型的能力,比如第一本XXX,第二步YYY,这种将多个步骤编排成一个流程的能力
Coze
在页面上,进行傻瓜式操作编排工作流的工具
LangChain
通过代码的方式编排工作流的框架
LangChain4J
会话
RAG知识库
Tools工具
中间件,类似Java与数据库中间的JDBC,LangChain4j就是打通大模型和Java微服务
大模型调用三件套
获得API-key
获得模型名
获得baseURL的开发地址
注解
@AiService
@SystemMessage
@UserMessage+@V
@Tool + @P
@MemoryId
@StructuredPrompt
pom
LangChain4J原生整合
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>1.0.0-beta3</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>1.0.0-beta3</version>
</dependency>
走代码配置三件套
原生利用反射AiServices.create(Assistant.class, model);
LangChain4J-Boot整合
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
<version>1.0.0-beta3</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
<version>1.0.0-beta3</version>
</dependency>
走配置文件配置三件套
与Boot整合才有@AiService
实际应用中需要解决的核心挑战
配置对话记忆如何保证多轮对话不会超过单次调用的token限制
解决思路
分层记忆策略
动态调整
监控和告警
成本控制
核心问题
预算不可预测
解决思路
缓存 + 限流 + 成本估算
检索质量
核心问题
找到真正相关的内容
解决思路
多重检索 + 重排序
listeners 监听器
Spring AOP中的环绕通知与异常通知
多模态
输入
也可以给HTTP地址
本地录入
可以在项目resources下保存
图片转码:通过Base64编码将图片转化为字符串
结合ImageContent和TextContent一起发送到模型进行处理
输出
是给的HTTPS地址
接口
持久化
ChatMemoryStore
淘汰策略
MessageWindowChatMemory
TokenWindowChatMemory
RAG
关键类
EmbeddingStorelngestor 组织结构分析
DocumentTransformer documentTransformer;// 文档转换
Documentsplitter documentsplitter;// 文档分割
textsegmentTransformer textSegmentTransformer;// 转换单个文本段落(例如,用于标准化或清理)
EmbeddingModel embeddingModel;// 为文本段落向量化
Embeddingstore<TextSegment>gmbeddingstors; // 存储生成的嵌入向量及其对应的文本段落
Document Loader (文档加载器)
ClassPathDocumentLoader
FileSystemDocumentLoader: 从文件系统加载文档
UrlDocumentLoader: 从 URL 加载文档
AmazonS3DocumentLoader: 从 Amazon S3 加载文档
AzureBlobStorageDocumentLoader: 从 Azure Blob 存储加载文档
GitHubDocumentLoader: 从 GitHub 仓库加载文档
TencentCosDocumentLoader: 从腾讯云 COS 加载文档
Document Parser (文档解析器)
ApachePdfBoxDocumentParser
可以解析 PDF 文件
ApacheTikaDocumentParser
可以自动检测和解析几乎所有现有的文件格式
Document Transformer (文档转换器)
DocumentTransformer 用于对文档执行各种转换,如清理、过滤、增强或总结。
Document Splitter (文档拆分器)
DocumentByParagraphSplitter: 按段落拆分
DocumentBySentenceSplitter: 按句子拆分
DocumentByWordSplitter: 按单词拆分
DocumentByCharacterSplitter: 按字符拆分
DocumentByRegexSplitter: 按正则表达式拆分
MCP
关键类
StdioMcpTransport
HttpMcpTransport
技术架构
纯Prompt模式
FunctionCalling
RAG
Fine-tuning
交互协议
MCP
Agent与 Tools(工具)的交互
Agent 需要调用外部工具和AP1、访问数据库、执行代码等
为了更方便操作外部数据源和工具
调用数据库
调用邮件
网络搜索
文件系统
调用股票,天气的实时数据
应用举例
开发部署
开发者通过自然语言指令,“部署新版本到测试环境”触发 MCP 链式调用 GitLab API(代码合并)、Jenkins APl(构建镜像)、Slack API(通知团队)
SQL查询
开发者通过自然语言输入,比如“查询某集团部门上个季度销售额”,就能查询出数据库的数据,并结合大模型进行回答,不再需要编写 SQL,MCP 自动转换为精准 SQL语句并执行。
环境
uvx
Python
npx
Typescript
MCP的通信机制
stdio(标准输入输出)
主要用在本地服务上,操作你本地的软件或者本地的文件。比如 Blender 这种就只能用 Stdio 因为它没有在线服务。
MCP默认通信方式
优点
这种方式适用于客户端和服务器在同一台机器上运行的场景,简单。
stdio模式无需外部网络依赖,通信速度快,适合快速响应的本地应用。
可靠性高,且易于调试
缺点
Stdio 的配置比较复杂,我们需要做些准备工作,你需要提前安装需要的命令行工具
stdio模式为单进程通信,无法并行处理多个客户端请求,同时由于进程资源开销较大,不适合在本地运行大量服务。(限制了其在更复杂分布式场景中的使用)
SSE(Server-Sent Events)
主要用在远程通信服务上,这个服务本身就有在线的 API,比如访问你的谷歌邮件,天气情况等。
场景
SSE方式适用于客户端和服务器位于不同物理位置的场景。
适用于实时数据更新、消息推送、轻量级监控和实时日志流等场景
对于分布式或远程部署的场景,基于 HTTP 和 SSE 的传输方式则更为合适,
优点
配置方式非常简单,基本上就一个链接就行,直接复制他的链接填上就行
角色
MCP主机(MCP Hosts)
AI应用程序
MCP客户端(MCP Clients)
MCP服务器(MCP Servers)
本地资源(Local Resources)
远程资源(Remote Resources)
平台
阿里云百炼
MCP.so
Glama
A2A
Agent to Agent Protocol
Agent与 Agent(其他智能体或用户)的交互
Agent 需要理解其他 Agent 的意图、协同完成任务、与用户进行自然的对话。
智能体与智能体之间的通信
模型压缩
让模型更小,以便节省成本和方便个人使用
压缩方法
量化
将模型中的浮点数,用更低精度表示,减少显存和计算
蒸馏
用参数量较大的大模型指导参数量较小的小模型
剪枝
删除模型中不重要的神经元,让模型更稀疏,以提高速度
用更低成本改善微调方式的方法
LoRA
Low-Rank Adaptation
其核心思想是在预训练模型的基础上,通过引入低秩矩阵来减少微调所需的参数量,从而提高训练效率并避免过拟合
思维链
从推理能力方向,增强模型能力的方式
套壳
封装现有大模型接口并对外提供服务
NLP
和文字相关的
自然语言处理
CV
和图片相关的
计算机视觉
闭源
Midjourney
开源
Stable Diffusion
绘画工作流
ComfuUI
和语音相关的
文本转语音
TTS
语音转文字
ASR
和视频相关的
AI视频生成应用
Sora
Kling
Dreamina
即梦
数字人应用
PyTorch
深度学习框架
vLLM
提升大语言模型推理速度的推理引擎
硬件
GPU
图像处理单元
专门针对人工智能的处理器
专门用于大规模神经网络训练与推理
TPU
Tensor Processing Unit
专门用于终端设备推理
NPU
Neural Processing Unit
AI加速芯片
Scaling Law
缩放定律
“Scaling”这个词在大模型语境下,直译是“缩放”,但实际指的是“规模化扩展”。
关于“大力出奇迹”的科学规律,但它精确地告诉了你“多大力能出多大奇迹”。
核心思想
模型越大(参数越多)、训练数据越多、投入的计算资源(算力)越多,模型的能力(比如回答问题的准确性、生成文本的流畅度)就会以可预测的方式变得越好。
核心资源
模型规模 (Model Scale)
数据规模 (Data Scale)
计算规模 (Compute Scale)
可解释性AI
局部可解释性
全局可解释性
反卷积
链式法则
激活函数
Activation Functions
应用于加权和的函数
将所有 输入与权重的乘积 相加,整合为一个单一数值;此时加权和很可能远超0到1这个范围,所以引入了激活函数进行缩放
类型
常用
线性激活函数
Liner Function
函数表达式
f(x) = x
表现特性
直接输出输入值本身
导数形式
f'(x) = 1
恒定为1
应用场景
在人工神经网络中,基本不用,因为不会对数据产生任何变化
通常在设计神经网络时,我们希望在层与层之间引入一定非线性
使用要点
非线性激活函数
ReLU
Rectified Linear Unit
修正线性单元
函数表达式
f(x) = max(0, x)
导数形式
f'(x) = 1 if x > 0 else 0
表现特性
在原点x等于0处,不可微分
应用场景
常用于在层间引入非线性
可以得到稀疏模型
因为那些输入为负的神经元会被置为0,从而让模型变得稀疏
使用要点
缺点
一旦某个神经元被"杀死",激活为0,它就不再能有效影响网络的参数。因为其梯度为0,在反向传播和链式法则中,始终乘出来都是0。
Leaky ReLU(带泄漏的ReLU)
它是ReLU的改进版或者变体
不同于ReLU在负区间直接置0,Leaky ReLU在负输入时采用一条带有小斜率的直线αx,其中α为斜率,取值范围为0~1之间。
函数表达式
f(x) = max(αx, x) (其中α是一个很小的常数,如0.01)
导数形式
f'(x) = 1 if x > 0 else α
表现特性
在原点x等于0处,不可微分
应用场景
使用要点
Sigmoid(S型函数)
函数表达式
f(x) = 1 / (1 + e^(-x))
导数形式
f'(x) = f(x) * (1 - f(x))
表现特性
阶跃函数的平滑版本,输出范围在0~1之间。
连续导数
在x=0处达到最大值,两端趋近于0
核心缺陷
梯度消失
当输入值的绝对值很大或者很小时,Sigmoid会饱和,梯度会接近于0,导致权重更新缓慢甚至停滞。
应用场景
现在常用于或者几乎只用于二分类问题的输出层
LSTM/GRU等循环神经网络中的门控函数
使用要点
Tanh(双曲正切函数)
函数表达式
f(x) = (e^x - e^(-x)) / (e^x + e^(-x))
导数形式
f'(x) = 1 - (f(x))^2
表现特性
斜率比Sigmoid更加陡峭
一定程度上缓解了梯度消失的问题
应用场景
有时被用作隐藏层激活函数,但是使用频率不如ReLU系列高
在RNN(如LSTM、GRU)中应用
使用要点
Softplus(软加函数)
ReLU 的平滑(Smooth)版本
函数表达式
f(x) = ln(1 + e^x)
导数形式
f'(x) = 1 / (1 + e^(-x)) = σ(x)
即 Sigmoid 函数
表现特性
平滑且可微:这是它最核心的特性。
应用场景
使用要点
Exponential Linear Unit (ELU)
函数表达式
导数形式
表现特性
应用场景
使用要点
ReLU
函数表达式
导数形式
表现特性
应用场景
使用要点
ReLU
函数表达式
导数形式
表现特性
应用场景
使用要点
高级
数学基础
导数
导数描述了一个函数在某一点处的“瞬时变化率”
想象一下,你正在开车,车速表上显示的“瞬时速度”(比如 60 km/h),其实就是你行驶距离关于时间的导数。它告诉你在“那一刻”,你的位置变化得有多快。
数学定义上
导数是函数值的增量与自变量增量的比值的极限
几何意义
函数曲线在某一点处的切线的斜率
为什么需要导数?
因为我们需要知道“下山”的方向。导数(或梯度)精确地告诉了我们这个方向。如果没有导数,我们就像在黑暗中摸索,不知道往哪走才能降低损失;有了导数,我们就有了一个明确的“指南针”。
偏导数
梯度
幻觉
类型
比较低端的语义理解错误/偏差
常出现在小参数量的模型上。随着模型参数量的提升,这种机器幻觉会很快消除(有人总希望把零点几B的模型部署在边缘设备上,并妄想着拥有比肩线上模型几百B参数同样的性能。。。
事实性错误
最常见的类型,如编造不存在的学术论文、法律条文、小说剧情/人物张冠李戴……在行业应用上是最棘手的问题,但也是最容易发现、最容易解决的问题——rag知识库、联网搜索、长上下文提示词工程,都能解决
数学计算幻觉
大语言模型的token化处理抹销了数字本身拥有的数学概念而成为语义信息,到此大语言模型天生不擅长解决数学问题。
在没有外部工具可调用的情况下,大模型数学计算的幻觉/错误率相当高。除非没有其他方案可选,一般在遇到大量需要数学处理的内容往往会选用其他技术路线绕过
长远规划幻觉
大模型基于token的回答,只能预测下一个字。而长远规划往往需要反复在多个时间线上来回徘徊,不断推翻原有的方案,这是大模型所不具备的
可能有点偏激,部分带有深入思考的模型现在已经展现部分能力,也可以通过手搓智能体的方式让模型借助外部工具,循环生成几轮……但实际效果和人类比起来依然不尽人意
智力题
有两个房间,一间房里有三盏灯,另一间房有控制着三盏灯的三个开关,这两个房间是 分割开的,从一间里不能看到另一间的情况.现在要求受训者分别进这两房间一次,然后判断出这三盏灯分别是由哪个开关控制的
1.先走进有开关的房间,将三个开关编号为a b c。
2.将开关a 打开5分钟,然后关闭,然后打开b
3.然后走到另一个房间,即可辨别出正亮着的灯是由b 开关控制的。再用手摸另两个灯泡 ,发热的是由开关a 控制的,另一个就一定是开关c了。
2.将开关a 打开5分钟,然后关闭,然后打开b
3.然后走到另一个房间,即可辨别出正亮着的灯是由b 开关控制的。再用手摸另两个灯泡 ,发热的是由开关a 控制的,另一个就一定是开关c了。
0 条评论
下一页