-
Notifications
You must be signed in to change notification settings - Fork 1
/
201906.txt
516 lines (403 loc) · 42.6 KB
/
201906.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
Spring默认的HandlerMapping是BeanNameUrlHandlerMapping和RequetMappingHandlerMapping,在resouces目录的DispatcherServlet.properties里的org.springframework.web.servlet.HandlerMapping的key进行记录。
Spring默认的HandlerAdapter是HttpRequestHandlerAdapter,SimpleControllerHandlerAdapter,AnnotationMethodHandlerAdapter
Spring默认的HandlerExceptionResolver是AnnotationMethodHandlerExceptionResolver,ResponseStatusExceptionResolver,DefaultHandlerExceptionResolver
Spring默认的ViewResolver是InternalResouceViewResolver
@ExceptionHandler
按照《Unix网络编程》的划分,IO模型可以分为:阻塞IO、非阻塞IO、IO复用、信号驱动IO和异步IO,按照POSIX标准来划分只分为两类:同步IO和异步IO。如何区分呢?首先一个IO操作其实分成了两个步骤:发起IO请求和实际的IO操作,同步IO和异步IO的区别就在于第二个步骤是否阻塞,如果实际的IO读写阻塞请求进程,那么就是同步IO,因此阻塞IO、非阻塞IO、IO服用、信号驱动IO都是同步IO,如果不阻塞,而是操作系统帮你做完IO操作再将结果返回给你,那么就是异步IO。阻塞IO和非阻塞IO的区别在于第一步,发起IO请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO。
Java2.0 引入了异步IO,包括文件和网络。为了支持AIO引入的新的类和接口:
java.nio.channels.AsynchronousChannel: 标识一个channel支持异步操作
java.nio.channels.AsynchronousServerSocketChannel: ServerSocket的aio版本,创建TCP服务端,绑定地址,监听端口等
java.nio.channels.AsynchronousSocketChannel: 面向流的异步socket channel,表示一个链接
java.nio.channels.AsynchronousChannelGroup: 异步Channel的分组管理,目的是为了资源共享。一个AsynchronousChannelGroup绑定一个线程池,这个线程池执行两个任务:处理IO事件和派发CompletionHandler。AsynchronousServerSocketChannel创建的时候可以传入一个AsynchronousChannelGroup,那么通过AsynchronousServerSocketChannel创建的AsynchronousSocketChannel将同属于一个组,共享资源。即线程池
java.nio.channels.CompletionHandler:异步IO操作结果的回调接口,用于定义在IO操作完成后所做的回调工作。AIO允许两种方式来处理异步操作的结果:返回的Future模式或者注册CompletionHandler,推荐使用CompletionHandler。这些CompletionHandler的调用是由AsynchronousChannelGroup的线程池派发的。显然线程池的大小是性能的关键因素。AsynchronousChannelGroup有三个静态方法来创建:
public static AsynchronousChannelGroup withFixedThreadPool(int nThreads, ThreadFactory threadFactory) throws IOException;
public static AsynchronousChannelGroup withCachedThreadPool(ExecutorService executor, int initialSize);
public static AsynchronousChannelGroup withThreadPool(ExecutorService executor) throws IOException;
集合类初始化时,尽量指定大小。
在MIME格式的电子邮件中,base64可以用来将binary的字节序列数据编码成ASCII字符序列构成的文本。使用时,在传输编码方式中指定base64。使用的字符包括大小写字母各26个,加上10个数字,和加号“+”,斜杠“/”,一共64个字符,等号“=”用来作为后缀用途。
编码后的数据比原始数据略长,为原来的4/3。在电子邮件中,根据RFC822规定,每76个字符,还需要加上一个回车换行。可以估算编码后数据长度大约为原长的135.1%。
转换的时候,将三个byte的数据,先后放入一个24bit的缓冲区中,先来的byte占高位。数据不足3byte的话,于缓冲区中剩下的Bit用0补足。然后,每次取出6个bit,按照其值选择ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/中的字符作为编码后的输出。不断进行,直到全部输入数据转换完成。
如果最后剩下两个输入数据,在编码结果后加1个“=”;如果最后剩下一个输入数据,编码结果后加2个“=”;如果没有剩下任何数据,就什么都不要加,这样才可以保证资料还原的正确性。
线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。 如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。另外,jstack工具还可以附属到正在运行的java程序中,看到当时运行的java程序的java stack和native stack的信息, 如果现在运行的java程序呈现hung的状态,jstack是非常有用的。
So,jstack命令主要用来查看Java线程的调用堆栈的,可以用来分析线程问题(如死锁)。
频繁GC问题或内存溢出问题
一、使用jps查看线程ID
二、使用jstat -gc 3331 250 20 查看gc情况,一般比较关注PERM区的情况,查看GC的增长情况。
三、使用jstat -gccause:额外输出上次GC原因
四、使用jmap -dump:format=b,file=heapDump 3331生成堆转储文件
五、使用jhat或者可视化工具(Eclipse Memory Analyzer 、IBM HeapAnalyzer)分析堆情况。
六、结合代码解决内存溢出或泄露问题。
死锁问题
一、使用jps查看线程ID
二、使用jstack 3331:查看线程情况
Guava是Google开源项目,包含了若干被Google的Java项目广泛依赖的核心库,其中的RateLimit提供了令牌桶算法实现:平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)实现。
对于Nginx接入层限流可以使用Nginx自带了两个模块:
1. 连接数限流模块:ngx_http_limit_conn_module
2. 漏桶算法实现的请求限流模块:ngx_http_limit_req_module
ApplicationListener, ApplicationContextInitializer
SpringFactoriesLoader里的META-INF/spring.factories
类从被加载到虚拟机内存中开始,到卸载出内存为止。它的整个生命周期包括:加载Loading, 验证Verification,准备Preparation, 解析Resolution,初始化Initialization,使用Using和卸载Unloading7个阶段。其中准备、验证、解析3个部分统称为链接Linking。
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。
索引的一些优化建议:
1. 索引并不是越多越好,要根据查询有针对性的创建,考虑在where和order by涉及的字段建立索引,可根据explain查看是否用了索引还是全表扫描;
2. 应尽量避免在where子句中对字段进行null值判断,否则将导致引擎放弃使用索引而进行全表扫描;
3. 值分布很稀少的字段不适合建索引,例如性别这种没有区分度的字段;
4. 字符字段只建前缀索引;
5. 字符字段最好不要做主键;
6. 不用外键,由程序保证外键相关的约束;
7. 尽量不用union,由程序保证约束;
8. 使用联合索引顺序和查询条件保持一致,同时删除不必要的单列索引。
这里需要注意的是:与SQL标准不同的地方在于InnoDB存储引擎中repeatable-read可重复读事务隔离级别下使用的是Next-Key Lock锁算法,因此可以避免幻读的产生,这与其他数据库系统(SQL Server)是不同的。所以说InnoDB存储引擎的默认支持的隔离级别是Repeatable-read,已经可以完全保证事务的隔离性要求,即达到了SQL标准的serializable隔离级别。
因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别read-committed,但是最新版本的InnoDB存储引擎默认使用repeatable-read并不会有任何性能损失。
InnoDB存储引擎在分布式事务的情况下,一般会用到serializable隔离级别。
G1是为多处理器和大内存的服务器而设计的,它根据运行JVM过程中构建的停顿预测模型(Pause Prediction Model)计算出来的历史数据来预测本次收集需要选择的Region数量,然后尽可能满足GC的停顿时间,G1期望能让JVM的GC成为简单的事情。G1旨在延迟性和吞吐量之间取得最佳的平衡,它尝试解决有如下问题的JAVA应用:
1. 堆大小达到几十个G甚至更大,超过50%的堆空间都是存活对象;
2. 对象分配和晋升的速度随着时间的推移有很大的影响;
3. 堆上严重的碎片化问题;
4. 可预测的停顿时间,避免长时间的停顿。
程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
索引优化:小心隐式类型转换
明确知道只会返回一条记录,可以加limit 1。这样可以让MySQL停止游标移动,提高查询效率。
对文本建立前缀索引。
选择足够长的前缀保证较高的区分度,同时又不能太长(以便节约空间)。
需要注意的一点是:前缀索引不能使用覆盖索引,因为从索引中获取不到完整的数据,还得回表查询。
建立索引的列不为NULL:只要列中包含有NULL值都将不会被包含在索引中,复合索引中只要有一列含有NULL值,那么这一列对于此复合索引就是无效的。
因此,在数据库设计时,除非有很特别的原因使用NULL值,不然尽量不要让字段的默认值为NULL。
先每隔1秒重试了5次,再用"指数规避"的时间间隔重试,2S,4S,8S,16S,32S,最后结束。
生存时间TTL:IP报文所允许通过的路由器的最大数量。每经过一个路由器,TTL减一,当为0时,路由器将该数据丢弃。TTL字段是由发送端初始设置一个8bit字段。推荐的初始值由分配数字RFC指定。发送ICMP回显应答时经常把TTL设为255.TTL可以防止数据报陷入路由循环。
TCP中的6bit TCP标志位:从左到右依次是紧急URG,确认ACK,推送PSH,复位RST,同步SYN,终止FIN。ack有效,同时psh有效。
TCP中的紧急指针。仅在URG=1时才有意义,它指出本报文段中的紧急数据的字节数。当URG=1时,发送方TCP就把紧急数据插入到本报文段的最前面,而在紧急数据后面的数据仍是普通数据。
标识:同一个数据报的唯一标识。当IP数据报被拆分时,会复制到每一个数据中。
HTTP通过文档过期机制和服务器再验证机制保持已缓存数据和服务器间的数据充分一致。
文档过期通过:Cache-Control:max-age, Expires头部字段来表示缓存的有效期。
If-Modified-Since:<date>
If-None-Match:<tags>
通常,服务器需要先生成数据,再进行传输,这时,可以计算数据的长度,并将其编码到Content-Length中。但是,有时,内容是动态生成的,服务器希望在数据生成之前就开始传输,这时,是没有办法知道数据大小的。这种情况下,就要用到传输编码来标注数据的结束的。
HTTP协议中通过如下两个首部来描述和控制传输编码:
1. Transfer-Encoding: 发送方告知接收方,我方已经进行了何种传输编码;典型值:chuncked分块编码;
2. TE:请求方告知服务器可以用哪种传输编码;trailers, chuncked接受分块编码,并且愿意接受这报文结尾上的拖挂。
HTTP有keep-alive机制,目的是可以在一个TCP连接上传输多个HTTP事务,以此提高通信效率。底层的TCP其实也有keep-alive机制,它是为了探测TCP连接的活跃性。TCP层的keep-alive可以在任何一方设置,可以是一端设置、两端同时设置或者两端都没有设置。新建socket的时候需要设置,从而使得协议栈调用相关函数tcpsetkeepalive,来激活连接的keep-alive属性。
当网络两端建立了TCP连接之后,闲置(双方没有任何数据流发送往来)时间超过tcp_keepalive_time后,服务器内核就会尝试向客户端发送侦测包,来判断TCP连接状况(有可能客户端崩溃、强制关闭应用、主机不可达)。如果没有收到对方的回答(ack包),则会在tcp_keepalive_interval后再次尝试发送侦测包,直到收到对方的ack,如果一直没有收到对方的ack,一共会尝试tcp_keepalive_probes次,每次的间隔时间在这里分别是15s, 30s, 45s, 60s, 75s。如果尝试 tcp_keepalive_probes次后,依然没有收到对方的ack包,则会丢弃该TCP连接。TCP连接默认闲置时间是2小时,一般设置为30分钟足够了。
TCP的结构:
源端口和目的端口在TCP层确定双方进程,序列号表示的是报文段数据中的第一个字节号,ACK表示确认号,该确认号的发送方期待接收的下一个序列号,即最后被成功接收的数据字节序列号加1,这个字段只有在ACK位被启用的时候才有效。
当新建一个连接时,从客户端发送到服务端的第一个报文段的SYN位被启用,这称为SYN报文段,这时序列号字段包含了本次连接的这个方向上要使用的第一个序列号,即初始序列号ISN,之后发送的数据是ISN加1,因此SYN位字段会消耗一个序列号,这意味着使用重传进行可靠传输。而不消耗序列号的ACK则不是。
头部长度以32位为单位,即4个字节为单位,它只有4位,最大为15,因此头部最大长度为60字节,而起最小为5,也就是头部最小为20字节(可变选项为空)。
ACK-确认,使得确认号有效。RST-重置连接(经常看到的reset by peer)就是此字段。SYN-用于初始化一个连接的序列号。FIN-该报文段的发送方已经结束向对方发送数据。
当一个连接被建立或被终止时,交换的报文段只包含TCP头部,而没有数据。
三次握手的作用就是:双方都能明确自己和对方的收、发能力是正常的。
第一、二握手后,服务端并不知道客户端的接收能力以及自己的发送能力是否正常。而在第三次握手时,服务端收到了客户端对第二次握手的回应。从服务端的角度,我在第二次握手的响应数据发送出去了,客户端接收到了。所以,我的发送能力是正常的。而客户端的接收能力也是正常的。
而从上面的过程可以看到,最少是需要三次握手过程的。两次达不到让双方都得出自己、对方的接收、发送能力都正常的结论。其实每次收到网络包的一方至少是可以得到:对方的发送、我方的接收是正常的。而每一步都是有关联的,下一次的“响应”是由于第一次的“请求”触发,因此每次握手其实是可以得到额外的结论的。比如第三次握手时,服务端收到数据包,表明看服务端只能得到客户端的发送能力、服务端的接收能力是正常的,但是结合第二次,说明服务端在第二次发送的响应包,客户端接收到了,并且作出了响应,从而得到额外的结论:客户端的接收、服务端的发送是正常的。
其实三次握手的目的并不只是让通信双方都了解到一个连接正在建立,还在于利用数据包的选项来传输特殊的信息,交换初始序列号ISN。
三次握手是指发送了3个报文段,4次握手是指发送了4个报文段。注意,SYN和FIN段都是会利用重传进行可靠传输的。
为什么建立连接是三次握手,而关闭连接却是四次挥手:
这是因为服务端在listen状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据但是还能接收数据,是否现在关闭发送数据通道,需要上层应用来决定,因此,ACK和FIN一般都会分开发送。
当外部连接请求到来时,TCP模块会首先查看max_syn_backlog,如果处于SYN_RCVD状态的连接数目超过这个阈值,进入的连接会被拒绝。根据tcp_abort_overflow字段来决定是直接丢弃,还是直接reset。
从服务端来看,三次握手,第一步server接收到client的syn后,把相关信息放到半连接队列中,同时回复syn+ack给client。第三步当收到客户端的ack,将连接加入到全连接队列。
一般,全连接队列比较小,会先满,此时半连接还没满。如果这时收到syn报文,则会进入半连接队列,没有问题。但是如果手动了三次握手中的第3部(ACK),则会根据tcp_abort_on_overflow字段来决定是直接丢弃,还是直接reset。此时,客户端发送了ACK,那么客户端认为三次握手完成,它认为服务端已经准备好了接收数据的准备。但此时服务端可能因为全连接队列满了而无法将连接放入,会重新发送第二步的syn+ack,如果这时有数据到来,服务器TCP模块会将数据存入队列。一段时间后,client端没收到回复,超时,连接异常,client会主动关闭连接。
缓存穿透是指查询一个一定不存在的数据。由于缓存不命中,并且出于容错考虑,如果从数据库查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,失去了缓存的意义。
缓存与数据库双写一致性问题:
对于操作缓存有两种方案:更新缓存或者删除缓存。
一般我们都是采取删除缓存缓存策略的,原因如下:
1. 高并发环境下,无论是先操作数据库还是后操作数据库而言,如果加上更新缓存,那就更加容易导致数据库与缓存数据不一致问题。(删除缓存直接和简单很多)
2. 如果每次更新了数据库,都要更新缓存【这里指的是频繁更新的场景,这会耗费一定的性能】,倒不如直接删除掉。等再次读取时,缓存里没有,那我到数据库找,在数据库找到再写到缓存里边(体现懒加载)
基于这两点,对于缓存在更新时而言,都是建议执行删除操作!
--------------------------------------------------------------
先更新数据库,再删除缓存方案:
正常的情况是这样的:
1. 先操作数据库,成功;
2. 再删除缓存,也成功;
如果原子性被破坏了:
第一步成功(操作数据库),第二步失败(删除缓存),会导致数据库里是新数据,而缓存里是旧数据。
如果第一步(操作数据库)就失败了,我们可以直接返回错误(Exception),不会出现数据不一致。
如果在高并发的场景下,出现数据库与缓存数据不一致的概率特别低,也不是没有:
缓存刚好失效
线程A查询数据库,得一个旧值
线程B将新值写入数据库
线程B删除缓存
线程A将查到的旧值写入缓存
要达成上述情况,还是说一句概率特别低:
因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。
对于这种策略,其实是一种设计模式:Cache Aside Pattern
删除缓存失败的解决思路:
- 将需要删除的key发送到消息队列中;
- 自己消费消息,获得需要删除的key;
- 不断重试删除操作,直到成功;
--------------------------------------------------------------
先删除缓存,再更新数据库:
正常情况是这样的:
- 先删除缓存,成功;
- 再更新数据库,也成功;
如果原子性被破坏了:
- 第一步删除缓存成功,第二步更新数据库失败,数据库和缓存的数据还是一致的;
- 如果第一步缓存删除就失败了,直接返回错误Exception,数据库和缓存的数据还是一致的;
看起来很美好但是在并发场景下,还是有问题:
- 线程A删除了缓存
- 线程B查询,发现缓存已经不存在;去数据库查询到旧的值;
- 线程B将旧值写入缓存;
- 线程A将新值写入数据库。
所以也会导致数据库和缓存不一致的问题。
并发下解决数据库与缓存不一致的思路:
-将删除缓存、修改数据库、读取缓存等的操作积压在队列里面,实现串行化。保证每步发生的动作是按顺序进行的。
--------------------------------------------------------------
对比两种策略的优缺点:
1. 先删除缓存,再更新数据库:在高并发表现不如意,在原子性被破坏时表现优异;
2. 先更新数据库,再删除缓存(Cache Aside Pattern设计模式):在高并发表现优异,在原子性被破坏时表现不如意。
其他保障数据一致性的方案:使用databus或者阿里的canal监听binlog进行更新。
所以系统A要判定一个核心数据是否写成功,如果系统B一共部署了3台机器的话,那么系统A必须在指定时间内收到2台系统B所在机器返回的写成功的响应。
此时系统A才能认为这条数据对系统B是写成功了。这就是所谓的Quorum机制。
----------------------------------------------------
如何将Redis优化到极致:
需要优化下列5个命令:
命令1:hset mall:sale:freq:ctrl:860000000000001 599055114591 1(hash结构,field表示购买的商品ID,value表示购买次数)
命令2:hset mall:sale:freq:ctrl:860000000000001 599055114592 2
命令3:expire mall:sale:freq:ctrl:860000000000001 3127(设置过期时间)
命令4:set mall:total:freq:ctrl:860000000000001 3
命令5:expire mall:total:freq:ctrl:860000000000001 3127(设置过期时间)
第一次优化:利用hmset命令将两条hset命令合二为一。
hmset mall:sale:freq:ctrl:860000000000001 599055114591 1 599055114592 2
expire mall:sale:freq:ctrl:860000000000001 3127
set mall:total:freq:ctrl:860000000000001 3
expire mall:total:freq:ctrl:860000000000001 3127
第二次优化:将set和expire命令合二为一
hmset mall:sale:freq:ctrl:860000000000001 599055114591 1 599055114592 2
expire mall:sale:freq:ctrl:860000000000001 3127
setex mall:total:freq:ctrl:860000000000001 3 3127
第三次优化:需要借助pipeline。不过需要注意在Redis Cluster中使用pipeline必须满足pipeline打包的所有命令key在Redis Cluster的同一个Slot上。如果打包命令的key不在同一个slot上,就会报错。所以需要分两批打包:
-- 这两条命令的key都是一样的,肯定在同一个Slot上
pipeline(
hmset mall:sale:freq:ctrl:860000000000001 599055114591 1 599055114592 2
expire mall:sale:freq:ctrl:860000000000001 3127
)
--上面那个key和下面这个key不在同一个slot(不一定)
setex mall:total:freq:ctrl:860000000000001 3 3127
第四次优化:这次优化利用了高级特性:hashtag。Redis Cluster总计有16*1024=16384个Slot。那么执行一条Redis命令时,计算Slot的公式如下:
Slot = CRC16(key)%16384。是否可以让计算slot只和key的一部分相关呢,可以利用hashtag特性,使用${}可以把参与slot计算的key的一部分标注出来。
优化后的命令如下:
pipeline(
hmset mall:sale:freq:ctrl:{860000000000001} 599055114591 1 599055114592 2
expire mall:sale:freq:ctrl:{860000000000001} 3127
setex mall:total:freq:ctrl:{860000000000001} 3 3127
)
----------------------------------------------------
多线程的作用:
1. 发挥多核CPU的优势;
2. 防止阻塞;
3. 利于建模;
获取堆栈信息:jstack <pid>;对于Linux系统,也可以使用kill -3 <pid>
一个线程出现了运行时异常会怎么样:
如果这个异常没有被捕获的话,这个线程就停止执行了。另外一个重要点:如果这个线程持有某个对象的监视器,那么这个对象监视器会被立即释放。
sleep和wait方法有什么区别:sleep方法和wait方法都可以用来放弃CPU执行权,不同点在于如果线程持有某个对象的监视器,sleep方法不会放弃这个对象的监视器,wait方法会放弃这个对象的监视器。
wait方法和notify/notifyAll方法在放弃对象监视器时有什么区别:
wait方法和notify/notifyAll方法在放弃对象监视器的时候区别在于:wait方法立即释放对象监视器,notify/notifyAll方法则会等待线程剩余代码执行完毕后才会放弃监视器。
如何判断线程持有某个对象的监视器锁? Thread的静态方法holdsLock(Object obj)可以判断当前线程是否持有某个对象的监视器锁。是个native方法。
如下:
public class Thread implements Runnable {
...
/*Returns true if and only if the current thread holds the monitor lock on the specified object.
This method is designed to allow a program to assert that the current thread already holds a specified lock:
assert Thread.holdsLock(obj);
*/
public static native boolean holdsLock(Object obj);
...
}
怎么唤醒一个阻塞的线程:
如果线程是因为调用了wait(), sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,无能无力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。
同步方法和同步块,哪个更好?同步块意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。一个原则:同步的范围越小越好。
另外,虽说同步的范围越少越好,但是在JVM中还是存在锁粗化的优化方法,这种方法就是把同步范围变大。这是有用的,比如StringBuffer,它是一个线程安全的类。最常用的append()方法是一个同步方法,写代码时会反复append(),这意味着反复的加锁->解锁,对性能不利,因为这意味着JVM虚拟机在这个线程上反复地在内核态和用户态之间进行切换,因为JVM会将多次append方法调用的代码进行一个锁粗化操作,将多次的append操作扩展到append方法的头尾,变成一个大的同步块,这样就减少了加锁->解锁的次数,有效地提升了代码执行的效率。
衡量一个哈希函数的好坏的重要指标就是发生碰撞的概率以及发生碰撞的解决方案。任何哈希函数基本都无法彻底避免碰撞,常见的解决碰撞的方法有以下几种:
1. 开放定址法;
2. 链地址法;
3. 再哈希法;
4. 建立公共溢出区。
为什么位运算可以实现取模运算呢?即:X%(2^n) = X & (2^n - 1)
为了对key的hashCode进行扰动计算,防止不同hashCode的高位不同但低位相同导致的hash冲突。简单来说,就是为了把高位的特征和低位的特征组合起来,降低哈希冲突的概率,也就是说,尽量做到任何一位的变化都能对最终得到的结果产生影响。
其实,使用位运算代替取模运算,除了性能之外,还有一个好处就是可以很好的解决负数问题。因为hashCode的结果是int,取值范围为:-2^31 ~2^31-1。这里面包含负数,对于一个负数取模有点麻烦。如果使用二进制的位运算,可以很好的避免这个问题。首先,不管hashCode的值是正数还是负数。length-1一定是正数,那么它的二进制第一位是0,这样两个数按位与运算之后,第一位一定是0,也就是得到的结果一定是正数。
实现大整数相乘的方案:
1. 竖列,把两整数按位依次相乘;复杂度:O(n^2)
2. 利用分治法,把两个大整数分成高位和低位两部分,转化成4个较小的乘积:
整数1 * 整数2
= (A * 10^(n/2) + B) * (C * 10^(n/2) + D)
= AC*10^n + AD*10^(n/2) + BC*10^(n/2) + BD
复杂度:O(n^2)
3. 上面方案2的优化:
整数1 * 整数2
= (A * 10^(n/2) + B) * (C * 10^(n/2) + D)
= AC*10^n + AD*10^(n/2) + BC*10^(n/2) + BD
= AC*10^n + ((A-B)(D-C) + AC + BD) * 10^(n/2) + BD
将原本的4次乘法3次加法,转化为3次乘法6次加法。
复杂度:O(n^1.59)
4. 快速傅里叶变换FFT;复杂度:O(NlogN)
Raft有效的解决了分布式一致性算法过于复杂及难于实现的问题。
Java中的CAS操作都是通过sun包下的Unsafe类实现,而Unsafe类中的方法都是native,由JVM实现。Linux的X86主要是通过cmpxchgl这个指令在CPU级别完成CAS操作的,但在多处理器下必须使用lock指令加锁来完成。
Java中提供了AtomicStampedReference/AtomicMarkableReference来处理会发生ABA问题的场景,主要是在对象中额外再增加一个标记来标识对象是否有过变更。
@SuppressWarnings("serial")
abstract class Striped64 extends Number {
@sun.misc.Contended
static final class Cell {
volatile long value;
Cell(long x) {value = x;}
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
}
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
private static final long valueOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> ak = Cell.class;
valueOffset = UNSAFE.objectFieldOffset
(ak.getDeclaredField("value"));
} catch (Exception e) {
throw new Error(e);
}
}
}
}
Callable与Runnable不同点:
1. Callable在任务结束时提供一个返回值,Runnable无法提供这个功能;
2. Callabel的call方法可以抛出异常,而Runnable的run方法不能抛出异常;
Spring解决Bean循环依赖的机制根据Spring框架定义的三级缓存来实现的,也就是说:三级缓存解决了Bean之间的循环依赖。
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implments SingletoBeanRegistry {
// Cache of singleton objects : bean name to bean instance
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
//Cache of singleton factories: bean name to ObjectFactory
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
// Cache of early singleton objects: bean name to bean instance
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
// Set of registered singletons, containing the bean name in registration order.
privte final Set<String> registeredSingletons = new LinkedHashSet<>(256);
// Names of beans that are currently in creation.
private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16));
...
}
Zuul 1.x基于Servlet,使用阻塞API,不支持任何长连接,如WebSockets,Spring Cloud Gateway使用非阻塞API,支持WebSockets,支持限流等新特性。
Spring Cloud Gateway中的相关概念:
Route(路由):这是网关的基本构建块。它有一个ID,一个目标URI,一组断言和一组过滤器定义。如果断言为真,则路由匹配;
Predicate(断言): 是Java8的Predicate。输入类型是一个ServerWebExchange。可以使用它来匹配来自HTTP请求的任何内容,例如headers或者参数;
Filter(过滤器):这是org.springframework.cloud.gateway.filter.GatewayFilter的实例,使用它修改请求和应答。
Spring Cloud Gateway features:
1. Built on Spring Framework 5, Project Reactor and Spring Boot 2.0;
2. Able to match routes on any request attribute.
3. Predicates and filters are specific to routes.
4. Hystrix Circuit Breaker integration.
5. Spring Cloud DiscoveryClient integration.
6. Easy to write Predicates and Filters.
7. Request Rate Limiting.
8. Path Rewriting.
Spring Cloud Gateway是使用Netty+Webflux实现,因此不需要引入Web模块。
Java的线程阻塞和唤醒是通过Unsafe类的park和unpark方法实现的,这两个方法在底层是使用操作系统提供的信号量机制来实现的。
线程从启动开始就会一直跑,除了操作系统的任务调度策略外,它只有在调用 park 的时候才会暂停运行。锁可以暂停线程的奥秘所在正是因为锁在底层调用了 park 方法。
public class Thread implements Runnable {
...
/**
The argument supplied to the current call to LockSupport.park.
Set by (private) LockSupport.setBlocker
Accessed using LockSupport.getBlocker
*/
volatile Object parkBlocker;
/* The object in which this thread is blocked in an interruptible I/O operation, if any.
The blocker's interrupt method should be invoked after setting this thread's interrupt status.
*/
private volatile Interruptible blocker;
private final Object blockerLock = new Object();
...
}
AbstractQueueSynchronizer类是一个抽象类,它是所有的锁队列管理器的父类,JDK中的各种形式的锁其内部的队列管理器都继承了这个类,它是Java并发世界的核心基石。比如ReentrantLock, ReadWriteLock, CountDownLatch, Semaphore, ThreadPoolExecutor内部的队列管理器都是它的子类。这个抽象类暴露了一些抽象方法,每一种锁都需要对这个管理器进行定制。
------------------------------------------
共享锁和排它锁
ReentrantLock的锁是排他锁,一个线程持有,其他线程都必须等待。而ReadWriteLock里面的读锁不是排他锁,它允许多线程同时持有读锁,这是共享锁。共享锁和排他锁是通过Node类里面的nextWaiter字段区分的。
------------------------------------------
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
...
static final class Node {
// Marker to indicate a node is waiting in shared mode
static final Node SHARED = new Node();
// Marker to indicate a node is waiting in exclusive mode
static final Node EXCLUSIVE = null;
...
/* Link to next node waiting on condition, or the special value SHARED.
Because condition queues are accessed only when holding in exclusive mode,
we just need a simple linked queue to hold nodes while they are waiting on conditions.
They are then transferred to the queue to re-acquire. And because conditions can only
be exclusive, we save a field by using special value to indicate shared mode.
*/
Node nextWaiter;
...
}
...
}
await()方法会一直阻塞在cond条件变量上直到被另外一个线程调用了cond.signal()或者cond.signalAll()方法后才会返回,await()阻塞时会自动释放当前线程持有的锁,await()被唤醒后会再次尝试持有锁(可能需要排队),拿到锁成功后await()方法才能成功返回。
阻塞在条件变量上的线程可以有多个,这些阻塞线程会被串联成一个条件等待队列。当 signalAll() 被调用时,会唤醒所有的阻塞线程,让所有的阻塞线程重新开始争抢锁。如果调用的是 signal() 只会唤醒队列头部的线程,这样可以避免「惊群问题」。
await() 方法必须立即释放锁,否则临界区状态就不能被其它线程修改,condition_is_true() 返回的结果也就不会改变。 这也是为什么条件变量必须由锁对象来创建,条件变量需要持有锁对象的引用这样才可以释放锁以及被 signal 唤醒后重新加锁。创建条件变量的锁必须是排他锁,如果是共享锁被 await() 方法释放了并不能保证临界区的状态可以被其它线程来修改,可以修改临界区状态的只能是排他锁。这也是为什么 ReadWriteLock.ReadLock 类的 newCondition 方法定义如下
条件等待队列:当多个线程await()在同一个条件变量上时,会形成一个条件等待队列。同一个锁可以创建多个条件变量,就会存在多个条件等待队列。这个队列和AQS的队列结构很接近,只不过它不是双向队列,而是单向队列。队列中的节点和AQS等待队列的节点是同一个类,但是节点指针不是prev和next,而是nextWaiter。
读写锁分为两个锁对象ReadLock和WriteLock,这两个锁对象共享同一个AQS。AQS的锁计数变量state将分为两个部分,前16bit为共享锁ReadLock计数,后16bit为互斥锁WriteLock计数。互斥锁记录的是当前写锁重入的次数,共享锁记录的是所有当前持有共享读锁的线程重入总次数。
读写锁同样也需要考虑公平锁和非公平锁。共享锁和互斥锁的公平锁策略和 ReentrantLock 一样,就是看看当前还有没有其它线程在排队,自己会乖乖排到队尾。非公平锁策略不一样,它会比较偏向于给写锁提供更多的机会。如果当前 AQS 队列里有任何读写请求的线程在排队,那么写锁可以直接去争抢,但是如果队头是写锁请求,那么读锁需要将机会让给写锁,去队尾排队。
毕竟读写锁适合读多写少的场合,对于偶尔出现一个写锁请求就应该得到更高的优先级去处理。
如果当前线程已经持有写锁,它可以继续加读锁,这是为了达成锁降级必须支持的逻辑。锁降级是指在持有写锁的情况下,再加读锁,再解除写锁。相比于先写解锁再加读锁而言,这样可以省去加锁二次排队的过程。因为锁降级的存在,锁计数中读写计数可以同时不为零。
All indexes other than the clustered index are known as secondary indexes. In InnoDB, each record in a secondary index contains the primary key columns for the row, as well as the columns specified for the secondary index. InnoDB uses this primary key value to search for the row in the clustered index. If the primary key is long, the secondary indexes use more space, so it is advantageous to have a short primary key.
ID作为主键时在特定的环境会存在一些问题,比如做DB主键的场景下,UUID就非常不适用:
1. MySQL官方有明确的建议主键要尽量越短越好,36个字符长度的UUID不符合要求;
2. 对MySQL索引不利:如果作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能。
类Snowflake方案的优缺点:
优点:
- 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
- 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
- 可以根据自身业务特性分配bit位,非常灵活。
缺点:
- 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务处于不可用状态。
MongoDB官方ObjectID可以算是和snowflake类似方法,通过"时间+机器码+pid+inc"共12个字节,通过4+3+2+3的方式最终标识成一个24字节长度的十六进制字符。
分布式数据库生成的缺点:
1. 系统水平扩展比较困难,比如定义好了步长和机器数之后,如果要添加机器怎么处理?假设现在只有一台机器发号是1,2,3,4,5(步长是1),这个时候需要扩容机器一台。可以这样做:把第二台机器的初始值设置得比第一台超过很多,比如14(假设在扩容时间之内第一台不可能发到14),同时设置步长为2,那么这台机器下发的号码都是14以后的偶数。然后摘掉第一台,把ID值保留为奇数,比如7,然后修改第一台的步长为2。让它符合我们定义的号段标准,对于这个例子来说就是让第一台以后只能产生奇数。扩容方案看起来复杂吗?貌似还好,现在想象一下如果我们线上有100台机器,这个时候要扩容该怎么做?简直是噩梦。所以系统水平扩展方案复杂难以实现。
2. ID没有了单调递增的特性,只能趋势递增,这个缺点对于一般业务需求不是很重要,可以容忍。
3. 数据库压力还是很大,每次获取ID都是读写一次数据库,只能靠堆机器来提高性能。
美团提出了两种改进方案:Leaf-segment和Leaf-snowflake方案。
Linux内核给每个进程都提供了一个独立的连续虚拟地址空间。
每个进程的虚拟地址分为内核空间和用户空间。但内核空间,其实关联的都是相同的物理内存。进程用户态只能访问用户空间内存;内核态可以访问内核空间内存。
内存映射就是将虚拟内存地址映射到物理内存地址,内核为每个进程都维护了一张页表,记录映射关系。
页表实际存储在CPU的内存管理单元MMU中,页表还有一个缓冲TLB(Translation Lookaside Buffer,转译后备缓冲器)
页的大小为4K,为了解决页过多,Linux提供了两种机制多级页表和大页(HugePage)。
多级页表就是把内存分成区块来管理,将原来的映射关系改成区块索引和区块内的偏移,这样可以大大减少页表的项数。
Linux使用四级页表管理内存页。
大页就是比普通页更大的内存块。
当访问虚拟地址在页表中找不到时,会产生缺页异常,进入内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。
内存性能指标:
1. 系统内存指标:
1.1 已用内存
1.2 剩余内存
1.3 可用内存
1.4 缺页异常:
1.4.1 主缺页异常
1.4.2 次缺页异常
1.5 缓存/缓冲区
1.5.1 使用量
1.5.2 命中率
1.6 Slabs
2. 进程内存指标:
2.1 虚拟内存(VSS)
2.2 常驻内存(RSS)
2.3 按比例分配共享内存后的物理内存(PSS)
2.4 独占内存(USS)
2.5 共享内存
2.6 Swap内存
2.7 缺页异常
2.7.1 主缺页异常
2.7.2 次缺页异常
3. swap:
3.1 已用空间
3.2 剩余空间
3.3 换入速度
3.4 换出速度
常用命令:free, top, vmstat, /proc/meminfo, cachestat, cachetop, sar, ps, pmap, memleak, pcstat, valgrind
进程内存分布:pmap
响应式宣言:
We want systems that are Responsive, Resilient, Elastic and Message Driven.
四个关键字:
1. Responsive:即时响应性,系统尽可能及时响应。
2. Resilient:回弹性,系统中出现故障时保持响应。通过复制、包含、隔离和委派来实现弹性。故障包含在每个组件中,使组件彼此隔离,从而确保系统的各个部分可以发生故障并可以恢复而不会损害整个系统。
3. Elastic: 弹性,系统中不断变化的工作负载下保持响应能力。
4. Message Driven消息驱动:响应式系统依靠异步消息传递在组件之间建立边界,以确保松耦合,隔离和位置透明。
Reactive Streams是一项倡议,旨在为具有无阻塞背压的异步流处理提供标准。
背压(Back Pressure):控制数据流量大小,数据流的订阅者可以和发布者协商自己可以处理的数据量。极端情况下,发布快速流量数据会压垮订阅者(例如,消耗内存和处理器)。背压可以让发布者匹配订阅者的处理能力。