[{"title":"java线上问题排查招式","url":"/2021/06/23/java线上问题排查1/","content":"\n线上故障主要会包括 cpu、磁盘、内存以及 网络 问题，而大多数故障可能会包含不止一个层面的问题，所以进行排查时候尽量四个方面依次排查一遍。\n<!--more-->\n同时例如 jstack 、jmap 等工具也是不囿于一个方面的问题的，基本上出问题就是 df、free、top 三连，然后依次 jstack、jmap 伺候，具体问题具体分析即可。\n## CPU \n\n一般来讲我们首先会排查cpu方面的问题。 cpu异常往往还是比较好定位的。原因包括业务逻辑问题(死循环)、频繁gc以及上下文切换过多。而最常见的往往是业务逻辑(或者框架逻辑)导致的，可以使用jstack来分析对应的堆栈情况。\n\n### 使用jstack分析cpu问题 \n\n我们先用`ps`命令找到对应进程的 pid(如果你有好几个目标进程，可以先用`top`看一下哪个占用比较高)。接着用`top -H -p pid`来找到cpu使用率比较高的一些线程\n\n[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HxsJyGad-1624412636454)(https://www.liangtengyu.com:9998/images/pic_1cbafec3.png)]\n\n然后将占用最高的pid转换为16进制`printf '%x\\n' pid`得到nid\n\n![pic_036e9491.png](https://img-blog.csdnimg.cn/img_convert/abdbf7985450e6c8633e7132ae8ad5e8.png)\n\n接着直接在jstack中找到相应的堆栈信息`jstack pid |grep 'nid' -C5 –color`\n\n![pic_f042c16d.png](https://img-blog.csdnimg.cn/img_convert/903badf9654387ffc22240ed057c45ed.png)\n\n可以看到我们已经找到了nid为0x42的堆栈信息，接着只要仔细分析一番即可。\n\n当然更常见的是我们对整个jstack文件进行分析，通常我们会比较关注WAITING和TIMED\\_WAITING的部分，BLOCKED就不用说了。我们可以使用命令`cat jstack.log | grep \"java.lang.Thread.State\" | sort -nr | uniq -c`来对jstack的状态有一个整体的把握，如果WAITING 之类的特别多，那么多半是有问题啦。\n\n![pic_aca0f408.png](https://img-blog.csdnimg.cn/img_convert/115ca10f2136cbc456227dbdb6e6976a.png)\n\n### 频繁gc \n\n当然我们还是会使用`jstack`来分析问题，但有时候我们可以先确定下gc是不是太频繁，使用`jstat -gc pid 1000`命令来对gc分代变化情况进行观察，1000表示采样间隔(ms)，S0C/S1C、S0U/S1U、EC/EU、OC/OU、MC/MU分别代表两个Survivor区、Eden区、老年代、元数据区的容量和使用量。YGC/YGT、FGC/FGCT、GCT则代表YoungGc、FullGc的耗时和次数以及总耗时。如果看到gc比较频繁，再针对gc方面做进一步分析。\n\n![pic_625dc8f2.png](https://img-blog.csdnimg.cn/img_convert/37ed99e1c3bf84ef42b40c11ee08debb.png)\n\n### 上下文切换 \n\n针对频繁上下文问题，我们可以使用`vmstat`命令来进行查看\n\n![pic_d7566043.png](https://img-blog.csdnimg.cn/img_convert/5282367f8e998c42cc5aa5d4137141ab.png)\n\ncs(context switch)一列则代表了上下文切换的次数。如果我们希望对特定的pid进行监控那么可以使用 `pidstat -w pid`命令，cswch和nvcswch表示自愿及非自愿切换。\n\n![pic_ce632354.png](https://img-blog.csdnimg.cn/img_convert/922f228dc2f0d8ef8b14350680da09f1.png)\n\n## 磁盘 \n\n磁盘问题和cpu一样是属于比较基础的。首先是磁盘空间方面，我们直接使用`df -hl`来查看文件系统状态\n\n![pic_451a90a1.png](https://img-blog.csdnimg.cn/img_convert/e3b672458b35b034e8bb799bec2ca446.png)\n\n更多时候，磁盘问题还是性能上的问题。我们可以通过iostat`iostat -d -k -x`来进行分析\n\n![pic_0ac6a54e.png](https://img-blog.csdnimg.cn/img_convert/739d40f7ee463fb388f099d98a86bcac.png)\n\n最后一列`%util`可以看到每块磁盘写入的程度，而`rrqpm/s`以及`wrqm/s`分别表示读写速度，一般就能帮助定位到具体哪块磁盘出现问题了。\n\n另外我们还需要知道是哪个进程在进行读写，一般来说开发自己心里有数，或者用`iotop`命令来进行定位文件读写的来源。\n\n![pic_48bcb059.png](https://img-blog.csdnimg.cn/img_convert/83d8741c54c9fd84c8423a1d5afe475b.png)不过这边拿到的是tid，我们要转换成pid，可以通过readlink命令来找到pid:`readlink -f /proc/*/task/tid/../..`。\n\n![pic_4bcbea3d.png](https://img-blog.csdnimg.cn/img_convert/335ddcdd5e3431f9c02b8dd2b676193d.png)找到pid之后就可以看这个进程具体的读写情况`cat /proc/pid/io`![pic_7f17d9c1.png](https://img-blog.csdnimg.cn/img_convert/80da7ee89e424d618199c5c8d6021698.png)我们还可以通过lsof命令来确定具体的文件读写情况`lsof -p pid`![pic_6b4842db.png](https://img-blog.csdnimg.cn/img_convert/8a149568cdc5338fd8528d63b3311865.png)\n\n## 内存 \n\n内存问题排查起来相对比CPU麻烦一些，场景也比较多。主要包括OOM、GC问题 和 堆外内存。一般来讲，我们会先用`free`命令先来检查一发内存的各种情况。![pic_18b01777.png](https://img-blog.csdnimg.cn/img_convert/5da5e16a3db4378736cf38842eca733d.png)\n\n### 堆内内存 \n\n内存问题大多还都是堆内内存问题。表象上主要分为OOM和StackOverflow。\n\n#### OOM \n\nJMV中的内存不足，OOM大致可以分为以下几种：\n\n```java\nException in thread \"main\" java.lang.OutOfMemoryError: unable to create new native thread\n```\n\n这个意思是没有足够的内存空间给线程分配java栈，基本上还是线程池代码写的有问题，比如说忘记shutdown，所以说应该首先从代码层面来寻找问题，使用`jstack`或者`jmap`。如果一切都正常，JVM方面可以通过指定`Xss`来减少单个thread stack的大小。另外也可以在系统层面，可以通过修改`/etc/security/limits.conf`nofile和nproc来增大os对线程的限制![pic_1a62a2b6.png](https://img-blog.csdnimg.cn/img_convert/bf3e1627d27cf19225cf6eceb06c5f7a.png)\n\n```java\nException in thread \"main\" java.lang.OutOfMemoryError: Java heap space\n```\n\n这个意思是堆的内存占用已经达到-Xmx设置的最大值，应该是最常见的OOM错误了。解决思路仍然是先应该在代码中找，怀疑存在内存泄漏，通过jstack和jmap去定位问题。如果说一切都正常，才需要通过调整`Xmx`的值来扩大内存。\n\n```java\nCaused by: java.lang.OutOfMemoryError: Meta space\n```\n\n这个意思是元数据区的内存占用已经达到`XX:MaxMetaspaceSize`设置的最大值，排查思路和上面的一致，参数方面可以通过`XX:MaxPermSize`来进行调整(这里就不说1.8以前的永久代了)。\n\n#### Stack Overflow \n\n栈内存溢出，这个大家见到也比较多。\n\n```java\nException in thread \"main\" java.lang.StackOverflowError\n```\n\n表示线程栈需要的内存大于Xss值，同样也是先进行排查，参数方面通过`Xss`来调整，但调整的太大可能又会引起OOM。\n\n#### 使用JMAP定位代码内存泄漏 \n\n上述关于OOM和StackOverflow的代码排查方面，我们一般使用JMAP`jmap -dump:format=b,file=filename pid`来导出dump文件![pic_10f3f1df.png](https://img-blog.csdnimg.cn/img_convert/99f32b9d39dc568889d659f61c765da0.png)通过mat(Eclipse Memory Analysis Tools)导入dump文件进行分析，内存泄漏问题一般我们直接选Leak Suspects即可，mat给出了内存泄漏的建议。另外也可以选择Top Consumers来查看最大对象报告。和线程相关的问题可以选择thread overview进行分析。除此之外就是选择Histogram类概览来自己慢慢分析，大家可以搜搜mat的相关教程。![pic_27919220.png](https://img-blog.csdnimg.cn/img_convert/f6fe1c941a7b0404cdd96531a2e83e2e.png)\n\n日常开发中，代码产生内存泄漏是比较常见的事，并且比较隐蔽，需要开发者更加关注细节。比如说每次请求都new对象，导致大量重复创建对象；进行文件流操作但未正确关闭；手动不当触发gc；ByteBuffer缓存分配不合理等都会造成代码OOM。\n\n另一方面，我们可以在启动参数中指定`-XX:+HeapDumpOnOutOfMemoryError`来保存OOM时的dump文件。\n\n#### gc问题和线程 \n\ngc问题除了影响cpu也会影响内存，排查思路也是一致的。一般先使用jstat来查看分代变化情况，比如youngGC或者fullGC次数是不是太多呀；EU、OU等指标增长是不是异常呀等。线程的话太多而且不被及时gc也会引发oom，大部分就是之前说的`unable to create new native thread`。除了jstack细细分析dump文件外，我们一般先会看下总体线程，通过`pstreee -p pid |wc -l`。\n\n![pic_be461707.png](https://img-blog.csdnimg.cn/img_convert/92daf6726ba2dece7ed386e3b4af607c.png)或者直接通过查看`/proc/pid/task`的数量即为线程数量。![pic_d37c93ed.png](https://img-blog.csdnimg.cn/img_convert/13a64411323c1ea456c6d445e3fcc240.png)\n\n### 堆外内存 \n\n如果碰到堆外内存溢出，那可真是太不幸了。首先堆外内存溢出表现就是物理常驻内存增长快，报错的话视使用方式都不确定，如果由于使用Netty导致的，那错误日志里可能会出现`OutOfDirectMemoryError`错误，如果直接是DirectByteBuffer，那会报`OutOfMemoryError: Direct buffer memory`。\n\n堆外内存溢出往往是和NIO的使用相关，一般我们先通过pmap来查看下进程占用的内存情况`pmap -x pid | sort -rn -k3 | head -30`，这段意思是查看对应pid倒序前30大的内存段。这边可以再一段时间后再跑一次命令看看内存增长情况，或者和正常机器比较可疑的内存段在哪里。![pic_8b7f32e3.png](https://img-blog.csdnimg.cn/img_convert/4026763068ed2ff9ae0b489c13c7adc6.png)我们如果确定有可疑的内存端，需要通过gdb来分析`gdb --batch --pid {pid} -ex \"dump memory filename.dump {内存起始地址} {内存起始地址+内存块大小}\"`\n\n![pic_9b98d661.png](https://img-blog.csdnimg.cn/img_convert/1e9a29ccf1adb94f136c394bac7e0bdd.png)\n\n获取dump文件后可用heaxdump进行查看`hexdump -C filename | less`，不过大多数看到的都是二进制乱码。\n\nNMT是Java7U40引入的HotSpot新特性，配合jcmd命令我们就可以看到具体内存组成了。需要在启动参数中加入 `-XX:NativeMemoryTracking=summary` 或者 `-XX:NativeMemoryTracking=detail`，会有略微性能损耗。\n\n一般对于堆外内存缓慢增长直到爆炸的情况来说，可以先设一个基线`jcmd pid VM.native_memory baseline`。![pic_376fbe14.png](https://img-blog.csdnimg.cn/img_convert/089edb6523b02d8d9fbe83f10981d2fd.png)然后等放一段时间后再去看看内存增长的情况，通过`jcmd pid VM.native_memory detail.diff(summary.diff)`做一下summary或者detail级别的diff。![pic_001150f9.png](https://img-blog.csdnimg.cn/img_convert/449a8dd068bf46cec3a5c2b2dd8b67c1.png)[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cebviJ64-1624412636474)(https://www.liangtengyu.com:9998/images/pic_c1d1f58e.png)]可以看到jcmd分析出来的内存十分详细，包括堆内、线程以及gc(所以上述其他内存异常其实都可以用nmt来分析)，这边堆外内存我们重点关注Internal的内存增长，如果增长十分明显的话那就是有问题了。detail级别的话还会有具体内存段的增长情况，如下图。![pic_857919c3.png](https://img-blog.csdnimg.cn/img_convert/cea1b14bf4fbf4d9526972b15fa1e11a.png)\n\n此外在系统层面，我们还可以使用strace命令来监控内存分配 `strace -f -e \"brk,mmap,munmap\" -p pid`这边内存分配信息主要包括了pid和内存地址。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OoJt874d-1624412636476)(https://www.liangtengyu.com:9998/images/pic_ea4d8b3d.png)]\n\n不过其实上面那些操作也很难定位到具体的问题点，关键还是要看错误日志栈，找到可疑的对象，搞清楚它的回收机制，然后去分析对应的对象。比如DirectByteBuffer分配内存的话，是需要full GC或者手动system.gc来进行回收的(所以最好不要使用`-XX:+DisableExplicitGC`)。那么其实我们可以跟踪一下DirectByteBuffer对象的内存情况，通过`jmap -histo:live pid`手动触发fullGC来看看堆外内存有没有被回收。如果被回收了，那么大概率是堆外内存本身分配的太小了，通过`-XX:MaxDirectMemorySize`进行调整。如果没有什么变化，那就要使用jmap去分析那些不能被gc的对象，以及和DirectByteBuffer之间的引用关系了。\n\n## GC问题 \n\n堆内内存泄漏总是和GC异常相伴。不过GC问题不只是和内存问题相关，还有可能引起CPU负载、网络问题等系列并发症，只是相对来说和内存联系紧密些，所以我们在此单独总结一下GC相关问题。\n\n我们在cpu章介绍了使用jstat来获取当前GC分代变化信息。而更多时候，我们是通过GC日志来排查问题的，在启动参数中加上`-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps`来开启GC日志。常见的Young GC、Full GC日志含义在此就不做赘述了。\n\n针对gc日志，我们就能大致推断出youngGC与fullGC是否过于频繁或者耗时过长，从而对症下药。我们下面将对G1垃圾收集器来做分析，这边也建议大家使用G1`-XX:+UseG1GC`。\n\n### youngGC过频繁 \n\nyoungGC频繁一般是短周期小对象较多，先考虑是不是Eden区/新生代设置的太小了，看能否通过调整-Xmn、-XX:SurvivorRatio等参数设置来解决问题。如果参数正常，但是young gc频率还是太高，就需要使用Jmap和MAT对dump文件进行进一步排查了。\n\n### youngGC耗时过长 \n\n耗时过长问题就要看GC日志里耗时耗在哪一块了。以G1日志为例，可以关注Root Scanning、Object Copy、Ref Proc等阶段。Ref Proc耗时长，就要注意引用相关的对象。Root Scanning耗时长，就要注意线程数、跨代引用。Object Copy则需要关注对象生存周期。而且耗时分析它需要横向比较，就是和其他项目或者正常时间段的耗时比较。比如说图中的Root Scanning和正常时间段比增长较多，那就是起的线程太多了。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h25u328b-1624412636476)(https://www.liangtengyu.com:9998/images/pic_5ab082f5.png)]\n\n### 触发fullGC \n\nG1中更多的还是mixedGC，但mixedGC可以和youngGC思路一样去排查。触发fullGC了一般都会有问题，G1会退化使用Serial收集器来完成垃圾的清理工作，暂停时长达到秒级别，可以说是半跪了。fullGC的原因可能包括以下这些，以及参数调整方面的一些思路：\n\n *  并发阶段失败：在并发标记阶段，MixGC之前老年代就被填满了，那么这时候G1就会放弃标记周期。这种情况，可能就需要增加堆大小，或者调整并发标记线程数 `-XX:ConcGCThreads`。\n *  晋升失败：在GC的时候没有足够的内存供存活/晋升对象使用，所以触发了Full GC。这时候可以通过 `-XX:G1ReservePercent`来增加预留内存百分比，减少 `-XX:InitiatingHeapOccupancyPercent`来提前启动标记， `-XX:ConcGCThreads`来增加标记线程数也是可以的。\n *  大对象分配失败：大对象找不到合适的region空间进行分配，就会进行fullGC，这种情况下可以增大内存或者增大 `-XX:G1HeapRegionSize`。\n *  程序主动执行 `System.gc()`：不要随便写就对了。\n\n另外，我们可以在启动参数中配置`-XX:HeapDumpPath=/xxx/dump.hprof`来dump fullGC相关的文件，并通过jinfo来进行gc前后的dump\n\n```java\njinfo -flag +HeapDumpBeforeFullGC pid \njinfo -flag +HeapDumpAfterFullGC pid\n```\n\n这样得到2份dump文件，对比后主要关注被gc掉的问题对象来定位问题。\n\n## 网络 \n\n涉及到网络层面的问题一般都比较复杂，场景多，定位难，成为了大多数开发的噩梦，应该是最复杂的了。这里会举一些例子，并从tcp层、应用层以及工具的使用等方面进行阐述。\n\n### 超时 \n\n超时错误大部分处在应用层面，所以这块着重理解概念。超时大体可以分为连接超时和读写超时，某些使用连接池的客户端框架还会存在获取连接超时和空闲连接清理超时。\n\n *  读写超时。readTimeout/writeTimeout，有些框架叫做so\\_timeout或者socketTimeout，均指的是数据读写超时。注意这边的超时大部分是指逻辑上的超时。soa的超时指的也是读超时。读写超时一般都只针对客户端设置。\n *  连接超时。connectionTimeout，客户端通常指与服务端建立连接的最大时间。服务端这边connectionTimeout就有些五花八门了，jetty中表示空闲连接清理时间，tomcat则表示连接维持的最大时间。\n *  其他。包括连接获取超时connectionAcquireTimeout和空闲连接清理超时idleConnectionTimeout。多用于使用连接池或队列的客户端或服务端框架。\n\n我们在设置各种超时时间中，需要确认的是尽量保持客户端的超时小于服务端的超时，以保证连接正常结束。\n\n在实际开发中，我们关心最多的应该是接口的读写超时了。\n\n如何设置合理的接口超时是一个问题。如果接口超时设置的过长，那么有可能会过多地占用服务端的tcp连接。而如果接口设置的过短，那么接口超时就会非常频繁。\n\n服务端接口明明rt降低，但客户端仍然一直超时又是另一个问题。这个问题其实很简单，客户端到服务端的链路包括网络传输、排队以及服务处理等，每一个环节都可能是耗时的原因。\n\n### TCP队列溢出 \n\ntcp队列溢出是个相对底层的错误，它可能会造成超时、rst等更表层的错误。因此错误也更隐蔽，所以我们单独说一说。![pic_954fc335.png](https://img-blog.csdnimg.cn/img_convert/1a80ee4bd95488ec977ed01b2bc7bc5f.png)\n\n如上图所示，这里有两个队列：syns queue(半连接队列）、accept queue（全连接队列）。三次握手，在server收到client的syn后，把消息放到syns queue，回复syn+ack给client，server收到client的ack，如果这时accept queue没满，那就从syns queue拿出暂存的信息放入accept queue中，否则按tcp\\_abort\\_on\\_overflow指示的执行。\n\ntcp\\_abort\\_on\\_overflow 0表示如果三次握手第三步的时候accept queue满了那么server扔掉client发过来的ack。tcp\\_abort\\_on\\_overflow 1则表示第三步的时候如果全连接队列满了，server发送一个rst包给client，表示废掉这个握手过程和这个连接，意味着日志里可能会有很多`connection reset / connection reset by peer`。\n\n那么在实际开发中，我们怎么能快速定位到tcp队列溢出呢？\n\nnetstat命令，执行netstat -s | egrep \"listen|LISTEN\"![pic_e37b49e3.png](https://img-blog.csdnimg.cn/img_convert/8aaa9fb3907bae3e450089ac70b1ce6c.png)如上图所示，overflowed表示全连接队列溢出的次数，sockets dropped表示半连接队列溢出的次数。\n\nss命令，执行ss -lnt![pic_9c2db1b0.png](https://img-blog.csdnimg.cn/img_convert/6b4b450a4e7ad99b0a0338ae78a2be6a.png)上面看到Send-Q 表示第三列的listen端口上的全连接队列最大为5，第一列Recv-Q为全连接队列当前使用了多少。\n\n接着我们看看怎么设置全连接、半连接队列大小吧：\n\n全连接队列的大小取决于min(backlog, somaxconn)。backlog是在socket创建的时候传入的，somaxconn是一个os级别的系统参数。而半连接队列的大小取决于max(64, /proc/sys/net/ipv4/tcp\\_max\\_syn\\_backlog)。\n\n在日常开发中，我们往往使用servlet容器作为服务端，所以我们有时候也需要关注容器的连接队列大小。在tomcat中backlog叫做`acceptCount`，在jetty里面则是`acceptQueueSize`。\n\n### RST异常 \n\nRST包表示连接重置，用于关闭一些无用的连接，通常表示异常关闭，区别于四次挥手。\n\n在实际开发中，我们往往会看到`connection reset / connection reset by peer`错误，这种情况就是RST包导致的。\n\n端口不存在\n\n如果像不存在的端口发出建立连接SYN请求，那么服务端发现自己并没有这个端口则会直接返回一个RST报文，用于中断连接。\n\n主动代替FIN终止连接\n\n一般来说，正常的连接关闭都是需要通过FIN报文实现，然而我们也可以用RST报文来代替FIN，表示直接终止连接。实际开发中，可设置SO\\_LINGER数值来控制，这种往往是故意的，来跳过TIMED\\_WAIT，提供交互效率，不闲就慎用。\n\n客户端或服务端有一边发生了异常，该方向对端发送RST以告知关闭连接\n\n我们上面讲的tcp队列溢出发送RST包其实也是属于这一种。这种往往是由于某些原因，一方无法再能正常处理请求连接了(比如程序崩了，队列满了)，从而告知另一方关闭连接。\n\n接收到的TCP报文不在已知的TCP连接内\n\n比如，一方机器由于网络实在太差TCP报文失踪了，另一方关闭了该连接，然后过了许久收到了之前失踪的TCP报文，但由于对应的TCP连接已不存在，那么会直接发一个RST包以便开启新的连接。\n\n一方长期未收到另一方的确认报文，在一定时间或重传次数后发出RST报文\n\n这种大多也和网络环境相关了，网络环境差可能会导致更多的RST报文。\n\n之前说过RST报文多会导致程序报错，在一个已关闭的连接上读操作会报`connection reset`，而在一个已关闭的连接上写操作则会报`connection reset by peer`。通常我们可能还会看到`broken pipe`错误，这是管道层面的错误，表示对已关闭的管道进行读写，往往是在收到RST，报出`connection reset`错后继续读写数据报的错，这个在glibc源码注释中也有介绍。\n\n我们在排查故障时候怎么确定有RST包的存在呢？当然是使用tcpdump命令进行抓包，并使用wireshark进行简单分析了。`tcpdump -i en0 tcp -w xxx.cap`，en0表示监听的网卡。![pic_6fb42727.png](https://img-blog.csdnimg.cn/img_convert/387c6d7e070165d14eea180da5d61e1e.png)\n\n接下来我们通过wireshark打开抓到的包，可能就能看到如下图所示，红色的就表示RST包了。![pic_46c66bbb.png](https://img-blog.csdnimg.cn/img_convert/226d71e2abe830d52ed9f8f162ee34bd.png)\n\n### TIME\\_WAIT和CLOSE\\_WAIT \n\nTIME\\_WAIT和CLOSE\\_WAIT是啥意思相信大家都知道。在线上时，我们可以直接用命令`netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'`来查看time-wait和close\\_wait的数量\n\n用ss命令会更快`ss -ant | awk '{++S[$1]} END {for(a in S) print a, S[a]}'`\n\n![pic_14987f2c.png](https://img-blog.csdnimg.cn/img_convert/023b4a7e7418307f4bfebf2ff0f6436c.png)\n\n#### TIME\\_WAIT \n\ntime\\_wait的存在一是为了丢失的数据包被后面连接复用，二是为了在2MSL的时间范围内正常关闭连接。它的存在其实会大大减少RST包的出现。\n\n过多的time\\_wait在短连接频繁的场景比较容易出现。这种情况可以在服务端做一些内核参数调优:\n\n```java\n#表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接，默认为0，表示关闭\nnet.ipv4.tcp_tw_reuse = 1\n#表示开启TCP连接中TIME-WAIT sockets的快速回收，默认为0，表示关闭\nnet.ipv4.tcp_tw_recycle = 1\n```\n\n当然我们不要忘记在NAT环境下因为时间戳错乱导致数据包被拒绝的坑了，另外的办法就是改小`tcp_max_tw_buckets`，超过这个数的time\\_wait都会被干掉，不过这也会导致报`time wait bucket table overflow`的错。\n\n#### CLOSE\\_WAIT \n\nclose\\_wait往往都是因为应用程序写的有问题，没有在ACK后再次发起FIN报文。close\\_wait出现的概率甚至比time\\_wait要更高，后果也更严重。往往是由于某个地方阻塞住了，没有正常关闭连接，从而渐渐地消耗完所有的线程。\n\n想要定位这类问题，最好是通过jstack来分析线程堆栈来排查问题，具体可参考上述章节。这里仅举一个例子。\n\n开发同学说应用上线后CLOSE\\_WAIT就一直增多，直到挂掉为止，jstack后找到比较可疑的堆栈是大部分线程都卡在了`countdownlatch.await`方法，找开发同学了解后得知使用了多线程但是确没有catch异常，修改后发现异常仅仅是最简单的升级sdk后常出现的`class not found`。\n\n出自：https://fredal.xin/java-error-check","tags":["技能包"],"categories":["技能包"]},{"title":"java线上问题排查","url":"/2021/06/23/java线上问题排查/","content":"\n\n## 前言 \n\n平时的工作中经常碰到很多疑难问题的处理，在解决问题的同时，有一些工具起到了相当大的作用，在此书写下来，一是作为笔记，可以让自己后续忘记了可快速翻阅。\n<!--more-->\n\n## Linux命令类 \n\n### tail \n\n最常用的tail -f\n\n```java\ntail -300f shopbase.log #倒数300行并进入实时监听文件写入模式\n```\n\n### grep \n\n```java\ngrep forest f.txt     #文件查找\ngrep forest f.txt cpf.txt #多文件查找\ngrep 'log' /home/admin -r -n #目录下查找所有符合关键字的文件\ncat f.txt | grep -i shopbase    \ngrep 'shopbase' /home/admin -r -n --include *.{vm,java} #指定文件后缀\ngrep 'shopbase' /home/admin -r -n --exclude *.{vm,java} #反匹配\nseq 10 | grep 5 -A 3    #上匹配\nseq 10 | grep 5 -B 3    #下匹配\nseq 10 | grep 5 -C 3    #上下匹配，平时用这个就妥了\ncat f.txt | grep -c 'SHOPBASE'\n```\n\n### awk \n\n1 基础命令\n\n```java\nawk '{print $4,$6}' f.txt\nawk '{print NR,$0}' f.txt cpf.txt    \nawk '{print FNR,$0}' f.txt cpf.txt\nawk '{print FNR,FILENAME,$0}' f.txt cpf.txt\nawk '{print FILENAME,\"NR=\"NR,\"FNR=\"FNR,\"$\"NF\"=\"$NF}' f.txt cpf.txt\necho 1:2:3:4 | awk -F: '{print $1,$2,$3,$4}'\n```\n\n2 匹配\n\n```java\nawk '/ldb/ {print}' f.txt   #匹配ldb\nawk '!/ldb/ {print}' f.txt  #不匹配ldb\nawk '/ldb/ && /LISTEN/ {print}' f.txt   #匹配ldb和LISTEN\nawk '$5 ~ /ldb/ {print}' f.txt #第五列匹配ldb\n```\n\n3 内建变量\n\nNR:NR表示从awk开始执行后，按照记录分隔符读取的数据次数，默认的记录分隔符为换行符，因此默认的就是读取的数据行数，NR可以理解为Number of Record的缩写。\n\nFNR:在awk处理多个输入文件的时候，在处理完第一个文件后，NR并不会从1开始，而是继续累加，因此就出现了FNR，每当处理一个新文件的时候，FNR就从1开始计数，FNR可以理解为File Number of Record。\n\nNF: NF表示目前的记录被分割的字段的数目，NF可以理解为Number of Field。\n\n### find \n\n```java\nsudo -u admin find /home/admin /tmp /usr -name \\*.log(多个目录去找)\nfind . -iname \\*.txt(大小写都匹配)\nfind . -type d(当前目录下的所有子目录)\nfind /usr -type l(当前目录下所有的符号链接)\nfind /usr -type l -name \"z*\" -ls(符号链接的详细信息 eg:inode,目录)\nfind /home/admin -size +250000k(超过250000k的文件，当然+改成-就是小于了)\nfind /home/admin f -perm 777 -exec ls -l {} \\; (按照权限查询文件)\nfind /home/admin -atime -1  1天内访问过的文件\nfind /home/admin -ctime -1  1天内状态改变过的文件    \nfind /home/admin -mtime -1  1天内修改过的文件\nfind /home/admin -amin -1  1分钟内访问过的文件\nfind /home/admin -cmin -1  1分钟内状态改变过的文件    \nfind /home/admin -mmin -1  1分钟内修改过的文件\n```\n\n### pgm \n\n批量查询vm-shopbase满足条件的日志\n\n```java\npgm -A -f vm-shopbase 'cat /home/admin/shopbase/logs/shopbase.log.2017-01-17|grep 2069861630'\n```\n\n### tsar \n\ntsar是咱公司自己的采集工具。很好用, 将历史收集到的数据持久化在磁盘上，所以我们快速来查询历史的系统数据。当然实时的应用情况也是可以查询的啦。大部分机器上都有安装。\n\n```java\ntsar  ##可以查看最近一天的各项指标\n```\n\n  \n\n\n![pic_ab097f25.png](https://img-blog.csdnimg.cn/img_convert/b0fbcc8fefca87c564e2c796e9e1e174.png)\n\n  \n\n\n```java\ntsar --live ##可以查看实时指标，默认五秒一刷\n```\n\n  \n\n\n![pic_3dced38e.png](https://img-blog.csdnimg.cn/img_convert/b4a9ed9ddbbf551648f83fd954e0d477.png)\n\n  \n\n\n```java\ntsar -d 20161218 ##指定查看某天的数据，貌似最多只能看四个月的数据\n```\n\n  \n\n\n![pic_5a95372b.png](https://img-blog.csdnimg.cn/img_convert/fd778cb83cc8026945fb91303410e232.png)\n\n  \n\n\n```java\ntsar --mem\ntsar --load\ntsar --cpu\n##当然这个也可以和-d参数配合来查询某天的单个指标的情况 \n```\n\n###     \n\n![pic_bd944bc9.png](https://img-blog.csdnimg.cn/img_convert/60b61f376be6c37024e27aa171c2083d.png)\n\n![pic_11e1b1df.png](https://img-blog.csdnimg.cn/img_convert/86b3529ed6bf823f1844d058113fe747.png)\n\n![pic_b7d57c33.png](https://img-blog.csdnimg.cn/img_convert/e6537c9d41e43f8930ae617d87a935c6.png)\n\n### top \n\ntop除了看一些基本信息之外，剩下的就是配合来查询vm的各种问题了\n\n```java\nps -ef | grep java\ntop -H -p pid\n```\n\n获得线程10进制转16进制后jstack去抓看这个线程到底在干啥\n\n### 其他 \n\n```java\nnetstat -nat|awk  '{print $6}'|sort|uniq -c|sort -rn \n#查看当前连接，注意close_wait偏高的情况，比如如下\n```\n\n  \n\n\n![pic_15b87e11.png](https://img-blog.csdnimg.cn/img_convert/33a661488dd1aebd00390a5babcf5b66.png)\n\n![pic_ac69ebc0.png](https://img-blog.csdnimg.cn/img_convert/79ac58f6bbe2d430980ef28749175443.png)\n\n## 排查利器 \n\n### btrace \n\n首当其冲的要说的是btrace。真是生产环境&预发的排查问题大杀器。简介什么的就不说了。直接上代码干\n\n1、查看当前谁调用了ArrayList的add方法，同时只打印当前ArrayList的size大于500的线程调用栈\n\n```java\n@OnMethod(clazz = \"java.util.ArrayList\", method=\"add\", location = @Location(value = Kind.CALL, clazz = \"/./\", method = \"/./\"))\npublic static void m(@ProbeClassName String probeClass, @ProbeMethodName String probeMethod, @TargetInstance Object instance, @TargetMethodOrField String method) {\n\nif(getInt(field(\"java.util.ArrayList\", \"size\"), instance) > 479){\n    println(\"check who ArrayList.add method:\" + probeClass + \"#\" + probeMethod  + \", method:\" + method + \", size:\" + getInt(field(\"java.util.ArrayList\", \"size\"), instance));\n    jstack();\n    println();\n    println(\"===========================\");\n    println();\n}\n}\n```\n\n2、监控当前服务方法被调用时返回的值以及请求的参数\n\n```java\n@OnMethod(clazz = \"com.taobao.sellerhome.transfer.biz.impl.C2CApplyerServiceImpl\", method=\"nav\", location = @Location(value = Kind.RETURN))\npublic static void mt(long userId, int current, int relation, String check, String redirectUrl, @Return AnyType result) {\n\n    println(\"parameter# userId:\" + userId + \", current:\" + current + \", relation:\" + relation + \", check:\" + check + \", redirectUrl:\" + redirectUrl + \", result:\" + result);\n}\n```\n\n其他功能集团的一些工具或多或少都有，就不说了。感兴趣的请移步。\n\n> https://github.com/btraceio/btrace\n\n#### 注意: \n\n *  经过观察，1.3.9的release输出不稳定，要多触发几次才能看到正确的结果\n *  正则表达式匹配trace类时范围一定要控制，否则极有可能出现跑满CPU导致应用卡死的情况\n *  由于是字节码注入的原理，想要应用恢复到正常情况，需要重启应用。\n\n### Greys \n\nGreys是杜琨的大作吧。说几个挺棒的功能(部分功能和btrace重合):\n\nsc -df xxx: 输出当前类的详情,包括源码位置和classloader结构\n\ntrace class method: 相当喜欢这个功能! 很早前可以早JProfiler看到这个功能。打印出当前方法调用的耗时情况，细分到每个方法。对排查方法性能时很有帮助，比如我之前这篇就是使用了trace命令来的:http://www.atatech.org/articles/52947。\n\n其他功能部分和btrace重合，可以选用，感兴趣的请移步。  \nhttp://www.atatech.org/articles/26247\n\n另外相关联的是arthas，他是基于Greys的，感兴趣的再移步http://mw.alibaba-inc.com/products/arthas/docs/middleware-container/arthas.wiki/home.html?spm=a1z9z.8109794.header.32.1lsoMc\n\n### javOSize \n\n就说一个功能  \nclasses：通过修改了字节码，改变了类的内容，即时生效。所以可以做到快速的在某个地方打个日志看看输出，缺点是对代码的侵入性太大。但是如果自己知道自己在干嘛，的确是不错的玩意儿。\n\n其他功能Greys和btrace都能很轻易做的到，不说了。\n\n可以看看我之前写的一篇javOSize的简介http://www.atatech.org/articles/38546  \n官网请移步http://www.javosize.com/\n\n### JProfiler \n\n之前判断许多问题要通过JProfiler，但是现在Greys和btrace基本都能搞定了。再加上出问题的基本上都是生产环境(网络隔离)，所以基本不怎么使用了，但是还是要标记一下。\n\n官网请移步https://www.ej-technologies.com/products/jprofiler/overview.html\n\n## 大杀器 \n\n### eclipseMAT \n\n可作为eclipse的插件，也可作为单独的程序打开。详情请移步http://www.eclipse.org/mat/\n\n### zprofiler \n\n集团内的开发应该是无人不知无人不晓了。简而言之一句话:有了zprofiler还要mat干嘛，详情请移步zprofiler.alibaba-inc.com\n\n## java三板斧，噢不对，是七把 \n\n### jps \n\n我只用一条命令：\n\n```java\nsudo -u admin /opt/taobao/java/bin/jps -mlvV\n```\n\n###     \n\n![pic_766514a4.png](https://img-blog.csdnimg.cn/img_convert/d35b62fa88567dcd94dddf290ee847b8.png)\n\n### jstack \n\n普通用法:\n\n```java\nsudo -u admin /opt/taobao/install/ajdk-8_1_1_fp1-b52/bin/jstack 2815\n```\n\n  \n\n\n![pic_3d704f14.png](https://img-blog.csdnimg.cn/img_convert/818323a396d5a0b5e5cd7bbf434f088b.png)\n\nnative+java栈:\n\n```java\nsudo -u admin /opt/taobao/install/ajdk-8_1_1_fp1-b52/bin/jstack -m 2815\n```\n\n###     \n\n![pic_0e704112.png](https://img-blog.csdnimg.cn/img_convert/587a343ca247ee0a3e28662c76d05373.png)\n\n###  \n\n### 推荐阅读：[jstack命令：教你如何排查多线程问题][jstack]。关注Java技术栈微信公众号，在后台回复关键字：Java，可以获取更多栈长整理的Java技术干货。 \n\n### jinfo \n\n可看系统启动的参数，如下\n\n```java\nsudo -u admin /opt/taobao/install/ajdk-8_1_1_fp1-b52/bin/jinfo -flags 2815\n```\n\n###     \n\n![pic_42972067.png](https://img-blog.csdnimg.cn/img_convert/eb6c68b0f3ad27871eabdb2f375e21af.png)\n\n### jmap \n\n两个用途\n\n1.查看堆的情况\n\n```java\nsudo -u admin /opt/taobao/install/ajdk-8_1_1_fp1-b52/bin/jmap -heap 2815\n```\n\n  \n\n\n![pic_2b7a061d.png](https://img-blog.csdnimg.cn/img_convert/f5183b0c56fb5ef47da160a73cb14ea4.png)\n\n2.dump\n\n```java\nsudo -u admin /opt/taobao/install/ajdk-8_1_1_fp1-b52/bin/jmap -dump:live,format=b,file=/tmp/heap2.bin 2815\n```\n\n或者\n\n```java\nsudo -u admin /opt/taobao/install/ajdk-8_1_1_fp1-b52/bin/jmap -dump:format=b,file=/tmp/heap3.bin 2815\n```\n\n3.看看堆都被谁占了? 再配合zprofiler和btrace，排查问题简直是如虎添翼\n\n```java\nsudo -u admin /opt/taobao/install/ajdk-8_1_1_fp1-b52/bin/jmap -histo 2815 | head -10\n```\n\n###     \n\n![pic_11629b6e.png](https://img-blog.csdnimg.cn/img_convert/397996c7816f95fd29021d184950b93e.png)\n\n### jstat \n\njstat参数众多，但是使用一个就够了\n\n```java\nsudo -u admin /opt/taobao/install/ajdk-8_1_1_fp1-b52/bin/jstat -gcutil 2815 1000 \n```\n\n###     \n\n![pic_bc0e4cdf.png](https://img-blog.csdnimg.cn/img_convert/7ad703f582d7a87bec0f80d465410161.png)\n\n### jdb \n\n时至今日，jdb也是经常使用的。\n\njdb可以用来预发debug,假设你预发的java\\_home是/opt/taobao/java/，远程调试端口是8000.那么\n\n```java\nsudo -u admin /opt/taobao/java/bin/jdb -attach 8000\n```\n\n  \n\n\n![pic_b91a276f.png](https://img-blog.csdnimg.cn/img_convert/599641893d854b6815373d96472a9524.png)\n\n出现以上代表jdb启动成功。后续可以进行设置断点进行调试。\n\n具体参数可见oracle官方说明http://docs.oracle.com/javase/7/docs/technotes/tools/windows/jdb.html\n\n### CHLSDB \n\nCHLSDB感觉很多情况下可以看到更好玩的东西，不详细叙述了。查询资料听说jstack和jmap等工具就是基于它的。\n\n```java\nsudo -u admin /opt/taobao/java/bin/java -classpath /opt/taobao/java/lib/sa-jdi.jar sun.jvm.hotspot.CLHSDB\n```\n\n更详细的可见R大此贴\n\n> http://rednaxelafx.iteye.com/blog/1847971\n\n## plugin of intellij idea \n\n### key promoter \n\n快捷键一次你记不住，多来几次你总能记住了吧？\n\n![pic_07854b0b.png](https://img-blog.csdnimg.cn/img_convert/486cdfd28504b27d36df928d0e53fd31.png)\n\n### maven helper \n\n分析maven依赖的好帮手。\n\n## VM options \n\n1、你的类到底是从哪个文件加载进来的？\n\n```java\n-XX:+TraceClassLoading\n结果形如[Loaded java.lang.invoke.MethodHandleImpl$Lazy from D:\\programme\\jdk\\jdk8U74\\jre\\lib\\rt.jar]\n```\n\n2、应用挂了输出dump文件\n\n```java\n-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/admin/logs/java.hprof\n集团的vm参数里边基本都有这个选项\n```\n\n## jar包冲突 \n\n把这个单独写个大标题不过分吧？每个人或多或少都处理过这种烦人的case。我特么下边这么多方案不信就搞不定你?\n\n```java\nmvn dependency:tree > ~/dependency.txt\n```\n\n打出所有依赖\n\n```java\nmvn dependency:tree -Dverbose -Dincludes=groupId:artifactId\n```\n\n只打出指定groupId和artifactId的依赖关系\n\n```java\n-XX:+TraceClassLoading\n```\n\nvm启动脚本加入。在tomcat启动脚本中可见加载类的详细信息\n\n```java\n -verbose\n```\n\nvm启动脚本加入。在tomcat启动脚本中可见加载类的详细信息\n\n```java\ngreys:sc\n```\n\ngreys的sc命令也能清晰的看到当前类是从哪里加载过来的\n\n```java\ntomcat-classloader-locate\n```\n\n通过以下url可以获知当前类是从哪里加载的\n\n```java\ncurl http://localhost:8006/classloader/locate?class=org.apache.xerces.xs.XSObject\n```\n\n## ALI-TOMCAT带给我们的惊喜(感谢务观) \n\n列出容器加载的jar列表\n\n```java\ncurl http://localhost:8006/classloader/jars\n```\n\n列出当前当当前类加载的实际jar包位置，解决类冲突时有用\n\n```java\ncurl http://localhost:8006/classloader/locate?class=org.apache.xerces.xs.XSObject\n```\n\n  \n\n\n![pic_05d6ef8e.png](https://img-blog.csdnimg.cn/img_convert/2ece2b5dae18ea90f51bb0e181e45870.png)\n\n## 其他 \n\n### gpref \n\n> http://www.atatech.org/articles/33317\n\n### dmesg \n\n如果发现自己的java进程悄无声息的消失了，几乎没有留下任何线索，那么dmesg一发，很有可能有你想要的。\n\n```java\nsudo dmesg|grep -i kill|less\n```\n\n去找关键字oom\\_killer。找到的结果类似如下:\n\n```java\n[6710782.021013] java invoked oom-killer: gfp_mask=0xd0, order=0, oom_adj=0, oom_scoe_adj=0\n[6710782.070639] [<ffffffff81118898>] ? oom_kill_process+0x68/0x140 \n[6710782.257588] Task in /LXC011175068174 killed as a result of limit of /LXC011175068174 \n[6710784.698347] Memory cgroup out of memory: Kill process 215701 (java) score 854 or sacrifice child \n[6710784.707978] Killed process 215701, UID 679, (java) total-vm:11017300kB, anon-rss:7152432kB, file-rss:1232kB\n```\n\n以上表明，对应的java进程被系统的OOM Killer给干掉了，得分为854.\n\n解释一下OOM killer（Out-Of-Memory killer），该机制会监控机器的内存资源消耗。当机器内存耗尽前，该机制会扫描所有的进程（按照一定规则计算，内存占用，时间等），挑选出得分最高的进程，然后杀死，从而保护机器。\n\ndmesg日志时间转换公式:\n\nlog实际时间=格林威治1970-01-01+(当前时间秒数-系统启动至今的秒数+dmesg打印的log时间)秒数：\n\n```java\ndate -d \"1970-01-01 UTC `echo \"$(date +%s)-$(cat /proc/uptime|cut -f 1 -d' ')+12288812.926194\"|bc ` seconds\"\n```\n\n剩下的，就是看看为什么内存这么大，触发了OOM-Killer了。\n\n## 新技能get \n\n### RateLimiter \n\n想要精细的控制QPS? 比如这样一个场景，你调用某个接口，对方明确需要你限制你的QPS在400之内你怎么控制？这个时候RateLimiter就有了用武之地。\n\n出自：https://developer.aliyun.com/article/69520","tags":["技能包"],"categories":["技能包"]},{"title":"java中的锁","url":"/2021/06/21/java中的锁/","content":"说说java中有哪些锁。\n<!--more-->\n *  乐观锁和悲观锁\n *  独占锁和共享锁\n *  互斥锁和读写锁\n *  公平锁和非公平锁\n *  可重入锁\n *  自旋锁\n *  分段锁\n *  锁升级（无锁|偏向锁|轻量级锁|重量级锁）\n *  锁优化技术（锁粗化、锁消除）\n\n# 乐观锁和悲观锁 \n\n悲观锁\n\n`悲观锁`对应于生活中悲观的人，悲观的人总是想着事情往坏的方向发展。\n\n举个生活中的例子，假设厕所只有一个坑位了，悲观锁上厕所会第一时间把门反锁上，这样其他人上厕所只能在门外等候，这种状态就是「阻塞」了。\n\n回到代码世界中，一个共享数据加了悲观锁，那线程每次想操作这个数据前都会假设其他线程也可能会操作这个数据，所以每次操作前都会上锁，这样其他线程想操作这个数据拿不到锁只能阻塞了。\n\n![pic_5f1a7bec.png](https://www.liangtengyu.com:9998/images/pic_5f1a7bec.png)\n\n在 Java 语言中 `synchronized` 和 `ReentrantLock`等就是典型的悲观锁，还有一些使用了 synchronized 关键字的容器类如 `HashTable` 等也是悲观锁的应用。\n\n乐观锁\n\n`乐观锁` 对应于生活中乐观的人，乐观的人总是想着事情往好的方向发展。\n\n举个生活中的例子，假设厕所只有一个坑位了，乐观锁认为：这荒郊野外的，又没有什么人，不会有人抢我坑位的，每次关门上锁多浪费时间，还是不加锁好了。你看乐观锁就是天生乐观！\n\n回到代码世界中，乐观锁操作数据时不会上锁，在更新的时候会判断一下在此期间是否有其他线程去更新这个数据。\n\n![pic_2e049f4f.png](https://www.liangtengyu.com:9998/images/pic_2e049f4f.png)\n\n乐观锁可以使用`版本号机制`和`CAS算法`实现。在 Java 语言中 `java.util.concurrent.atomic`包下的原子类就是使用CAS 乐观锁实现的。\n\n两种锁的使用场景\n\n悲观锁和乐观锁没有孰优孰劣，有其各自适应的场景。\n\n乐观锁适用于写比较少（冲突比较小）的场景，因为不用上锁、释放锁，省去了锁的开销，从而提升了吞吐量。\n\n如果是写多读少的场景，即冲突比较严重，线程间竞争激励，使用乐观锁就是导致线程不断进行重试，这样可能还降低了性能，这种场景下使用悲观锁就比较合适。\n\n# 独占锁和共享锁 \n\n独占锁\n\n`独占锁`是指锁一次只能被一个线程所持有。如果一个线程对数据加上排他锁后，那么其他线程不能再对该数据加任何类型的锁。获得独占锁的线程即能读数据又能修改数据。\n\n![pic_4cf02119.png](https://www.liangtengyu.com:9998/images/pic_4cf02119.png)\n\nJDK中的`synchronized`和`java.util.concurrent(JUC)`包中Lock的实现类就是独占锁。\n\n共享锁\n\n`共享锁`是指锁可被多个线程所持有。如果一个线程对数据加上共享锁后，那么其他线程只能对数据再加共享锁，不能加独占锁。获得共享锁的线程只能读数据，不能修改数据。\n\n![pic_0722f6a5.png](https://www.liangtengyu.com:9998/images/pic_0722f6a5.png)\n\n在 JDK 中 `ReentrantReadWriteLock` 就是一种共享锁。\n\n# 互斥锁和读写锁 \n\n互斥锁\n\n`互斥锁`是独占锁的一种常规实现，是指某一资源同时只允许一个访问者对其进行访问，具有唯一性和排它性。\n\n![pic_46bb564c.png](https://www.liangtengyu.com:9998/images/pic_46bb564c.png)\n\n互斥锁一次只能一个线程拥有互斥锁，其他线程只有等待。\n\n读写锁\n\n`读写锁`是共享锁的一种具体实现。读写锁管理一组锁，一个是只读的锁，一个是写锁。\n\n读锁可以在没有写锁的时候被多个线程同时持有，而写锁是独占的。写锁的优先级要高于读锁，一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。\n\n读写锁相比于互斥锁并发程度更高，每次只有一个写线程，但是同时可以有多个线程并发读。\n\n![pic_661c9303.png](https://www.liangtengyu.com:9998/images/pic_661c9303.png)\n\n在 JDK 中定义了一个读写锁的接口：`ReadWriteLock`\n\n```java\npublic interface ReadWriteLock {\n    /**\n     * 获取读锁\n     */\n    Lock readLock();\n\n    /**\n     * 获取写锁\n     */\n    Lock writeLock();\n}\n```\n\n`ReentrantReadWriteLock` 实现了`ReadWriteLock`接口，具体实现这里不展开，后续会深入源码解析。\n\n# 公平锁和非公平锁 \n\n公平锁\n\n`公平锁`是指多个线程按照申请锁的顺序来获取锁，这里类似排队买票，先来的人先买，后来的人在队尾排着，这是公平的。\n\n![pic_bcffd79d.png](https://www.liangtengyu.com:9998/images/pic_bcffd79d.png)\n\n在 java 中可以通过构造函数初始化公平锁\n\n```java\n/**\n* 创建一个可重入锁，true 表示公平锁，false 表示非公平锁。默认非公平锁\n*/\nLock lock = new ReentrantLock(true);\n```\n\n非公平锁\n\n`非公平锁`是指多个线程获取锁的顺序并不是按照申请锁的顺序，有可能后申请的线程比先申请的线程优先获取锁，在高并发环境下，有可能造成优先级翻转，或者饥饿的状态（某个线程一直得不到锁）。\n\n![pic_b5a4483d.png](https://www.liangtengyu.com:9998/images/pic_b5a4483d.png)\n\n在 java 中 synchronized 关键字是非公平锁，ReentrantLock默认也是非公平锁。\n\n```java\n/**\n* 创建一个可重入锁，true 表示公平锁，false 表示非公平锁。默认非公平锁\n*/\nLock lock = new ReentrantLock(false);\n```\n\n# 可重入锁 \n\n`可重入锁`又称之为`递归锁`，是指同一个线程在外层方法获取了锁，在进入内层方法会自动获取锁。\n\n![pic_d15c456b.png](https://www.liangtengyu.com:9998/images/pic_d15c456b.png)\n\n对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁。对于Synchronized而言，也是一个可重入锁。\n\n敲黑板：可重入锁的一个好处是可一定程度避免死锁。\n\n以 synchronized 为例，看一下下面的代码：\n\n```java\npublic synchronized void mehtodA() throws Exception{\n // Do some magic tings\n mehtodB();\n}\n\npublic synchronized void mehtodB() throws Exception{\n // Do some magic tings\n}\n```\n\n上面的代码中 methodA 调用 methodB，如果一个线程调用methodA 已经获取了锁再去调用 methodB 就不需要再次获取锁了，这就是可重入锁的特性。如果不是可重入锁的话，mehtodB 可能不会被当前线程执行，可能造成死锁。\n\n# 自旋锁 \n\n`自旋锁`是指线程在没有获得锁时不是被直接挂起，而是执行一个忙循环，这个忙循环就是所谓的自旋。\n\n![pic_955a59ab.png](https://www.liangtengyu.com:9998/images/pic_955a59ab.png)\n\n自旋锁的目的是为了减少线程被挂起的几率，因为线程的挂起和唤醒也都是耗资源的操作。\n\n如果锁被另一个线程占用的时间比较长，即使自旋了之后当前线程还是会被挂起，忙循环就会变成浪费系统资源的操作，反而降低了整体性能。因此自旋锁是不适应锁占用时间长的并发情况的。\n\n在 Java 中，`AtomicInteger` 类有自旋的操作，我们看一下代码：\n\n```java\npublic final int getAndAddInt(Object o, long offset, int delta) {\n    int v;\n    do {\n        v = getIntVolatile(o, offset);\n    } while (!compareAndSwapInt(o, offset, v, v + delta));\n    return v;\n}\n```\n\nCAS 操作如果失败就会一直循环获取当前 value 值然后重试。\n\n另外自适应自旋锁也需要了解一下。\n\n在JDK1.6又引入了自适应自旋，这个就比较智能了，自旋时间不再固定，由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。如果虚拟机认为这次自旋也很有可能再次成功那就会次序较多的时间，如果自旋很少成功，那以后可能就直接省略掉自旋过程，避免浪费处理器资源。\n\n# 分段锁 \n\n`分段锁` 是一种锁的设计，并不是具体的一种锁。\n\n分段锁设计目的是将锁的粒度进一步细化，当操作不需要更新整个数组的时候，就仅仅针对数组中的一项进行加锁操作。\n\n![pic_8cc4dad8.png](https://www.liangtengyu.com:9998/images/pic_8cc4dad8.png)\n\n在 Java 语言中 CurrentHashMap 底层就用了分段锁，使用Segment，就可以进行并发使用了。\n\n# 锁升级（无锁|偏向锁|轻量级锁|重量级锁） \n\nJDK1.6 为了提升性能减少获得锁和释放锁所带来的消耗，引入了4种锁的状态：`无锁`、`偏向锁`、`轻量级锁`和`重量级锁`，它会随着多线程的竞争情况逐渐升级，但不能降级。\n\n无锁\n\n`无锁`状态其实就是上面讲的乐观锁，这里不再赘述。\n\n偏向锁\n\nJava偏向锁(Biased Locking)是指它会偏向于第一个访问锁的线程，如果在运行过程中，只有一个线程访问加锁的资源，不存在多线程竞争的情况，那么线程是不需要重复获取锁的，这种情况下，就会给线程加一个偏向锁。\n\n偏向锁的实现是通过控制对象`Mark Word`的标志位来实现的，如果当前是`可偏向状态`，需要进一步判断对象头存储的线程 ID 是否与当前线程 ID 一致，如果一致直接进入。\n\n轻量级锁\n\n当线程竞争变得比较激烈时，偏向锁就会升级为`轻量级锁`，轻量级锁认为虽然竞争是存在的，但是理想情况下竞争的程度很低，通过`自旋方式`等待上一个线程释放锁。\n\n重量级锁\n\n如果线程并发进一步加剧，线程的自旋超过了一定次数，或者一个线程持有锁，一个线程在自旋，又来了第三个线程访问时（反正就是竞争继续加大了），轻量级锁就会膨胀为`重量级锁`，重量级锁会使除了此时拥有锁的线程以外的线程都阻塞。\n\n升级到重量级锁其实就是互斥锁了，一个线程拿到锁，其余线程都会处于阻塞等待状态。\n\n在 Java 中，synchronized 关键字内部实现原理就是锁升级的过程：无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁。这一过程在后续讲解 synchronized 关键字的原理时会详细介绍。\n\n# 锁优化技术（锁粗化、锁消除） \n\n锁粗化\n\n`锁粗化`就是将多个同步块的数量减少，并将单个同步块的作用范围扩大，本质上就是将多次上锁、解锁的请求合并为一次同步请求。\n\n举个例子，一个循环体中有一个代码同步块，每次循环都会执行加锁解锁操作。\n\n```java\nprivate static final Object LOCK = new Object();\n\nfor(int i = 0;i < 100; i++) {\n    synchronized(LOCK){\n        // do some magic things\n    }\n}\n```\n\n经过`锁粗化`后就变成下面这个样子了：\n\n```java\n synchronized(LOCK){\n     for(int i = 0;i < 100; i++) {\n        // do some magic things\n    }\n}\n```\n\n锁消除\n\n`锁消除`是指虚拟机编译器在运行时检测到了共享数据没有竞争的锁，从而将这些锁进行消除。\n\n举个例子让大家更好理解。\n\n```java\npublic String test(String s1, String s2){\n    StringBuffer stringBuffer = new StringBuffer();\n    stringBuffer.append(s1);\n    stringBuffer.append(s2);\n    return stringBuffer.toString();\n}\n```\n\n上面代码中有一个 test 方法，主要作用是将字符串 s1 和字符串 s2 串联起来。\n\ntest 方法中三个变量s1, s2, stringBuffer， 它们都是局部变量，局部变量是在栈上的，栈是线程私有的，所以就算有多个线程访问 test 方法也是线程安全的。\n\n我们都知道 StringBuffer 是线程安全的类，append 方法是同步方法，但是 test 方法本来就是线程安全的，为了提升效率，虚拟机帮我们消除了这些同步锁，这个过程就被称为`锁消除`。\n\n```java\nStringBuffer.class\n\n// append 是同步方法\npublic synchronized StringBuffer append(String str) {\n    toStringCache = null;\n    super.append(str);\n    return this;\n}\n```\n\n一张图总结：\n\n前面讲了 Java 语言中各种各种的锁，最后再通过六个问题统一总结一下：\n\n![pic_588aa6a8.png](https://www.liangtengyu.com:9998/images/pic_588aa6a8.png)","tags":["锁"],"categories":["java基础"]},{"title":"跨域问题的解决方案","url":"/2021/06/16/跨域问题的解决方案/","content":"1.什么是跨域？  \n当一个页面请求url的协议、域名、端口三者之间任何一者与当前页面url不同即为跨域。举个例子：\n<!--more-->\n<table> \n <thead> \n  <tr> \n   <th>当前页面url</th> \n   <th>被请求页面url</th> \n   <th>是否跨域</th> \n   <th>原因</th> \n  </tr> \n </thead> \n <tbody> \n  <tr> \n   <td>http://www.yzfree.com/</td> \n   <td>http://www.yzfree.com/index.html</td> \n   <td>否</td> \n   <td>同源（协议、域名、端口号相同）</td> \n  </tr> \n  <tr> \n   <td>http://www.yzfree.com/</td> \n   <td>https://www.yzfree.com/index.html</td> \n   <td>跨域</td> \n   <td>协议不同（http/https）</td> \n  </tr> \n  <tr> \n   <td>http://www.yzfree.com/</td> \n   <td>http://www.baidu.com/</td> \n   <td>跨域</td> \n   <td>主域名不同（yzfree/baidu）</td> \n  </tr> \n  <tr> \n   <td>http://www.yzfree.com/</td> \n   <td>http://blog.yzfree.com/</td> \n   <td>跨域</td> \n   <td>子域名不同（www/blog）</td> \n  </tr> \n  <tr> \n   <td>http://www.yzfree.com:8080/</td> \n   <td>http://www.yzfree.com:8089/</td> \n   <td>跨域</td> \n   <td>端口号不同（8080/8089）</td> \n  </tr> \n </tbody> \n</table>\n\n2.为什么会出现跨域问题  \n出现跨域问题是源于浏览器的同源策略限制的。同源策略（Same origin policy）是一种约定，它是浏览器针对安全功能的一种实现，如果缺少了同源策略，浏览器很容易受到XSS，CSFR等网络攻击。\n\n3.跨域会导致什么问题  \n不能读取非同源网页中的 Cookie、LocalStorage 等数据  \n不能接触非同源网页的 DOM 结构  \n不能向非同源地址发送 AJAX 请求  \n跨域的现象：  \n![pic_2fd84e71.png](https://www.liangtengyu.com:9998/images/pic_2fd84e71.png)\n\n注意：html有一些特殊标签例如：`<img> <script> <link> <frame>` 等具有跨域的特性，可以直接访问非同源地址，可放心使用。\n\n4.跨域问题的解决方案  \n4.1 JSONP\n\nJSONP 的方式就是通过添加一个 script 元素，远程获取 js 代码并执行。  \n整个逻辑就是：前端向服务器发送请求并指定回调函数为：test ，后端返回 js代码 “test(110)“ ，前端收到并执行  \n注意：这种方式只只支持get请求。\n\n①原生实现：\n\n```java\n<script src=\"http://yzfree.com/v1?callback=test\"></script>\n<script type=\"text/javascript\">\n    function test(res){\n        console.log(res.data);\n    }\n</script>\n```\n\n② jQuery ajax：\n\n```java\n$.ajax({\n    url: 'http://yzfree.com/v1',\n    type: 'get',\n    dataType: 'jsonp', \n    jsonpCallback: \"test\", \n    data: {\n     \n     }\n});\n```\n\n③ Vue.js\n\n```java\nthis.$http.jsonp('http://yzfree.com/v1', {\n    params: {\n     },\n    jsonp: 'test'\n}).then((res) => {\n    console.log(res); \n})\n```\n\n4.2 CORS\n\nCORS 跨域资源分享（Cross-Origin Resource Sharing）的办法是让每一个页面需要返回一个名为Access-Control-Allow-Origin的http头来允许非同源的站点访问  \n一般请求只需在服务器端设置Access-Control-Allow-Origin，如果是带cookie的跨域请求那么前后端都要进行设置  \n【前端】\n\n①原生ajax\n\n```java\nvar xhr = new XMLHttpRequest();\n// 设置可以携带cookie\nxhr.withCredentials = true;\n​\nxhr.open('post', 'http://yzfree.com/v1', true);\nxhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');\nxhr.send('user=gwx');\n​\nxhr.onreadystatechange = function() {\n    if (xhr.readyState == 4 && xhr.status == 200) {\n        alert(xhr.responseText);\n    }\n};\n```\n\n② jQuery ajax\n\n```java\n$.ajax({\n   url: 'http://yzfree.com/v1',\n   type: 'get'\n  data: {\n     \n     }\n  xhrFields: \n      // 设置可以携带cookie\n      withCredentials: true\n  }\n})\n```\n\n  \n③vue-resource\n\n```java\n// 设置可以携带cookie\nVue.http.options.credentials = true\n```\n\n④ axios\n\n```java\n// 设置可以携带cookie\naxios.defaults.withCredentials = true\n```\n\n【后端SpringBoot】  \n在服务器响应客户端的时候，带上Access-Control-Allow-Origin  \n① 使用 @CrossOrigin 注解\n\n```java\n@RequestMapping(\"/v1\")\n@RestController\n//@CrossOrigin //不限制指定域名，都可以访问\n@CrossOrigin(\"https://yzfree.com\") // 指定域名\npublic class CorsTestController {   \n    @GetMapping(\"/test\")\n    public String sayHello() {\n        return \"success\";\n    }\n}\n```\n\n② CORS全局配置\n\n```java\n@Configuration\npublic class CorsConfig {\n    private CorsConfiguration buildConfig() {\n        CorsConfiguration corsConfiguration = new CorsConfiguration();\n        corsConfiguration.addAllowedOrigin(\"*\"); // 允许任何域名使用\n        corsConfiguration.addAllowedHeader(\"*\"); // 允许任何头\n        corsConfiguration.addAllowedMethod(\"*\"); // 允许任何方法（post、get等）\n        return corsConfiguration;\n    }\n​\n    @Bean\n    public CorsFilter corsFilter() {\n        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();\n        source.registerCorsConfiguration(\"/**\", buildConfig()); \n        return new CorsFilter(source);\n    }\n}\n```\n\n③ 拦截器实现\n\n```java\n@Component\npublic class CorsFilter implements Filter {\n​\n    @Override\n    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)\n            throws IOException, ServletException {\n        HttpServletResponse res = (HttpServletResponse) response;\n        res.addHeader(\"Access-Control-Allow-Credentials\", \"true\");\n        res.addHeader(\"Access-Control-Allow-Origin\", \"*\");\n        res.addHeader(\"Access-Control-Allow-Methods\", \"GET, POST, DELETE, PUT\");\n        res.addHeader(\"Access-Control-Allow-Headers\", \"Content-Type,X-CAF-Authorization-Token,sessionToken,X-TOKEN\");\n        if (((HttpServletRequest) request).getMethod().equals(\"OPTIONS\")) {\n            response.getWriter().println(\"ok\");\n            return;\n        }\n        chain.doFilter(request, response);\n    }\n    @Override\n    public void destroy() {\n    }\n    @Override\n    public void init(FilterConfig filterConfig) throws ServletException {\n    }\n}\n```\n\n4.3 代理proxy\n\n通过中间件来实现，浏览器有跨域限制，但是服务器没有。如果 changeOrigin 设置为true，那么本地会虚拟一个 node 服务端接收你的请求并转发\n\n```java\nproxyTable: {\n '/api': {\n  target: '目标地址', //目标地址\n  changeOrigin: true, //是否跨域\n  pathRewrite: {\n   '^/api': '' //路径重写\n   }\n }\n}\n​\n\n//使用axios\n this.$axios.post(\"/api/gwx\",{\n   发送的数据\n  }).then(data=>{\n   console.log(data);\n  })\n```\n\n4.4 nginx反向代理\n\n通过修改 nginx.conf 配置文件做代理转发\n\n```java\nserver {\n    listen 80;\n    server_name www.yzfree.com;\n​\n location ^~/gwx/ {\n            proxy_pass http://xxx.com:80;//你要转发的地址\n            proxy_set_header Host $host:$server_port;\n            proxy_set_header Remote_Addr $remote_addr;\n            proxy_set_header X-Real-IP $remote_addr;\n            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n            client_max_body_size    2048m;\n        }\n```\n\n以上就是常见的几种跨域解决方案。","tags":["技能包"],"categories":["解决方案"]},{"title":"gRPC 网关，针对 HTTP 2.0 长连接性能优化，提升吞吐量","url":"/2021/06/16/gRpc提升吞吐量/","content":"\n## 最近要搞个网关GateWay，由于系统间请求调用是基于gRPC框架，所以网关第一职责就是能接收并转发gRPC请求，大致的系统架构如下所示: \n<!--more-->\n##  \n\n简单看下即可，由于含有定制化业务背景，架构图看不懂也没关系，后面我会对里面的核心技术点单独剖析讲解\n\n![pic_8c1699c6.png](https://www.liangtengyu.com:9998/images/pic_8c1699c6.png)\n\n为什么要引入网关？请求链路多了一跳，性能有损耗不说，一旦宕机就全部玩完了！\n\n但现实就是这样，不是你想怎么样，就能怎么样！\n\n  \n\n\n![pic_5947a979.png](https://www.liangtengyu.com:9998/images/pic_5947a979.png)\n\n  \n\n\n有时技术方案绕一个大圈子，就是为了解决一个无法避开的因素。这个`因素`可能是多方面：\n\n *  可能是技术上的需求，比如要做监控统计，需要在上层某个位置加个拦截层，收集数据，统一处理\n *  可能是技术实现遇到巨大挑战，至少是当前技术团队研发实力解决不了这个难题\n *  可能上下文会话关联，一个任务要触发多次请求，但始终要在一台机器上完成全部处理\n *  可能是政策因素，为了数据安全，你必须走这一绕。\n\n  \n\n\n本文引入的网关就是安全原因，由于一些公司的安全限制，外部服务无法直接访问公司内部的计算节点，需要引入一个前置网关，负责反向代理、请求路由转发、数据通信、调用监控等。\n\n 1、问题抽象，技术选型 \n\n##  \n\n上面的业务架构可能比较复杂，不了解业务背景同学很容易被绕晕。那么我们简化一些，抽象出一个具体要解决的问题，简化描述。\n\n  \n\n\n![pic_c50dd704.png](https://www.liangtengyu.com:9998/images/pic_c50dd704.png)\n\n  \n\n\n过程分为三步：  \n\n\n1、client端发起gPRC调用（基于HTTP2），请求打到gRPC网关\n\n2、网关接到请求，根据请求约定的参数标识，从Redis缓存里查询目标服务器的映射关系\n\n3、最后，网关将请求转发给目标服务器，获取响应结果，将数据原路返回。\n\n> gRPC必须使用 HTTP/2 传输数据，支持明文和TLS加密数据，支持流数据的交互。充分利用 HTTP/2 连接的多路复用和流式特性。\n\n  \n\n\n技术选型\n\n1、最早计划采用`Netty`来做，但由于`gRPC`的proto模板不是我们定义的，所以解析成本很高，另外还要读取请求Header中的数据，开发难度较大，所以这个便作为了备选方案。\n\n2、另一种改变思路，往反向代理框架方向寻找，重新回到主流的Nginx这条线，但是nginx采用C语言开发，如果是基于常规的`负载均衡策略`转发请求，倒是没什么大的问题。但是，我们内部有依赖任务资源关系，也间接决定着要依赖外部的存储系统。\n\nNginx适合处理静态内容，做一个静态web服务器，但我们又看重其高性能，最后我们选型 Openresty``\n\n> OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台，其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。\n\n 2、Openresty 代码 SHOW \n\n  \n\n\n##  \n\n```java\nhttp {\n    include       mime.types;\n    default_type  application/octet-stream;\n    access_log  logs/access.log  main;\n    sendfile        on;\n    keepalive_timeout  120;\n    client_max_body_size 3000M;\n    server {\n        listen   8091   http2;\n        location / {\n            set $target_url  '' ;\n            access_by_lua_block{\n                local headers = ngx.req.get_headers(0)\n                local jobid= headers[\"jobid\"]\n                local redis = require \"resty.redis\"\n                local red = redis:new()\n                red:set_timeouts(1000) -- 1 sec\n                local ok, err = red:connect(\"156.9.1.2\", 6379)\n                local res, err = red:get(jobid)\n                ngx.var.target_url = res\n            }\n            grpc_pass   grpc://$target_url;\n        }\n    }\n}\n```\n\n  \n\n\n 3、性能压测 \n\n  \n\n\n1、Client 端机器，压测期间，观察网络连接：\n\n![pic_aa555b09.png](https://www.liangtengyu.com:9998/images/pic_aa555b09.png)\n\n![pic_109d0acc.png](https://www.liangtengyu.com:9998/images/pic_109d0acc.png)\n\n结论：  \n\n\n并发压测场景下，请求会转发到三台网关服务器，每台服务器处于`TIME_WAIT`状态的TCP连接并不多。可见此段连接基本能达到连接复用效果。\n\n  \n\n\n2、gRPC网关机器，压测期间，观察网络连接情况：\n\n![pic_999f99a3.png](https://www.liangtengyu.com:9998/images/pic_999f99a3.png)\n\n有大量的请求连接处于`TIME_WAIT`状态。按照端口号可以分为两大类：`6379`和 `40928`  \n\n\n```java\n[root@tf-gw-64bd9f775c-qvpcx nginx]#  netstat -na | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'\nLISTEN 2\nESTABLISHED 6\nTIME_WAIT 27500\n```\n\n通过linux shell 统计命令，`172.16.66.46`服务器有27500个TCP连接处于 `TIME_WAIT`\n\n```java\n[root@tf-gw-64bd9f775c-qvpcx nginx]#  netstat -na | grep 6379 |awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'\nESTABLISHED 1\nTIME_WAIT 13701\n```\n\n其中，连接redis（redis的访问端口 6379） 并处于 `TIME_WAIT` 状态有 13701 个连接\n\n```java\n[root@tf-gw-64bd9f775c-qvpcx nginx]#  netstat -na | grep 40928 |awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'\nESTABLISHED 2\nTIME_WAIT 13671\n```\n\n其中，连接后端Server目标服务器 并处于 `TIME_WAIT` 状态有 13671 个连接。两者的连接数基本相等，因为每一次转发请求都要查询一次Redis。\n\n![pic_2db77749.png](https://www.liangtengyu.com:9998/images/pic_2db77749.png)\n\n  \n\n\n结论汇总：  \n\n\n1、client端发送请求到网关，目前已经维持长连接，满足要求。\n\n2、gRPC网关连接Redis缓存服务器，目前是短连接，每次请求都去创建一个连接，性能开销太大。需要单独优化\n\n3、gRPC网关转发请求到目标服务器，目前也是短连接，用完即废弃，完全没有发挥Http2.0的长连接优势。需要单独优化\n\n4、什么是 TIME\\_WAIT\n\n  \n\n\n统计服务器tcp连接状态处于`TIME_WAIT`的命令脚本：\n\n```java\nnetstat -anpt | grep TIME_WAIT | wc -l \n```\n\n  \n\n\n我们都知道TCP是三次握手，四次挥手。那挥手具体过程是什么？\n\n1、主动关闭连接的一方，调用close()，协议层发送FIN包，主动关闭方进入FIN\\_WAIT\\_1状态\n\n2、被动关闭的一方收到FIN包后，协议层回复ACK；然后被动关闭的一方，进入CLOSE\\_WAIT状态，主动关闭的一方等待对方关闭，则进入FIN\\_WAIT\\_2状态；此时，主动关闭的一方 等待 被动关闭一方的应用程序，调用close()操作\n\n3、被动关闭的一方在完成所有数据发送后，调用close()操作；此时，协议层发送FIN包给主动关闭的一方，等待对方的ACK，被动关闭的一方进入LAST\\_ACK状态；\n\n4、主动关闭的一方收到FIN包，协议层回复ACK；此时，主动关闭连接的一方，进入TIME\\_WAIT状态；而被动关闭的一方，进入CLOSED状态\n\n5、等待 2MSL（Maximum Segment Lifetime， 报文最大生存时间），主动关闭的一方，结束TIME\\_WAIT，进入CLOSED状态\n\n2MSL到底有多长呢？这个不一定，1分钟、2分钟或者4分钟，还有的30秒。不同的发行版可能会不同。在`Centos 7.6.1810` 的3.10内核版本上是60秒。\n\n  \n\n\n来张TCP状态机大图，一目了然：\n\n![pic_1a34745f.png](https://www.liangtengyu.com:9998/images/pic_1a34745f.png)\n\n  \n\n\n为什么一定要有 TIME\\_WAIT ？  \n\n\n虽然双方都同意关闭连接了，而且握手的4个报文也都协调和发送完毕，按理可以直接到CLOSED状态。但是网络是不可靠的，发起方无法确保最后发送的ACK报文一定被对方收到，比如丢包或延迟到达，对方处于LAST\\_ACK状态下的SOCKET可能会因为超时未收到ACK报文，而重发FIN报文。所以TIME\\_WAIT状态的作用就是用来重发可能丢失的ACK报文。\n\n简单讲，TIME\\_WAIT之所以等待2MSL的时长，是为了避免因为网络丢包或者网络延迟而造成的tcp传输不可靠，而这个TIME\\_WAIT状态则可以最大限度的提升网络传输的可靠性。\n\n> 注意：一个连接没有进入 CLOSED 状态之前，这个连接是不能被重用的！\n\n  \n\n\n如何优化 TIME\\_WAIT 过多的问题\n\n1、调整系统内核参数\n\n```java\nnet.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时，启用cookies来处理，可防范少量SYN攻击，默认为0，表示关闭；\nnet.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将 TIME-WAIT sockets重新用于新的TCP连接，默认为0，表示关闭；\nnet.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中 TIME-WAIT sockets的快速回收，默认为0，表示关闭。\nnet.ipv4.tcp_fin_timeout =  修改系统默认的 TIMEOUT 时间\nnet.ipv4.tcp_max_tw_buckets = 5000 表示系统同时保持TIME_WAIT套接字的最大数量，(默认是18000). 当TIME_WAIT连接数量达到给定的值时，所有的TIME_WAIT连接会被立刻清除，并打印警告信息。但这种粗暴的清理掉所有的连接，意味着有些连接并没有成功等待2MSL，就会造成通讯异常。一般不建议调整\nnet.ipv4.tcp_timestamps = 1(默认即为1)60s内同一源ip主机的socket connect请求中的timestamp必须是递增的。也就是说服务器打开了 tcp_tw_reccycle了，就会检查时间戳，如果对方发来的包的时间戳是乱跳的或者说时间戳是滞后的，那么服务器就会丢掉不回包，现在很多公司都用LVS做负载均衡，通常是前面一台LVS，后面多台后端服务器，这其实就是NAT，当请求到达LVS后，它修改地址数据后便转发给后端服务器，但不会修改时间戳数据，对于后端服务器来说，请求的源地址就是LVS的地址，加上端口会复用，所以从后端服务器的角度看，原本不同客户端的请求经过LVS的转发，就可能会被认为是同一个连接，加之不同客户端的时间可能不一致，所以就会出现时间戳错乱的现象，于是后面的数据包就被丢弃了，具体的表现通常是是客户端明明发送的SYN，但服务端就是不响应ACK，还可以通过下面命令来确认数据包不断被丢弃的现象，所以根据情况使用\n\n其他优化：\nnet.ipv4.ip_local_port_range = 1024 65535 ，增加可用端口范围，让系统拥有的更多的端口来建立链接，这里有个问题需要注意，对于这个设置系统就会从1025~65535这个范围内随机分配端口来用于连接，如果我们服务的使用端口比如8080刚好在这个范围之内，在升级服务期间，可能会出现8080端口被其他随机分配的链接给占用掉\nnet.ipv4.ip_local_reserved_ports = 7005,8001-8100 针对上面的问题，我们可以设置这个参数来告诉系统给我们预留哪些端口，不可以用于自动分配。\n```\n\n2、将短连接优化为长连接\n\n短连接工作模式：连接->传输数据->关闭连接\n\n长连接工作模式：连接->传输数据->保持连接 -> 传输数据-> 。。。->关闭连接\n\n  \n\n\n5、访问 Redis 短连接优化\n\n  \n\n\n高并发编程中，必须要使用连接池技术，把短链接改成长连接。也就是改成创建连接、收发数据、收发数据... 拆除连接，这样我们就可以减少大量创建连接、拆除连接的时间。从性能上来说肯定要比短连接好很多\n\n在 OpenResty 中，可以设置`set_keepalive` 函数，来支持长连接。\n\n`set_keepalive` 函数有两个参数：\n\n *  第一个参数：连接的最大空闲时间\n *  第二个参数：连接池大小\n\n```java\nlocal res, err = red:get(jobid)\n// redis操作完后，将连接放回到连接池中\n// 连接池大小设置成40，连接最大空闲时间设置成10秒\nred:set_keepalive(10000, 40)\n```\n\nreload nginx配置后，重新压测\n\n![pic_84abbb8b.png](https://www.liangtengyu.com:9998/images/pic_84abbb8b.png)\n\n结论：redis的连接数基本控制在40个以内。  \n\n\n> 其他的参数设置可以参考：\n> \n> https://github.com/openresty/lua-resty-redis\\#set\\_keepalive\n\n6、访问目标 Server 机器短连接优化\n\n  \n\n\nnginx 提供了一个`upstream`模块，用来控制负载均衡、内容分发。提供了以下几种负载算法：\n\n *  轮询（默认）。每个请求按时间顺序逐一分配到不同的后端服务器，如果后端服务器down掉，能自动剔除。\n *  weight(权重)。指定轮询几率，weight和访问比率成正比，用于后端服务器性能不均的情况。\n *  ip\\_hash。每个请求按访问ip的hash结果分配，这样每个访客固定访问一个后端服务器，可以解决session的问题。\n *  fair（第三方）。按后端服务器的响应时间来分配请求，响应时间短的优先分配。\n *  url\\_hash（第三方）。按访问url的hash结果来分配请求，使每个url定向到同一个后端服务器，后端服务器为缓存时比较有效。\n\n由于 `upstream`提供了`keepalive`函数，每个工作进程的高速缓存中保留的到上游服务器的空闲保持连接的最大数量，可以保持连接复用，从而减少TCP连接频繁的创建、销毁性能开销。\n\n  \n\n\n缺点：\n\nNginx官方的`upstream`不支持动态修改，而我们的目标地址是动态变化，请求时根据业务规则动态实时查询路由。为了解决这个动态性问题，我们引入`OpenResty`的`balancer_by_lua_block`。\n\n通过编写Lua脚本方式，来扩展`upstream`功能。\n\n修改`nginx.conf`的`upstream`，动态获取路由目标的IP和Port，并完成请求的转发，核心代码如下：\n\n```java\n upstream grpcservers {\n    balancer_by_lua_block{\n      local balancer = require \"ngx.balancer\"\n      local host = ngx.var.target_ip\n      local port = ngx.var.target_port\n      local ok, err = balancer.set_current_peer(host, port)\n      if not ok then\n         ngx.log(ngx.ERR, \"failed to set the current peer: \", err)\n         return ngx.exit(500)\n      end\n    }\n    keepalive 40;\n }\n```\n\n修改配置后，重启Nginx，继续压测，观察结果：\n\n![pic_fd6634df.png](https://www.liangtengyu.com:9998/images/pic_fd6634df.png)\n\nTCP连接基本都处于`ESTABLISHED`状态，优化前的`TIME_WAIT`状态几乎没有了。  \n\n\n```java\n[root@tf-gw-64bd9f775c-qvpcx nginx]#  netstat -na | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'\nLISTEN 2\nESTABLISHED 86\nTIME_WAIT 242\n```\n\n写在最后\n\n本文主要是解决gRPC的请求转发问题，构建一个网关系统，技术选型OpenResty，既保留了Nginx的高性能又兼具了OpenResty动态易扩展。然后针对编写的LUA代码，性能压测，不断调整优化，解决各个链路区间的TCP连接保证可重复使用。  \n\n出自：https://mp.weixin.qq.com/s/JtlSRbUVPWc_1gjuV6NxNw","tags":["架构","网关"],"categories":["网关"]},{"title":"公司为什么不让写sql","url":"/2021/06/12/我不写sql的原因/","content":"\n# 开始\n前几天有个同事告诉我说，他发现有个外包人员在他负责的项目里下毒，写了一堆让人烦躁的sql，改都改不掉，一开始就跟那人说了不要写复杂sql不要写复杂sql，还是写了，妈的，气死了。\n<!--more-->\n为什么同事这么火大，用脚想也知道，复杂的sql让人难以维护，但是仅仅是难以维护这么简单吗？显然不是，真相远远没有你想象的那么简单。\n\n事实上，公司几乎都让开发人员用orm单表查询，然后在代码中组装，禁止开发人员写联表sql查询。\n\n下面我来分析一波：\n\n# 对比\n## 关联查询\n在实际项目开发中，前端一个列表要展示的数据往往不是来源于单表，所以我们经常要关联多表进行查询，类似下面这样：\n\n```\nselect a.id,a.staffName,b.orgName,c.roleName\nfrom\nstaff a \njoin org b on a.org_id = b.id\njoin role c on a.role_id = c.id\nwhere a.name = \"喝水\"\n```\n## 单表查询+代码层逻辑组装\n但是，我们也可以分三次单表查询，在代码中组装前端需要展示的字段。伪代码：\n\n```\n// 1.先按条件过滤查询staff\nselect * from staff where name = \"喝水\";\n// 2.从步骤1查询的结果中拿到org_id和role_id的集合，\n比如org_id的集合为（1，2），role_id的集合为（1，2，3）\n//3.单表查询org。并得到map<orgId,orgName>\nselect * from org where id in （1,2）;\n//4.单表查询role。得到map<roleId,roleName>\nselect * from role where id in （1,2,3）;\n//5.遍历步骤1的集合，冗余前端需要展示的orgName和roleName字段\n\n```\n\n表面看起来，单表查询+代码层逻辑组装的方式，不但查询了三次数据库，代码层还多了组装逻辑的代码。你肯定会问，这不是复杂化了吗？其实这种方式有许多好处：\n\n## 好处\n\n## 1.代码复用\n关联查询的sql基本没法复用，但是拆分成单表查询，就像是一个个积木，其他地方需要都可以复用这段代码。\n## 2.利于后续维护\n上面的业务场景可能还看不出来，如果关联查询很复杂，写的sql可读性必将很差，过了一段时间自己都会看不懂，更不用说后续接手你的兄弟。\n\n再一个后续业务如果变动，导致表的结构发生变化，原先写的联表查询 sql 也会变得不可用。但是我们如果是使用单表查询，这时候只需要修改其中一个查询，非常利于维护。\n## 3.效率\n实际上 mysql 并不推荐使用join和子查询去实现复杂查询。\n\njoin联表查询，会自动优化为小表驱动大表，通过索引字段进行关联。如果表数据量较小的话效率还是可以的。\n\n多表关联查询是笛卡尔乘积的方式，数据量一旦上去，需要检索的数据是以几何倍上升的。另外多表关联查询的索引设计也要好好考虑，如果索引设计的不合理，大数据量下的多表关联查询，很可能让数据库拉垮。\n\n相比之下，用单表查询+代码层逻辑组装的方式，业务逻辑更清晰，优化维护更方便，单表索引的设计也更简单，大数据量下的查询效率更高。如此说来，多几行代码，多几次数据库查询可以换取这些优点，还是挺不错的。\n\n子查询就更不用谈，效率极差。因为执行子查询时，mysql 需要创建临时表，查询完成后再删除临时表，这里多了一个创建和删除临时表的步骤，所以子查询的效率会受到影响。\n## 4.可扩展\n当数据量大到一定程度，join 查询不利于分库分表，目前 mysql 的分布式中间件,对于跨库 join 的表现来看并不好。\n\n而拆分为单表查询+代码层做冗余处理，可以更容易对数据库进行分库分表，更容易做到高性能和可扩展。\n\n# 总结\n一个系统的瓶颈往往是出在数据库上，所以不要在数据库中做业务逻辑处理，建议数据库只是作为数据存储的工具。\n\n\n\n\n\n\n\n","tags":["mybatis","架构"],"categories":["mybatis"]},{"title":"如何保证 api 接口安全","url":"/2021/06/11/api的安全性/","content":"\n### 一、摘要 \n\n在实际的业务开发过程中，我们常常会碰到需要与第三方互联网公司进行技术对接，例如支付宝支付对接、微信支付对接、高德地图查询对接等等服务，如果你是一个创业型互联网公司，大部分可能都是对接别的公司api接口。\n<!--more-->\n当你的公司体量上来了，这个时候可能有一些公司开始找你进行技术对接了，转变成由你来提供api接口，那这个时候，我们应该如何设计并保证API接口安全呢？\n\n### 二、方案介绍 \n\n最常用的方案，主要有两种：\n\n *  token方案\n *  接口签名\n\n#### 2.1、token方案 \n\n其中 token 方案，是一种在web端使用最广的接口鉴权方案，我记得在之前写过一篇《手把手教你，使用JWT实现单点登录》的文章，里面介绍的比较详细，有兴趣的朋友可以看一下，没了解的也没关系，我们在此简单的介绍一下 token 方案。\n\n![pic_ba1bdd5c.png](https://www.liangtengyu.com:9998/images/pic_ba1bdd5c.png)\n\n从上图，我们可以很清晰的看到，token 方案的实现主要有以下几个步骤：\n\n *  1、用户登录成功之后，服务端会给用户生成一个唯一有效的凭证，这个有效值被称为token\n *  2、当用户每次请求其他的业务接口时，需要在请求头部带上token\n *  3、服务端接受到客户端业务接口请求时，会验证token的合法性，如果不合法会提示给客户端；如果合法，才会进入业务处理流程。\n\n在实际使用过程中，当用户登录成功之后，生成的token存放在redis中时是有时效的，一般设置为2个小时，过了2个小时之后会自动失效，这个时候我们就需要重新登录，然后再次获取有效token。\n\ntoken方案，是目前业务类型的项目当中使用最广的方案，而且实用性非常高，可以很有效的防止黑客们进行抓包、爬取数据。\n\n但是 token 方案也有一些缺点！最明显的就是与第三方公司进行接口对接的时候，当你的接口请求量非常大，这个时候 token 突然失效了，会有大量的接口请求失败。\n\n这个我深有体会，我记得在很早的时候，跟一家中、大型互联网公司进行联调的时候，他们提供给我的接口对接方案就是token方案，当时我司的流量高峰期时候，请求他们的接口大量报错，原因就是因为token失效了，当token失效时，我们会调用他们刷新token接口，刷新完成之后，在token失效与重新刷新token这个时间间隔期间，就会出现大量的请求失败的日志，因此在实际API对接过程中，我不推荐大家采用 token方案。\n\n#### 2.2、接口签名 \n\n接口签名，顾名思义，就是通过一些签名规则对参数进行签名，然后把签名的信息放入请求头部，服务端收到客户端请求之后，同样的只需要按照已定的规则生产对应的签名串与客户端的签名信息进行对比，如果一致，就进入业务处理流程；如果不通过，就提示签名验证失败。\n\n![pic_73078ba4.png](https://www.liangtengyu.com:9998/images/pic_73078ba4.png)\n\n在接口签名方案中，主要有四个核心参数：\n\n *  1、appid表示应用ID，其中与之匹配的还有appsecret，表示应用密钥，用于数据的签名加密，不同的对接项目分配不同的appid和appsecret，保证数据安全\n *  2、timestamp 表示时间戳，当请求的时间戳与服务器中的时间戳，差值在5分钟之内，属于有效请求，不在此范围内，属于无效请求\n *  3、nonce 表示临时流水号，用于防止重复提交验证\n *  4、signature 表示签名字段，用于判断接口请求是否有效。\n\n其中签名的生成规则，分两个步骤：\n\n *  第一步：对请求参数进行一次md5加密签名\n\n```java\n//步骤一\nString 参数1 = 请求方式 + 请求URL相对地址 + 请求Body字符串;\nString 参数1加密结果= md5(参数1)\n```\n\n *  第二步：对第一步签名结果，再进行一次md5加密签名\n\n```java\n//步骤二\nString 参数2 = appsecret + timestamp + nonce + 参数1加密结果;\nString 参数2加密结果= md5(参数2)\n```\n\n参数2加密结果，就是我们要的最终签名串。\n\n接口签名方案，尤其是在接口请求量很大的情况下，依然很稳定。\n\n换句话说，你可以将接口签名看作成对token方案的一种补充。\n\n但是如果想把接口签名方案，推广到前后端对接，答案是：不适合。\n\n因为签名计算非常复杂，其次，就是容易泄漏appsecret！\n\n说了这么多，下面我们就一起来用程序实践一下吧！\n\n### 二、程序实践 \n\n#### 2.1、token方案 \n\n就像上文所说，token方案重点在于，当用户登录成功之后，我们只需要生成好对应的token，然后将其返回给前端，在下次请求业务接口的时候，需要把token带上。\n\n具体的实践，也可以分两种：\n\n *  第一种：采用uuid生成token，然后将token存放在redis中，同时设置有效期2哥小时\n *  第二种：采用JWT工具来生成token，这种token是可以跨平台的，天然支持分布式，其实本质也是采用时间戳+密钥，来生成一个token。\n\n下面，我们介绍的是第二种实现方式。\n\n首先，编写一个jwt 工具。\n\n```java\npublic class JwtTokenUtil {\n    //定义token返回头部\n    public static final String AUTH_HEADER_KEY = \"Authorization\";\n    //token前缀\n    public static final String TOKEN_PREFIX = \"Bearer \";\n    //签名密钥\n    public static final String KEY = \"q3t6w9z$C&F)J@NcQfTjWnZr4u7x\";\n    //有效期默认为 2hour\n    public static final Long EXPIRATION_TIME = 1000L*60*60*2;\n    /**\n     * 创建TOKEN\n     * @param content\n     * @return\n     */\n    public static String createToken(String content){\n        return TOKEN_PREFIX + JWT.create()\n                .withSubject(content)\n                .withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME))\n                .sign(Algorithm.HMAC512(KEY));\n    }\n    /**\n     * 验证token\n     * @param token\n     */\n    public static String verifyToken(String token) throws Exception {\n        try {\n            return JWT.require(Algorithm.HMAC512(KEY))\n                    .build()\n                    .verify(token.replace(TOKEN_PREFIX, \"\"))\n                    .getSubject();\n        } catch (TokenExpiredException e){\n            throw new Exception(\"token已失效，请重新登录\",e);\n        } catch (JWTVerificationException e) {\n            throw new Exception(\"token验证失败！\",e);\n        }\n    }\n}\n```\n\n接着，我们在登录的时候，生成一个token，然后返回给客户端。\n\n```java\n@RequestMapping(value = \"/login\", method = RequestMethod.POST, produces = {\"application/json;charset=UTF-8\"})\npublic UserVo login(@RequestBody UserDto userDto, HttpServletResponse response){\n    //...参数合法性验证\n    //从数据库获取用户信息\n    User dbUser = userService.selectByUserNo(userDto.getUserNo);\n    //....用户、密码验证\n    //创建token，并将token放在响应头\n    UserToken userToken = new UserToken();\n    BeanUtils.copyProperties(dbUser,userToken);\n    String token = JwtTokenUtil.createToken(JSONObject.toJSONString(userToken));\n    response.setHeader(JwtTokenUtil.AUTH_HEADER_KEY, token);\n    //定义返回结果\n    UserVo result = new UserVo();\n    BeanUtils.copyProperties(dbUser,result);\n    return result;\n}\n```\n\n最后，编写一个统一拦截器，用于验证客户端传入的token是否有效。\n\n```java\n@Slf4j\npublic class AuthenticationInterceptor implements HandlerInterceptor {\n    @Override\n    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {\n        // 从http请求头中取出token\n        final String token = request.getHeader(JwtTokenUtil.AUTH_HEADER_KEY);\n        //如果不是映射到方法，直接通过\n        if(!(handler instanceof HandlerMethod)){\n            return true;\n        }\n        //如果是方法探测，直接通过\n        if (HttpMethod.OPTIONS.equals(request.getMethod())) {\n            response.setStatus(HttpServletResponse.SC_OK);\n            return true;\n        }\n        //如果方法有JwtIgnore注解，直接通过\n        HandlerMethod handlerMethod = (HandlerMethod) handler;\n        Method method=handlerMethod.getMethod();\n        if (method.isAnnotationPresent(JwtIgnore.class)) {\n            JwtIgnore jwtIgnore = method.getAnnotation(JwtIgnore.class);\n            if(jwtIgnore.value()){\n                return true;\n            }\n        }\n        LocalAssert.isStringEmpty(token, \"token为空，鉴权失败！\");\n        //验证，并获取token内部信息\n        String userToken = JwtTokenUtil.verifyToken(token);\n        //将token放入本地缓存\n        WebContextUtil.setUserToken(userToken);\n        return true;\n    }\n    @Override\n    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {\n        //方法结束后，移除缓存的token\n        WebContextUtil.removeUserToken();\n    }\n}\n```\n\n在生成token的时候，我们可以将一些基本的用户信息，例如用户ID、用户姓名，存入token中，这样当token鉴权通过之后，我们只需要通过解析里面的信息，即可获取对应的用户ID，可以省下去数据库查询一些基本信息的操作。\n\n同时，使用的过程中，尽量不要存放敏感信息，因为很容易被黑客解析！\n\n#### 2.2、接口签名 \n\n同样的思路，站在服务端验证的角度，我们可以先编写一个签名拦截器，验证客户端传入的参数是否合法，只要有一项不合法，就提示错误。\n\n具体代码实践如下：\n\n```java\npublic class SignInterceptor implements HandlerInterceptor {\n\n    @Autowired\n    private AppSecretService appSecretService;\n\n    @Autowired\n    private RedisUtil redisUtil;\n\n    @Override\n    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)\n            throws Exception {\n        //appId验证\n        final String appId = request.getHeader(\"appid\");\n        if(StringUtils.isEmpty(appId)){\n            throw new CommonException(\"appid不能为空\");\n        }\n        String appSecret = appSecretService.getAppSecretByAppId(appId);\n        if(StringUtils.isEmpty(appSecret)){\n            throw new CommonException(\"appid不合法\");\n        }\n        //时间戳验证\n        final String timestamp = request.getHeader(\"timestamp\");\n        if(StringUtils.isEmpty(timestamp)){\n            throw new CommonException(\"timestamp不能为空\");\n        }\n        //大于5分钟，非法请求\n        long diff = System.currentTimeMillis() - Long.parseLong(timestamp);\n        if(Math.abs(diff) > 1000 * 60 * 5){\n            throw new CommonException(\"timestamp已过期\");\n        }\n        //临时流水号，防止重复提交\n        final String nonce = request.getHeader(\"nonce\");\n        if(StringUtils.isEmpty(nonce)){\n            throw new CommonException(\"nonce不能为空\");\n        }\n        //验证签名\n        final String signature = request.getHeader(\"signature\");\n        if(StringUtils.isEmpty(nonce)){\n            throw new CommonException(\"signature不能为空\");\n        }\n        final String method = request.getMethod();\n        final String url = request.getRequestURI();\n        final String body = StreamUtils.copyToString(request.getInputStream(), Charset.forName(\"UTF-8\"));\n        String signResult = SignUtil.getSignature(method, url, body, timestamp, nonce, appSecret);\n        if(!signature.equals(signResult)){\n            throw new CommonException(\"签名验证失败\");\n        }\n        //检查是否重复请求\n        String key = appId + \"_\" + timestamp + \"_\" + nonce;\n        if(redisUtil.exist(key)){\n            throw new CommonException(\"当前请求正在处理，请不要重复提交\");\n        }\n        //设置5分钟\n        redisUtil.save(key, signResult, 5*60);\n        request.setAttribute(\"reidsKey\",key);\n    }\n\n    @Override\n    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)\n            throws Exception {\n        //请求处理完毕之后，移除缓存\n        String value = request.getAttribute(\"reidsKey\");\n        if(!StringUtils.isEmpty(value)){\n            redisUtil.remove(value);\n        }\n    }\n\n}\n```\n\n签名工具类`SignUtil`：\n\n```java\npublic class SignUtil {\n\n    /**\n     * 签名计算\n     * @param method\n     * @param url\n     * @param body\n     * @param timestamp\n     * @param nonce\n     * @param appSecret\n     * @return\n     */\n    public static String getSignature(String method, String url, String body, String timestamp, String nonce, String appSecret){\n        //第一层签名\n        String requestStr1 = method + url + body + appSecret;\n        String signResult1 = DigestUtils.md5Hex(requestStr1);\n        //第二层签名\n        String requestStr2 = appSecret + timestamp + nonce + signResult1;\n        String signResult2 = DigestUtils.md5Hex(requestStr2);\n        return signResult2;\n    }\n}\n```\n\n签名计算，可以换成`hamc`方式进行计算，思路大致一样。\n\n### 三、小结 \n\n上面介绍的token和接口签名方案，对外都可以对提供的接口起到保护作用，防止别人篡改请求，或者模拟请求。\n\n但是缺少对数据自身的安全保护，即请求的参数和返回的数据都是有可能被别人拦截获取的，而这些数据又是明文的，所以只要被拦截，就能获得相应的业务数据。\n\n对于这种情况，推荐大家对请求参数和返回参数进行加密处理，例如RSA、AES等加密工具。\n\n同时，在生产环境，采用`https`方式进行传输，可以起到很好的安全保护作用！\n","tags":["mybatis","架构"],"categories":["mybatis"]},{"title":"我的 MySQL 经验之谈","url":"/2021/06/05/mysql开发实践/","content":"# 表结构设计\n\n昨天有个粉丝问我关于 MySQL 在实际企业中的运用以及要注意哪些问题，今天我来总结一下：\n<!--more-->\n## 1.主键\n\n在实际项目中，主键id推荐使用数据库自增ID（类型为bigint）和雪花算法生成的随机ID。\n\n业务量小，采用自增ID；业务量大，推荐采用雪花算法。\n\n使用自增id的缺点：\n\n1、自增id如果暴露，容易被人发现规律\n\n2、对于高并发的情况下，innodb引擎在按主键进行插入的时候会造成明显的锁争用，主键的上界会成为争抢的热点。\n\n3、单表数据量达到一定程度后要分库分表，导致ID重复，解决起来比较麻烦\n\n## 2.外键\n不要使用外键与其它表进行关联，避免高并发场景的性能问题\n\n1、外键是极影响并发性能的，因为当存在外键约束的时候，MySQL会进行即时检查，每次insert和update都要要去扫描此记录是否满足\n\n2、耦合度高，后期很难进行分库分表\n## 3.合适的字段类型和长度\n\n数据库的资源是很宝贵的，合适的字段类型和长度，不但节约数据库表空间和节约索引存储空间，更重要的是提升检索速度\n\n1、对于固定长度的坚决使用char/tiyint等类型\n\n2、对于不固定长度但是确定了总长度的使用varchar类型\n\n3、不要用varchar/char 存储长字符串,直接用text。并且让长字符串拆分到另一个表，保持主表尽量瘦小\n\n## 4.字段冗余\n允许适当冗余其他表的字段，以提高查询性能，但必须考虑数据一致而且不要冗余过长的字段\n\n## 5.字段默认值\n避免将字段默认值设为null\n\n对MySQL来说，会使得索引、索引统计和值的比较都更加复杂\nNULL会参与字段比较，所以对效率有一部分影响，比如!=, <>等\n\n# 索引设计\n\n## 1.覆盖索引\n对于count和group场景，请使用覆盖索引，提高查询性能。\n\n索引就像是一本书的目录，如果查询的内容都只和目录上的内容有关，那mysql只要扫描索引结构就能得到查询结果，比如我给student表增加了个索引：\n\n```\nALTER TABLE `student` ADD INDEX index_name (name,gender);\n```\n用到索引覆盖的sql语句：\n\n```\nselect name,gender from student;\nselect name,gender from student where name='不高兴就喝水' and gender=1;\nselect name,gender from student group by name,gender;\nselect name,gender,count(1) from student group by name,gender;\nselect name from student group by name;\nselect name ,count(1) from student group by name;\n\n```\n## 2.复合索引\n设计索引的时候尽量使用复合索引，并将区分度高的字段放在前面\n\n那么什么是区分度高的字段呢？\n\n执行如下语句，假设查询结果为 0.9,0.1,1000，可以看到name列的选择性最高，因此将其作为联合索引的第一列，即建立(name, gender)的联合索引\n```\nselect count(distinct name) / count(*), count(distinct gender) / count(*), count(*) from student\n```\n根据索引最左匹配原则，能够触发这个联合索引的sql语句是：\n\n```\nselect name,gender from student where name=\"不高兴就喝水\" and gender=1\nselect name,gender from student where gender=1 and name=\"不高兴就喝水\" ;\nselect name,gender from student where name=\"不高兴就喝水\";\nselect name,gender from student where age=18 name='不高兴就喝水';\n\n```\n## 3.索引失效\n以下几个操作会引起索引失效：\n\n1.在索引列上做计算、函数、转换类型等操作\n\n2.违反最左匹配原则\n\n3.like以通配符开头（例如：'%喝水'）\n\n4.防止隐式转换，比如：索引的字段为字符串类型，查询的时候不加单引号（ name为vachar类型，查询的时候 where name = 1）\n\n5.or连接，等等..\n\n## 4.唯一索引\n对于需要保证表中唯一的字段，即使在应用层做了校验，也必须建立唯一索引\n\n注意：在性能上，唯一索引在查询时的性能要比非聚集索引高，但是在插入与更新时要比非聚集索引低\n\n\n## 5.长字符索引\n在长度较长的字段上建立索引时，必须指定索引长度，没必要对全字段建立索引\n\n索引的长度与区分度是一对矛盾体，一般对于字符串类型的字段，设置索引的长度为 20，区分度会高达 90%以上，可以使用以下sql来确定区分度：\n\n```\nselect count(distinct left(列名,索引长度)) / count(*) from 表名\n```\n# 语句设计\n## 1.逻辑删除\n大多时候，删除操作应该采用逻辑删除，不能物理删除。\n我们必须承认数据是无价之宝，在很多时候，数据的价值是远远高于人工成本的。\n\n正式环境的数据库账号往往是没有delete权限的，避免误操作，删库跑路等等\n\n并且update操作比delete性能高\n\n## 2.in的使用\n虽然in的数量 MySQL 并没有做具体的限制，但对整个 SQL 语句的长度做了限制。 不要进行 in 大量数据集合的操作，若实在无法避免，可以分批次查询，一次in 一定数量集。 \n\n\n## 3.inner join的使用\n当我们使用关联查询的时候，用小表驱动大表的方式效率会提升很多。而 inner join 会自动的进行小表驱动大表的优化\n\n## 4.触发器和存储过程\n避免使用触发器和存储过程，难以调试和扩展不说，更是没有可移植性，这些边缘功能最好不用。\n\n## 5.count\nmysql5.7对count(*) 进行了优化\n\n所以现在 count( *)和count(1)的执行效率是一样的。\n\n而count(字段)因为有sql解析的过程，不仅效率会慢，而且不会对null值进行统计\n\n## 6.避免大事务\n\n大事务就是运行的时间比较长，操作的数据比较多的事务\n大事务会影响数据库的性能，应当尽量把大事务拆成若干个小事务，禁止写过于复杂的sql语句，除了造成大事务不说，还会让别人头大，无法维护。\n\n目前能想到的就是这，欢迎补充～\n\n","tags":["mybatis","架构"],"categories":["mybatis"]},{"title":"mybatis的架构原理","url":"/2021/05/25/mybatis架构原理/","content":"### MyBatis功能架构设计 \n<!--more-->\n\n![pic_2ba29528.png](https://www.liangtengyu.com:9998/images/pic_2ba29528.png)\n\nimage.png\n\n###### 功能架构讲解： \n\n我们把Mybatis的功能架构分为三层：\n\n(1)API接口层：提供给外部使用的接口API，开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。\n\n(2)数据处理层：负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。\n\n(3)基础支撑层：负责最基础的功能支撑，包括连接管理、事务管理、配置加载和缓存处理，这些都是共用的东西，将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。  \n  \n\n\n### 框架架构 \n\n![pic_46f4a630.png](https://www.liangtengyu.com:9998/images/pic_46f4a630.png)\n\n###### 框架架构讲解： \n\n这张图从上往下看。MyBatis的初始化，会从mybatis-config.xml配置文件，解析构造成Configuration这个类，就是图中的红框。\n\n(1)加载配置：配置来源于两个地方，一处是配置文件，一处是Java代码的注解，将SQL的配置信息加载成为一个个MappedStatement对象（包括了传入参数映射配置、执行的SQL语句、结果映射配置），存储在内存中。\n\n(2)SQL解析：当API接口层接收到调用请求时，会接收到传入SQL的ID和传入对象（可以是Map、JavaBean或者基本数据类型），Mybatis会根据SQL的ID找到对应的MappedStatement，然后根据传入参数对象对MappedStatement进行解析，解析后可以得到最终要执行的SQL语句和参数。\n\n(3)SQL执行：将最终得到的SQL和参数拿到数据库进行执行，得到操作数据库的结果。\n\n(4)结果映射：将操作数据库的结果按照映射的配置进行转换，可以转换成HashMap、JavaBean或者基本数据类型，并将最终结果返回。  \n  \n\n\n### MyBatis核心类 \n\n###### 1、SqlSessionFactoryBuilder \n\n每一个MyBatis的应用程序的入口是SqlSessionFactoryBuilder。\n\n它的作用是通过XML配置文件创建Configuration对象（当然也可以在程序中自行创建），然后通过build方法创建SqlSessionFactory对象。没有必要每次访问Mybatis就创建一次SqlSessionFactoryBuilder，通常的做法是创建一个全局的对象就可以了。示例程序如下：\n\n```java\nprivate static SqlSessionFactoryBuilder sqlSessionFactoryBuilder;\nprivate static SqlSessionFactory sqlSessionFactory;\n\nprivate static void init() throws IOException {\n    String resource = \"mybatis-config.xml\";\n    Reader reader = Resources.getResourceAsReader(resource);\n    sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();\n    sqlSessionFactory = sqlSessionFactoryBuilder.build(reader);\n}\n```\n\n`org.apache.ibatis.session.Configuration 是mybatis初始化的核心。`\n\nmybatis-config.xml中的配置，最后会解析xml成Configuration这个类。\n\nSqlSessionFactoryBuilder根据传入的数据流(XML)生成Configuration对象，然后根据Configuration对象创建默认的SqlSessionFactory实例。\n\n###### 2、SqlSessionFactory对象由SqlSessionFactoryBuilder创建： \n\n它的主要功能是创建SqlSession对象，和SqlSessionFactoryBuilder对象一样，没有必要每次访问Mybatis就创建一次SqlSessionFactory，通常的做法是创建一个全局的对象就可以了。SqlSessionFactory对象一个必要的属性是Configuration对象，它是保存Mybatis全局配置的一个配置对象，通常由SqlSessionFactoryBuilder从XML配置文件创建。这里给出一个简单的示例：\n\n```java\n<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<!DOCTYPE configuration PUBLIC \n   \"-//mybatis.org//DTD Config 3.0//EN\"\n   \"http://mybatis.org/dtd/mybatis-3-config.dtd\">\n<configuration>\n   <!-- 配置别名 -->\n   <typeAliases>\n       <typeAlias type=\"org.iMybatis.abc.dao.UserDao\" alias=\"UserDao\" />\n       <typeAlias type=\"org.iMybatis.abc.dto.UserDto\" alias=\"UserDto\" />\n   </typeAliases>\n    \n   <!-- 配置环境变量 -->\n   <environments default=\"development\">\n       <environment id=\"development\">\n           <transactionManager type=\"JDBC\" />\n           <dataSource type=\"POOLED\">\n               <property name=\"driver\" value=\"com.mysql.jdbc.Driver\" />\n               <property name=\"url\" value=\"jdbc:mysql://127.0.0.1:3306/iMybatis?characterEncoding=GBK\" />\n               <property name=\"username\" value=\"iMybatis\" />\n               <property name=\"password\" value=\"iMybatis\" />\n           </dataSource>\n       </environment>\n   </environments>\n   \n   <!-- 配置mappers -->\n   <mappers>\n       <mapper resource=\"org/iMybatis/abc/dao/UserDao.xml\" />\n   </mappers>\n   \n</configuration>\n```\n\n###### 3、SqlSession \n\nSqlSession对象的主要功能是完成一次数据库的访问和结果的映射，它类似于数据库的session概念，由于不是线程安全的，所以SqlSession对象的作用域需限制方法内。SqlSession的默认实现类是DefaultSqlSession，它有两个必须配置的属性：Configuration和Executor。Configuration前文已经描述这里不再多说。SqlSession对数据库的操作都是通过Executor来完成的。\n\nSqlSession ：默认创建DefaultSqlSession 并且开启一级缓存，创建执行器 、赋值。\n\nSqlSession有一个重要的方法getMapper，顾名思义，这个方式是用来获取Mapper对象的。什么是Mapper对象？根据Mybatis的官方手册，应用程序除了要初始并启动Mybatis之外，还需要定义一些接口，接口里定义访问数据库的方法，存放接口的包路径下需要放置同名的XML配置文件。\n\nSqlSession的getMapper方法是联系应用程序和Mybatis纽带，应用程序访问getMapper时，Mybatis会根据传入的接口类型和对应的XML配置文件生成一个代理对象，这个代理对象就叫Mapper对象。应用程序获得Mapper对象后，就应该通过这个Mapper对象来访问Mybatis的SqlSession对象，这样就达到里插入到Mybatis流程的目的。\n\n```java\nSqlSession session= sqlSessionFactory.openSession();  \nUserDao userDao = session.getMapper(UserDao.class);  \nUserDto user = new UserDto();  \nuser.setUsername(\"iMybatis\");  \nList<UserDto> users = userDao.queryUsers(user);  \n\npublic interface UserDao {\n    public List<UserDto> queryUsers(UserDto user) throws Exception;\n}\n\n<?xml version=\"1.0\" encoding=\"UTF-8\" ?>  \n<!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">  \n<mapper namespace=\"org.iMybatis.abc.dao.UserDao\">  \n    <select id=\"queryUsers\" parameterType=\"UserDto\" resultType=\"UserDto\"  \n        useCache=\"false\">  \n        <![CDATA[ \n        select * from t_user t where t.username = #{username} \n        ]]>  \n    </select>  \n</mapper>\n```\n\n###### 4、Executor \n\nExecutor对象在创建Configuration对象的时候创建，并且缓存在Configuration对象里。Executor对象的主要功能是调用StatementHandler访问数据库，并将查询结果存入缓存中（如果配置了缓存的话）。\n\n###### 5、StatementHandler \n\nStatementHandler是真正访问数据库的地方，并调用ResultSetHandler处理查询结果。\n\n###### 6、ResultSetHandler \n\n处理查询结果。  \n  \n\n\n### MyBatis成员层次&职责 \n\n![pic_a7986057.png](https://www.liangtengyu.com:9998/images/pic_a7986057.png)\n\nimage.png\n\n1.  SqlSession 作为MyBatis工作的主要顶层API，表示和数据库交互的会话，完成必要数据库增删改查功能\n2.  Executor MyBatis执行器，是MyBatis 调度的核心，负责SQL语句的生成和查询缓存的维护\n3.  StatementHandler 封装了JDBC Statement操作，负责对JDBCstatement的操作，如设置参数、将Statement结果集转换成List集合。\n4.  ParameterHandler 负责对用户传递的参数转换成JDBC Statement 所需要的参数\n5.  ResultSetHandler \\*负责将JDBC返回的ResultSet结果集对象转换成List类型的集合；\n6.  TypeHandler 负责java数据类型和jdbc数据类型之间的映射和转换\n7.  MappedStatement MappedStatement维护了一条<select|update|delete|insert>节点的封\n8.  SqlSource 负责根据用户传递的parameterObject，动态地生成SQL语句，将信息封装到BoundSql对象中，并返回\n9.  BoundSql 表示动态生成的SQL语句以及相应的参数信息\n10. Configuration MyBatis所有的配置信息都维持在Configuration对象之中\n\n来源：https://www.jianshu.com/p/15781ec742f2","tags":["mybatis","架构"],"categories":["mybatis"]},{"title":"JVM修炼《1》之java类的加载机制","url":"/2021/05/08/java虚拟机1/","content":"当程序使用某个类时，如果该类还没被初始化，加载到内存中，则系统会通过加载、连接、初始化三个过程来对该类进行初始化。该过程就被称为类的初始化。\n<!--more-->\n# 1.什么是类加载\n类的加载指的是将类的.class文件中的二进制数据读入到内存中，将其放在运行时数据区的方法区内，然后在堆区创建一个java.lang.Class对象，用来封装类在方法区内的数据结构。\n\n类加载的最终产品是位于堆区中的Class对象，Class对象封装了类在方法区内的数据结构，并且向Java程序员提供了访问方法区内的数据结构的接口。\n\n![WechatIMG207.png](https://i.loli.net/2021/05/08/inIJwKNvlWEA5jG.png)\n\n类加载器并不需要等到某个类被“首次主动使用”时再加载它，JVM规范允许类加载器在预料某个类将要被使用时就预先加载它，如果在预先加载的过程中遇到了.class文件缺失或存在错误，类加载器必须在程序首次主动使用该类时才报告错误（LinkageError错误）如果这个类一直没有被程序主动使用，那么类加载器就不会报告错误。\n\n## 加载.class文件的方式\n\n- 从本地系统中直接加载\n- 通过网络下载.class文件\n- 从zip，jar等归档文件中加载.class文件\n- 从专有数据库中提取.class文件\n- 将Java源文件动态编译为.class文件\n\n# 2.类的生命周期\n类的生命周期如下：\n![WechatIMG208.png](https://i.loli.net/2021/05/08/3bLJwQ4MGFzYKXt.png)\n\n其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中，加载、验证、准备和初始化这四个阶段发生的顺序是确定的，而解析阶段则不一定，它在某些情况下可以在初始化阶段之后开始，这是为了支持Java语言的运行时绑定（也成为动态绑定或晚期绑定）。另外注意这里的几个阶段是按顺序开始，而不是按顺序进行或完成，因为这些阶段通常都是互相交叉地混合进行的，通常在一个阶段执行的过程中调用或激活另一个阶段。\n\n## 加载\n查找并加载类的二进制数据加载时类加载过程的第一个阶段，在加载阶段，虚拟机需要完成以下三件事情：\n\n- 通过一个类的全限定名来获取其定义的二进制字节流。\n- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。\n- 在Java堆中生成一个代表这个类的java.lang.Class对象，作为对方法区中这些数据的访问入口。\n\n相对于类加载的其他阶段而言，加载阶段（准确地说，是加载阶段获取类的二进制字节流的动作）是可控性最强的阶段，因为开发人员既可以使用系统提供的类加载器来完成加载，也可以自定义自己的类加载器来完成加载。\n\n加载阶段完成后，虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中，而且在Java堆中也创建一个java.lang.Class类的对象，这样便可以通过该对象访问方法区中的这些数据。\n\n## 连接\n### 验证：确保被加载的类的正确性\n\n验证是连接阶段的第一步，这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求，并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作：\n\n**文件格式验证**：验证字节流是否符合Class文件格式的规范；例如：是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。\n\n**元数据验证**：对字节码描述的信息进行语义分析（注意：对比javac编译阶段的语义分析），以保证其描述的信息符合Java语言规范的要求；例如：这个类是否有父类，除了java.lang.Object之外。\n\n**字节码验证**：通过数据流和控制流分析，确定程序语义是合法的、符合逻辑的。\n\n**符号引用验证**：确保解析动作能正确执行。\n验证阶段是非常重要的，但不是必须的，它对程序运行期没有影响，如果所引用的类经过反复验证，那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施，以缩短虚拟机类加载的时间。\n\n### 准备：为类的静态变量分配内存，并将其初始化为默认值\n\n准备阶段是正式为类变量分配内存并设置类变量初始值的阶段，这些内存都将在方法区中分配。对于该阶段有以下几点需要注意：\n\n1、这时候进行内存分配的仅包括类变量（static），而不包括实例变量，实例变量会在对象实例化时随着对象一块分配在Java堆中。\n\n2、这里所设置的初始值通常情况下是数据类型默认的零值（如0、0L、null、false等），而不是被在Java代码中被显式地赋予的值。\n\n假设一个类变量的定义为：\n\n\n```\npublic static int value = 3；\n```\n\n\n那么变量value在准备阶段过后的初始值为0，而不是3，因为这时候尚未开始执行任何Java方法，而把value赋值为3的public static指令是在程序编译后，存放于类构造器<clinit>（）方法之中的，所以把value赋值为3的动作将在初始化阶段才会执行。\n\n这里还需要注意如下几点：\n\n对基本数据类型来说，对于类变量（static）和全局变量，如果不显式地对其赋值而直接使用，则系统会为其赋予默认的零值，而对于局部变量来说，在使用前必须显式地为其赋值，否则编译时不通过。\n\n对于同时被static和final修饰的常量，必须在声明的时候就为其显式地赋值，否则编译时不通过；而只被final修饰的常量则既可以在声明时显式地为其赋值，也可以在类初始化时显式地为其赋值，总之，在使用前必须为其显式地赋值，系统不会为其赋予默认零值。\n\n对于引用数据类型reference来说，如数组引用、对象引用等，如果没有对其进行显式地赋值而直接使用，系统都会为其赋予默认的零值，即null。\n\n如果在数组初始化时没有对数组中的各元素赋值，那么其中的元素将根据对应的数据类型而被赋予默认的零值。\n\n3、如果类字段的字段属性表中存在ConstantValue属性，即同时被final和static修饰，那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。\n\n假设上面的类变量value被定义为： \n\n\n```\npublic static final int value = 3；\n```\n\n\n编译时Javac将会为value生成ConstantValue属性，在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。我们可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中\n\n### 解析：把类中的符号引用转换为直接引用\n解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程，解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标，可以是任何字面量。\n\n直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。\n\n### 初始化\n\n初始化，为类的静态变量赋予正确的初始值，JVM负责对类进行初始化，主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式：\n\n①声明类变量是指定初始值\n\n②使用静态代码块为类变量指定初始值\nJVM初始化步骤\n\n1、假如这个类还没有被加载和连接，则程序先加载并连接该类\n\n2、假如该类的直接父类还没有被初始化，则先初始化其直接父类\n\n3、假如类中有初始化语句，则系统依次执行这些初始化语句\n\n类初始化时机：只有当对类的主动使用的时候才会导致类的初始化，类的主动使用包括以下六种：\n\n- 创建类的实例，也就是new的方式\n- 访问某个类或接口的静态变量，或者对该静态变量赋值\n- 调用类的静态方法\n- 反射（如Class.forName(“com.shengsiyuan.Test”)）\n- 初始化某个类的子类，则其父类也会被初始化\n- Java虚拟机启动时被标明为启动类的类（Java Test），直接使用java.exe命令来运行某个主类\n### 结束生命周期\n在如下几种情况下，Java虚拟机将结束生命周期\n\n- 执行了System.exit()方法\n- 程序正常执行结束\n- 程序在执行过程中遇到了异常或错误而异常终止\n- 由于操作系统出现错误而导致Java虚拟机进程终止\n\n# 3.类加载器\n下面我们看这段代码：\n```\npackage cn.thinkjoy.uc.service.impl.business.recruit;\n\npublic class ClassLoaderTest {\n     public static void main(String[] args) {\n        ClassLoader loader = Thread.currentThread().getContextClassLoader();\n        System.out.println(loader);\n        System.out.println(loader.getParent());\n        System.out.println(loader.getParent().getParent());\n    }\n}\n```\n运行后输出：\n\n```\nsun.misc.Launcher$AppClassLoader@18b4aac2\nsun.misc.Launcher$ExtClassLoader@28ba21f3\nnull\n```\n从上面的结果可以看出，并没有获取到ExtClassLoader的父Loader，原因是Bootstrap Loader（引导类加载器）是用C语言实现的，找不到一个确定的返回父Loader的方式，于是就返回null。\n\n这几种类加载器的层次关系如下图所示：\n\n![WechatIMG209.png](https://i.loli.net/2021/05/08/U3rTBKCfuDs5lyE.png)\n\n**注意：这里父类加载器并不是通过继承关系来实现的，而是采用组合实现的。**\n\n站在Java虚拟机的角度来讲，只存在两种不同的类加载器：启动类加载器：它使用C++实现（这里仅限于Hotspot，也就是JDK1.5之后默认的虚拟机，有很多其他的虚拟机是用Java语言实现的），是虚拟机自身的一部分；所有其它的类加载器：这些类加载器都由Java语言实现，独立于虚拟机之外，并且全部继承自抽象类java.lang.ClassLoader，这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。\n\n站在Java开发人员的角度来看，类加载器可以大致划分为以下三类：\n\n**启动类加载器**：Bootstrap ClassLoader，负责加载存放在JDK\\jre\\lib(JDK代表JDK的安装目录，下同)下，或被-Xbootclasspath参数指定的路径中的，并且能被虚拟机识别的类库（如rt.jar，所有的java.开头的类均被Bootstrap ClassLoader加载）。启动类加载器是无法被Java程序直接引用的。\n\n**扩展类加载器**：Extension ClassLoader，该加载器由sun.misc.Launcher$ExtClassLoader实现，它负责加载JDK\\jre\\lib\\ext目录中，或者由java.ext.dirs系统变量指定的路径中的所有类库（如javax.开头的类），开发者可以直接使用扩展类加载器。\n\n**应用程序类加载器**：Application ClassLoader，该类加载器由sun.misc.Launcher$AppClassLoader来实现，它负责加载用户类路径（ClassPath）所指定的类，开发者可以直接使用该类加载器，如果应用程序中没有自定义过自己的类加载器，一般情况下这个就是程序中默认的类加载器。\n\n应用程序都是由这三种类加载器互相配合进行加载的，如果有必要，我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件，因此如果编写了自己的ClassLoader，便可以做到如下几点：\n\n1、在执行非置信代码之前，自动验证数字签名。\n\n2、动态地创建符合用户特定需要的定制化构建类。\n\n3、从特定的场所取得java class，例如数据库中和网络中。\n\n## JVM类加载机制\n\n**全盘负责**，当一个类加载器负责加载某个Class时，该Class所依赖的和引用的其他Class也将由该类加载器负责载入，除非显示使用另外一个类加载器来载入\n\n**父类委托**，先让父类加载器试图加载该类，只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类\n\n**缓存机制**，缓存机制将会保证所有加载过的Class都会被缓存，当程序中需要使用某个Class时，类加载器先从缓存区寻找该Class，只有缓存区不存在，系统才会读取该类对应的二进制数据，并将其转换成Class对象，存入缓存区。这就是为什么修改了Class后，必须重启JVM，程序的修改才会生效\n\n# 4.类的加载\n类加载有三种方式：\n\n1、命令行启动应用时候由JVM初始化加载\n\n2、通过Class.forName()方法动态加载\n\n3、通过ClassLoader.loadClass()方法动态加载\n\n例子：\n\n```\npackage cn.thinkjoy.uc.service.impl.business.recruit;\n\nimport sun.jvm.hotspot.HelloWorld;\n\npublic class loaderTest {\n        public static void main(String[] args) throws ClassNotFoundException { \n                ClassLoader loader = HelloWorld.class.getClassLoader();\n                System.out.println(loader); \n                //使用ClassLoader.loadClass()来加载类，不会执行初始化块 \n                loader.loadClass(\"Test2\"); \n                //使用Class.forName()来加载类，默认会执行初始化块 \n                //Class.forName(\"Test2\"); \n                //使用Class.forName()来加载类，并指定ClassLoader，初始化时不执行静态块 \n                //Class.forName(\"Test2\", false, loader); \n        } \n}\n```\n\n```\npublic class Test2 { \n        static { \n                System.out.println(\"静态初始化块代码执行了！\"); \n        } \n}\n```\n分别切换加载方式，会有不同的输出结果。\n\nClass.forName()和ClassLoader.loadClass()区别\n\n**Class.forName()**：将类的.class文件加载到jvm中之外，还会对类进行解释，执行类中的static块；\n\n**ClassLoader.loadClass()**：只干一件事情，就是将.class文件加载到jvm中，不会执行static中的内容,只有在newInstance才会去执行static块。\n\n**Class.forName(name, initialize, loader)**：带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数，创建类的对象 。\n# 5.双亲委派模型\n双亲委派模型的工作流程是：如果一个类加载器收到了类加载的请求，它首先不会自己去尝试加载这个类，而是把请求委托给父加载器去完成，依次向上，因此，所有的类加载请求最终都应该被传递到顶层的启动类加载器中，只有当父加载器在它的搜索范围中没有找到所需的类时，即无法完成该加载，子加载器才会尝试自己去加载该类。\n\n双亲委派机制:\n\n1、当AppClassLoader加载一个class时，它首先不会自己去尝试加载这个类，而是把类加载请求委派给父类加载器ExtClassLoader去完成。\n\n2、当ExtClassLoader加载一个class时，它首先也不会自己去尝试加载这个类，而是把类加载请求委派给BootStrapClassLoader去完成。\n\n3、如果BootStrapClassLoader加载失败（例如在$JAVA_HOME/jre/lib里未查找到该class），会使用ExtClassLoader来尝试加载；\n\n4、若ExtClassLoader也加载失败，则会使用AppClassLoader来加载，如果AppClassLoader也加载失败，则会报出异常ClassNotFoundException。\nClassLoader源码分析：\n\n\n```\npublic Class<?> loadClass(String name)throws ClassNotFoundException {\n        return loadClass(name, false);\n}\n\nprotected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {\n        // 首先判断该类型是否已经被加载\n        Class c = findLoadedClass(name);\n        if (c == null) {\n            //如果没有被加载，就委托给父类加载或者委派给启动类加载器加载\n            try {\n                if (parent != null) {\n                     //如果存在父类加载器，就委派给父类加载器加载\n                    c = parent.loadClass(name, false);\n                } else {\n                //如果不存在父类加载器，就检查是否是由启动类加载器加载的类，通过调用本地方法native Class findBootstrapClass(String name)\n                    c = findBootstrapClass0(name);\n                }\n            } catch (ClassNotFoundException e) {\n             // 如果父类加载器和启动类加载器都不能完成加载任务，才调用自身的加载功能\n                c = findClass(name);\n            }\n        }\n        if (resolve) {\n            resolveClass(c);\n        }\n        return c;\n    }\n```\n双亲委派模型意义：\n\n1.系统类防止内存中出现多份同样的字节码\n\n2.保证Java程序安全稳定运行\n# 6.自定义类加载器\n通常情况下，我们都是直接使用系统类加载器。但是，有的时候，我们也需要自定义类加载器。比如应用是通过网络来传输 Java类的字节码，为保证安全性，这些字节码经过了加密处理，这时系统类加载器就无法对其进行加载，这样则需要自定义类加载器来实现。自定义类加载器一般都是继承自ClassLoader类，从上面对loadClass方法来分析来看，我们只需要重写 findClass 方法即可。下面我们通过一个示例来演示自定义类加载器的流程：\n\n```\nimport java.io.*;\n\npublic class MyClassLoader extends ClassLoader {\n    private String root;\n\n    protected Class<?> findClass(String name) throws ClassNotFoundException {\n        byte[] classData = loadClassData(name);\n        if (classData == null) {\n            throw new ClassNotFoundException();\n        } else {\n            return defineClass(name, classData, 0, classData.length);\n        }\n    }\n\n    private byte[] loadClassData(String className) {\n        String fileName = root + File.separatorChar\n                + className.replace('.', File.separatorChar) + \".class\";\n        try {\n            InputStream ins = new FileInputStream(fileName);\n            ByteArrayOutputStream baos = new ByteArrayOutputStream();\n            int bufferSize = 1024;\n            byte[] buffer = new byte[bufferSize];\n            int length = 0;\n            while ((length = ins.read(buffer)) != -1) {\n                baos.write(buffer, 0, length);\n            }\n            return baos.toByteArray();\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        return null;\n    }\n\n    public String getRoot() {\n        return root;\n    }\n\n    public void setRoot(String root) {\n        this.root = root;\n    }\n\n    public static void main(String[] args)  {\n\n        MyClassLoader classLoader = new MyClassLoader();\n        classLoader.setRoot(\"E:\\\\temp\");\n\n        Class<?> testClass = null;\n        try {\n            testClass = classLoader.loadClass(\"com.neo.classloader.Test2\");\n            Object object = testClass.newInstance();\n            System.out.println(object.getClass().getClassLoader());\n        } catch (ClassNotFoundException e) {\n            e.printStackTrace();\n        } catch (InstantiationException e) {\n            e.printStackTrace();\n        } catch (IllegalAccessException e) {\n            e.printStackTrace();\n        }\n    }\n}\n```\n自定义类加载器的核心在于对字节码文件的获取，如果是加密的字节码则需要在该类中对文件进行解密。由于这里只是演示，我并未对class文件进行加密，因此没有解密的过程。这里有几点需要注意：\n\n1、这里传递的文件名需要是类的全限定性名称，即com.paddx.test.classloading.Test格式的，因为 defineClass 方法是按这种格式进行处理的。\n\n2、最好不要重写loadClass方法，因为这样容易破坏双亲委托模式。\n\n3、这类Test 类本身可以被 AppClassLoader类加载，因此我们不能把com/paddx/test/classloading/Test.class放在类路径下。否则，由于双亲委托机制的存在，会直接导致该类由AppClassLoader加载，而不会通过我们自定义类加载器来加载。\n\n参考相关文章：\n\n[https://segmentfault.com/a/1190000005608960](https://note.youdao.com/)\n[https://blog.csdn.net/ns_code/article/details/17881581](https://note.youdao.com/)\n[https://blog.csdn.net/duoyu779553/article/details/105878755](https://note.youdao.com/)","tags":["jvm","java基础"],"categories":["jvm"]},{"title":"怎么保证数据库和缓存的一致性","url":"/2021/05/06/缓存一致性/","content":"\n# 缓存的用法\n在项目中我们经常用缓存来缓解数据库的压力：\n<!--more-->\n![4.png](https://i.loli.net/2021/05/06/TkCxneg6Xb8wz1l.png)\n\n但是对于写缓存，你知道怎么保证缓存与数据库的数据一致性吗？\n让我们来探讨一下。\n# 先更新数据库，再更新缓存\n![1.png](https://i.loli.net/2021/05/06/vPIEsakKgzhNir5.png)\n请求B是最后请求的，那么应该是他最后更新缓存为正确的数据，但是有可能请求A处理的更慢，所以请求A更新了最后的缓存。\n\n# 先删除缓存，再更新数据库\n\n先删除缓存，数据库还没有更新成功，此时如果读取缓存，缓存不存在，去数据库中读取到的是旧值，然后更新缓存，缓存不一致发生。如图：\n![5.png](https://i.loli.net/2021/05/06/OSJALXQ6mfpdrnh.png)\n# 延时双删\n上面的问题可以用延时双删的方案来解决，思路是，更新完数据库之后，再sleep一段时间，然后再次删除缓存。\n\nsleep的时间要对业务读写缓存的时间做出评估，sleep时间大于读写缓存的时间即可。\n\n流程如下：\n![7.png](https://i.loli.net/2021/05/06/t1e3CwEA4Rj8smi.png)\n\n1. 线程1删除缓存，然后去更新数据库\n \n2. 线程2来读缓存，发现缓存已经被删除，所以直接从数据库中读取，这时候由于线程1还没有更新完成，所以读到的是旧值，然后把旧值写入缓存\n\n3. 线程1，根据估算的时间，sleep，由于sleep的时间大于线程2读数据+写缓存的时间，所以缓存被再次删除\n\n4. 如果还有其他线程来读取缓存的话，就会再次从数据库中读取到最新值\n\n# 先更新数据库，再删除缓存\n当然，这样也会有并发问题。\n比如：\n\n![3.png](https://i.loli.net/2021/05/06/gPr6noaKz9Q7D1R.png)\n\n但是数据库读操作速度远快于写操作，所以存在脏数据的可能性为0。\n当然如果您问，如果真的存在怎么办？\n简单，双删就行了，即第一次删除缓存之后，等待一段时间重新再删一次。\n当然您如果还问，删除缓存失败了怎么办，解决方法如下：\n![8.png](https://i.loli.net/2021/05/06/yzOYQ2cDkgLGjn5.png)\n即引入消息队列，删除缓存失败的记录下来重复删除，直到成功为止。如此一来，万无一失。\n\n# 其他解决方案\n## 设置缓存过期时间\n每次放入缓存的时候，设置一个过期时间，比如5分钟，以后的操作只修改数据库，不操作缓存，等待缓存超时后从数据库重新读取。\n\n如果对于一致性要求不是很高的情况，可以采用这种方案。\n\n这个方案还会有另外一个问题，就是如果数据更新的特别频繁，不一致性的问题就很大了。\n\n# 总结\n首先，我们要明确一点，缓存不是更新，而应该是删除。\n\n为什么呢？\n我们用先更新数据库，再删除缓存来举例。\n\n如果是更新的话，那就是先更新数据库，再更新缓存。\n\n举个例子：如果数据库1小时内更新了1000次，那么缓存也要更新1000次，但是这个缓存可能在1小时内只被读取了1次，那么这1000次的更新有必要吗？\n\n反过来，如果是删除的话，就算数据库更新了1000次，那么也只是做了1次缓存删除，只有当缓存真正被读取的时候才去数据库读取。\n\n删除缓存有以下两种方式：\n\n1.先删除缓存，再更新数据库。解决方案是使用延迟双删。\n\n2.先更新数据库，再删除缓存。解决方案是消息队列或者其他binlog同步，引入消息队列会带来更多的问题，并不推荐直接使用。\n\n针对缓存一致性要求不是很高的场景，那么只通过设置超时时间就行了。","tags":["技能包","解决方案"],"categories":["解决方案"]},{"title":"《微服务设计》第12章 总结","url":"/2021/04/24/微服务设计12/","content":"待输出\n<!--more-->","tags":["架构","微服务"],"categories":["架构探险","微服务设计"]},{"title":"《微服务设计》第11章 规模化微服务","url":"/2021/04/24/微服务设计11/","content":"待输出\n<!--more-->","tags":["架构","微服务"],"categories":["架构探险","微服务设计"]},{"title":"《微服务设计》第10章 康威定律和系统设计","url":"/2021/04/24/微服务设计10/","content":"待输出\n<!--more-->","tags":["架构","微服务"],"categories":["架构探险","微服务设计"]},{"title":"《微服务设计》第9章 安全","url":"/2021/04/24/微服务设计9/","content":"待输出\n<!--more-->","tags":["架构","微服务"],"categories":["架构探险","微服务设计"]},{"title":"《微服务设计》第8章 监控","url":"/2021/04/24/微服务设计8/","content":"待输出\n<!--more-->","tags":["架构","微服务"],"categories":["架构探险","微服务设计"]},{"title":"《微服务设计》第7章 测试","url":"/2021/04/24/微服务设计7/","content":"待输出\n<!--more-->","tags":["架构","微服务"],"categories":["架构探险","微服务设计"]},{"title":"《微服务设计》第6章 部署","url":"/2021/04/24/微服务设计6/","content":"待输出\n<!--more-->","tags":["架构","微服务"],"categories":["架构探险","微服务设计"]},{"title":"《微服务设计》第5章 分解单块系统","url":"/2021/04/24/微服务设计5/","content":"待输出\n<!--more-->","tags":["架构","微服务"],"categories":["架构探险","微服务设计"]},{"title":"《微服务设计》第4章 集成","url":"/2021/04/24/微服务设计4/","content":"集成是微服务相关技术中最重要的一个。做的好，可以保持微服务的自治性，做的不好会带来灾难。\n你总提及的那个词，它的含义与你想表达的意思并不一样。\n<!--more-->\n# 1.理想的集成技术\n## 1.1避免破坏性修改\n如果在一个微服务的响应中添加一个字段，服务的消费方不应该受到影响。\n## 1.2保证API的技术无关性\n微服务之间的通信应该是与技术无关的。\n## 1.3使服务的消费方易于使用\n如果消费方使用该服务比登天还难，那么无论该微服务多漂亮都没用任何意义。但同时，易于使用的服务可能内部封装了很多细节，这会增加耦合。\n## 1.4隐藏内部实现细节\n消费方与服务方的内部细节应该是分开的，如果与细节绑定，则意味着改变服务内部的一些变化，消费方也要跟着修改，这会增加修改的成本。\n# 2.数据库共享？\n共享数据库是最快的集成方式，但这种方式应该避免。\n\n如果共享了数据库，那么外部的服务则能查看内部的实现细节，并与其绑定在一起，存储在数据库中的数据结构对所有人来说都是平等的，数据库是一个很大的共享API，那么为了不影响其他服务，必须非常小心地避免修改与其他服务相关的表结构。这样的情况下，需要做大量的回归测试来保证功能的正确性。\n\n其次，消费方与服务方的特定技术绑定在了一起，如果消费方要从关系型数据库换成非关系型数据库，那么这样是不容易实现的。这不符合低耦合的原则。\n\n最后，那么原本由服务方提供的修改，现在可以由各个消费方直接操作数据库来完成，而且每个消费方可能都会有一套自己的修改方法。这不符合高内聚的原则。\n# 3.同步与异步\n如果使用同步方式进行通信，发起方发起一个远程调用后，发起方会阻塞自己并等待整个操作的完成。\n\n异步通信对于运行时间较长的任务来说比较有用，异步的通信模型有两种。\n\n一种是请求/响应方式，这与同步的不同，它是在发起一个请求时，同时注册一个回调，当服务端操作结束之后，会调用该回调。\n\n一种是基于事件的方式，服务提供方不发起请求，而是发布一个事件，然后期待调用方接收消息，并知道该怎么做，服务提供方不需要知道该或者什么会对此做出响应，这也意味着，你可以在不影响服务提供方的情况下对该事件添加新的订阅。\n# 4.编排与协同\n假如你现在一家网站上注册账户，在注册账户系统做了下面三件事：\n\n（1）在客户的积分账户上创建一条记录\n\n（2）通过EMS系统发送一个欢迎礼包\n\n（3）向客户发送欢迎电子邮件\n\n当考虑实现时，编排是一种架构风格，它由一个中心大脑来指导并驱动整个流程。它可由客户管理这个服务来承担，在创建客户时会跟积分账户服务、电子邮件服务及EMS服务通过请求/响应的方式进行通信。客户管理服务可以对当前进行到了哪一步进行跟踪，它会检查积分账户是否创建成功、电子邮件是否发送出去、EMS包裹是否寄出。这种方式的缺点是：客户管理服务承担了太多职责，它会成为网关结构的中心和很多逻辑的起点。这样会导致少量的“上帝”服务，而与其打资产的那些服务通常会沦为贫血的CRUD服务。\n\n另一种实现的风格则是协同，它仅仅告知各个系统各自的职责，具体的实现留给他们自己。就上例而言，客户管理服务创建一个事件，邮件服务、积分服务、EMS服务会订阅这些事件并做相应的处理，如果其他的服务也关心客户创建这件事情，它们只需要简单的订阅该事件即可。这种方式能显著地消除耦合，但这需要额外做一些监控工作，以保证其正确进行。我们可以建一个跟业务流程相匹配的监控服务，分别监控每个服务。\n# 5.远程过程调用\n\nrpc带来的问题，用dubbo举例：\n- 耦合问题\n- \n刚开始使用Dubbo大家都会很容易的接受Provider和Consumer的jar包方式进行服务的管理工作。而慢慢的逐渐深入使用会逐渐的体会到Dubbo的API JAR包变成了一种约束。这样就非常间接，不明显的将Provider和Consumer绑定在一起。如果其中一方出现问题，就会造成另外一方的一些问题。\n\n- 语言锁定\n\n微服务的一个准则就是每个服务可以独立的演进，独立发展。可以通过不同的编程语言对服务进行编写。而Dubbo和类似的RPC实现方式使用Jar包的方式发布接口，那么就只能使用JVM上语言进行Jar的解析与加载工作。导致必须使用相同的语言进行rpc接口的调用。\n\n- 上下文传递\n\nHttp是一种无状态服务，那并不代表RPC必须是一种无状态服务。在Http协议通过不断的发展在协议中传递了一些有状态的上下文信息，这样可以为服务提供一些有状态的信息以便在业务处理过程中使用。而RPC是一种更加纯粹的无状态服务，它没有标准化的规范导致不可能形成完善的解决方案。在Dubbo中可以借助多种方式进行上下文的传递工作，不过实现起来比较复杂。其中包括：Dubbo 上下文信息，事件通知，协议扩展\n\n- 版本兼容问题\n\n向下兼容问题对于每个软件来说都是一个非常棘手的问题。一方面我们需要让我们的软件持续的发展，另一方面需要兼容之前的代码。现在Dubbo上如果需要对接口进行新加或者变更的时候就会发现需要重新发布Dubbo API的Jar包。这样对于Provider和Consumer都是工作负担。使用版本号控制Dubbo API版本号时就得进行多服务实例启动。这个问题在Dubbo中没有很好的解决。兼容性\n负载均衡就剩下一种方案：客户端负载均衡。\n对于负载均衡来说Dubbo直接使用了客户端负载均衡的策略完成，直接摒弃了服务端负载均衡的可能行。这种情况下对于负载均衡的动态控制与动态管理工作就会形成问题。\n\n- 发布过程支撑问题\n\n线上发布一般基于新旧并存平滑过度的方式进行灰度发布，而对于长链接的Dubbo。不能很好的支撑蓝绿发布，灰度发布方式。\n\n- 运维能力\n\n1. 故障隔离能力：\n\n就现阶段技术而言，没有中很好的方式进行可以进行接口（http和rpc）的故障降级与隔离方式。导致服务中一个接口（http和rpc）发生故障后可能传播到整个服务甚至整个系统中。\n2. 服务隔离能力：\n\n对于整个系统来说部分业务在docker外，部分服务在docker内时就很难进行处理。一套体系最好在一个注册中心中进行服务组册与发现工作。做服务隔离就非常困难。多注册中心\n\n3. 指标监控能力：\n\n指标监控对于线上业务服务来说是不可或缺的内容。但是对于Dubbo来说支持的比较弱。只有几个点完成这个，所以有些鸡肋的感觉。\n4. 服务检测能力：\n\n每个线上服务都需要不断的检测服务的状态，接口响应情况等。对于使用dubbo或heissian方法的检测几乎不太可能。\n\n# 6.版本管理\n## 6.1尽可能推迟\n## 6.2及早发现破坏性修改\n尽量对修改的影响进行全面的回归。\n## 6.3使用语义话的版本管理\n## 6.4不同的接口并存\n当不得不这么做时，我们的生产环境可以同时存在接口的新老版本。\n\n假如一个接口存在着V1、V2、V3三种版本，我们可以在所有对V1的请求转换给V2，然后V2转换给V3，这样是一种平滑的过度，首先扩张服务的能力，对新老两种都支持，然后等老的消费者都采用了新的方式，再通过收缩API去掉旧的功能。\n\n我们也可以在URI中存放版本信息，但同时我们需要一套方法来对不同的请求进行路由。\n## 6.5.同时使用多个版本的服务\n短期内同时使用两个版本的服务是合理的，尤其是当你做蓝绿部署或者金丝雀发布时，在这些情况下，不同版本的服务可能只会存在几分钟或者几个小时，而且一般只会有两个版本。升级\n# 7.用户界面\n\n# 8.与第三方软件的集成","tags":["架构","微服务"],"categories":["架构探险","微服务设计"]},{"title":"《微服务设计》第3章 如何建模服务","url":"/2021/04/24/微服务设计3/","content":"在本章中，我们会讨论如何确定服务之间的边界，以期最大化微服务的好处，避开它的劣势。\n<!--more-->\n# 1.什么样的服务是好服务\n专注在两个重要的概念上：松耦合和高内聚。\n\n这两个概念在不同的上下文中被大量使用，尤其是在面向对象编程中，所以，我们先讨论一下这两个概念在微服务中是什么含义。\n## 1.1松耦合\n如果做到了服务之间的松耦合，那么修改一个服务就不需要修改另一个服务。使用微服务最重要的一点是，能够独立修改及部署单个服务而不需要修改系统的其他部分，这真的非常重要。\n\n什么会导致紧耦合呢？一个典型的错误是，使用紧耦合的方式做服务之间的集成，从而使得一个服务的修改会致使其消费者的修改。\n\n一个松耦合的服务应该尽可能少地知道与之协作的那些服务的信息。这也意味着，应该限制两个服务之间不同调用形式的数量，因为除了潜在的性能问题之外，过度的通信可能会导致紧耦合。\n\n## 1.2高内聚\n我们希望把相关的行为聚集在一起，把不相关的行为放在别处。为什么呢？因为如果你要改变某个行为的话，最好能够只在一个地方进行修改，然后就可以尽快地发布。如果需要在很多不同的地方做这些修改，那么可能就需要同时发布多个微服务才能交付这个功能。在多个不同的地方进行修改会很慢，同时部署多个服务风险也很高，这两者都是我们想要避免的。\n\n所以，找到问题域的边界就可以确保相关的行为能放在同一个地方，并且它们会和其他边界以尽量松耦合的形式进行通信。\n# 2.限界上下文\nEric Evans 的《领域驱动设计》一书主要专注如何对现实世界的领域进行建模。该书中有很多非常棒的想法，比如通用语言、仓储、抽象等。其中 Evans 引入的一个很重要的概念是限界上下文（bounded context），刚听到这个概念的时候，我深受启发。他认为任何一个给定的领域都包含多个限界上下文，每个限界上下文中的东西（Eric 更常使用模型这个词，应该比“东西”好得多）分成两部分，一部分不需要与外部通信，另一部分则需要。每个上下文都有明确的接口，该接口决定了它会暴露哪些模型给其他的上下文。\n\n另一个我比较喜欢的限界上下文的定义是：“一个由显式边界限定的特定职责。”（http://blog.sapiensworks.com/post/2012/04/17/DDD-The-Bounded-Context-Explained.aspx）如果你想要从一个限界上下文中获取信息，或者向其发起请求，需要使用模型和它的显式边界进行通信。在这本书中，Evans 使用细胞作为比喻：“细胞之所以会存在，是因为细胞膜定义了什么在细胞内，什么在细胞外，并且确定了什么物质可以通过细胞膜。”\n\n## 2.1共享的隐藏模型\n\n财务部门和仓库就可以是两个独立的限界上下文。它们都有明确的对外接口（在存货报告、工资单等方面），也都有着只需要自己知道的一些细节（铲车、计算器）。\n\n![WechatIMG191.jpeg](https://i.loli.net/2021/04/24/qOTdXjpPcg7658n.jpg)\n\n有时候，同一个名字在不同的上下文中有着完全不同的含义。比如，退货表示的是客户退回的一些东西。在客户的上下文中，退货意味着打印运送标签、寄送包裹，然后等待退款。在仓库的上下文中，退货表示的是一个即将到来的包裹，而且这个包裹会重新入库。退货这个概念会与将要执行的任务相关，比如我们可能会发起一个重新入库的请求。这个退货的共享模型会在多个不同的进程中使用，并且在每个限界上下文中都会存在相应的实体，不过，这些实体仅仅是在每个上下文的内部表示而已。\n## 2.2模块和服务\n明白应该共享特定的模型，而不应该共享内部表示这个道理之后，就可以避免潜在的紧耦合（即我们不希望成为的样子）风险。我们还识别出了领域内的一些边界，边界内部是相关性比较高的业务功能，从而得到高内聚。这些限界上下文可以很好地形成组合边界。\n\n就像在第 1 章中讨论过的，在同一个进程内使用模块来减少彼此之间的耦合也是一种选择。刚开始开发一个代码库的时候，这可能是比较好的办法。所以一旦你发现了领域内部的限界上下文，一定要使用模块对其进行建模，同时使用共享和隐藏模型。\n\n所以，如果服务边界和领域的限界上下文能保持一致，并且微服务可以很好地表示这些限界上下文的话，那么恭喜你，你跨出了走向高内聚低耦合的微服务架构的第一步。\n\n## 2.3过早划分\n过早地划分带来的问题：例如一个服务没有划分好，后期导致了很多跨服务的修改，而这些修改的代价相当高。团队逐渐又把这些服务合并成了一个单块系统，从而给所有人时间去理解服务边界到底应该在哪。一年之后，团队识别出了出非常稳定的边界，并据此将这个单块系统拆分成多个微服务。当然这并不是我见过的唯一一个过早划分的例子。过早将一个系统划分成为微服务的代价非常高，尤其是在面对新领域时。很多时候，将一个已有的代码库划分成微服务，要比从头开始构建微服务简单得多。\n# 3.业务功能\n当你在思考组织内的限界上下文时，不应该从共享数据的角度来考虑，而应该从这些上下文能够提供的功能来考虑。比如，仓库的一个功能是提供当前的库存清单，财务上下文能够提供月末账目或者为一个新招的员工创建工资单。为了实现这些功能，可能需要交换存储信息的模型，但是我见过太多只考虑模型从而导致贫血的、基于 CRUD（create，read， update，delete）的服务。所以首先要问自己“这个上下文是做什么用的”，然后再考虑“它需要什么样的数据”。\n\n建模服务时，应该将这些功能作为关键操作提供给其协作者（其他服务）。\n\n# 4.逐步划分上下文\n一开始你会识别出一些粗粒度的限界上下文，而这些限界上下文可能又包含一些嵌套的限界上下文。举个例子，你可以把仓库分解成为不同的部分：订单处理、库存管理、货物接受等。当考虑微服务的边界时，首先考虑比较大的、粗粒度的那些上下文，然后当发现合适的缝隙后，再进一步划分出那些嵌套的上下文。\n# 5.关于业务概念的沟通\n修改系统的目的是为了满足业务需求。我们会修改面向客户的功能。如果把系统分解成为限界上下文来表示领域的话，那么对于某个功能所要做的修改，就更倾向于局限在一个单独的微服务边界之内。这样就减小了修改的范围，并能够更快地进行部署。\n\n微服务之间如何就同一个业务概念进行通信，也是一件很重要的事情。基于业务领域的软件建模不应该止于限界上下文的概念。在组织内部共享的那些相同的术语和想法，也应该被反映到服务的接口上。以跟组织内通信相同的方式，来思考微服务之间的通信形式是非常有用的。事实上，通信形式在整个组织范围内都非常重要。\n\n# 6.技术边界\n按照技术边界对服务边界进行建模会形成洋葱架构，因为它有很多层。但是也并不总是错误的。比如，当一个组织想要达到某个性能目标时，这种划分方式反而更合理。然而一般来讲，这不应该成为你考虑的首要方式。\n\n# 7.总结\n在本章中，你学到了什么是好的服务，以及如何在问题空间中寻找能达到高内聚低耦合的接缝。限界上下文是寻找这些接缝的一个非常重要的工具，通过将微服务与这些边界相匹配，可以保证最终的系统能够得到微服务提供的所有好处。\n\n本章讨论的内容比较宽泛，下一章的内容技术性会更强。在实现服务间接口方面存在很多的陷阱，从而会引入各种各样的麻烦。如果不想系统乱成一团麻，就必须深入讨论一下该话题。\n\n\n","tags":["架构","微服务"],"categories":["架构探险","微服务设计"]},{"title":"《微服务设计》第2章 演化式架构师","url":"/2021/04/24/微服务设计2/","content":"# 1.不准确的比较\n你总提及的那个词，它的含义与你想表达的意思并不一样。\n<!--more-->\n“建筑师”和“架构师”在英文中都是 architect，而“架构师”这个词的含义借鉴的是建筑师在建筑中的角色。\n\n# 2.架构师的演化视角\n与建造建筑物相比，在软件中我们会面临大量的需求变更，使用的工具和技术也具有多样性。我们创造的东西并不是在某个时间点之后就不再变化了，甚至在发布到生产环境之后，软件还能继续演化。因此架构师必须改变那种从一开始就要设计出完美产品的想法，相反我们应该设计出一个合理框架。\n\n其实，有一个角色可以更好地跟 IT 架构师相类比。那就是城市规划师，而不是建筑师。如果你玩过 SimCity，那么你应该很熟悉城市规划师这个角色。城市规划师的职责是优化城镇布局，使其更易于现有居民生活，同时也会考虑一些未来的因素。为了达到这个目的，他需要收集各种各样的信息。规划师影响城市演化的方法很有趣，他不会直接说“在那个地方盖一栋这样的楼”，相反他会对城市进行分区。就像在 SimCity 中一样，你可能会把城市的某一部分规划成为工业区，另外一部分规划成为居民区，然后其他人会自己决定具体要盖什么建筑物。当然这个决定会受到一定的约束，比如工厂一定要盖在工业区。城市规划师更多考虑的是人和公共设施如何从一个区域移到另一个区域，而不是具体在每个区域中发生的事情。\n\n城市规划师就像建筑师一样，需要知道什么时候他的计划没有得到执行。尽管他会引入较少的规范，并尽量少地对发展的方向进行纠正，但是如果有人决定要在住宅区建造一个污水池，他应该能制止。\n\n所以我们的架构师应该像城市规划师那样专注在大方向上，只在很有限的情况下参与到非常具体的细节实现中来。他们需要保证系统不但能够满足当前的需求，还能够应对将来的变化。而且他们还应该保证在这个系统上工作的开发人员要和使用这个系统的用户一样开心。听起来这是很高的标准，那么从哪里开始呢？\n# 3.分区\n前面我们将架构师比作城市规划师，那么在这个比喻里面，区域的概念对应的是什么呢？它们应该是我们的服务边界，或者是一些粗粒度的服务群组。作为架构师，不应该过多关注每个区域内发生的事情，而应该多关注区域之间的事情。这意味着我们应该考虑不同的服务之间如何交互，或者说保证我们能够对整个系统的健康状态进行监控。至于多大程度地介入区域内部事务，在不同的情况下则有所不同。很多组织采用微服务是为了使团队的自治性最大化，如果你就处在这样的组织中，那么你会更多地依靠团队来做出正确的局部决定。\n\n\n```\n代码架构师\n\n如果想确保我们创造的系统对开发人员足够友好，那么架构师需要理解他们的决定对系统会造成怎样的影响。\n最低的要求是：架构师需要花时间和团队在一起工作，理想情况下他们应该一起进行编码。\n对于实施结对编程的团队来说，架构师很容易花一定的时间和团队成员进行结对。\n理想情况下，你应该参与普通的工作，这样才能真正理解普通的工作是什么样子。\n架构师和团队真正坐在一起，这件事情再怎么强调也不过分！相比通过电话进行沟通或者只看看团队的代码，一起和团队工作的这种方式会更加有效。\n至于和团队在一起工作的频率可以取决于团队的大小，关键是它必须成为日常工作的一部分。\n如果你和四个团队在一起工作，那么每四周和每个团队都工作半天，可以帮助你有效地和团队进行沟通，并了解他们都在做什么。\n```\n# 4.一个原则性的方法\n\n==**规则对于智者来说是指导，对于愚蠢者来说是遵从。**==\n\n做系统设计方面的决定通常都是在做取舍，而在微服务架构中，你要做很多取舍！当选择一个数据存储技术时，你会选择不太熟悉但能够带来更好可伸缩性的技术吗？在系统中存在两种技术栈是否可接受？那三种呢？做某些决策所需要的信息很容易获取，这些还算是容易的。但是有些决策所需要的信息难以完全获取，那又该怎么办呢？\n\n基于要达到的目标去定义一些原则和实践对做设计来说非常有好处。接下来让我们对它们做一些讨论。\n## 4.1战略目标\n做一名架构师已经很困难了，但幸运的是，通常我们不需要定义战略目标！战略目标关心的是公司的走向以及如何才能让自己的客户满意。这些战略目标的层次一般都很高，但通常不会涉及技术这个层面，一般只在公司或者部门层面制定。\n## 4.2原则\n为了和更大的目标保持一致，我们会制定一些具体的规则，并称之为原则，它不是一成不变的。举个例子，如果组织的一个战略目标是缩短新功能上线的周期，那么一个可能的原则是，交付团队应该对整个软件生命周期有完全的控制权，这样他们就可以及时交付任何就绪的功能，而不受其他团队的限制。如果组织的另一个目标是在其他国家快速增长业务，你需要使用的原则可能就是，整个系统必须能够方便地部署到相应的国家，从而符合该国家对数据存储地理位置方面的要求。\n## 4.3实践\n我们通过相应的实践来保证原则能够得到实施，这些实践能够指导我们如何完成任务。通常这些实践是技术相关的，而且是比较底层的，所以任何一个开发人员都能够理解。这些实践包括代码规范、日志数据集中捕获或者 HTTP/REST 作为标准集成风格等。由于实践比较偏技术层面，所以其改变的频率会高于原则。\n\n就像原则那样，有时候实践也会反映出组织内的一些限制。比如，如果你只支持 CentOS，那么相应的实践就应该考虑这个因素。\n\n实践应该巩固原则。比如前面我们提过一个原则是开发团队应该可以对软件开发全流程有控制权，相应的实践就是所有的服务都部署在不同的 AWS 账户中，从而可以提供资源的自助管理和与其他团队的隔离。\n\n## 4.4将原则和实践结合\n有些东西对一些人来说是原则，对另一些人来说则可能是实践。比如，你可能会把使用 HTTP/REST 作为原则，而不是实践。这也没什么问题，关键是要有一些重要的原则来指导系统的演化，同时也要有一些细节来指导如何实现这些原则。对于一个足够小的群组，比如单个团队来说，将原则和实践进行结合是没问题的。但是在一个大型组织中，技术和工作实践可能不一样，在不同的地方需要的实践可能也不同。不过这也没关系，只要它们都能够映射到相同的原则即可。比如一个 .NET 团队可能有一套实践，一个 Java 团队有另一套实践，但背后的原则是相同的。\n# 5.要求的标准\n在优化单个服务自治性的同时，也要兼顾全局。一种能帮助我们实现平衡的方法就是，清楚地定义出一个好服务应有的属性。\n\n## 5.1监控\n能够清晰地描绘出跨服务系统的健康状态非常关键。这必须在系统级别而非单个服务级别进行考虑。简单起见，我建议确保所有的服务使用同样的方式报告健康状态及其与监控相关的数据。\n\n你可能会选择使用推送机制，也就是说，每个服务主动把数据推送到某个集中的位置。你可以使用 Graphite 来收集指标数据，使用 Nagios 来检测健康状态，或者使用轮询系统来从各个节点收集数据，但无论你的选择是什么，都应尽量保持标准化。每个服务内的技术应该对外不透明，并且不要为了服务的具体实现而改变监控系统。日志功能和监控情况类似：也需要集中式管理。\n## 5.2接口\n选用少数几种明确的接口技术有助于新消费者的集成。使用一种标准方式很好，两种也不太坏，但是 20 种不同的集成技术就太糟糕了。这里说的不仅仅是关于接口的技术和协议。举个例子，如果你选用了 HTTP/REST，在 URL 中你会使用动词还是名词？你会如何处理资源的分页？你会如何处理不同版本的 API ？\n## 5.3架构安全性\n一个运行异常的服务可能会毁了整个系统，而这种后果是我们无法承担的，所以，必须保证每个服务都可以应对下游服务的错误请求。没有很好处理下游错误请求的服务越多，我们的系统就会越脆弱。你可以至少让每个下游服务使用它们自己的连接池，进一步让每个服务使用一个断路器。\n\n# 6.代码治理\n聚在一起，就如何做事情达成共识是一个好主意。但是，花时间保证人们按照这个共识来做事情就没那么有趣了，因为在各个服务中使用这些标准做法会成为开发人员的负担。我坚信应该使用简单的方式把事情做对。我见过的比较奏效的两种方式是，提供范例和服务代码模板。\n## 6.1范例\n编写文档是有用的。我很清楚这样做的价值，这也正是我写这本书的原因。但是开发人员更喜欢可以查看和运行的代码。如果你有一些很好的实践希望别人采纳，那么给出一系列的代码范例会很有帮助。这样做的一个初衷是：如果在系统中人们有比较好的代码范例可以模仿，那么他们也就不会错得很离谱。\n\n理想情况下，你提供的优秀范例应该来自真实项目，而不是专门实现的一个完美的例子。因为如果你的范例来自真正运行的代码，那么就可以保证其中所体现的那些原则都是合理的。\n\n## 6.2裁剪服务代码模板\n如果能够让所有的开发人员很容易地遵守大部分的指导原则，那就太棒了。一种可能的方式是，当开发人员想要实现一个新服务时，所有实现核心属性的那些代码都应该是现成的。\n\n有一点需要注意的是，创建服务代码模板不是某个中心化工具的职责，也不是指导（即使是通过代码）我们应怎样工作的架构团队的职责。应该通过合作的方式定义出这些实践，所以你的团队也需要负责更新这个模板（内部开源的方式能够很好地完成这项工作）。\n\n如果你强制团队使用它，一定要确保它能够简化开发人员的工作，而不是使其复杂化。你还需要知道，重用代码可能引入的危险。在重用代码的驱动下，我们可能会引入服务之间的耦合。\n# 7.技术债务\n有时候可能无法完全遵守技术愿景，比如为了发布一些紧急的特性，你可能会忽略一些约束。其实这仅仅是另一个需要做的取舍而已。我们的技术愿景有其本身的道理，所以偏离了这个愿景短期可能会带来利益，但是长期来看是要付出代价的。可以使用技术债务的概念来帮助我们理解这个取舍，就像在真实世界中欠的债务需要偿还一样，累积的技术债务也是如此。\n\n不光走捷径会引入技术债务。有时候系统的目标会发生改变，并且与现有的实现不符，这种情况也会产生技术债务。\n\n架构师的职责就是从更高的层次出发，理解如何做权衡。理解债务的层次及其对系统的影响非常重要。对于某些组织来说，架构师应该能够提供一些温和的指导，然后让团队自行决定如何偿还这些技术债务。而其他的组织就需要更加结构化的方式，比如维护一个债务列表，并且定期回顾。\n# 8.例外管理\n原则和实践可以指导我们如何构建系统。那么，如果系统偏离了这些指导又会发生什么呢？有时候我们会决定针对某个规则破一次例，然后把它记录下来。\n\n现实中的情况是多种多样的，如果你所在的组织对开发人员有非常多的限制，那么微服务可能并不适合你。\n# 9.集中治理和领导\n架构师的部分职责是治理。那么治理又是什么意思呢？\n\n**治理通过评估干系人的需求、当前情况及下一步的可能性来确保企业目标的达成，通过排优先级和做决策来设定方向。对于已经达成一致的方向和目标进行监督。**\n\n在 IT 的上下文中有很多事情需要治理，而架构师会承担技术治理这部分的职责。如果说，架构师的一个职责是确保有一个技术愿景，那么治理就是要确保我们构建的系统符合这个愿景，而且在需要的时候还应对愿景进行演化。\n\n架构师会对很多事情负责。他们需要确保有一组可以指导开发的原则，并且这些原则要与组织的战略相符。他们还需要确保，以这些原则为指导衍生出来的实践不会给开发人员带来痛苦。他们需要了解新技术，需要知道在什么时候做怎样的取舍。上述这些职责已经相当多了，但是他们还需要让同事也理解这些决定和取舍，并执行下去。对了，还有前面提到的：他们还需要花时间和团队一起工作，甚至是编码，从而了解所做的决定对团队造成了怎样的影响。\n\n需要在一定程度上相信你的团队。你没法替代他们去骑车。你会看着他们摇摇晃晃地前行，但是，如果每次你看到他们要跌倒就上去扶一把，他们永远都学不会。而且无论如何，他们真正跌倒的次数会比你想象的要少！但是，如果他们马上就要驶入车流繁忙的大马路，或者附近的鸭子池塘，你就必须站出来了。类似地，作为一名架构师，你必须要在团队驶向类似鸭子池塘这样的地方时抓紧他们。还有一点要注意的是，即使你很清楚什么是对的，然后尝试去控制团队，也可能会破坏和团队的关系，并且会使团队感觉他们没有话语权。有时候按照一个你不同意的决定走下去反而是正确的，知道什么时候可以这么做，什么时候不要这么做是很困难的，但有时也很关键。\n\n# 10.建设团队\n对于一个系统技术愿景的主要负责人来说，执行愿景不仅仅等同于做技术决定，和你一起工作的那些人自然会做这些决定。对于技术领导人来说，更重要的事情是帮助你的队友成长，帮助他们理解这个愿景，并保证他们可以积极地参与到愿景的实现和调整中来。\n\n伟大的软件来自于伟大的人。所以如果你只担心技术问题，那么恐怕你看到的问题远远不及一半。\n\n# 总结\n\n总结一下，一个演进式架构师应该承担的职责。\n\n- 愿景\n\n确保在系统级有一个经过充分沟通的技术愿景，这个愿景应该可以帮助你满足客户和组织的需求。\n\n- 同理心\n\n理解你所做的决定对客户和同事带来的影响。\n\n- 合作\n\n和尽量多的同事进行沟通，从而更好地对愿景进行定义、修订及执行。\n\n- 适应性\n\n确保在你的客户和组织需要的时候调整技术愿景。\n\n- 自治性\n\n在标准化和团队自治之间寻找一个正确的平衡点。\n\n- 治理\n\n确保系统按照技术愿景的要求实现。\n\n演进式架构师应该理解，成功要靠不断地取舍来实现。总会存在一些原因需要你改变工作的方式，但是具体做哪些改变就只能依赖于自己的经验了。而僵化地固守自己的想法无疑是最糟糕的做法。","tags":["架构","微服务"],"categories":["架构探险","微服务设计"]},{"title":"怎么做到服务的优雅上下线","url":"/2021/04/19/怎么做到服务的优雅上下线/","content":"在平常工作中，服务如果重启，怎么保证一个请求不会被中断处理的呢？\n也就是如何做到让服务优雅的上下线。\n<!--more-->\n# 什么是优雅上下线\n关于\"优雅上下线\"这个词，我没找到官方的解释，我尝试解释一下这是什么。\n首先，上线、下线大家一定都很清楚，比如我们一次应用发布过程中，就需要先将应用服务停掉，然后再把服务启动起来。这个过程就包含了一次下线和一次上线。\n那么，\"优雅\"怎么理解呢？\n先说什么情况我们认为不优雅：\n\n1、服务停止时，没有关闭对应的监控，导致应用停止后发生大量报警。\n\n2、应用停止时，没有通知外部调用方，很多请求还会过来，导致很多调用失败。\n\n3、应用停止时，有线程正在执行中，执行了一半，JVM进程就被干掉了。\n\n4、应用启动时，服务还没准备好，就开始对外提供服务，导致很多失败调用。\n\n5、应用启动时，没有检查应用的健康状态，就开始对外提供服务，导致很多失败调用。\n\n以上，都是我们认为的不优雅的情况，那么，反过来，优雅上下线就是一种避免上述情况发生的手段。\n一个应用的优雅上下线涉及到的内容其实有很多，从底层的操作系统、容器层面，到编程语言、框架层面，再到应用架构层面，涉及到的知识很广泛。\n\n其实，优雅上下线中，最重要的还是优雅下线。因为如果下线过程不优雅的话，就会发生很多调用失败了、服务找不到等问题。所以很多时候，大家也会提优雅停机这样的概念。\n\n本文后面介绍的优雅上下线也重点关注优雅停机的过程。\n# 操作系统&容器的优雅上下线\n我们知道，kill -9之所以不建议使用，是因为kill -9特别强硬，系统会发出SIGKILL信号，他要求接收到该信号的程序应该立即结束运行，不能被阻塞或者忽略。\n\n这个过程显然是不优雅的，因为应用立刻停止的话，就没办法做收尾动作。而更优雅的方式是kill -15。\n\n当使用kill -15时，系统会发送一个SIGTERM的信号给对应的程序。当程序接收到该信号后，具体要如何处理是自己可以决定的。\n\nkill -15会通知到应用程序，这就是操作系统对于优雅上下线的最基本的支持。\n\n以前，在操作系统之上就是应用程序了，但是，自从容器化技术推出之后，在操作系统和应用程序之间，多了一个容器层，而Docker、k8s等容器其实也是支持优雅上下线的。\n\n如Docker中同样提供了两个命令， docker stop 和 docker kill\ndocker stop就像kill -15一样，他会向容器内的进程发送SIGTERM信号，在10S之后（可通过参数指定）再发送SIGKILL信号。\n\n而docker kill就像kill -9，直接发送SIGKILL信号。\n# JVM的优雅上下线\n在操作系统、容器等对优雅上下线有了基本的支持之后，在接收到docker stop、kill -15等命令后，会通知应用进程进行进程关闭。\n\n而Java应用在运行时就是一个独立运行的进程，这个进程是如何关闭的呢？\n\nJava程序的终止运行是基于JVM的关闭实现的，JVM关闭方式分为正常关闭、强制关闭和异常关闭3种。\n\n这其中，正常关闭就是支持优雅上下线的。正常关闭过程中，JVM可以做一些清理动作，比如删除临时文件。\n\n当然，开发者也是可以自定义做一些额外的事情的，比如通知应用框架优雅上下线操作。\n\n而这种机制是通过JDK中提供的shutdown hook实现的。JDK提供了Java.Runtime.addShutdownHook(Thread hook)方法，可以注册一个JVM关闭的钩子。\n\n例子如下：\n\n```\npackage com.test;\n\n    public class ShutdownHookTest {\n\n        public static void main(String[] args) {\n\n            boolean flag = true;\n\n            Runtime.getRuntime().addShutdownHook(new Thread(() -> {\n\n                System.out.println(\"hook execute...\");\n\n            }));\n\n            while (flag) {\n\n                // app is runing\n\n            }\n\n            System.out.println(\"main thread execute end...\");\n\n        }\n\n    }\n\n```\n执行命令：\n\n```\njps\n 6520 ShutdownHookTest\n 6521 Jps\nkill 6520\n```\n控制台输出内容：\n\n```\n hook execute...\n\n Process finished with exit code 143 (interrupted by signal 15: SIGTERM)\n```\n可以看到，当我们使用kill（默认kill -15）关闭进程的时候，程序会先执行我注册的shutdownHook，然后再退出，并且会给出一个提示：interrupted by signal 15: SIGTERM\n# Spring的优雅上下线\n有了JVM提供的shutdown hook之后，很多框架都可以通过这个机制来做优雅下线的支持。\n比如Spring，他就会向JVM注册一个shutdown hook，在接收到关闭通知的时候，进行bean的销毁，容器的销毁处理等操作。\n同时，作为一个成熟的框架，Spring也提供了事件机制，可以借助这个机制实现更多的优雅上下线功能。\nApplicationListener是Spring事件机制的一部分，与抽象类ApplicationEvent类配合来完成ApplicationContext的事件机制。\n开发者可以实现ApplicationListener接口，监听到 Spring 容器的关闭事件（ContextClosedEvent），来做一些特殊的处理：\n\n```\n@Component\n\n    public class MyListener implements ApplicationListener<ContextClosedEvent> {\n\n        @Override\n\n        public void onApplicationEvent(ContextClosedEvent event) {\n\n            // 做容器关闭之前的清理工作\n\n        }\n\n    }\n```\n# Dubbo的优雅上下线\n因为Spring中提供了ApplicationListener接口，帮助我们来监听容器关闭事件，那么，很多web容器、框架等就可以借助这个机制来做自己的优雅上下线操作。\n如tomcat、dubbo等都是这么做的。\n这里简答说一下Dubbo的，在Dubbo的官网中，有关于优雅停机的介绍：\n![WechatIMG188.jpeg](https://i.loli.net/2021/04/19/5nRCTzufWOiSKYU.jpg)\n应用在停机时，接收到关闭通知时，会先把自己标记为不接受（发起）新请求，然后再等待10s（默认是10秒）的时候，等执行中的线程执行完。\n\n那么，之所以他能做这些事，是因为从操作系统、到JVM、到Spring等都对优雅停机做了很好的支持。\n\n目前，Dubbo中实现方式如下，同样是用到了Spring的事件机制：\n\n```\npublic class SpringExtensionFactory implements ExtensionFactory {\n\n        public static void addApplicationContext(ApplicationContext context) {\n\n            CONTEXTS.add(context);\n\n            if (context instanceof ConfigurableApplicationContext) {\n\n                ((ConfigurableApplicationContext) context).registerShutdownHook();\n\n                DubboShutdownHook.getDubboShutdownHook().unregister();\n\n            }\n\n            BeanFactoryUtils.addApplicationListener(context, SHUTDOWN_HOOK_LISTENER);\n\n        }\n\n    }\n```\n# 总结\n本文从操作系统开始，分别介绍了Linux、Docker、JVM、Spring、Dubbo等对优雅停机的支持。\n可以看到，一个简单的优雅停机功能，上下游需要这么多底层基础设施和上层应用的支持。\n相信通过学习本文，你一定对优雅上下线有了更多的了解。\n除此之外，我还希望你，通过本文以后，遇到一些实际问题的时候，可以想到文中提到的shutdown hook机制、Spring的event机制。很多时候，这些机制都能帮助我们解决很多问题。\n\n参考：\n\nhttps://mp.weixin.qq.com/s/qBKaRt34zeSI0OzBEAnu7g\n\nhttps://zhuanlan.zhihu.com/p/29093407\n\nhttps://www.cnkirito.moe/dubbo-gracefully-shutdown/\n\nhttps://www.cnkirito.moe/dubbo-gracefully-shutdown/\n","tags":["技能包","部署"],"categories":["技能包"]},{"title":"《微服务设计》第1章 微服务","url":"/2021/04/18/微服务设计1/","content":"最近在重读《微服务设计》这本书，我会输出我认为的精华部分所在，希望自己能有更多的总结领悟和思想碰撞，从而提升自己。重新审视微服务该如何设计。开始吧：\n<!--more-->\n# 1.什么是微服务\n微服务就是一些协同工作的小而自治的服务。\n## 1.1 很小，专注于做好一件事\n随着新功能的增加，代码库会越变越大。时间久了代码库会越来越庞大，以至于想要知道应该在什么地方做修改都很困难。尽管我们想在巨大的代码库中做到清晰地模块化，但事实上这些模块之间的界限很难维护。相似的功能代码开始在代码库中随处可见，这让修复bug和修改原有实现更加困难。\n微服务将内聚性这个理念应用在独立的服务上。根据业务的边界来确定服务的边界，这样就很容易确定某个功能代码应该放在哪里。由于该服务专注于某个边界之内，因此可以很好地避免由于代码库过大而衍生出来的很多让人头痛的问题。\n那么代码库多小才算小？作者给出的一个比较老套的方案是：足够小即可，不要过小。那么换句话说，如果你不再感觉你的代码库过大，可能他就足够小了。还有一点就是该服务是否能够很好的与团队结构相匹配。\n服务越小，微服务架构的优点和缺点也就越明显。使用的服务越小，独立性带来的好处就越多。但是管理大量服务带来的复杂性也会越来越大。\n如果你能够很好的处理这一复杂性，那就可以尽情地使用较小的服务了。\n## 1.2 自治性\n一个微服务就是一个独立的实体。它可以独立地部署在PAAS上，也可以作为一个操作系统的进程存在。我们要尽量避免把多个服务部署在同一台机器上，尽管这种隔离性会带来一些代价。\n服务直接均通过网络调用进行通信，从而加强了服务直接的隔离性，避免紧耦合。\n这些服务应该可以彼此间独立进行修改，对于一个服务来说，我们应该考虑暴露应该暴露的部分，如果暴露过多服务消费方和提供方就会产生耦合。这回使得服务提供方和消费方直接产生额外的协调工作，从而降低服务的自治性。\n服务提供者会暴露出API，然后服务之间通过这些API进行通信。如果系统没有很好地进行解耦，那么一旦出现问题，所有的功能都将不可用。\n# 2.微服务的主要好处\n## 2.1 技术异构性\n在一个由多个服务相互协作的系统中，可以在不同的服务中使用最适合该服务的技术。尝试使用一种适合所有场景的标准化技术，会使得所有的场景都无法得到很好的支持。\n微服务可以帮助我们轻松地采用不同的技术：\n![WechatIMG186.png](https://i.loli.net/2021/04/18/LDNp6jw9mdAUuGB.png)\n## 2.2 弹性\n弹性工程学的一个关键概念是舱壁虎。其实服务边界就是一个很显然的舱壁。微服务可以改进弹性，但你还是需要谨慎对待，因为一旦使用了分布式系统，网络就是个问题，机器也是个问题。因此我们需要了解出现问题时应该如何对用户进行友好展示。\n## 2.3 扩展\n庞大的单体服务只能作为一个整体进行扩展。即使系统中只有一部分存在性能问题，也需要对整个服务进行扩展。如果使用较小的服务，则可以选择只对需要扩展的服务进行扩展，这样就可以把那些不需要扩展的服务运行在性能更差的硬件上从而节省成本。\n![WechatIMG10455.png](https://i.loli.net/2021/04/18/29HVyxR1jODXJpM.png)\n## 2.4 简化部署\n在几百万代码行的单体应用程序中，即使你只修改了一行代码，也要重新部署整个服务发布该变更。\n在微服务架构中，各个服务的部署是独立的，这样就能更快的对特定部分的代码进行部署。如果真的出了问题，也只会影响一个服务，并且容易快速回滚。\n## 2.5 与组织架构相匹配\n微服务架构可以将架构和组织结构相匹配，避免出现过大的代码库，从而获得理想的团队大小和生产力。服务的所有权也可以在团队直接迁移，从而避免异地团队的出现。\n## 2.6 可组合性\n在微服务架构中，系统会开放很多接口供外部使用。而单体应用只能提供一个非常粗粒度的接口供外部使用。所以微服务架构可以达到可重用，可组合的目的。\n## 对可代替性的优化\n想想看，在一个庞大的单体应用中你敢不敢在一天内删掉上百行代码，并且确性不会引发问题。所以使用微服务架构的团队可以在需要时轻易地重写服务，或者删除不再使用的服务。\n# 3.面向服务的架构\nSOA是一种设计方法，其中包含多个服务，而服务之间通过配合最终会提供一系列的功能。SOA本身是一个很好的想法，但尽管做了很多尝试，人们还是无法在如何做好SOA这件事情上达成共识。因为业界大部分的尝试都没能把它当作一个整体来看待，因此很难给出一个比该领域现有厂家提供的方案更好呢替代方案，也就是没有对应的标准和方法论。所以在实施SOA时会遇到一些问题：通信协议如何选择、第三方中间件如何选择、服务粒度如何确定等。而这事实上就是微服务架构，你也可以认为微服务架构是SOA的一种特定方法。\n# 4.没有银弹\n软件工程没有银弹，微服务也是如此，它不是免费的午餐，更不是银弹。选择微服务的同时，你需要在部署、测试、监控等方面做很多的工作，你还需要考虑如何扩展系统，并且保证他们的弹性，还需要处理分布式事务与CAP相关的问题。\n每个公司、组织及系统都不一样。微服务是否适合你，或者说你能够在多大的程度上采用微服务，取决于很多因素。\n\n架构师承担了驱动系统演化的职责，而引入微服务之后的一个主要挑战就是，架构师职责的相应变化。下一章会讲到有哪些方法可以保证我们从这个新架构中受益。\n\n\n","tags":["架构","微服务"],"categories":["架构探险","微服务设计"]},{"title":"安装bamboo，使用postgresql存储数据","url":"/2021/04/16/安装bamboo/","content":"\n# 1、编辑dockerfile 文件\n\n```\nFROM atlassian/bamboo-server:7.2.1\n\nUSER root\n```\n<!--more-->\n# 2、将代理破解包加入容器\n\n```\nCOPY \"atlassian-agent.jar\" /opt/atlassian/bamboo/\nCOPY \"mysql-connector-java-8.0.18.jar\" /opt/atlassian/bamboo/lib/\n```\n\n# 3、设置启动加载代理包\n\n```\nRUN echo 'export CATALINA_OPTS=\"-javaagent:/opt/atlassian/bamboo/atlassian-agent.jar ${CATALINA_OPTS}\"' >> /opt/atlassian/bamboo/bin/setenv.sh\n```\n\n\n# 4、编译dockerfile文件，生成镜像\n\n```\ndocker build -t hl/bamboo:7.2.1\n```\n\n# 5、运行容器\n\n```\ndocker run -d --name hl-bamboo \\\n--restart always \\\n-p 8085:8085 \\\n-p 54663:54663 \\\n-e TZ=\"Asia/Shanghai\" \\\n-v /home/atlassian/bamboo/data:/var/atlassian/application-data/bamboo \\\n-v /home/atlassian/bamboo/settings.xml:/opt/maven/settings.xml \\\nhl/bamboo:7.2.1\n```\n\n\n# 6、破解\n\n```\njava -jar atlassian-agent.jar -d -m bamboo@guwenxiang.com -n bamboo -p bamboo -o http://47.28.176.98 -s xxxx-xxxx-xxxx\n```\n","tags":["linux"],"categories":["服务器","bamboo"]},{"title":"我的概要设计思路","url":"/2021/04/16/我的概要设计思路/","content":"如下：\n<!--more-->\n![我的概要设计思路.png](https://i.loli.net/2021/04/16/1bIT79ZoDFnUeBO.png)","tags":["项目管理","概要设计"],"categories":["项目管理","概要设计"]},{"title":"个人想法","url":"/2021/04/15/开始/","content":"这是一个在线个人笔记，记录我的总结和输出，写下来就不会丢。\n<!--more-->\n\n借用王安石的《游褒禅山记》激励自己吧：夫夷以近，则游者众；险以远，则至者少；而世之奇伟瑰怪非常之观，常在于险远而人之所罕至焉，故非有志者不能至也。\n\n![WechatIMG189.jpeg](https://i.loli.net/2021/04/19/E7eh1kNU4JnWqtx.jpg)\n","tags":["生活"],"categories":["生活"]}]