-
Notifications
You must be signed in to change notification settings - Fork 1
/
search.xml
537 lines (537 loc) · 655 KB
/
search.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title><![CDATA[window terminal 玩法大全]]></title>
<url>%2F2020%2F07%2F01%2Fwindow-terminal-%E7%8E%A9%E6%B3%95%E5%A4%A7%E5%85%A8%2F</url>
<content type="text"><![CDATA[window terminal 玩法大全添加到右键菜单第一步,创建文件夹首先打开cmd,注意,gitbash无效。 运行下方命令: 1mkdir "%USERPROFILE%\AppData\Local\Terminal" 第二步,下载window terminal ico图标打开下方网址,将图标复制到上方,命令的路径 https://raw.githubusercontent.com/microsoft/terminal/master/res/terminal.ico 第三步,添加注册表文件创建文件,代码如下,重命名成.reg格式 12345678Windows Registry Editor Version 5.00[HKEY_CLASSES_ROOT\Directory\Background\shell\wt]@="Windows Terminal here""Icon"="C:\\Users\\[你的用户名]\\AppData\\Local\\Terminal\\terminal.ico"[HKEY_CLASSES_ROOT\Directory\Background\shell\wt\command]@="C:\\Users\\[你的用户名]\\AppData\\Local\\Microsoft\\WindowsApps\\wt.exe"]]></content>
<tags>
<tag>软件分享</tag>
</tags>
</entry>
<entry>
<title><![CDATA[快速git pull更新所有项目文件]]></title>
<url>%2F2019%2F09%2F17%2F%E5%BF%AB%E9%80%9Fgit-pull%E6%89%80%E6%9C%89%E9%A1%B9%E7%9B%AE%E6%96%87%E4%BB%B6%2F</url>
<content type="text"><![CDATA[河蟹,本山大王回来了。回想一下,已经大概整整7个月没用更新博客了!!!等我意识到这个问题的时候,居然连markdown都不会写了,罪恶罪恶。今日,重拾我的纸币,继续更新博客,请各位看客给予鼓励。 需求分析故事的开始是这样的,有位搬砖工,名字叫小徐,他所在的公司用的是VMware Horizon 7虚拟桌面环境,软件开发根本连不上外部网络,这让他非常苦恼。 突然有一天,公司内部大变革,要开始使用maven私服,要开始使用内部PaaS,这让小徐心里乐开了花。“可是新知识太多,这该如何学习呢?”小徐泛起了嘀咕。 功夫不负有心人,在小徐的日夜努力中,在一个号称全球最大的同性交友网站上,找到了很多学习资料。小徐在自己查询外部互联网资料的笔记本上,熟练的git clone各种各样的仓库,很快,满满一个硬盘都是这些仓库的文件夹。 时间过的很快,转眼7个月过去了。 技术变化的很快,转眼,满满硬盘的学习资料都过时了,怎么一下子将他们更新呢? 以上就是小徐向我提出了需求。 设计软件python是世界上最好的语言!😆 设计思路很简单,判断当前目录的所有文件夹中,是否其中包含后缀是.git的文件夹,如果是,则为仓库,进行更新,不是就跳过。 其中需要依赖gitpython这个包,来进行git pull操作。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748import gitfrom git import *import threadingimport osdef isGitDir(dir): repdir = os.path.join(os.path.abspath('.'), dir) repgitdir = os.path.join(repdir, '.git') if not os.path.exists(repgitdir): return False return Truedef updateSub(subdir): repdir = os.path.join(os.path.abspath('.'), subdir) try: repo = git.Repo(repdir) if repo.is_dirty(): dirSubDir.append(subdir) return remote = repo.remote() print("start pulling from remote for: %s\r\n" %subdir) remote.pull() print("Done pulling for %s\r\n" %subdir) except NoSuchPathError as e: pass except InvalidGitRepositoryError as e: pass finally: passcurrDir = os.path.abspath('.')subDirs = [x for x in os.listdir('.') if isGitDir(x)]print("ready to update git repo:")for dir in subDirs: print(dir+ '\r\n')dirSubDir = []poole = []for subdir in subDirs: t = threading.Thread(target=updateSub,args=(subdir,)) t.start() poole.append(t)for t in poole: t.join(30)print('\r\n')if len(dirSubDir)!= 0: print('these repos have uncommitted changes:') for dirtyDir in dirSubDir: print('dir %s has uncommited change, please check' % dirtyDir) 测试投产打开电脑,在cmd中运行pip install gitpython或者conda install gitpython,来安装必要的包。 将上述文件保存到仓库的目录,并且命名为update.py,右键使用VS Code运行,即可发现目录中所有仓库都成功git pull 了,大成功! 续1s时间写完这个博客的时候,偷偷看了一下记录博客浏览量的网站,爆炸了!!!这7个月我不在线的日子,这么多人访问我博客啊。很开心,谢谢你那么可爱,还一直关注着我~❤😝 全片结束,觉得我写的不错?想要了解更多精彩新姿势?赶快打开我的👉个人博客 👈吧!]]></content>
<tags>
<tag>分享</tag>
<tag>窍门</tag>
</tags>
</entry>
<entry>
<title><![CDATA[分享一下微服务监控软件以及其部署]]></title>
<url>%2F2019%2F02%2F15%2F%E5%88%86%E4%BA%AB%E4%B8%80%E4%B8%8B%E5%BE%AE%E6%9C%8D%E5%8A%A1%E7%9B%91%E6%8E%A7%E8%BD%AF%E4%BB%B6%E4%BB%A5%E5%8F%8A%E5%85%B6%E9%83%A8%E7%BD%B2%2F</url>
<content type="text"><![CDATA[最近在寻找有利的监控软件为我的微服务献身,于是乎整理文档,试验,花了很长时间。今天分享一下整理的部分工具,有时间会继续在此post中更新的!🙃 想要解锁更多新姿势?请访问我的博客。 influxDB什么是influxDBinfluxDB是一个分布式时间序列数据库。cAdvisor仅仅显示实时信息,但是不存储监视数据。因此,我们需要提供时序数据库用于存储cAdvisor组件所提供的监控信息,以便显示除实时信息之外的时序数据。 influxDB安装(1)下载镜像1docker pull tutum/influxdb (2)创建容器1234567docker run ‐di \‐p 8083:8083 \‐p 8086:8086 \‐‐expose 8090 \‐‐expose 8099 \‐‐name influxsrv \tutum/influxdb 端口概述: 8083端口:web访问端口 8086:数据写入端口打开浏览器 http://192.168.184.135:8083/ (ip自觉替换) influxDB常用操作创建数据库1CREATE DATABASE "cadvisor" 回车创建数据库1SHOW DATABASES 查看数据库 创建用户并授权创建用户1CREATE USER "cadvisor" WITH PASSWORD 'cadvisor' WITH ALL PRIVILEGES 查看用户1SHOW USRES 用户授权123grant all privileges on cadvisor to cadvisorgrant WRITE on cadvisor to cadvisorgrant READ on cadvisor to cadvisor 查看采集的数据切换到cadvisor数据库,使用以下命令查看采集的数据1SHOW MEASUREMENTS 如果想采集系统的数据,我们需要使用Cadvisor软件来实现 More官方文档 : https://github.com/influxdata/docs.influxdata.com cAdvisor啥么是cAdvisorGoogle开源的用于监控基础设施应用的工具,它是一个强大的监控工具,不需要任何配置就可以通过运行在Docker主机上的容器来监控Docker容器,而且可以监控Docker主机。 cAdvisor安装(1)下载镜像docker pull google/cadvisor(2)创建容器 12345docker run ‐‐volume=/:/rootfs:ro ‐‐volume=/var/run:/var/run:rw ‐‐volume=/sys:/sys:ro ‐‐volume=/var/lib/docker/:/var/lib/docker:ro ‐‐publish=8080:8080 ‐‐detach=true ‐‐link influxsrv:influxsrv ‐‐name=cadvisorgoogle/cadvisor ‐storage_driver=influxdb ‐storage_driver_db=cadvisor ‐storage_driver_host=influxsrv:8086 WEB前端访问地址http://192.168.184.135:8080/containers/ (请自觉替换实际ip)性能指标含义参照如下地址https://blog.csdn.net/ZHANG_H_A/article/details/53097084再次查看influxDB,发现已经有很多数据被采集进去了。 GrafanaGrafana是一个可视化面板(Dashboard),有着非常漂亮的图表和布局展示,功能齐全的度量仪表盘和图形编辑器。支持Graphite、zabbix、InfluxDB、Prometheus和OpenTSDB作为数据源。 Grafana主要特性:灵活丰富的图形化选项;可以混合多种风格;支持白天和夜间模式;多个数据源。 Grafana安装(1)下载镜像docker pull grafana/grafana(2)创建容器1234docker run ‐d ‐p 3001:3000 ‐e INFLUXDB_HOST=influxsrv ‐eINFLUXDB_PORT=8086 ‐e INFLUXDB_NAME=cadvisor ‐e INFLUXDB_USER=cadvisor ‐eINFLUXDB_PASS=cadvisor ‐‐link influxsrv:influxsrv ‐‐name grafanagrafana/grafana 注意,INFLUXDB配置请与上文我的写的配置相同。(3)访问http://192.168.184.135:3001 (自觉更换ip)用户名密码均为admin 使用//TODO pinpoint是啥pinpoint是开源在github上的一款APM监控工具,它是用Java编写的,用于大规模分布式系统监控。它对性能的影响最小(只增加约3%资源利用率),安装agent是无侵入式的,只需要在被测试的Tomcat中加上3句话,打下探针,就可以监控整套程序了。 所需软件1234567hbase-1.2.6-bin.tar.gzpinpoint-1.6.2.zip pinpoint源码,使用hbase-create.hbase脚本初始化hbasepinpoint-collector-1.6.2.war collector模块,使用tomcat进行部署pinpoint-web-1.6.2.war web管控台,使用tomcat进行部署pinpoint-agent-1.6.2.tar.gz 不需要部署,在被监控应用一端apache-tomcat-8.5.15.zip 准备两个tomcat环境,一个部署pinpoint-collector-1.6.2.war,另一个部署pinpoint-web-1.6.2.war 安装hbase环境配置解压hbase-1.2.6-bin.tar.gz,修改hbase/conf/hbase-site.xml文件内容如下123456789101112131415161718<configuration><property><name>hbase.rootdir</name><value>file:///pinpoint/data/hbase</value></property><property><name>hbase.zookeeper.property.dataDir</name><value>/pinpoint/data/zookeeper</value></property><property><name>hbase.master.port</name><value>60000</value></property><property><name>hbase.regionserver.port</name><value>60020</value></property></configuration> 修改hbase/conf/hbase-env.sh文件,设置JAVA_HOME环境变量export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home 启动hbasehbase/bin/start-hbase.sh 执行jps查看是否有HMaster进程访问 http://127.0.0.1:16010/master-status 查看hbase web管控台导入hbase初始化脚步1234567891011121314151617181920212223242526272829303132333435363738hbase/bin/hbase shell /pinpoint-1.6.2/hbase/scripts/hbase-create.hbase...0 row(s) in 1.4160 seconds0 row(s) in 1.2420 seconds0 row(s) in 2.2410 seconds0 row(s) in 1.2400 seconds0 row(s) in 1.2450 seconds0 row(s) in 1.2420 seconds0 row(s) in 1.2360 seconds0 row(s) in 1.2400 seconds0 row(s) in 1.2370 seconds0 row(s) in 2.2450 seconds0 row(s) in 4.2480 seconds0 row(s) in 1.2310 seconds0 row(s) in 1.2430 seconds0 row(s) in 1.2390 seconds0 row(s) in 1.2440 seconds0 row(s) in 1.2440 secondsTABLEAgentEventAgentInfoAgentLifeCycleAgentStatAgentStatV2ApiMetaDataApplicationIndexApplicationMapStatisticsCallee_Ver2ApplicationMapStatisticsCaller_Ver2ApplicationMapStatisticsSelf_Ver2ApplicationTraceIndexHostApplicationMap_Ver2SqlMetaData_Ver2StringMetaDataTraceV2Traces16 row(s) in 0.0190 seconds 进入hbase shell12345hbase/bin/hbase shellHBase Shell; enter 'help<RETURN>' for list of supported commands.Type "exit<RETURN>" to leave the HBase ShellVersion 1.2.6, rUnknown, Mon May 29 02:25:32 CDT 2017hbase(main):001:0> 输入status ‘detailed’查看刚才初始化的表,是否存在1hbase(main):001:0> status 'detailed' 访问 http://127.0.0.1:16010/master-status 查看hbase web管控台 部署collector解压apache-tomcat-8.5.15.zip并重命名为collector拷贝pinpoint-collector-1.6.2.war到collector\webapps\目录下并重命名为ROOT.war涉及到两个配置文件 hbase.properties由于使用的为hbase自带的zookeeper即hbase和zookeeper在同一台机器上,则不需要修改collector/webapps/ROOT/WEB-INF/classes/hbase.properties文件 pinpoint-collector.properties注意观察该配置文件中中的三个端口9994、9995、9996 默认不需要修改,其中9994为collector监听的tcp端口,9995为collector监听的udp端口 执行collector/bin/startup.sh启动collector部署web管控台解压apache-tomcat-8.5.15.zip并重命名为web拷贝pinpoint-web-1.6.2.war到web\webapps\目录下并重命名为ROOT.war同样留意web/webapps/ROOT/WEB-INF/classes/hbase.properties中的配置,若与hbase在同一台机器则不需要修改执行web/bin/startup.sh启动web管控台(注意修改tomcat端口号,防止冲突,这里修改端口为8088)浏览器访问 http://127.0.0.1:8088/ 查看pinpoint管控台 监控spring boot应用这里有一个spring-boot-example.jar的应用,现在要使用pinpoint来对其监控跟踪。操作很简单,分两步解压pinpoint-agent-1.6.2.tar.gz并对其进行配置1tar zxvf pinpoint-agent-1.6.2.tar.gz -C pinpoint-agent 修改pinpoint-agent/pinpoint.config中的配置与collector服务一致。此处因为pinpoint-agent与collector在同一台机器,因此默认配置即可不需要修改。为spring-boot应用配置pinpoint-agent启动spring-boot应用时添加如下参数123-javaagent:$AGENT_PATH/pinpoint-bootstrap-$VERSION.jar-Dpinpoint.agentId-Dpinpoint.applicationName 本例中的启动参数为1java -javaagent:/pinpoint-agent/pinpoint-bootstrap-1.6.2.jar -Dpinpoint.agentId=spring-boot-app -Dpinpoint.applicationName=spring-boot-app -jar spring-boot-docker-example-1.0.jar 访问 http://127.0.0.1:8088/ ,第一次访问可能没有数据,可以先访问下应用,然后在刷新pinpoint管控台即可 morehttps://www.cnblogs.com/yyhh/p/6106472.html Open-FalconOpen-Falcon,小米家的开源监控系统。 特点1、强大灵活的数据采集:自动发现,支持falcon-agent、snmp、支持用户主动push、用户自定义插件支持、opentsdb data model like(timestamp、endpoint、metric、key-value tags) 2、水平扩展能力:支持每个周期上亿次的数据采集、告警判定、历史数据存储和查询 3、高效率的告警策略管理:高效的portal、支持策略模板、模板继承和覆盖、多种告警方式、支持callback调用 4、人性化的告警设置:最大告警次数、告警级别、告警恢复通知、告警暂停、不同时段不同阈值、支持维护周期 5、高效率的graph组件:单机支撑200万metric的上报、归档、存储(周期为1分钟) 6、高效的历史数据query组件:采用rrdtool的数据归档策略,秒级返回上百个metric一年的历史数据 7、dashboard:多维度的数据展示,用户自定义Screen 8、高可用:整个系统无核心单点,易运维,易部署,可水平扩展 9、开发语言: 整个系统的后端,全部golang编写,portal和dashboard使用python编写。 社区和文档支持http://book.open-falcon.org/zh/index.html 环境准备安装redisyum安装1yum install -y redis 修改配置redis.conf1vi /etc/redis.conf 启动redis:redis-server & 安装mysql1yum install -y mysql-server 启动mysql:service mysqld start查看mysql状态:service mysqld status 初始化mysql数据库表数据默认没有设置密码,执行的时候出现输入密码,直接回车。12345678cd /tmp/ && git clone https://github.com/open-falcon/falcon-plus.git cd /tmp/falcon-plus/scripts/mysql/db_schema/mysql -h 127.0.0.1 -u root -p < 1_uic-db-schema.sqlmysql -h 127.0.0.1 -u root -p < 2_portal-db-schema.sqlmysql -h 127.0.0.1 -u root -p < 3_dashboard-db-schema.sqlmysql -h 127.0.0.1 -u root -p < 4_graph-db-schema.sqlmysql -h 127.0.0.1 -u root -p < 5_alarms-db-schema.sqlrm -rf /tmp/falcon-plus/ 设置mysql的root用户密码:mysql –u root查看mysql用户和密码select user,host,password from mysql.user; 发现查询密码都是空,然后开始设置root的密码为bigdata1mysql> set password for root@localhost=password('bigdata'); 退出:mysql>exit 下载编译后的二进制包12cd /data/program/softwarewget https://github.com/open-falcon/falcon-plus/releases/download/v0.2.1/open-falcon-v0.2.1.tar.gz 后端安装创建工作目录123export FALCON_HOME=/home/workexport WORKSPACE=$FALCON_HOME/open-falconmkdir -p $WORKSPACE 解压二进制包12cd /data/program/softwaretar -xzvf open-falcon-v0.2.1.tar.gz -C $WORKSPACE 配置数据库账号和密码12cd $WORKSPACEgrep -Ilr 3306 ./ | xargs -n1 -- sed -i 's/root:/root:bigdata/g' 注意root:后面默认密码为空. 启动查看目录下包括Open-Falcon的所有组件12cd $WORKSPACE./open-falcon start 检查所有模块的启动状况./open-falcon check 更多命令行工具12345678910111213141516./open-falcon [start|stop|restart|check|monitor|reload] module./open-falcon start agent./open-falcon check falcon-graph UP 53007 falcon-hbs UP 53014 falcon-judge UP 53020 falcon-transfer UP 53026 falcon-nodata UP 53032 falcon-aggregator UP 53038 falcon-agent UP 53044 falcon-gateway UP 53050 falcon-api UP 53056 falcon-alarm UP 53063For debugging , You can check $WorkDir/$moduleName/log/logs/xxx.log 前端安装创建工作目录这一步在创建后端服务的时候已经建立好,所以不需要再进行操作。1234export HOME=/home/workexport WORKSPACE=$HOME/open-falconmkdir -p $WORKSPACEcd $WORKSPACE 克隆前端组件代码12cd $WORKSPACEgit clone https://github.com/open-falcon/dashboard.git 安装依赖包12345yum install -y python-virtualenvyum install -y python-develyum install -y openldap-develyum install -y mysql-develyum groupinstall "Development tools" 12cd $WORKSPACE/dashboard/virtualenv ./env 1./env/bin/pip install -r pip_requirements.txt -i https://pypi.douban.com/simple 注意:如果执行上面有问题,就直接执行./env/bin/pip install -r pip_requirements.txt 修改配置dashboard的配置文件为: ‘rrd/config.py’,请根据实际情况修改1234## API_ADDR 表示后端api组件的地址API_ADDR = "http://127.0.0.1:8080/api/v1" ## 根据实际情况,修改PORTAL_DB_*, 默认用户名为root,默认密码为""## 根据实际情况,修改ALARM_DB_*, 默认用户名为root,默认密码为"" 启动1bash control start open http://127.0.0.1:8081 in your browser. 停止bash control stop 安装-Agent介绍agent用于采集机器负载监控指标,比如cpu.idle、load.1min、disk.io.util等等,每隔60秒push给Transfer。agent与Transfer建立了长连接,数据发送速度比较快,agent提供了一个http接口/v1/push用于接收用户手工push的一些数据,然后通过长连接迅速转发给Transfer。 部署agent需要部署到所有要被监控的机器上,比如公司有10万台机器,那就要部署10万个agent。agent本身资源消耗很少,不用担心。首先找到之前后端服务的解压目录:/home/work/open-falcon/拷贝agent到需要监控的服务器上面scp -r agent/ root@dst1:/home/work/open-falcon拷贝open-falcon到需要监控的服务器上面scp -r open-falcon root@dst1:/home/work/open-falcon修改配置文件:配置文件必须叫cfg.json,如下参照修改:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354{ "debug": true, # 控制一些debug信息的输出,生产环境通常设置为false "hostname": "", # agent采集了数据发给transfer,endpoint就设置为了hostname,默认通过`hostname`获取,如果配置中配置了hostname,就用配置中的 "ip": "", # agent与hbs心跳的时候会把自己的ip地址发给hbs,agent会自动探测本机ip,如果不想让agent自动探测,可以手工修改该配置 "plugin": { "enabled": false, # 默认不开启插件机制 "dir": "./plugin", # 把放置插件脚本的git repo clone到这个目录 "git": "https://github.com/open-falcon/plugin.git", # 放置插件脚本的git repo地址 "logs": "./logs" # 插件执行的log,如果插件执行有问题,可以去这个目录看log }, "heartbeat": { "enabled": true, # 此处enabled要设置为true "addr": "127.0.0.1:6030", # hbs的地址,端口是hbs的rpc端口 "interval": 60, # 心跳周期,单位是秒 "timeout": 1000 # 连接hbs的超时时间,单位是毫秒 }, "transfer": { "enabled": true, "addrs": [ "127.0.0.1:18433" ], # transfer的地址,端口是transfer的rpc端口, 可以支持写多个transfer的地址,agent会保证HA "interval": 60, # 采集周期,单位是秒,即agent一分钟采集一次数据发给transfer "timeout": 1000 # 连接transfer的超时时间,单位是毫秒 }, "http": { "enabled": true, # 是否要监听http端口 "listen": ":1988", "backdoor": false }, "collector": { "ifacePrefix": ["eth", "em"], # 默认配置只会采集网卡名称前缀是eth、em的网卡流量,配置为空就会采集所有的,lo的也会采集。可以从/proc/net/dev看到各个网卡的流量信息 "mountPoint": [] }, "default_tags": { }, "ignore": { # 默认采集了200多个metric,可以通过ignore设置为不采集 "cpu.busy": true, "df.bytes.free": true, "df.bytes.total": true, "df.bytes.used": true, "df.bytes.used.percent": true, "df.inodes.total": true, "df.inodes.free": true, "df.inodes.used": true, "df.inodes.used.percent": true, "mem.memtotal": true, "mem.memused": true, "mem.memused.percent": true, "mem.memfree": true, "mem.swaptotal": true, "mem.swapused": true, "mem.swapfree": true }} 启动./open-falcon start agent 启动进程./open-falcon stop agent 停止进程./open-falcon monitor agent 查看日志看var目录下的log是否正常,或者浏览器访问其1988端口。另外agent提供了一个–check参数,可以检查agent是否可以正常跑在当前机器上./falcon-agent --check可进入监控界面查看 安装数据转发服务-Transfer简介transfer是数据转发服务。它接收agent上报的数据,然后按照哈希规则进行数据分片、并将分片后的数据分别push给graph&judge等组件。 服务部署服务部署,包括配置修改、启动服务、检验服务、停止服务等。这之前,需要将安装包解压到服务的部署目录下。12# 修改配置, 配置项含义见下文vim cfg.json 12# 启动服务./open-falcon start transfer 12# 校验服务,这里假定服务开启了6060的http监听端口。检验结果为ok表明服务正常启动。curl -s "127.0.0.1:6060/health" 12# 停止服务./open-falcon stop transfer 12# 查看日志./open-falcon monitor transfer 配置说明配置文件默认为./cfg.jsondebug: true/false, 如果为true,日志中会打印debug信息123456789101112131415161718192021222324252627282930313233343536373839404142434445minStep: 30, 允许上报的数据最小间隔,默认为30秒http - enabled: true/false, 表示是否开启该http端口,该端口为控制端口,主要用来对transfer发送控制命令、统计命令、debug命令等 - listen: 表示监听的http端口rpc - enabled: true/false, 表示是否开启该jsonrpc数据接收端口, Agent发送数据使用的就是该端口 - listen: 表示监听的http端口socket #即将被废弃,请避免使用 - enabled: true/false, 表示是否开启该telnet方式的数据接收端口,这是为了方便用户一行行的发送数据给transfer - listen: 表示监听的http端口judge - enabled: true/false, 表示是否开启向judge发送数据 - batch: 数据转发的批量大小,可以加快发送速度,建议保持默认值 - connTimeout: 单位是毫秒,与后端建立连接的超时时间,可以根据网络质量微调,建议保持默认 - callTimeout: 单位是毫秒,发送数据给后端的超时时间,可以根据网络质量微调,建议保持默认 - pingMethod: 后端提供的ping接口,用来探测连接是否可用,必须保持默认 - maxConns: 连接池相关配置,最大连接数,建议保持默认 - maxIdle: 连接池相关配置,最大空闲连接数,建议保持默认 - replicas: 这是一致性hash算法需要的节点副本数量,建议不要变更,保持默认即可 - cluster: key-value形式的字典,表示后端的judge列表,其中key代表后端judge名字,value代表的是具体的ip:portgraph - enabled: true/false, 表示是否开启向graph发送数据 - batch: 数据转发的批量大小,可以加快发送速度,建议保持默认值 - connTimeout: 单位是毫秒,与后端建立连接的超时时间,可以根据网络质量微调,建议保持默认 - callTimeout: 单位是毫秒,发送数据给后端的超时时间,可以根据网络质量微调,建议保持默认 - pingMethod: 后端提供的ping接口,用来探测连接是否可用,必须保持默认 - maxConns: 连接池相关配置,最大连接数,建议保持默认 - maxIdle: 连接池相关配置,最大空闲连接数,建议保持默认 - replicas: 这是一致性hash算法需要的节点副本数量,建议不要变更,保持默认即可 - cluster: key-value形式的字典,表示后端的graph列表,其中key代表后端graph名字,value代表的是具体的ip:port(多个地址用逗号隔开, transfer会将同一份数据发送至各个地址,利用这个特性可以实现数据的多重备份)tsdb - enabled: true/false, 表示是否开启向open tsdb发送数据 - batch: 数据转发的批量大小,可以加快发送速度 - connTimeout: 单位是毫秒,与后端建立连接的超时时间,可以根据网络质量微调,建议保持默认 - callTimeout: 单位是毫秒,发送数据给后端的超时时间,可以根据网络质量微调,建议保持默认 - maxConns: 连接池相关配置,最大连接数,建议保持默认 - maxIdle: 连接池相关配置,最大空闲连接数,建议保持默认 - retry: 连接后端的重试次数和发送数据的重试次数 - address: tsdb地址或者tsdb集群vip地址, 通过tcp连接tsdb. 部署完成transfer组件后,请修改agent的配置,使其指向正确的transfer地址。在安装完graph和judge后,请修改transfer的相应配置、使其能够正确寻址到这两个组件。 安装查询组件-API介绍api组件,提供统一的restAPI操作接口。比如:api组件接收查询请求,根据一致性哈希算法去相应的graph实例查询不同metric的数据,然后汇总拿到的数据,最后统一返回给用户。 服务部署服务部署,包括配置修改、启动服务、检验服务、停止服务等。这之前,需要将安装包解压到服务的部署目录下。1234567891011# 修改配置, 配置项含义见下文, 注意graph集群的配置vim cfg.json# 启动服务./open-falcon start api# 停止服务./open-falcon stop api# 查看日志./open-falcon monitor api 服务说明注意: 请确保 graphs的内容与transfer的配置完全一致1234567891011121314151617181920212223242526272829{ "log_level": "debug", "db": { //数据库相关的连接配置信息 "faclon_portal": "root:@tcp(127.0.0.1:3306)/falcon_portal?charset=utf8&parseTime=True&loc=Local", "graph": "root:@tcp(127.0.0.1:3306)/graph?charset=utf8&parseTime=True&loc=Local", "uic": "root:@tcp(127.0.0.1:3306)/uic?charset=utf8&parseTime=True&loc=Local", "dashboard": "root:@tcp(127.0.0.1:3306)/dashboard?charset=utf8&parseTime=True&loc=Local", "alarms": "root:@tcp(127.0.0.1:3306)/alarms?charset=utf8&parseTime=True&loc=Local", "db_bug": true }, "graphs": { // graph模块的部署列表信息 "cluster": { "graph-00": "127.0.0.1:6070" }, "max_conns": 100, "max_idle": 100, "conn_timeout": 1000, "call_timeout": 5000, "numberOfReplicas": 500 }, "metric_list_file": "./api/data/metric", "web_port": ":8080", // http监听端口 "access_control": true, // 如果设置为false,那么任何用户都可以具备管理员权限 "salt": "pleaseinputwhichyouareusingnow", //数据库加密密码的时候的salt "skip_auth": false, //如果设置为true,那么访问api就不需要经过认证 "default_token": "default-token-used-in-server-side", //用于服务端各模块间的访问授权 "gen_doc": false, "gen_doc_path": "doc/module.html"} 备注: 部署完成api组件后,请修改dashboard组件的配置、使其能够正确寻址到api组件。请确保api组件的graph列表 与 transfer的配置 一致。 安装绘图数据的组件- Graph介绍graph是存储绘图数据的组件。graph组件 接收transfer组件推送上来的监控数据,同时处理api组件的查询请求、返回绘图数据。 服务部署服务部署,包括配置修改、启动服务、检验服务、停止服务等。这之前,需要将安装包解压到服务的部署目录下。(通知之前的一样,拷贝需要的包到指定的服务器) 修改配置, 配置项含义见下文12345678910vim cfg.json# 启动服务./open-falcon start graph# 停止服务./open-falcon stop graph# 查看日志 ./open-falcon monitor graph 三、 配置说明配置文件默认为./cfg.json,配置如下:123456789101112131415161718192021222324252627{ "debug": false, //true or false, 是否开启debug日志 "http": { "enabled": true, //true or false, 表示是否开启该http端口,该端口为控制端口,主要用来对graph发送控制命令、统计命令、debug命令 "listen": "0.0.0.0:6071" //表示监听的http端口 }, "rpc": { "enabled": true, //true or false, 表示是否开启该rpc端口,该端口为数据接收端口 "listen": "0.0.0.0:6070" //表示监听的rpc端口 }, "rrd": { "storage": "./data/6070" // 历史数据的文件存储路径(如有必要,请修改为合适的路径) }, "db": { "dsn": "root:@tcp(127.0.0.1:3306)/graph?loc=Local&parseTime=true", //MySQL的连接信息,默认用户名是root,密码为空,host为127.0.0.1,database为graph(如有必要,请修改) "maxIdle": 4 //MySQL连接池配置,连接池允许的最大连接数,保持默认即可 }, "callTimeout": 5000, //RPC调用超时时间,单位ms "migrate": { //扩容graph时历史数据自动迁移 "enabled": false, //true or false, 表示graph是否处于数据迁移状态 "concurrency": 2, //数据迁移时的并发连接数,建议保持默认 "replicas": 500, //这是一致性hash算法需要的节点副本数量,建议不要变更,保持默认即可(必须和transfer的配置中保持一致) "cluster": { //未扩容前老的graph实例列表 "graph-00" : "127.0.0.1:6070" } }} 备注如果上图红框中出现同一台服务器的不同名字的配置,则进入数据库,进行如下操作:进入数据库:mysql –u root –p查看所有数据库:show databses;选择数据库:use graph;查看表:show tables;查询表:select * from endpoint;删除不需要的数据:delete from endpoint where id=153;如下可以不操作:可以一起删除falcon_portal库中的host表中的无用数据。 Mysql监控工作原理在数据采集一节中我们介绍了常见的监控数据源。open-falcon作为一个监控框架,可以去采集任何系统的监控指标数据,只要将监控数据组织为open-falcon规范的格式就OK了。MySQL的数据采集可以通过mymon来做。mymon是一个cron,每分钟跑一次,配置文件中配置了数据库连接地址,mymon连到该数据库,采集一些监控指标,比如global status, global variables, slave status等等,然后组装为open-falcon规定的格式的数据,post给本机的falcon-agent。falcon-agent提供了一个http接口,使用方法可以参考数据采集中的例子。比如我们有1000台机器都部署了MySQL实例,可以在这1000台机器上分别部署1000个cron,即:与数据库实例一一对应。 配置安装下载地址:https://github.com/open-falcon/mymon安装:设置$GOPATH:export $GOPATH =/src/123456789mkdir -p $GOPATH/src/github.com/open-falconcd $GOPATH/src/github.com/open-falcongit clone https://github.com/open-falcon/mymon.gitcd mymongo get ./...go build -o mymonecho '* * * * * cd $GOPATH/src/github.com/open-falcon/mymon && ./mymon -c etc/mon.cfg' > /etc/cron.d/mymon 执行go get ./…的时候出现如下错误:12package golang.org/x/crypto/ssh/terminal: unrecognized import path "golang.org/x/crypto/ssh/terminal" (https fetch: Get https://golang.org/x/crypto/ssh/terminal?go-get=1: dial tcp 216.239.37.1:443: i/o timeout)package golang.org/x/sys/unix: unrecognized import path "golang.org/x/sys/unix" (https fetch: Get https://golang.org/x/sys/unix?go-get=1: dial tcp 216.239.37.1:443: i/o timeout) 解决办法:方法一:直接下载文件,然后把解压出来的文件夹放在src里。下载地址:https://pan.baidu.com/s/1boVAtJp 方法二:直接从git上下载对应文件放到src下面。mkdir -p $GOPATH/src/golang.org/xcd $GOPATH/src/golang.org/xgit clone https://github.com/golang/crypto.gitgit clone https://github.com/golang/sys.git 修改配置文件:/src/github.com/open-falcon/mymon/etcvi mon.cfg1234567891011121314151617181920[default] log_file=mymon.log # 日志路径和文件名 # Panic 0 # Fatal 1 # Error 2 # Warn 3 # Info 4 # Debug 5 log_level=4 # 日志级别 falcon_client=http://127.0.0.1:1988/v1/push # falcon agent连接地址 #自定义endpoint endpoint=127.0.0.1 #若不设置则使用OS的hostname [mysql] user=root # 数据库用户名 password= # 数据库密码 host=127.0.0.1 # 数据库连接地址 port=3306 # 数据库端口 采集成功! Redis监控介绍~Redis的数据采集可以通过采集脚本redis-monitor 或者 redismon来做。 redis-monitor是一个cron,每分钟跑一次采集脚本redis-monitor.py,其中配置了redis服务的地址,redis-monitor连到redis实例,采集一些监控指标,比如connected_clients、used_memory等等,然后组装为open-falcon规定的格式的数据,post给本机的falcon-agent。falcon-agent提供了一个http接口,使用方法可以参考数据采集中的例子。 比如,我们有1000台机器都部署了Redis实例,可以在这1000台机器上分别部署1000个cron,即:与Redis实例一一对应。 安装下载地址:https://github.com/iambocai/falcon-monit-scripts 进入目录:/data/program/software/falcon-monit-scripts-master/redis 修改配置文件:vi redis-monitor.py修改对应连接到agent的地址: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122#!/bin/env python#-*- coding:utf-8 -*-__author__ = 'iambocai'import jsonimport timeimport socketimport osimport reimport sysimport commandsimport urllib2, base64class RedisStats: # ĺýÍý?ăŔíťĐů??ÝťßţÓđredisŁŹ??ůťŘüîÜ?ôđ???Óđredis-cliÖŘ? _redis_cli = '/usr/local/redis/redis-cli' _stat_regex = re.compile(ur'(\w+):([0-9]+\.?[0-9]*)\r') def __init__(self, port='6379', passwd=None, host='127.0.0.1'): self._cmd = '%s -h %s -p %s info' % (self._redis_cli, host, port) if passwd not in ['', None]: self._cmd = '%s -h %s -p %s -a %s info' % (self._redis_cli, host, port, passwd) def stats(self): ' Return a dict containing redis stats ' info = commands.getoutput(self._cmd) return dict(self._stat_regex.findall(info))def main(): ip = "dst6-redis" timestamp = int(time.time()) step = 60 # inst_listńéÜÁđíÖőredisŰŐöÇŮţËěÖŞřúŁŹďďßí???ŢÁŰŐöÇńé?ö˘portűúpasswordŁŹËď?ŢĹéÄ????îÜŰ°Űö?ÔđŁŹĺýŁş # inst_list = [ i for i in commands.getoutput("find /etc/ -name 'redis*.conf'" ).split('\n') ] insts_list = [ '/usr/local/redis/redis.conf' ] p = [] monit_keys = [ ('connected_clients','GAUGE'), ('blocked_clients','GAUGE'), ('used_memory','GAUGE'), ('used_memory_rss','GAUGE'), ('mem_fragmentation_ratio','GAUGE'), ('total_commands_processed','COUNTER'), ('rejected_connections','COUNTER'), ('expired_keys','COUNTER'), ('evicted_keys','COUNTER'), ('keyspace_hits','COUNTER'), ('keyspace_misses','COUNTER'), ('keyspace_hit_ratio','GAUGE'), ] for inst in insts_list: port = commands.getoutput("sed -n 's/^port *\([0-9]\{4,5\}\)/\\1/p' %s" % inst) passwd = commands.getoutput("sed -n 's/^requirepass *\([^ ]*\)/\\1/p' %s" % inst) metric = "redis" endpoint = ip tags = 'port=%s' % port try: conn = RedisStats(port, passwd) stats = conn.stats() except Exception,e: continue for key,vtype in monit_keys: #ěéŢÁÖŐ÷úÜâîÜredisńéinfo?őóîÜăáăÓ?á´ŁŹĺýÍýĚŔá´ěéŢÁä˛?âÍéŠóúó˘îÜkeyöŚÔŻ? if key not in stats.keys(): continue #?ߊ٤ńéáă if key == 'keyspace_hit_ratio': try: value = float(stats['keyspace_hits'])/(int(stats['keyspace_hits']) + int(stats['keyspace_misses'])) except ZeroDivisionError: value = 0 #áďř¸áăăŔÝŠďĂ? elif key == 'mem_fragmentation_ratio': value = float(stats[key]) else: #ĐěöâîÜÔ´óúó˘ŕ÷counterŁŹint try: value = int(stats[key]) except: continue i = { 'Metric': '%s.%s' % (metric, key), 'Endpoint': endpoint, 'Timestamp': timestamp, 'Step': step, 'Value': value, 'CounterType': vtype, 'TAGS': tags } p.append(i) print json.dumps(p, sort_keys=True,indent=4) method = "POST" handler = urllib2.HTTPHandler() opener = urllib2.build_opener(handler) url = 'http://127.0.0.1:1988/v1/push' request = urllib2.Request(url, data=json.dumps(p) ) request.add_header("Content-Type",'application/json') request.get_method = lambda: method try: connection = opener.open(request) except urllib2.HTTPError,e: connection = e # check. Substitute with appropriate HTTP code. if connection.code == 200: print connection.read() else: print '{"err":1,"msg":"%s"}' % connectionif __name__ == '__main__': proc = commands.getoutput(' ps -ef|grep %s|grep -v grep|wc -l ' % os.path.basename(sys.argv[0])) sys.stdout.flush() if int(proc) < 5: main() 启动测试python redis-monitor.py 将脚本加入crontab执行即可 查看服务状态:service crond status 编辑:crontab –e 加入命令,一分钟执行一次,然后保存。 1*/1 * * * * python /data/program/software/falcon-monit-scripts-master/redis/redis-monitor.py 重启服务service crond restart 汇报字段 key tag type note redis.connected_clients port GAUGE 已连接客户端的数量 redis.blocked_clients port GAUGE 正在等待阻塞命令(BLPOP、BRPOP、BRPOPLPUSH)的客户端的数量 redis.used_memory port GAUGE 由 Redis 分配器分配的内存总量,以字节(byte)为单位 redis.used_memory_rss port GAUGE 从操作系统的角度,返回 Redis 已分配的内存总量(俗称常驻集大小) redis.mem_fragmentation_ratio port GAUGE used_memory_rss 和 used_memory 之间的比率 redis.total_commands_processed port COUNTER 采集周期内执行命令总数 redis.rejected_connections port COUNTER 采集周期内拒绝连接总数 redis.expired_keys port COUNTER 采集周期内过期key总数 redis.evicted_keys port COUNTER 采集周期内踢出key总数 redis.keyspace_hits port COUNTER 采集周期内key命中总数 redis.keyspace_misses port COUNTER 采集周期内key拒绝总数 redis.keyspace_hit_ratio port GAUGE 访问命中率 如需增减字段,请修改monit_keys变量 Rabbitmq监控介绍在数据采集一节中我们介绍了常见的监控数据源。open-falcon作为一个监控框架,可以去采集任何系统的监控指标数据,只要将监控数据组织为open-falcon规范的格式就OK了。 RMQ的数据采集可以通过脚本rabbitmq-monitor来做。 rabbitmq-monitor是一个cron,每分钟跑一次脚本rabbitmq-monitor.py,其中配置了RMQ的用户名&密码等,脚本连到该RMQ实例,采集一些监控指标,比如messages_ready、messages_total、deliver_rate、publish_rate等等,然后组装为open-falcon规定的格式的数据,post给本机的falcon-agent。falcon-agent提供了一个http接口,使用方法可以参考数据采集中的例子。 安装配置下载地址:https://github.com/iambocai/falcon-monit-scripts/tree/master/rabbitmq修改配置:vi rabbitmq-monitor.py 1、根据实际部署情况,修改15,16行的rabbitmq-server管理端口和登录用户名密码 2、确认1中配置的rabbitmq用户有你想监控的queue/vhosts的权限3、将脚本加入crontab即可新建脚本 : vi rabbitmq_cron12* * * * * root (cd /data/program/software/falcon-monit-scripts-master/rabbitmq && python rabbitmq-monitor.py > /dev/null)cp rabbitmq_cron /etc/cron.d/ 汇报字段 key tag type note rabbitmq.messages_ready name(Queue名字) GAUGE 队列中处于等待被消费状态消息数 rabbitmq.messages_unacknowledged name(Queue名字) GAUGE 队列中处于消费中状态的消息数 rabbitmq.messages_total name(Queue名字) GAUGE 队列中所有未完成消费的消息数,等于messages_ready+messages_unacknowledged rabbitmq.ack_rate name(Queue名字) GAUGE 消费者ack的速率 rabbitmq.deliver_rate name(Queue名字) GAUGE deliver的速率 rabbitmq.deliver_get_rate name(Queue名字) GAUGE deliver_get的速率 rabbitmq.publish_rate name(Queue名字) GAUGE publish的速率 完全片结束,觉得我写的不错?想要了解更多精彩新姿势?赶快打开我的👉个人博客 👈吧! 谢谢你那么可爱,还一直关注着我~❤😝]]></content>
<tags>
<tag>分享</tag>
<tag>软件教学</tag>
</tags>
</entry>
<entry>
<title><![CDATA[记录一次Gitlab安装并与Gitbook集成的经历]]></title>
<url>%2F2019%2F01%2F25%2F%E8%AE%B0%E5%BD%95%E4%B8%80%E6%AC%A1Gitlab%E5%AE%89%E8%A3%85%E5%B9%B6%E4%B8%8EGitbook%E9%9B%86%E6%88%90%E7%9A%84%E7%BB%8F%E5%8E%86%2F</url>
<content type="text"><![CDATA[老板今天又来提需求啦d=====( ̄▽ ̄*)b 需要把公司的一些技术文档放到一个平台上,员工阅读,同时文档要有较好的维护性,成本尽可能低。 所以,就有今天博客的内容了! 想要解锁更多新姿势?请访问我的个人博客https://tengshe789.github.io/(😘 安装gitlab1. 安装并配置必要的依赖关系在CentOS 7(和RedHat / Oracle / Scientific Linux 7)上,以下命令还将在系统防火墙中打开HTTP和SSH访问。 12345sudo yum install -y curl policycoreutils-python openssh-serversudo systemctl enable sshdsudo systemctl start sshdsudo firewall-cmd --permanent --add-service=httpsudo systemctl reload firewalld 接下来,安装Postfix以发送通知电子邮件。如果您想使用其他解决方案发送电子邮件,请在安装GitLab后跳过此步骤并[配置外部SMTP服务器](https://docs.gitlab.com/omnibus/settings/smtp.html)。 123sudo yum install postfixsudo systemctl enable postfixsudo systemctl start postfix 在Postfix安装期间,可能会出现配置屏幕。选择“Internet Site”并按Enter键。使用服务器的外部DNS作为“邮件名称”,然后按Enter键。如果出现其他屏幕,请继续按Enter键接受默认值。 2. 添加Gitlab包存储库并安装包Add the GitLab package repository. 1curl https://packages.gitlab.com/install/repositories/gitlab/gitlab-ee/script.rpm.sh | sudo bash 接下来,安装GitLab包。将http:// gitlab.example.com更改为您要访问GitLab实例的URL。安装将自动配置并启动该URL的GitLab。 HTTPS在安装后需要其他配置。 1sudo EXTERNAL_URL="http://192.168.190.132:33333" yum install -y gitlab-ee 3. 登陆并验证在您第一次访问时,您将被重定向到密码重置屏幕。提供初始管理员帐户的密码,您将被重定向回登录屏幕。使用默认帐户的用户名root登录。 See our documentation for detailed instructions on installing and configuration. 与gitbook集成安装nodeJs只能安装第三方仓库中存在的版本,一般落后 node 最新版本 安装EPEL 仓库yum install epel-release 安装 nodejsyum install nodejs 安装 npm yum install npm 安装gitbookGitBook 是一个基于 Node.js 的命令行工具,可使用 Github/Git 和 Markdown 来制作精美的电子书。GitBook支持输出以下几种文档格式 静态站点:GitBook默认输出该种格式 PDF:需要安装gitbook-pdf依赖 eBook:需要安装ebook-convert 12npm install gitbook -gnpm install -g gitbook-cli QA 出现npm ERR! Error: connect ECONNREFUSED 104.16.24.35:443问题使用如下语句关闭代理解决问题:npm config set proxy null 安装GitLab Ci Runner 添加repositorycurl -L https://packages.gitlab.com/install/repositories/runner/gitlab-ci-multi-runner/script.rpm.sh | sudo bash 安装包yum install gitlab-runner 执行上述命令之后,之后的流程如下: 第一处红线:输入部署完成的gitlab地址比如http://111.111.111.111:8080/ 第二处红线: 登陆gitlab,先点击右上方Admin Settings,再找到左侧列表的中的overview一栏找到runner选项,找到并在shell中输入token. 剩下的过程随便写即可 最后看到Runner registered successfully表示注册成功。 最后可以登录gitlab进行验证,有如图所有列表说明注册成功 配置持续集成https://docs.gitlab.com/ce/ci/quick_start/README.html#creating-a-gitlab-ciyml-file 在仓库下,创建.gitlab-ci.yml,写入以下配置: 123456789stages: - buildGenerateHTML: stage: build script: - p=`pwd` - echo $p - gitbook build - gitbook serve 打开gitlab里面的pineline,查看log。 QA 出现This job is in pending state and is waiting to be picked by a runner错误 解决:没找到优雅的方式。粗暴点,可以在服务器端重启runner,使用命令gitlab-ci-multi-runner restart。 本地使用 将仓库clone下来,gitbook init bookname 創建書名爲bookname的書 写书,必要时使用gitbook serve 预览书。 创建.gitlab-ci.yml,写入配置。然后使用git命令上传到书库。 QA CI/CD的log界面出现无法删除图书错误 解决: CI/CD图形界面,右侧,点击cancel即可。 出现RangeError: Maximum call stack size exceeded错误 解决: 文章内容过长,导致js堆栈溢出。 参考gitbook配置实例https://gitlab.com/pages/gitbook#building-locally 其实类似gitbook的软件还有很多,就譬如docsify,大家可以多查阅相关资料使用。 完全片结束,觉得我写的不错?想要了解更多精彩新姿势?赶快打开我的👉个人博客 👈吧! 谢谢你那么可爱,还一直关注着我~❤😝]]></content>
<tags>
<tag>操作系统</tag>
<tag>分享</tag>
<tag>软件教学</tag>
<tag>git</tag>
</tags>
</entry>
<entry>
<title><![CDATA[从0开始部署Rancher2.0到微服务容器部署与持续集成]]></title>
<url>%2F2019%2F01%2F22%2F%E4%BB%8E0%E5%BC%80%E5%A7%8B%E9%83%A8%E7%BD%B2Rancher2-0%2F</url>
<content type="text"><![CDATA[Rancher是一个开源的企业级容器管理平台。通过Rancher,企业再也不必自己使用一系列的开源软件去从头搭建容器服务平台。Rancher提供了在生产环境中使用的管理Docker和Kubernetes的全栈化容器部署与管理平台。今天分享一下我从0开始部署Rancher2.0到微服务容器部署与持续集成的历程。 想要解锁更多新姿势?请访问我的个人博客https://tengshe789.github.io/(😘 docker安装请看我上篇博文。 容器下安装rancher基础环境配置切换到受支持的docker版本1sudo ros engine switch docker-17.03.2-ce 关闭防火墙(可选)或者放行相应端口对于刚刚接触Rancher的用户,建议在关闭防火墙的测试环境或桌面虚拟机来运行rancher,以避免出现网络通信问题。 关闭防火墙 1、CentOS systemctl stop firewalld.service && systemctl disable firewalld.service 2、Ubuntu ufw disable 端口放行 端口放行请查看端口需求 CentOS关闭selinux1sudo sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config 配置主机时间、时区、系统语言 查看时区 date -R或者timedatectl 修改时区 ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime 修改系统语言环境 sudo echo 'LANG="en_US.UTF-8"' >> /etc/profile;source /etc/profile 配置主机NTP时间同步 Kernel性能调优1234567cat >> /etc/sysctl.conf<<EOFnet.ipv4.ip_forward=1net.bridge.bridge-nf-call-iptables=1net.ipv4.neigh.default.gc_thresh1=4096net.ipv4.neigh.default.gc_thresh2=6144net.ipv4.neigh.default.gc_thresh3=8192EOF Hosts配置每台主机的hosts(/etc/hosts),添加host_ip $hostname到/etc/hosts文件中。 安装运行rancher网络组件安装rancher以后可能会碰到一些莫名其妙的连接问题,譬如找不到命名虚拟机下能使用curl命名访问其他虚拟机,但是容器却不能访问其他容器,这时候就需要先安装weave网络组件了。 1docker pull weaveworks/weave-npc:2.5.0 正式安装可以参考如下命令安装rancher 1sudo docker run -d --restart=unless-stopped -p 80:80 -p 443:443 rancher/rancher 当然命令可以扩展,详情规则如下: 1$sudo docker run -d -v <主机路径>:/var/lib/rancher/ --restart=unless-stopped -p 80:80 -p 443:443 rancher/rancher:stable 登陆rancherRancher Server容器启动很快速,不到一分钟你就可以通过https://<server_ip>访问Rancher UI。 第一次登录会要求设置管理员密码,默认管理员账号为: admin。一旦Rancher Server成功安装,用户界面将指导你添加第一个集群 创建K8S集群首先,右下角找语言选项,选中文。点击添加集群。 接下来写一下集群的名称,点开如图所示的选项,把命令复制一下,粘贴到宿主机中运行。 注意,rancher2.0基于k8s,而k8s需要etcd节点和control节点。现在,rancher会自动帮你勾选上worker节点,如果你是单机环境,必须要全部勾选这三个主机角色在复制命令。 Docker私有仓库私有仓库搭建与配置(1)拉取私有仓库镜像(此步省略)1docker pull registry (2)启动私有仓库容器1docker run -di --name=registry -p 5000:5000 registry (3)打开浏览器 输入地址http://192.168.190.137:5000/v2/_catalog 看到123{repositories: [ ]} 表示私有仓库搭建成功并且内容为空 (4)修改docker的daemon.json1vim /etc/docker/daemon.json 添加如下内容:123{ "insecure-registries":[<server ip>:5000]} (5)重启docker 服务注意,如果上面的daemon.json格式出错,此使docker会报错,启动起不来。请仔细查看。1systemctl restart docker 镜像上传至私有仓库(1)标记此镜像为私有仓库的镜像1docker tag <imgName> <server ip>:5000/<仓库 name> (2)再次启动私服容器1docker start registry (3)上传标记的镜像1docker push <server ip>:5000/<仓库 name> 更多私有仓库经历过版本迭代,也有授权认证等功能,想要了解更多资料请看博文,https://www.cnblogs.com/wade-luffy/p/6590849.html dockerfileDockerfile是由一系列命令和参数构成的脚本,这些命令应用于基础镜像并最终创建一个新的镜像。 对于开发人员:可以为开发团队提供一个完全一致的开发环境; 对于测试人员:可以直接拿开发时所构建的镜像或者通过Dockerfile文件构建一个新的镜像开始工作了; 对于运维人员:在部署时,可以实现应用的无缝移植 dockerfile内容详解1234567891011121314151617181920# FROM : 指定基础镜像,要在哪个镜像建立FROM anapsix/alpine-java:8_server-jre_unlimited ## 我要的微服务需要依赖jre环境# 容器卷,持久化的环境VOLUME /tmp# MAINTAINER:指定维护者信息MAINTAINER [email protected]# RUN:在镜像中要执行的命令 这个命名是同步容器内时间,防止部分操作出错的。不需要可以不执行RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtimeRUN mkdir -p /miaosha-redis# WORKDIR:指定当前工作目录,相当于 cdWORKDIR /miaosha-redis# EXPOSE:指定容器要打开的端口EXPOSE 8080# ADD:相当于 COPY,但是比 COPY 功能更强大 COPY :复制本地主机的 (为 Dockerfile 所在目录的相对路径)到容器中的ADD ./miaosha-redis/target/miaosha-redis.jar ./# 容器启动时执行指令ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "app.jar"]# 容器启动时执行指令CMD ["--spring.profiles.active=test"] 执行命令构建镜像在dockerfile的目录下,输入1docker build [options] PATH | URL docker build: 用 Dockerfile 构建镜像的命令关键词。 [OPTIONS] : 命令选项,常用的指令包括 -t 指定镜像的名字,-f 显示指定构建镜像的 Dockerfile 文件(Dockerfile 可不在当前路径下),如果不使用 -f,则默认将上下文路径下的名为 Dockerfile 的文件认为是构建镜像的 “Dockerfile” 。 上下文路径|URL: 指定构建镜像的上下文的路径,构建镜像的过程中,可以且只可以引用上下文中的任何文件 。 查看镜像是否建立完成1docker images 创建容器1docker run -di --name=景象名称 /bin/bash 微服务部署微服务部署有两种方法: 手动部署: 首先基于源码打包生成jar包(或war包),将jar包(或war包)上传至虚拟机并拷贝至JDK容器。 通过Maven插件自动部署。 对于数量众多的微服务,手动部署无疑是非常麻烦的做法,并且容易出错。所以我们这里学习如何自动部署,这也是企业实际开发中经常使用的方法。 Maven插件自动部署步骤:(1)修改宿主机的docker配置,让其可以远程访问 1vi /lib/systemd/system/docker.service 其中ExecStart=后添加配置 ‐H tcp://0.0.0.0:2375 ‐H unix:///var/run/docker.sock (2)刷新配置,重启服务 123systemctl daemon‐reloadsystemctl restart dockerdocker start registry (3)微服务的maven工程pom.xml 增加配置123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596 <build> <finalName>${project.name}</finalName> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> </resource> </resources> <pluginManagement> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring-boot.version}</version> <configuration> <finalName>${project.build.finalName}</finalName> </configuration> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin><!-- docker的maven插件,官网:https://github.com/spotify/docker-maven-plugin --> <plugin> <groupId>com.spotify</groupId> <artifactId>docker-maven-plugin</artifactId> <version>0.4.12</version> <configuration> <!-- 注意imageName一定要是符合正则[a-z0-9-_.]的,否则构建不会成功 --> <!-- 详见:https://github.com/spotify/docker-maven-plugin Invalid repository name ... only [a-z0-9-_.] are allowed--> <imageName>${registry.url}/${project.name}:0.0.1</imageName> <dockerHost>${docker.url}</dockerHost> <dockerDirectory>${project.basedir}</dockerDirectory> <baseImage>java</baseImage> <entryPoint>["java", "-jar", "/${project.build.finalName}.jar"]</entryPoint> <resources> <resource> <targetPath>/</targetPath> <directory>${project.build.directory}</directory> <include>${project.build.finalName}.jar</include> </resource> </resources> <serverId>docker-hub</serverId> <registryUrl>https://index.docker.io/v1/</registryUrl> </configuration> </plugin> <plugin> <groupId>com.spotify</groupId> <artifactId>dockerfile-maven-plugin</artifactId> <version>1.4.4</version> <executions> <execution> <id>default</id> <goals> <goal>build</goal> <goal>push</goal> </goals> </execution> </executions> <dependencies> <dependency> <groupId>javax.activation</groupId> <artifactId>activation</artifactId> <version>1.1.1</version> </dependency> </dependencies> <configuration> <!--上下文路径配置,此处设置为项目根路径--> <contextDirectory>${project.basedir}</contextDirectory> <useMavenSettingsForAuth>true</useMavenSettingsForAuth> <repository>${registryUrl}/hush/${project.build.finalName}</repository> <tag>${imageVersion}</tag> <!--作为Dockerfile文件传入--> <buildArgs> <JAR_FILE>${project.build.finalName}.jar</JAR_FILE> </buildArgs> </configuration> </plugin> </plugins> </pluginManagement> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.0</version> <configuration> <target>${maven.compiler.target}</target> <source>${maven.compiler.source}</source> <encoding>UTF-8</encoding> </configuration> </plugin> </plugins> </build> 使用以上插件可以自动生成dockerfile (5)在windows的CMD下,进入微服务工程所在的目录,输入以下命令,进行打包和上传镜像 1mvn clean package docker:build -DpushImage (6)去镜像库查看镜像浏览器访问http://<server ip>:5000/v2/_catalog ,输出了我们上一步命名的镜像名称,则上传成功。 (7)在虚拟机查看镜像与启动容器 1docker images 打完以后显示镜像名称。下面该启动容器了!1docker run -di --name=服务名称 -p 容器内服务的端口号:容器发布的端口号 <server ip>:5000/镜像名称 持续集成持续部署什么是持续集成持续集成 Continuous integration ,简称CI随着软件开发复杂度的不断提高,团队开发成员间如何更好地协同工作以确保软件开发的质量已经慢慢成为开发过程中不可回避的问题。尤其是近些年来,敏捷(Agile)在软件工程领域越来越红火,如何能再不断变化的需求中快速适应和保证软件的质量也显得尤其的重要。持续集成正是针对这一类问题的一种软件开发实践。它倡导团队开发成员必须经常集成他们的工作,甚至每天都可能发生多次集成。而每次的集成都是通过自动化的构建来验证,包括自动编译、发布和测试,从而尽快地发现集成错误,让团队能够更快的开发内聚的软件。 持续集成的特点它是一个自动化的周期性的集成测试过程,从检出代码、编译构建、运行测试、结果记录、测试统计等都是自动完成的,无需人工干预;需要有专门的集成服务器来执行集成构建;需要有代码托管工具支持,我们下一小节将介绍Git以及可视化界面Gogs的使用 持续集成作用保证团队开发人员提交代码的质量,减轻了软件发布时的压力;持续集成中的任何一个环节都是自动完成的,无需太多的人工干预,有利于减少重复过程以节省时间、费用和工作量; rancher和持续集成rancher支持与GitHub平台与GitLab平台持续集成。接下来我会新开篇文章讲讲git平台私服的搭建以及持续集成。 参考资料rancher官方文档]]></content>
<tags>
<tag>操作系统</tag>
<tag>k8s</tag>
<tag>分享</tag>
<tag>软件教学</tag>
</tags>
</entry>
<entry>
<title><![CDATA[记录docker 安装与调优]]></title>
<url>%2F2019%2F01%2F18%2F%E8%AE%B0%E5%BD%95docker-%E5%AE%89%E8%A3%85%E4%B8%8E%E8%B0%83%E4%BC%98%2F</url>
<content type="text"><![CDATA[k8s是容器编排系统,需要docker的支持,今天分享一波docker的安装与调优过程。 不了解docker的童鞋可以参考博文,本篇文章的主题不在讲解docker原理~ 想要解锁更多新姿势?请访问https://tengshe789.github.io/🧐🧐🧐🧐🧐🧐🧐🧐🧐 docker 安装centos7下更新 yum 源1yum -y update 修改系统源 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455sudo cp /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.bakcat > /etc/yum.repos.d/CentOS-Base.repo << EOF[base]name=CentOS-$releasever - Base - mirrors.aliyun.comfailovermethod=prioritybaseurl=http://mirrors.aliyun.com/centos/$releasever/os/$basearch/ http://mirrors.aliyuncs.com/centos/$releasever/os/$basearch/ http://mirrors.cloud.aliyuncs.com/centos/$releasever/os/$basearch/gpgcheck=1gpgkey=http://mirrors.aliyun.com/centos/RPM-GPG-KEY-CentOS-7#released updates[updates]name=CentOS-$releasever - Updates - mirrors.aliyun.comfailovermethod=prioritybaseurl=http://mirrors.aliyun.com/centos/$releasever/updates/$basearch/ http://mirrors.aliyuncs.com/centos/$releasever/updates/$basearch/ http://mirrors.cloud.aliyuncs.com/centos/$releasever/updates/$basearch/gpgcheck=1gpgkey=http://mirrors.aliyun.com/centos/RPM-GPG-KEY-CentOS-7#additional packages that may be useful[extras]name=CentOS-$releasever - Extras - mirrors.aliyun.comfailovermethod=prioritybaseurl=http://mirrors.aliyun.com/centos/$releasever/extras/$basearch/ http://mirrors.aliyuncs.com/centos/$releasever/extras/$basearch/ http://mirrors.cloud.aliyuncs.com/centos/$releasever/extras/$basearch/gpgcheck=1gpgkey=http://mirrors.aliyun.com/centos/RPM-GPG-KEY-CentOS-7#additional packages that extend functionality of existing packages[centosplus]name=CentOS-$releasever - Plus - mirrors.aliyun.comfailovermethod=prioritybaseurl=http://mirrors.aliyun.com/centos/$releasever/centosplus/$basearch/ http://mirrors.aliyuncs.com/centos/$releasever/centosplus/$basearch/ http://mirrors.cloud.aliyuncs.com/centos/$releasever/centosplus/$basearch/gpgcheck=1enabled=0gpgkey=http://mirrors.aliyun.com/centos/RPM-GPG-KEY-CentOS-7#contrib - packages by Centos Users[contrib]name=CentOS-$releasever - Contrib - mirrors.aliyun.comfailovermethod=prioritybaseurl=http://mirrors.aliyun.com/centos/$releasever/contrib/$basearch/ http://mirrors.aliyuncs.com/centos/$releasever/contrib/$basearch/ http://mirrors.cloud.aliyuncs.com/centos/$releasever/contrib/$basearch/gpgcheck=1enabled=0gpgkey=http://mirrors.aliyun.com/centos/RPM-GPG-KEY-CentOS-7EOF 安装 netstat(用于查看当前机器端口占用情况)1234#安装yum install net-tools#使用netstat -ntlp 添加 yum 源12345678cat >/etc/yum.repos.d/docker.repo <<-EOF [dockerrepo] name=Docker Repository baseurl=https://yum.dockerproject.org/repo/main/centos/7 enabled=1 gpgcheck=1 gpgkey=https://yum.dockerproject.org/gpgEOF 安装 docker1yum install -y docker-selinux 1yum install -y docker-engine 配置docker 配置所有ip的数据包转发 1234vi /lib/systemd/system/docker.service #找到ExecStart=xxx,在这行下面加入一行,内容如下:(k8s的网络需要)ExecStartPost=/sbin/iptables -I FORWARD -s 0.0.0.0/0 -j ACCEPT 启动服务 12345#设置 docker 开机服务启动systemctl enable docker.service #立即启动 docker 服务$ systemctl start docker.service docker ce 因为CentOS的安全限制,通过RKE安装K8S集群时候无法使用root账户。所以,建议CentOS用户使用非root用户来运行docker,不管是RKE还是custom安装k8s,详情查看无法为主机配置SSH隧道。 1234567891011121314151617181920212223242526272829303132333435# 添加用户(可选)sudo adduser `<new_user>`# 为新用户设置密码sudo passwd `<new_user>`# 为新用户添加sudo权限sudo echo '<new_user> ALL=(ALL) ALL' >> /etc/sudoers# 卸载旧版本Docker软件sudo yum remove docker \ docker-client \ docker-client-latest \ docker-common \ docker-latest \ docker-latest-logrotate \ docker-logrotate \ docker-selinux \ docker-engine-selinux \ docker-engine \ container*# 定义安装版本export docker_version=17.03.2# step 1: 安装必要的一些系统工具sudo yum update -ysudo yum install -y yum-utils device-mapper-persistent-data lvm2 bash-completion# Step 2: 添加软件源信息sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo# Step 3: 更新并安装 Docker-CEsudo yum makecache allversion=$(yum list docker-ce.x86_64 --showduplicates | sort -r|grep ${docker_version}|awk '{print $2}')sudo yum -y install --setopt=obsoletes=0 docker-ce-${version} docker-ce-selinux-${version}# 如果已经安装高版本Docker,可进行降级安装(可选)yum downgrade --setopt=obsoletes=0 -y docker-ce-${version} docker-ce-selinux-${version}# 把当前用户加入docker组sudo usermod -aG docker `<new_user>`# 设置开机启动sudo systemctl enable docker Ubuntu下卸载旧版本(如果有的话)1$ apt-get remove docker docker-engine docker.io 更新apt-get源1$ add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" 修改系统源 1234567891011121314151617sudo cp /etc/apt/sources.list /etc/apt/sources.list.bakcat > /etc/apt/sources.list << EOFdeb http://mirrors.aliyun.com/ubuntu/ xenial maindeb-src http://mirrors.aliyun.com/ubuntu/ xenial maindeb http://mirrors.aliyun.com/ubuntu/ xenial-updates maindeb-src http://mirrors.aliyun.com/ubuntu/ xenial-updates maindeb http://mirrors.aliyun.com/ubuntu/ xenial universedeb-src http://mirrors.aliyun.com/ubuntu/ xenial universedeb http://mirrors.aliyun.com/ubuntu/ xenial-updates universedeb-src http://mirrors.aliyun.com/ubuntu/ xenial-updates universedeb http://mirrors.aliyun.com/ubuntu/ xenial-security maindeb-src http://mirrors.aliyun.com/ubuntu/ xenial-security maindeb http://mirrors.aliyun.com/ubuntu/ xenial-security universedeb-src http://mirrors.aliyun.com/ubuntu/ xenial-security universeEOF 1$ apt-get update 安装apt的https支持包并添加gpg秘钥123456$ apt-get install \ apt-transport-https \ ca-certificates \ curl \ software-properties-common$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - 安装docker-ce 安装最新的稳定版 1$ apt-get install -y docker-ce 安装指定版本 12345#获取版本列表$ apt-cache madison docker-ce #指定版本安装(比如版本是17.09.1~ce-0~ubuntu)$ apt-get install -y docker-ce=17.09.1~ce-0~ubuntu 接受所有ip的数据包转发 1234$ vi /lib/systemd/system/docker.service #找到ExecStart=xxx,在这行上面加入一行,内容如下:(k8s的网络需要)ExecStartPost=/sbin/iptables -I FORWARD -s 0.0.0.0/0 -j ACCEPT 启动服务 12$ systemctl daemon-reload$ service docker start 遇到问题可以参考:[官方教程][1] 部署k8s前系统的设置关闭、禁用防火墙(让所有机器之间都可以通过任意端口建立连接)123$ ufw disable#查看状态$ ufw status 设置系统参数 - 允许路由转发,不对bridge的数据进行处理123456789#写入配置文件$ cat <<EOF > /etc/sysctl.d/k8s.confnet.ipv4.ip_forward = 1net.bridge.bridge-nf-call-ip6tables = 1net.bridge.bridge-nf-call-iptables = 1EOF #生效配置文件$ sysctl -p /etc/sysctl.d/k8s.conf 配置host文件1234567#配置host,使每个Node都可以通过名字解析到ip地址$ vim /etc/hosts#加入如下片段(ip地址和servername替换成自己的)192.168.190.137 ubuntu01192.168.190.136 ubuntu03192.168.190.132 centos7192.168.190.135 ubuntu02 调优对于通过systemd来管理服务的系统(比如CentOS7.X、Ubuntu16.X), Docker有两处可以配置参数: 一个是docker.service服务配置文件,一个是Docker daemon配置文件daemon.json。 docker.service 对于CentOS系统,docker.service默认位于/usr/lib/systemd/system/docker.service;对于Ubuntu系统,docker.service默认位于/lib/systemd/system/docker.service daemon.json daemon.json默认位于/etc/docker/daemon.json,如果没有可手动创建,基于systemd管理的系统都是相同的路径。通过修改daemon.json来改过Docker配置,也是Docker官方推荐的方法。 以下说明均基于systemd,并通过/etc/docker/daemon.json来修改配置。 调优与配置配置镜像下载和上传并发数从Docker1.12开始,支持自定义下载和上传镜像的并发数,默认值上传为3个并发,下载为5个并发。通过添加”max-concurrent-downloads”和”max-concurrent-uploads”参数对其修改: 12"max-concurrent-downloads": 3,"max-concurrent-uploads": 5 配置镜像加速地址Rancher从v1.6.15开始到v2.x.x,Rancher系统相关的所有镜像(包括1.6.x上的K8S镜像)都托管在Dockerhub仓库。Dockerhub节点在国外,国内直接拉取镜像会有些缓慢。为了加速镜像的下载,可以给Docker配置国内的镜像地址。 编辑/etc/docker/daemon.json加入以下内容 123{"registry-mirrors": ["https://7bezldxe.mirror.aliyuncs.com/","https://IP:PORT/"]} 可以设置多个registry-mirrors地址,以数组形式书写,地址需要添加协议头(https或者http)。 配置insecure-registries私有仓库Docker默认只信任TLS加密的仓库地址(https),所有非https仓库默认无法登陆也无法拉取镜像。insecure-registries字面意思为不安全的仓库,通过添加这个参数对非https仓库进行授信。可以设置多个insecure-registries地址,以数组形式书写,地址不能添加协议头(http)。 编辑/etc/docker/daemon.json加入以下内容: 123{"insecure-registries": ["192.168.1.100","IP:PORT"]} 配置Docker存储驱动OverlayFS是一个新一代的联合文件系统,类似于AUFS,但速度更快,实现更简单。Docker为OverlayFS提供了两个存储驱动程序:旧版的overlay,新版的overlay2(更稳定)。 先决条件: overlay2: Linux内核版本4.0或更高版本,或使用内核版本3.10.0-514+的RHEL或CentOS。 overlay: 主机Linux内核版本3.18+ 支持的磁盘文件系统 ext4(仅限RHEL 7.1) xfs(RHEL7.2及更高版本),需要启用d_type=true。 >具体详情参考 Docker Use the OverlayFS storage driver 编辑/etc/docker/daemon.json加入以下内容 1234{"storage-driver": "overlay2","storage-opts": ["overlay2.override_kernel_check=true"]} 配置日志驱动容器在运行时会产生大量日志文件,很容易占满磁盘空间。通过配置日志驱动来限制文件大小与文件的数量。 >限制单个日志文件为100M,最多产生3个日志文件 1234567{"log-driver": "json-file","log-opts": { "max-size": "100m", "max-file": "3" }} 常见问题Ubuntu\Debian系统 ,docker info提示WARNING: No swap limit supportUbuntu\Debian系统下,默认cgroups未开启swap account功能,这样会导致设置容器内存或者swap资源限制不生效。可以通过以下命令解决: 12sudo sed -i 's/GRUB_CMDLINE_LINUX="/GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount=1 /g' /etc/default/grubsudo update-grub 注意通过以上命令可自动配置参数,如果/etc/default/grub非默认配置,需根据实际参数做调整。提示以上配置完成后,建议重启一次主机。 调优配置总结1、Docker镜像下载最大并发数 通过配置镜像上传\下载并发数max-concurrent-downloads,max-concurrent-uploads,缩短镜像上传\下载的时间。 2、配置镜像加速地址 通过配置镜像加速地址registry-mirrors,可以很大程度提高镜像下载速度。 3、配置Docker存储驱动 OverlayFS是一个新一代的联合文件系统,类似于AUFS,但速度更快,实现更简单。Docker为OverlayFS提供了两个存储驱动程序:旧版的overlay,新版的overlay2(更稳定)。 4、配置日志文件大小 容器中会产生大量日志文件,很容器占满磁盘空间。通过设置日志文件大小,可以有效控制日志文件对磁盘的占用量。例如: 5、开启WARNING: No swap limit support,WARNING: No memory limit support支持 对于Ubuntu\Debian系统,执行docker info命令时能看到警告WARNING: No swap limit support或者WARNING: No memory limit support。因为Ubuntu\Debian系统默认关闭了swap account或者功能,这样会导致设置容器内存或者swap资源限制不生效,解决方法。 最终命令输入如下: 123456789101112131415161718touch /etc/docker/daemon.jsoncat > /etc/docker/daemon.json <<EOF{ "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "3" }, "max-concurrent-downloads": 10, "max-concurrent-uploads": 10, "registry-mirrors": ["https://7bezldxe.mirror.aliyuncs.com"], "storage-driver": "overlay2", "storage-opts": [ "overlay2.override_kernel_check=true" ]}EOFsystemctl daemon-reload && systemctl restart docker 完参考资料http://www.docker.org.cn/ https://www.cnrancher.com/docs/rancher/v2.x/cn/overview/quick-start-guide/ 结束全片结束,觉得我写的不错?想要了解更多精彩新姿势?赶快打开我的👉个人博客 👈吧! 谢谢你那么可爱,还一直关注着我~❤😝]]></content>
<tags>
<tag>操作系统</tag>
<tag>k8s</tag>
<tag>软件教学</tag>
</tags>
</entry>
<entry>
<title><![CDATA[记录一次Ubuntu的连接]]></title>
<url>%2F2019%2F01%2F18%2F%E8%AE%B0%E5%BD%95%E4%B8%80%E6%AC%A1Ubuntu%E7%9A%84%E8%BF%9E%E6%8E%A5%2F</url>
<content type="text"><![CDATA[最近公司的spring cloud 云平台即将构造完成,转而要进入部署与建设平台的过程。故准备研究一下kubernetes,可是最近手头特别紧,我部门又没有独立的云服务器,而且网速很慢没法下载centos镜像,故准备安装虚拟机,使用我从未用过的Ubuntu来解决问题。这就有了这次文章的主题。 想要解锁更多新姿势?请访问我的个人博客https://tengshe789.github.io/(😘 Ubuntu开机第一次设置我是用的是Ubuntu desktop x64版,不是服务器版,带有图形化界面。装系统,开机,点击左上角设置,使用crtl+f快捷键组合,分别搜索lock,power save,将锁屏和省电全部关闭。结束。 SSH连接默认Ubuntu是不能使用ssh连接的,需要在系统内安装和配置ssh。 准备工作打开终端,输入sudo apt-get update来更新源列表。 成功后,输入sudo apt-get install openssh-server根据提示来安装ssh服务。 接下来,输入sudo apt install net-tools根据提示来安装网络工具。 最后,输入sudo apt-get install vim-gtk来安装vim工具(vim大法好!) 结束。 设置ssh查看ssh服务是否启动输入sudo ps -e |grep ssh,结果如下 如果有sshd进程,则说明ssh服务已经启动,如果没有启动,输入sudo service ssh start,ssh服务就会启动。 使用gedit修改配置文件输入sudo gedit /etc/ssh/sshd_config,把配置文件中的”PermitRootLogin without-password”加一个”#”号,把它注释掉,再增加一句”PermitRootLogin yes”,修改成功。 查找IP地址输入ifconfig,根据ip配置到ssh连接工具就好。 完成!结束 此片完了~ 想要了解更多精彩新姿势?请访问我的个人博客 。]]></content>
<tags>
<tag>操作系统</tag>
<tag>软件教学</tag>
</tags>
</entry>
<entry>
<title><![CDATA[将我的java程序组件部署到maven中央仓库]]></title>
<url>%2F2019%2F01%2F17%2F%E5%B0%86%E6%88%91%E7%9A%84%E7%A8%8B%E5%BA%8F%E7%BB%84%E4%BB%B6%E6%B3%A8%E5%86%8C%E5%88%B0maven%2F</url>
<content type="text"><![CDATA[前一阵子工作有点忙,也比较焦虑。没有时间写一些博客文章,今天来补充一下。 想要解锁更多新姿势?请访问我的博客。😏 做点什么?面临毕业,不想一直咸鱼,可是又什么都不会,做点什么的?思来想去,也是为了给自己少一点麻烦,就决定把自己半年前那个秒杀商城项目重构一下,放到云平台上。不过既然都是云平台了,肯定会有各种支撑服务,就譬如说缓存中心,日志中心,监控中心,权限管理中心等等,但是这些功能,在别的项目中也可以用到,这就带来一些不必要的麻烦。 这时候,就推荐使用maven来统一管理了。 前些日子,趁着毕业生创业指导开课的时候,我把缓存中心敲完了。然后将他部署到maven中央仓库。下面我就来分享一下部署到中央仓库的那些坑🕳。 部署到中央仓库准备工作 打开浏览器, sonatype 官网,这个地址,注册一个账号,并且记住用户名密码 下载好git客户端 提交issue在 https://issues.sonatype.org/secure/CreateIssue.jspa?issuetype=21&pid=10134 中新建一个“Project ticket”。 下面引以下Spiderman.L大佬博客上的图片(太懒了不想重新填一遍),照着这个步骤来。 点击Create之后,你就会进入一个 jira 页面(sonatype 使用jira 来追踪每一个项目进度),与此同时你注册时使用的邮箱中也会收到一封邮件提示你,任务创建成功,正在等待处理。 等待时间创建完毕后就等待一段时间,刷新页面。当状态变为“resolved”,然后你就可以使用maven上传项目了。或者就等着接收 sonatype的反馈邮件,确认已经为你创建好了新项目 回复commit发布完后,可能几分钟,也可能几小时,会有工作人员问你是否有Group Id对应的那个域名的所有权,如果有的话就回复有,并且按照他说的三个方法来(下图为参考语句),然后就会得到Configuration has been prepared的回复,这个时候就可以准备发布了。 我用的是第一个方法,因为我的域名是从阿里云上买的,我在阿里云平台的域名管理中心处,添加了一个TXT记录到这个ticket上,这样他会自己检测。 当然了,如果自己没有域名的话可以挂在开源的域名下面,例如com.github.tengshe789,这样一样可以发布。 最差的方法,就是用个人域名邮箱发送给他邮件。 获得部署权限做完上一步,他会在美国工作日时间里回复部署的相关信息,如图: 当我们的ticket最终状态变为Resolved的时候,表示我们有权限可以上传东西了。 maven本地设置找到你安装maven的目录下面,打开maven目录\conf\settings.xml文件。在servers标签中填写如下信息 123456789101112<servers> <server> <id>sonatype-nexus-snapshots</id> <username>tengshe789(我的用户名)</username> <password>(我的sonatype密码)</password> </server> <server> <id>sonatype-nexus-staging</id> <username>tengshe789(我的用户名)</username> <password>(我的sonatype密码)</password> </server></servers> java程序pom.XML设置按照下面代码添加。 12345<scm> <connection>scm:git:https://github.com/tengshe789/miaocache.git</connection> <developerConnection>scm:git:https://github.com/tengshe789/miaocache.git</developerConnection> <url>https://github.com/tengshe789/miaocache</url> <tag>0.0.1</tag> </scm> 这些地址填的是你github上的地址。 12345678910111213141516<distributionManagement> <snapshotRepository> <!-- 这个id与我上一步在setting.xml中设置的id一致的 --> <id>sonatype-nexus-snapshots</id> <name>OSS Snapshots Repository</name> <!-- 这里的url就是Issue中回复的snapshots 的repo地址--> <url>https://oss.sonatype.org/content/repositories/snapshots</url> </snapshotRepository> <repository> <!-- 这个id与我上一步在setting.xml中设置的id一致的 --> <id>sonatype-nexus-staging</id> <name>OSS Staging Repository</name> <!-- 这里的url就是Issue中回复的staging 的repo地址--> <url>https://oss.sonatype.org/service/local/staging/deploy/maven2</url> </repository></distributionManagement> 这些地址直接复制上就行。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130 <build> <finalName>${project.name}</finalName> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> </resource> </resources> <pluginManagement> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring-boot.version}</version> <configuration> <finalName>${project.build.finalName}</finalName> </configuration> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </pluginManagement> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.0</version> <configuration> <target>${maven.compiler.target}</target> <source>${maven.compiler.source}</source> <encoding>UTF-8</encoding> </configuration> </plugin> </plugins> </build> <repositories> <!--阿里云主仓库,代理了maven central和jcenter仓库--> <repository> <id>aliyun</id> <name>aliyun</name> <url>https://maven.aliyun.com/repository/public</url> <releases> <enabled>true</enabled> </releases> <snapshots> <enabled>false</enabled> </snapshots> </repository> <!--阿里云代理Spring 官方仓库--> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://maven.aliyun.com/repository/spring</url> <releases> <enabled>true</enabled> </releases> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories> <pluginRepositories> <!--阿里云代理Spring 插件仓库--> <pluginRepository> <id>spring-plugin</id> <name>spring-plugin</name> <url>https://maven.aliyun.com/repository/spring-plugin</url> <releases> <enabled>true</enabled> </releases> <snapshots> <enabled>false</enabled> </snapshots> </pluginRepository> </pluginRepositories> <profiles> <profile> <id>release</id> <build> <plugins> <!-- Source --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-source-plugin</artifactId> <version>2.2.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>jar-no-fork</goal> </goals> </execution> </executions> </plugin> <!-- Javadoc --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-javadoc-plugin</artifactId> <version>2.9.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>jar</goal> </goals> </execution> </executions> </plugin> </plugins> </build> <distributionManagement> <snapshotRepository> <id>sonatype-nexus-snapshots</id> <name>OSS Snapshots Repository</name> <url>https://oss.sonatype.org/content/repositories/snapshots</url> </snapshotRepository> <repository> <id>sonatype-nexus-staging</id> <name>OSS Staging Repository</name> <!-- 这里的url就是Issue中回复的staging 的repo地址--> <url>https://oss.sonatype.org/service/local/staging/deploy/maven2</url> </repository> </distributionManagement> </profile> </profiles></project> 下面地址是通用的插件和profiles配置,不能少,按照需求来设置。有能力的同学自己研究一下。 使用gpg生成密钥打开git窗口,输入gpg --gen-key。这时候会出现一大片英文,看不懂的同学不要紧,跟着我做就行。 1gpg --list-keys 这个命令是查看自己key的编号。找到 pub对应的那个编号,例如pub 2048R/8C473F5C上传到key验证库: 1gpg --keyserver hkp://keyserver.ubuntu.com:11371 --send-keys 8C473F5C maven命令打开idea,找到下面的终端,输入maven命令。 1mvn clean deploy -P sonatype-oss-release -Darguments="gpg.passphrase=设置gpg设置密钥时候输入的Passphrase" 参考资料https://www.cnblogs.com/aimqqroad-13/p/9645776.html https://blog.csdn.net/sun5769675/article/details/78519948 https://www.jianshu.com/p/bb930e9ad5f4 结束【版权申明】此片为原创内容,使用 CC BY-NC-SA 3.0授权条款,请遵守对应的义务,即被授权人有义务在所有副本中都必须包含版权声明。谢谢合作~ 想要解锁更多新姿势?请访问我的个人博客https://tengshe789.github.io/(😘]]></content>
<tags>
<tag>分享</tag>
</tags>
</entry>
<entry>
<title><![CDATA[新年快乐]]></title>
<url>%2F2019%2F01%2F01%2F%E6%96%B0%E5%B9%B4%E5%BF%AB%E4%B9%90%2F</url>
<content type="text"><![CDATA[最是一年春好处,绝胜烟柳满皇都。 转眼间元旦到了,首先呢,祝福所有看到这篇文章的你,元旦快乐,生活美满,心想事成。 昨夜刚刚从青岛回到济南的学校宿舍,在学校吃了一顿食堂,躺在宿舍里看平成最后的红白歌会。意外的发现一个超漂亮很有仙气的小姐姐,西野七濑 ! 我觉得我的心瞬间就被好几团大大的棉花填充了,冬天凛冽的寒风也丝毫带不足我心的一丝热量, 如图 有没有?是不是仙气十足。 可惜七濑酱已经宣布退役了,这是最后一场在乃木坂46的表演。知道这个消息的我也是比较惋惜,在推特和ins上用日文鼓励了小姐姐以后,安然入睡~ 一觉醒来,早上9点。迷糊中拿起手机,抢了几个凌晨群友发的红包,抖擞抖擞精神。接下来的任务有很多,我一一列了个清单: 抽空看玉洁 元旦期间就要上交毕业论文的开题报告,可我因为玩彩虹六号到现在还没思考写什么。。老师说随便做就行,那我又纠结起来了。用java的微服务秒杀商城?还是用python做房地产价格走势爬虫中心?第一种花时间多但还算熟练,第二张时间成本多一些。 明天要去北京出差 过年回家买火车票。 因为没有靠谱的工作所有还在准备工作面试。阿里巴巴的内推到现在还没给我发面试结果,😫继续准备 就这样把~朝着自己心中不断努力!新的一年要加油!–9012]]></content>
<tags>
<tag>初</tag>
</tags>
</entry>
<entry>
<title><![CDATA[用单例模式来说线程安全]]></title>
<url>%2F2018%2F12%2F13%2F%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F%E4%B8%8E%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%2F</url>
<content type="text"><![CDATA[上一篇博文讲了有关java和内存那些事情,今天来延申一下,结合设计模式的单例模式,来说说线程安全那些事情。 想要解锁更多新姿势?请访问https://tengshe789.github.io/ 单例模式单例模式大家应该都不陌生,为了保证系统中,应用的类一个类只有一个实例。传统课本上单例模式分两种,一种饿汉式,一种懒汉式。对应的代码如下: 懒汉式12345678910111213141516171819202122/** * 懒汉模式 * 单例实例在第一次使用时进行创建 */public class SingletonExample1 { // 私有构造函数 private SingletonExample1() { } // 单例对象 private static SingletonExample1 instance = null; // 静态的工厂方法 public static SingletonExample1 getInstance() { if (instance == null) { instance = new SingletonExample1(); } return instance; }} 懒汉式的实例是在第一次使用时创建的,相应的静态工厂办法会先判断有没有实例,没有实例在进行创建。 然而这种创建方法时线程不安全的,如果有两个线程,同一时刻拿到单例对象,要去静态工厂办法访问,由于工厂办法没有锁,那么很有可能这两个线程最终会拿到两个实例。 饿汉式12345678910111213141516171819/** * 饿汉模式 * 单例实例在类装载时进行创建 */public class SingletonExample2 { // 私有构造函数 private SingletonExample2() { } // 单例对象 private static SingletonExample2 instance = new SingletonExample2(); // 静态的工厂方法 public static SingletonExample2 getInstance() { return instance; }} 相对于上面那种懒汉式,饿汉式是线程安全的。直接将单例对象用static修饰,把实例对象放到堆内存中,保证了多个线程在访问时的可见性。但是缺点也是很大的,正是由于把实例对象放到堆内存中,这样应用一加载就会看到对应实例,极大浪费内存。 尝试用synchronized改造懒汉式插一句嘴,synchronized底层原理,主要是两个指令实现的,分别是monitorenter和monitorexit指令,下面是我从网络上找到的对应指令的解释: monitorenter: 每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下: 1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。 2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1. 3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。 monitorexit: 执行monitorexit的线程必须是objectref所对应的monitor的所有者。 指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。 通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。 毫无疑问,懒汉式的性能是出色的,我们为什么不在懒汉式的基础上使用synchronized修饰呢? 下面是相关代码: 12345678910111213141516171819202122/** * 懒汉模式 * 单例实例在第一次使用时进行创建 */public class SingletonExample3 { // 私有构造函数 private SingletonExample3() { } // 单例对象 private static SingletonExample3 instance = null; // 静态的工厂方法 public static synchronized SingletonExample3 getInstance() { if (instance == null) { instance = new SingletonExample3(); } return instance; }} 加了synchronized修饰后的工厂方法,意味着在同一时间内只允许一个线程访问。这毫无疑问是线程安全的。但是这同时是不被推荐的,为什么呢?和上面使用static修饰的懒汉模式不同,这个工厂方法,在同一时间段内只允许一个线程访问,极大的限制cpu资源,性能极其差! 双重同步锁单例模式那么我们吸取上面的教训,可不可以在使用synchronized修饰基础上在加以改进呢? 12345678910111213141516171819202122232425/** * 懒汉模式 -》 双重同步锁单例模式 * 单例实例在第一次使用时进行创建 */public class SingletonExample4 { // 私有构造函数 private SingletonExample4() { } // 单例对象 private static SingletonExample4 instance = null; // 静态的工厂方法 public static SingletonExample4 getInstance() { if (instance == null) { // 双重检测机制 // B synchronized (SingletonExample4.class) { // 同步锁 if (instance == null) { instance = new SingletonExample4(); // A - 3 } } } return instance; }} 很多人会认为这种办法是最佳的解决办法了,其实不是,这也是线程不安全的。怎么说呢? 当线程进入同步锁,走到instance = new SingletonExample4();时,JVM会进行如下操作: memory = allocate() 分配对象的内存空间 ctorInstance() 初始化对象 instance = memory 设置instance指向刚分配的内存 单线程情况下肯定没问题,但是在多线程情况下,JVM和CPU的优化中可能会执行指令重排。上面的第二步和第三步中,由于没有前后必然关系,cpu可能随时调换第二步和第三步的执行顺序。也就是会发生132这种顺序。 单例对象 volatile + 双重检测机制吸取教训继续改进! 123456789101112131415161718192021222324252627282930/** * 懒汉模式 -》 双重同步锁单例模式 * 单例实例在第一次使用时进行创建 */public class SingletonExample5 { // 私有构造函数 private SingletonExample5() { } // 1、memory = allocate() 分配对象的内存空间 // 2、ctorInstance() 初始化对象 // 3、instance = memory 设置instance指向刚分配的内存 // 单例对象 volatile + 双重检测机制 -> 禁止指令重排 private volatile static SingletonExample5 instance = null; // 静态的工厂方法 public static SingletonExample5 getInstance() { if (instance == null) { // 双重检测机制 // B synchronized (SingletonExample5.class) { // 同步锁 if (instance == null) { instance = new SingletonExample5(); // A - 3 } } } return instance; }} volatile的相关功能请看我之前的博客。到这里,懒汉模式改进就完成了。 枚举模式-》最安全1234567891011121314151617181920212223242526272829/** * 枚举模式:最安全 */public class SingletonExample6 { // 私有构造函数 private SingletonExample6() { } public static SingletonExample6 getInstance() { return Singleton.INSTANCE.getInstance(); } private enum Singleton { INSTANCE; private SingletonExample6 singleton; // JVM保证这个方法绝对只调用一次 Singleton() { singleton = new SingletonExample6(); } public SingletonExample6 getInstance() { return singleton; } }} 最推荐的是使用枚举类实现单例模式,这是线程安全的。JVM会保证枚举类中的构造方法只调用一次,因此使用枚举会保证只实例化一次。 参考资料Java并发编程:Synchronized及其实现原理 全片结束,觉得我写的不错?想要了解更多精彩新姿势?赶快打开我的👉个人博客 👈吧! 谢谢你那么可爱,还一直关注着我~❤😝]]></content>
<tags>
<tag>java</tag>
<tag>并发</tag>
<tag>设计模式</tag>
</tags>
</entry>
<entry>
<title><![CDATA[分享一下我常用的高并发测试工具]]></title>
<url>%2F2018%2F12%2F13%2F%E5%88%86%E4%BA%AB%E4%B8%80%E4%B8%8B%E6%88%91%E5%B8%B8%E7%94%A8%E7%9A%84%E9%AB%98%E5%B9%B6%E5%8F%91%E6%B5%8B%E8%AF%95%E5%B7%A5%E5%85%B7%2F</url>
<content type="text"><![CDATA[我半年前这个学习项目👉 https://github.com/tengshe789/-miaosha 👈又有人点赞了23333,看来技术还没死。那么今天分享一下这个秒杀系统使用的测试工具~ 想要解锁更多新姿势?请访问我的博客 Postman请求模拟工具下载安装日后更新 UI 使用测试界面右侧有个蓝色的巨大的button,上面写着send,旁边有各种参数。想测试的话,选择请求类型,请求数据等等,点击send就好。 环境最右上角齿轮形状的图标,添加你需要的键和值。 访问的时候直接使用/接口就行 测试记录一清二楚 新增collectionhistory旁边有collection,可以使用并发测试。 新建如图: Iterations选择并发次数,delay选择延迟时间。 结果我就不贴图了,大伙自己探索吧。 apache benchApacheBench 是 Apache 服务器自带的一个web压力测试工具,简称ab。ab又是一个命令行工具,对发起负载的本机要求很低,根据ab命令可以创建很多的并发访问线程,模拟多个访问者同时对某一URL地址进行访问,因此可以用来测试目标服务器的负载压力。总的来说ab工具小巧简单,上手学习较快,可以提供需要的基本性能指标,但是没有图形化结果,不能监控。 下载安装https://www.cnblogs.com/Ryana/p/6279232.html 使用说说命令含义 最开头的命令ab是apache bench的缩写,使用他即意味着使用这个软件 -n就是new了。 参数1000是测试请求次数 -c就是Concurrency并发,后面的参数50,是同时允许的请求数 结果如下图👇 结果依次的含义为: 并发量50 整个测试所用时间0.466 完成请求数1000 失败请求数目0 所有请求响应数据的长度总和(包括所有http头部和正文数据的长度,不包括http请求数据的长度)136000B HTML正文数据总长度4000B 吞吐率(TPS)2144.47 用户平均请求等待时间23.316ms 服务器平均请求等待时间0.466 单位时间内的数据长度284.81 Apache JMeterApache JMeter是Apache组织开发的基于Java的压力测试工具。用于对软件做压力测试,它最初被设计用于Web应用测试,但后来扩展到其他测试领域。 它可以用于测试静态和动态资源,例如静态文件、Java 小服务程序、CGI 脚本、Java 对象、数据库、FTP 服务器, 等等。JMeter 可以用于对服务器、网络或对象模拟巨大的负载,来自不同压力类别下测试它们的强度和分析整体性能。另外,JMeter能够对应用程序做功能/回归测试,通过创建带有断言的脚本来验证你的程序返回了你期望的结果。为了最大限度的灵活性,JMeter允许使用正则表达式创建断言。(复制的百度百科的233🙃) 下载安装https://jmeter.apache.org/ UI 使用新建测试如图,在右侧新建线程组,自己取个名。看右侧。 线程属性的线程数是代表多少用户访问这个系统。这里表示有50个用户访问系统。 第二行R-U P,是虚拟用户增长时长。一般的话时间设置成最合乎常规的,用户数量增长最快的时间段。 第三行,默认为一,意思是一个虚拟用户默认运行完一边后就停止了。旁边的框框,意味着测试运行起来就永远不停了 新建http请求回到左边侧栏,右键线程组,新建HTTP请求.如图 新建监听监听就是以图表形式表现出来测试的结果。 完全片结束,觉得我写的不错?想要了解更多精彩新姿势?赶快打开我的👉个人博客 👈吧! 谢谢你那么可爱,还一直关注着我~❤😝]]></content>
<tags>
<tag>并发</tag>
<tag>分享</tag>
<tag>软件教学</tag>
<tag>窍门</tag>
<tag>测试</tag>
</tags>
</entry>
<entry>
<title><![CDATA[看Pig开源项目的云踩坑记]]></title>
<url>%2F2018%2F12%2F12%2F%E5%86%B7%E5%86%B7Pig%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE%E5%AD%A6%E4%B9%A0%2F</url>
<content type="text"><![CDATA[最近看到一个有趣的开源项目pig,主要的技术点在认证授权中心,spring security oauth,zuul网关实现,Elastic-Job定时任务,趁着刚刚入门微服务,赶快写个博客分析一下。此篇文章主要用于个人备忘。如果有不对,请批评。😭 由于每个模块篇幅较长,且部分内容和前文有重叠,干货和图片较少,阅读时使用旁边的导航功能体验较佳。😉 想要解锁更多新姿势?请访问https://tengshe789.github.io/ 说明本篇文章是对基于spring boot1.5的pig 1版本做的分析,不是收费的pigx 2版本。 开源项目地址https://gitee.com/log4j/pig 配置中心:https://gitee.com/cqzqxq_lxh/pig-config 冷冷官方地址https://pig4cloud.com/zh-cn/index.html 体验地址http://pigx.pig4cloud.com/#/wel/index 项目启动顺序请确保启动顺序(要先启动认证中心,再启动网关) eureka config auth gateway upms 认证中心老规矩,自上到下看代码,先从接口层看起 请求rest接口1234567891011121314151617181920212223242526272829303132333435363738@RestController@RequestMapping("/authentication")public class AuthenticationController { @Autowired @Qualifier("consumerTokenServices") private ConsumerTokenServices consumerTokenServices; /** * 认证页面 * @return ModelAndView */ @GetMapping("/require") public ModelAndView require() { return new ModelAndView("ftl/login"); } /** * 用户信息校验 * @param authentication 信息 * @return 用户信息 */ @RequestMapping("/user") public Object user(Authentication authentication) { return authentication.getPrincipal(); } /** * 清除Redis中 accesstoken refreshtoken * * @param accesstoken accesstoken * @return true/false */ @PostMapping("/removeToken") @CacheEvict(value = SecurityConstants.TOKEN_USER_DETAIL, key = "#accesstoken") public R<Boolean> removeToken(String accesstoken) { return new R<>( consumerTokenServices.revokeToken(accesstoken)); }} 接口层有三个接口路径,第一个应该没用,剩下两个是校验用户信息的/user和清除Redis中 accesstoken 与refreshtoken的/removeToken 框架配置框架配置下面这段代码时配置各种spring security配置,包括登陆界面url是"/authentication/require"啦。如果不使用默认的弹出框而使用自己的页面,表单的action是"/authentication/form"啦。使用自己定义的过滤规则啦。禁用csrf啦(自行搜索csrf,jwt验证不需要防跨域,但是需要使用xss过滤)。使用手机登陆配置啦。 1234567891011121314151617181920212223@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER - 1)@Configuration@EnableWebSecuritypublic class PigSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { @Autowired private FilterIgnorePropertiesConfig filterIgnorePropertiesConfig; @Autowired private MobileSecurityConfigurer mobileSecurityConfigurer; @Override public void configure(HttpSecurity http) throws Exception { ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.formLogin().loginPage("/authentication/require") .loginProcessingUrl("/authentication/form") .and() .authorizeRequests(); filterIgnorePropertiesConfig.getUrls().forEach(url -> registry.antMatchers(url).permitAll()); registry.anyRequest().authenticated() .and() .csrf().disable(); http.apply(mobileSecurityConfigurer); }} 校验用户信息读配置类和接口层,我们知道了,总的逻辑大概就是用户登陆了以后,使用spring security框架的认证来获取权限。 我们一步一步看,边猜想边来。接口处有"ftl/login",这大概就是使用freemarker模板,login信息携带的token会传到用户信息校验url"/user"上,可作者直接使用Authentication返回一个getPrincipal(),就没了,根本没看见自定义的代码,这是怎么回事呢? 原来,作者使用spring security框架,使用框架来实现校验信息。 打卡config包下的PigAuthorizationConfig,我们来一探究竟。 使用spring security 实现 授权服务器注明,阅读此处模块需要OAUTH基础,https://tengshe789.github.io/2018/12/02/%E6%84%9F%E6%80%A7%E8%AE%A4%E8%AF%86jwt/#more 这里简单提一下,spring security oauth里有两个概念,授权服务器和资源服务器。 授权服务器是根据授权许可给访问的客户端发放access token令牌的,提供认证、授权服务; 资源服务器需要验证这个access token,客户端才能访问对应服务。 客户详细信息服务配置ClientDetailsServiceConfigurer(AuthorizationServerConfigurer 的一个回调配置项) 能够使用内存或者JDBC来实现客户端详情服务(ClientDetailsService),Spring Security OAuth2的配置方法是编写@Configuration类继承AuthorizationServerConfigurerAdapter,然后重写void configure(ClientDetailsServiceConfigurer clients)方法 下面代码主要逻辑是,使用spring security框架封装的简单sql连接器,查询客户端的详细信息👇 1234567@Override public void configure(` clients) throws Exception { JdbcClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource); clientDetailsService.setSelectClientDetailsSql(SecurityConstants.DEFAULT_SELECT_STATEMENT); clientDetailsService.setFindClientDetailsSql(SecurityConstants.DEFAULT_FIND_STATEMENT); clients.withClientDetails(clientDetailsService); } 相关的sql语句如下,由于耦合度较大,我将sql声明语句改了一改,方面阅读: 123456789101112131415/** * 默认的查询语句 */ String DEFAULT_FIND_STATEMENT = "select " + "client_id, client_secret, resource_ids, scope, " + "authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, " + "refresh_token_validity, additional_information, autoapprove" + " from sys_oauth_client_details" + " order by client_id"; /** * 按条件client_id 查询 */ String DEFAULT_SELECT_STATEMENT = "select " +"client_id, client_secret, resource_ids, scope, " + "authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, " + "refresh_token_validity, additional_information, autoapprove" + " from sys_oauth_client_details" + " where client_id = ?"; 相关数据库信息如下: 授权服务器端点配置器endpoints参数是什么?所有获取令牌的请求都将会在Spring MVC controller endpoints中进行处理 1234567891011121314@Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) { //token增强配置 TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); tokenEnhancerChain.setTokenEnhancers( Arrays.asList(tokenEnhancer(), jwtAccessTokenConverter())); endpoints .tokenStore(redisTokenStore()) .tokenEnhancer(tokenEnhancerChain) .authenticationManager(authenticationManager) .reuseRefreshTokens(false) .userDetailsService(userDetailsService); } token增强器(自定义token信息中携带的信息)有时候需要额外的信息加到token返回中,这部分也可以自定义,此时我们可以自定义一个TokenEnhancer,来自定义生成token携带的信息。TokenEnhancer接口提供一个 enhance(OAuth2AccessToken var1, OAuth2Authentication var2) 方法,用于对token信息的添加,信息来源于OAuth2Authentication。 作者将生成的accessToken中,加上了自己的名字,加上了userId 12345678910111213@Bean public TokenEnhancer tokenEnhancer() { return (accessToken, authentication) -> { final Map<String, Object> additionalInfo = new HashMap<>(2); additionalInfo.put("license", SecurityConstants.PIG_LICENSE); UserDetailsImpl user = (UserDetailsImpl) authentication.getUserAuthentication().getPrincipal(); if (user != null) { additionalInfo.put("userId", user.getUserId()); } ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo); return accessToken; }; } JWT转换器(自定义token信息中添加的信息)JWT中,需要在token中携带额外的信息,这样可以在服务之间共享部分用户信息,spring security默认在JWT的token中加入了user_name,如果我们需要额外的信息,需要自定义这部分内容。 JwtAccessTokenConverter是使用JWT替换默认的Token的转换器,而token令牌默认是有签名的,且资源服务器需要验证这个签名。此处的加密及验签包括两种方式: 对称加密 非对称加密(公钥密钥) 对称加密需要授权服务器和资源服务器存储同一key值,而非对称加密可使用密钥加密,暴露公钥给资源服务器验签 12345678910111213141516171819 public class PigJwtAccessTokenConverter extends JwtAccessTokenConverter { @Override public Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) { Map<String, Object> representation = (Map<String, Object>) super.convertAccessToken(token, authentication); representation.put("license", SecurityConstants.PIG_LICENSE); return representation; } @Override public OAuth2AccessToken extractAccessToken(String value, Map<String, ?> map) { return super.extractAccessToken(value, map); } @Override public OAuth2Authentication extractAuthentication(Map<String, ?> map) { return super.extractAuthentication(map); }} redis与token使用鉴权的endpoint将加上自己名字的token放入redis,redis连接器用的srping data redis框架 1234567891011121314/** * tokenstore 定制化处理 * * @return TokenStore * 1. 如果使用的 redis-cluster 模式请使用 PigRedisTokenStore * PigRedisTokenStore tokenStore = new PigRedisTokenStore(); * tokenStore.setRedisTemplate(redisTemplate); */ @Bean public TokenStore redisTokenStore() { RedisTokenStore tokenStore = new RedisTokenStore(redisConnectionFactory); tokenStore.setPrefix(SecurityConstants.PIG_PREFIX); return tokenStore; } 授权服务器安全配置器1234567@Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security .allowFormAuthenticationForClients() .tokenKeyAccess("isAuthenticated()") .checkTokenAccess("permitAll()"); } 自定义实现的手机号 认证服务接口层先看接口层,这里和pig-upms-service联动,给了三个路径,用户使用手机号码登陆可通过三个路径发送请求 12345678910111213141516171819202122232425262728@FeignClient(name = "pig-upms-service", fallback = UserServiceFallbackImpl.class)public interface UserService { /** * 通过用户名查询用户、角色信息 * * @param username 用户名 * @return UserVo */ @GetMapping("/user/findUserByUsername/{username}") UserVO findUserByUsername(@PathVariable("username") String username); /** * 通过手机号查询用户、角色信息 * * @param mobile 手机号 * @return UserVo */ @GetMapping("/user/findUserByMobile/{mobile}") UserVO findUserByMobile(@PathVariable("mobile") String mobile); /** * 根据OpenId查询用户信息 * @param openId openId * @return UserVo */ @GetMapping("/user/findUserByOpenId/{openId}") UserVO findUserByOpenId(@PathVariable("openId") String openId);} 配置类重写SecurityConfigurerAdapter的方法,通过http请求,找出有关手机号的token,用token找出相关用户的信息,已Authentication方式保存。拿到信息后,使用过滤器验证 12345678910111213141516171819@Componentpublic class MobileSecurityConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { @Autowired private AuthenticationSuccessHandler mobileLoginSuccessHandler; @Autowired private UserService userService; @Override public void configure(HttpSecurity http) throws Exception { MobileAuthenticationFilter mobileAuthenticationFilter = new MobileAuthenticationFilter(); mobileAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); mobileAuthenticationFilter.setAuthenticationSuccessHandler(mobileLoginSuccessHandler); MobileAuthenticationProvider mobileAuthenticationProvider = new MobileAuthenticationProvider(); mobileAuthenticationProvider.setUserService(userService); http.authenticationProvider(mobileAuthenticationProvider) .addFilterAfter(mobileAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); }} 手机号登录校验逻辑MobileAuthenticationProvider在spring security 中,AuthenticationManage管理一系列的AuthenticationProvider,而每一个Provider都会通UserDetailsService和UserDetail来返回一个以MobileAuthenticationToken实现的带用户以及权限的Authentication 此处逻辑是,通过UserService查找已有用户的手机号码,生成对应的UserDetails,使用UserDetails生成手机验证Authentication 123456789101112131415161718192021222324@Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { MobileAuthenticationToken mobileAuthenticationToken = (MobileAuthenticationToken) authentication; UserVO userVo = userService.findUserByMobile((String) mobileAuthenticationToken.getPrincipal()); if (userVo == null) { throw new UsernameNotFoundException("手机号不存在:" + mobileAuthenticationToken.getPrincipal()); } UserDetailsImpl userDetails = buildUserDeatils(userVo); MobileAuthenticationToken authenticationToken = new MobileAuthenticationToken(userDetails, userDetails.getAuthorities()); authenticationToken.setDetails(mobileAuthenticationToken.getDetails()); return authenticationToken; } private UserDetailsImpl buildUserDeatils(UserVO userVo) { return new UserDetailsImpl(userVo); } @Override public boolean supports(Class<?> authentication) { return MobileAuthenticationToken.class.isAssignableFrom(authentication); } 手机号登录令牌类MobileAuthenticationTokenMobileAuthenticationToken继承AbstractAuthenticationToken实现Authentication所以当在页面中输入手机之后首先会进入到MobileAuthenticationToken验证(Authentication),然后生成的Authentication会被交由我上面说的AuthenticationManager来进行管理 1234567891011121314151617181920212223242526272829303132333435363738394041424344public class MobileAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; private final Object principal; public MobileAuthenticationToken(String mobile) { super(null); this.principal = mobile; setAuthenticated(false); } public MobileAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; super.setAuthenticated(true); } @Override public Object getPrincipal() { return this.principal; } @Override public Object getCredentials() { return null; } @Override public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { if (isAuthenticated) { throw new IllegalArgumentException( "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); } super.setAuthenticated(false); } @Override public void eraseCredentials() { super.eraseCredentials(); }} 手机号登录验证filter判断http请求是否是post,不是则返回错误。 根据request请求拿到moblie信息,使用moblie信息返回手机号码登陆成功的oauth token。 12345678910111213141516171819202122@Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (postOnly && !request.getMethod().equals(HttpMethod.POST.name())) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } String mobile = obtainMobile(request); if (mobile == null) { mobile = ""; } mobile = mobile.trim(); MobileAuthenticationToken mobileAuthenticationToken = new MobileAuthenticationToken(mobile); setDetails(request, mobileAuthenticationToken); return this.getAuthenticationManager().authenticate(mobileAuthenticationToken); } 手机登陆成功的处理器MobileLoginSuccessHandler这个处理器可以返回手机号登录成功的oauth token,但是要将oauth token传输出去必须配合上面的手机号登录验证filter 逻辑都在注释中 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667@Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { String header = request.getHeader("Authorization"); if (header == null || !header.startsWith(BASIC_)) { throw new UnapprovedClientAuthenticationException("请求头中client信息为空"); } try { String[] tokens = AuthUtils.extractAndDecodeHeader(header); assert tokens.length == 2; String clientId = tokens[0]; ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId); //校验secret if (!clientDetails.getClientSecret().equals(tokens[1])) { throw new InvalidClientException("Given client ID does not match authenticated client"); } TokenRequest tokenRequest = new TokenRequest(MapUtil.newHashMap(), clientId, clientDetails.getScope(), "mobile"); //校验scope new DefaultOAuth2RequestValidator().validateScope(tokenRequest, clientDetails); OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails); OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication); OAuth2AccessToken oAuth2AccessToken = authorizationServerTokenServices.createAccessToken(oAuth2Authentication); log.info("获取token 成功:{}", oAuth2AccessToken.getValue()); response.setCharacterEncoding(CommonConstant.UTF8); response.setContentType(CommonConstant.CONTENT_TYPE); PrintWriter printWriter = response.getWriter(); printWriter.append(objectMapper.writeValueAsString(oAuth2AccessToken)); } catch (IOException e) { throw new BadCredentialsException( "Failed to decode basic authentication token"); } }/** * 从header 请求中的clientId/clientsecect * * @param header header中的参数 * @throws CheckedException if the Basic header is not present or is not valid * Base64 */ public static String[] extractAndDecodeHeader(String header) throws IOException { byte[] base64Token = header.substring(6).getBytes("UTF-8"); byte[] decoded; try { decoded = Base64.decode(base64Token); } catch (IllegalArgumentException e) { throw new CheckedException( "Failed to decode basic authentication token"); } String token = new String(decoded, CommonConstant.UTF8); int delim = token.indexOf(":"); if (delim == -1) { throw new CheckedException("Invalid basic authentication token"); } return new String[]{token.substring(0, delim), token.substring(delim + 1)}; } 其他配置redis集群挺好的模板,收藏一下 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196public class PigRedisTokenStore implements TokenStore { private static final String ACCESS = "access:"; private static final String AUTH_TO_ACCESS = "auth_to_access:"; private static final String AUTH = "auth:"; private static final String REFRESH_AUTH = "refresh_auth:"; private static final String ACCESS_TO_REFRESH = "access_to_refresh:"; private static final String REFRESH = "refresh:"; private static final String REFRESH_TO_ACCESS = "refresh_to_access:"; private static final String CLIENT_ID_TO_ACCESS = "client_id_to_access:"; private static final String UNAME_TO_ACCESS = "uname_to_access:"; private RedisTemplate<String, Object> redisTemplate; public RedisTemplate<String, Object> getRedisTemplate() { return redisTemplate; } public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) { this.redisTemplate = redisTemplate; } private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator(); public void setAuthenticationKeyGenerator(AuthenticationKeyGenerator authenticationKeyGenerator) { this.authenticationKeyGenerator = authenticationKeyGenerator; } @Override public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) { String key = authenticationKeyGenerator.extractKey(authentication); OAuth2AccessToken accessToken = (OAuth2AccessToken) redisTemplate.opsForValue().get(AUTH_TO_ACCESS + key); if (accessToken != null && !key.equals(authenticationKeyGenerator.extractKey(readAuthentication(accessToken.getValue())))) { storeAccessToken(accessToken, authentication); } return accessToken; } @Override public OAuth2Authentication readAuthentication(OAuth2AccessToken token) { return readAuthentication(token.getValue()); } @Override public OAuth2Authentication readAuthentication(String token) { return (OAuth2Authentication) this.redisTemplate.opsForValue().get(AUTH + token); } @Override public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token) { return readAuthenticationForRefreshToken(token.getValue()); } public OAuth2Authentication readAuthenticationForRefreshToken(String token) { return (OAuth2Authentication) this.redisTemplate.opsForValue().get(REFRESH_AUTH + token); } @Override public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) { this.redisTemplate.opsForValue().set(ACCESS + token.getValue(), token); this.redisTemplate.opsForValue().set(AUTH + token.getValue(), authentication); this.redisTemplate.opsForValue().set(AUTH_TO_ACCESS + authenticationKeyGenerator.extractKey(authentication), token); if (!authentication.isClientOnly()) { redisTemplate.opsForList().rightPush(UNAME_TO_ACCESS + getApprovalKey(authentication), token); } redisTemplate.opsForList().rightPush(CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId(), token); if (token.getExpiration() != null) { int seconds = token.getExpiresIn(); redisTemplate.expire(ACCESS + token.getValue(), seconds, TimeUnit.SECONDS); redisTemplate.expire(AUTH + token.getValue(), seconds, TimeUnit.SECONDS); redisTemplate.expire(AUTH_TO_ACCESS + authenticationKeyGenerator.extractKey(authentication), seconds, TimeUnit.SECONDS); redisTemplate.expire(CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId(), seconds, TimeUnit.SECONDS); redisTemplate.expire(UNAME_TO_ACCESS + getApprovalKey(authentication), seconds, TimeUnit.SECONDS); } if (token.getRefreshToken() != null && token.getRefreshToken().getValue() != null) { this.redisTemplate.opsForValue().set(REFRESH_TO_ACCESS + token.getRefreshToken().getValue(), token.getValue()); this.redisTemplate.opsForValue().set(ACCESS_TO_REFRESH + token.getValue(), token.getRefreshToken().getValue()); } } private String getApprovalKey(OAuth2Authentication authentication) { String userName = authentication.getUserAuthentication() == null ? "" : authentication.getUserAuthentication() .getName(); return getApprovalKey(authentication.getOAuth2Request().getClientId(), userName); } private String getApprovalKey(String clientId, String userName) { return clientId + (userName == null ? "" : ":" + userName); } @Override public void removeAccessToken(OAuth2AccessToken accessToken) { removeAccessToken(accessToken.getValue()); } @Override public OAuth2AccessToken readAccessToken(String tokenValue) { return (OAuth2AccessToken) this.redisTemplate.opsForValue().get(ACCESS + tokenValue); } public void removeAccessToken(String tokenValue) { OAuth2AccessToken removed = (OAuth2AccessToken) redisTemplate.opsForValue().get(ACCESS + tokenValue); // caller to do that OAuth2Authentication authentication = (OAuth2Authentication) this.redisTemplate.opsForValue().get(AUTH + tokenValue); this.redisTemplate.delete(AUTH + tokenValue); redisTemplate.delete(ACCESS + tokenValue); this.redisTemplate.delete(ACCESS_TO_REFRESH + tokenValue); if (authentication != null) { this.redisTemplate.delete(AUTH_TO_ACCESS + authenticationKeyGenerator.extractKey(authentication)); String clientId = authentication.getOAuth2Request().getClientId(); redisTemplate.opsForList().leftPop(UNAME_TO_ACCESS + getApprovalKey(clientId, authentication.getName())); redisTemplate.opsForList().leftPop(CLIENT_ID_TO_ACCESS + clientId); this.redisTemplate.delete(AUTH_TO_ACCESS + authenticationKeyGenerator.extractKey(authentication)); } } @Override public void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication) { this.redisTemplate.opsForValue().set(REFRESH + refreshToken.getValue(), refreshToken); this.redisTemplate.opsForValue().set(REFRESH_AUTH + refreshToken.getValue(), authentication); } @Override public OAuth2RefreshToken readRefreshToken(String tokenValue) { return (OAuth2RefreshToken) this.redisTemplate.opsForValue().get(REFRESH + tokenValue); } @Override public void removeRefreshToken(OAuth2RefreshToken refreshToken) { removeRefreshToken(refreshToken.getValue()); } public void removeRefreshToken(String tokenValue) { this.redisTemplate.delete(REFRESH + tokenValue); this.redisTemplate.delete(REFRESH_AUTH + tokenValue); this.redisTemplate.delete(REFRESH_TO_ACCESS + tokenValue); } @Override public void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) { removeAccessTokenUsingRefreshToken(refreshToken.getValue()); } private void removeAccessTokenUsingRefreshToken(String refreshToken) { String token = (String) this.redisTemplate.opsForValue().get(REFRESH_TO_ACCESS + refreshToken); if (token != null) { redisTemplate.delete(ACCESS + token); } } @Override public Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName) { List<Object> result = redisTemplate.opsForList().range(UNAME_TO_ACCESS + getApprovalKey(clientId, userName), 0, -1); if (result == null || result.size() == 0) { return Collections.emptySet(); } List<OAuth2AccessToken> accessTokens = new ArrayList<>(result.size()); for (Iterator<Object> it = result.iterator(); it.hasNext(); ) { OAuth2AccessToken accessToken = (OAuth2AccessToken) it.next(); accessTokens.add(accessToken); } return Collections.unmodifiableCollection(accessTokens); } @Override public Collection<OAuth2AccessToken> findTokensByClientId(String clientId) { List<Object> result = redisTemplate.opsForList().range((CLIENT_ID_TO_ACCESS + clientId), 0, -1); if (result == null || result.size() == 0) { return Collections.emptySet(); } List<OAuth2AccessToken> accessTokens = new ArrayList<>(result.size()); for (Iterator<Object> it = result.iterator(); it.hasNext(); ) { OAuth2AccessToken accessToken = (OAuth2AccessToken) it.next(); accessTokens.add(accessToken); } return Collections.unmodifiableCollection(accessTokens); }} 服务网关模块网关主体在包pig\pig-gateway\src\main\java\com\github\pig\gateway下 作者使用了Zuul做为网关,它Netflix开源的微服务网关,可以和Eureka,Ribbon,Hystrix等组件配合使用。 Zuul组件的核心是一系列的过滤器,这些过滤器可以完成以下功能: 身份认证和安全: 识别每一个资源的验证要求,并拒绝那些不符的请求 审查与监控: 动态路由:动态将请求路由到不同后端集群 压力测试:逐渐增加指向集群的流量,以了解性能 负载分配:为每一种负载类型分配对应容量,并弃用超出限定值的请求 静态响应处理:边缘位置进行响应,避免转发到内部集群 多区域弹性:跨域AWS Region进行请求路由,旨在实现ELB(ElasticLoad Balancing)使用多样化 多种功能的过滤器过滤器Zuul组件的核心是一系列的过滤器,我们先从过滤器下手。 网关统一异常过滤器123456789101112131415161718192021222324@Componentpublic class ErrorHandlerFilter extends ZuulFilter { @Autowired private LogSendService logSendService; @Override public String filterType() { return ERROR_TYPE; } @Override public int filterOrder() { return SEND_RESPONSE_FILTER_ORDER + 1; } @Override public boolean shouldFilter() { RequestContext requestContext = RequestContext.getCurrentContext(); return requestContext.getThrowable() != null; } @Override public Object run() { RequestContext requestContext = RequestContext.getCurrentContext(); logSendService.send(requestContext); return null; }} 作者以原生zuul过滤器为基础加了日志配置,优先级为+1,数字越大优先级越低。 XSS过滤器123456public class XssSecurityFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper(request); filterChain.doFilter(xssRequest, response); } 重写springMVC里面的的确保在一次请求只通过一次filter的类OncePerRequestFilter,添加一条https://gitee.com/renrenio/renren-fast的工具类`XssHttpServletRequestWrapper`为过滤链条。 1234567891011@Override public ServletInputStream getInputStream() throws IOException {····略 //xss过滤 json = xssEncode(json); final ByteArrayInputStream bis = new ByteArrayInputStream(json.getBytes("utf-8")); return new ServletInputStream() {···略 } }; } 密码过滤器DecodePasswordFilter此过滤器优先级为+2.每当一个请求不是请求/oauth/token或者/mobile/token这个地址时,都会解析使用aes解码器password。 12345678910111213141516171819202122232425@Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); Map<String, List<String>> params = ctx.getRequestQueryParams(); if (params == null) { return null; } List<String> passList = params.get(PASSWORD); if (CollUtil.isEmpty(passList)) { return null; } String password = passList.get(0); if (StrUtil.isNotBlank(password)) { try { password = decryptAES(password, key); } catch (Exception e) { log.error("密码解密失败:{}", password); } params.put(PASSWORD, CollUtil.newArrayList(password.trim())); } ctx.setRequestQueryParams(params); return null; } 校验码过滤器ValidateCodeFilter逻辑作者都写在注释中了,此处使用了redis做为服务端验证码的缓存 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192** * 是否校验验证码 * 1. 判断验证码开关是否开启 * 2. 判断请求是否登录请求 * 2.1 判断是不是刷新请求(不用单独在建立刷新客户端) * 3. 判断终端是否支持 * * @return true/false */ @Override public boolean shouldFilter() { HttpServletRequest request = RequestContext.getCurrentContext().getRequest(); if (!StrUtil.containsAnyIgnoreCase(request.getRequestURI(), SecurityConstants.OAUTH_TOKEN_URL, SecurityConstants.MOBILE_TOKEN_URL)) { return false; } if (SecurityConstants.REFRESH_TOKEN.equals(request.getParameter(GRANT_TYPE))) { return false; } try { String[] clientInfos = AuthUtils.extractAndDecodeHeader(request); if (CollUtil.containsAny(filterIgnorePropertiesConfig.getClients(), Arrays.asList(clientInfos))) { return false; } } catch (IOException e) { log.error("解析终端信息失败", e); } return true; } @Override public Object run() { try { checkCode(RequestContext.getCurrentContext().getRequest()); } catch (ValidateCodeException e) { RequestContext ctx = RequestContext.getCurrentContext(); R<String> result = new R<>(e); result.setCode(478); ctx.setResponseStatusCode(478); ctx.setSendZuulResponse(false); ctx.getResponse().setContentType("application/json;charset=UTF-8"); ctx.setResponseBody(JSONObject.toJSONString(result)); } return null; } /** * 检查code * * @param httpServletRequest request * @throws ValidateCodeException 验证码校验异常 */ private void checkCode(HttpServletRequest httpServletRequest) throws ValidateCodeException { String code = httpServletRequest.getParameter("code"); if (StrUtil.isBlank(code)) { throw new ValidateCodeException("请输入验证码"); } String randomStr = httpServletRequest.getParameter("randomStr"); if (StrUtil.isBlank(randomStr)) { randomStr = httpServletRequest.getParameter("mobile"); } String key = SecurityConstants.DEFAULT_CODE_KEY + randomStr; if (!redisTemplate.hasKey(key)) { throw new ValidateCodeException(EXPIRED_CAPTCHA_ERROR); } Object codeObj = redisTemplate.opsForValue().get(key); if (codeObj == null) { throw new ValidateCodeException(EXPIRED_CAPTCHA_ERROR); } String saveCode = codeObj.toString(); if (StrUtil.isBlank(saveCode)) { redisTemplate.delete(key); throw new ValidateCodeException(EXPIRED_CAPTCHA_ERROR); } if (!StrUtil.equals(saveCode, code)) { redisTemplate.delete(key); throw new ValidateCodeException("验证码错误,请重新输入"); } redisTemplate.delete(key); } 灰度发布灰度发布,已经不是一个很新的概念了.一个产品,如果需要快速迭代开发上线,又要保证质量,保证刚上线的系统,一旦出现问题那么可以很快的控制影响面,就需要设计一套灰度发布系统. 灰度发布系统的作用在于,可以根据自己的配置,来将用户的流量导到新上线的系统上,来快速验证新的功能修改,而一旦出问题,也可以马上的恢复,简单的说,就是一套A/BTest系统. 初始化下面是灰度路由初始化类: 12345678910111213@Configuration@ConditionalOnClass(DiscoveryEnabledNIWSServerList.class)@AutoConfigureBefore(RibbonClientConfiguration.class)@ConditionalOnProperty(value = "zuul.ribbon.metadata.enabled")public class RibbonMetaFilterAutoConfiguration { @Bean @ConditionalOnMissingBean @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public ZoneAvoidanceRule metadataAwareRule() { return new MetadataCanaryRuleHandler(); }} 灰度发布有关过滤器AccessFilter首先重写filterOrder()方法,使这个过滤器在在RateLimitPreFilter之前运行,不会出现空指针问题。此处优先级FORM_BODY_WRAPPER_FILTER_ORDER-1. 12345678910111213141516171819202122232425262728293031323334353637@Componentpublic class AccessFilter extends ZuulFilter { @Value("${zuul.ribbon.metadata.enabled:false}") private boolean canary; @Override public String filterType() { return FilterConstants.PRE_TYPE; } @Override public int filterOrder() { return FORM_BODY_WRAPPER_FILTER_ORDER - 1; } @Override public boolean shouldFilter() { return true; } @Override public Object run() { RequestContext requestContext = RequestContext.getCurrentContext(); String version = requestContext.getRequest().getHeader(SecurityConstants.VERSION); if (canary && StrUtil.isNotBlank(version)) { RibbonVersionHolder.setContext(version); } requestContext.set("startTime", System.currentTimeMillis()); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null) { requestContext.addZuulRequestHeader(SecurityConstants.USER_HEADER, authentication.getName()); requestContext.addZuulRequestHeader(SecurityConstants.ROLE_HEADER, CollectionUtil.join(authentication.getAuthorities(), ",")); } return null; }} 核心方法在run()上,首先受到request请求,拿到他的版本约束信息,然后根据选择添加token 路由微服务断言处理器MetadataCanaryRuleHandler自定义ribbon路由规则匹配多版本请求,实现灰度发布。复合判断server所在区域的性能和server的可用性选择server,即,使用ZoneAvoidancePredicate和AvailabilityPredicate来判断是否选择某个server,前一个判断判定一个zone的运行性能是否可用,剔除不可用的zone(的所有server),AvailabilityPredicate用于过滤掉连接数过多的Server。 此处逻辑是 eureka metadata (主机名,IP地址,端口号,状态页健康检查等信息,或者通过配置文件自定义元数据)存在版本定义时候进行判断 不存在 metadata 直接返回true 1234567891011121314151617181920212223242526272829@Override public AbstractServerPredicate getPredicate() { return new AbstractServerPredicate() { @Override public boolean apply(PredicateKey predicateKey) { String targetVersion = RibbonVersionHolder.getContext(); RibbonVersionHolder.clearContext(); if (StrUtil.isBlank(targetVersion)) { log.debug("客户端未配置目标版本直接路由"); return true; } DiscoveryEnabledServer server = (DiscoveryEnabledServer) predicateKey.getServer(); final Map<String, String> metadata = server.getInstanceInfo().getMetadata(); if (StrUtil.isBlank(metadata.get(SecurityConstants.VERSION))) { log.debug("当前微服务{} 未配置版本直接路由"); return true; } if (metadata.get(SecurityConstants.VERSION).equals(targetVersion)) { return true; } else { log.debug("当前微服务{} 版本为{},目标版本{} 匹配失败", server.getInstanceInfo().getAppName() , metadata.get(SecurityConstants.VERSION), targetVersion); return false; } } }; } 动态路由配置123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687public class DynamicRouteLocator extends DiscoveryClientRouteLocator { private ZuulProperties properties; private RedisTemplate redisTemplate; public DynamicRouteLocator(String servletPath, DiscoveryClient discovery, ZuulProperties properties, ServiceInstance localServiceInstance, RedisTemplate redisTemplate) { super(servletPath, discovery, properties, localServiceInstance); this.properties = properties; this.redisTemplate = redisTemplate; } /** * 重写路由配置 * <p> * 1. properties 配置。 * 2. eureka 默认配置。 * 3. DB数据库配置。 * * @return 路由表 */ @Override protected LinkedHashMap<String, ZuulProperties.ZuulRoute> locateRoutes() { LinkedHashMap<String, ZuulProperties.ZuulRoute> routesMap = new LinkedHashMap<>(); //读取properties配置、eureka默认配置 routesMap.putAll(super.locateRoutes()); log.debug("初始默认的路由配置完成"); routesMap.putAll(locateRoutesFromDb()); LinkedHashMap<String, ZuulProperties.ZuulRoute> values = new LinkedHashMap<>(); for (Map.Entry<String, ZuulProperties.ZuulRoute> entry : routesMap.entrySet()) { String path = entry.getKey(); if (!path.startsWith("/")) { path = "/" + path; } if (StrUtil.isNotBlank(this.properties.getPrefix())) { path = this.properties.getPrefix() + path; if (!path.startsWith("/")) { path = "/" + path; } } values.put(path, entry.getValue()); } return values; } /** * Redis中保存的,没有从upms拉去,避免启动链路依赖问题(取舍),网关依赖业务模块的问题 * * @return */ private Map<String, ZuulProperties.ZuulRoute> locateRoutesFromDb() { Map<String, ZuulProperties.ZuulRoute> routes = new LinkedHashMap<>(); Object obj = redisTemplate.opsForValue().get(CommonConstant.ROUTE_KEY); if (obj == null) { return routes; } List<SysZuulRoute> results = (List<SysZuulRoute>) obj; for (SysZuulRoute result : results) { if (StrUtil.isBlank(result.getPath()) && StrUtil.isBlank(result.getUrl())) { continue; } ZuulProperties.ZuulRoute zuulRoute = new ZuulProperties.ZuulRoute(); try { zuulRoute.setId(result.getServiceId()); zuulRoute.setPath(result.getPath()); zuulRoute.setServiceId(result.getServiceId()); zuulRoute.setRetryable(StrUtil.equals(result.getRetryable(), "0") ? Boolean.FALSE : Boolean.TRUE); zuulRoute.setStripPrefix(StrUtil.equals(result.getStripPrefix(), "0") ? Boolean.FALSE : Boolean.TRUE); zuulRoute.setUrl(result.getUrl()); List<String> sensitiveHeadersList = StrUtil.splitTrim(result.getSensitiveheadersList(), ","); if (sensitiveHeadersList != null) { Set<String> sensitiveHeaderSet = CollUtil.newHashSet(); sensitiveHeadersList.forEach(sensitiveHeader -> sensitiveHeaderSet.add(sensitiveHeader)); zuulRoute.setSensitiveHeaders(sensitiveHeaderSet); zuulRoute.setCustomSensitiveHeaders(true); } } catch (Exception e) { log.error("从数据库加载路由配置异常", e); } log.debug("添加数据库自定义的路由配置,path:{},serviceId:{}", zuulRoute.getPath(), zuulRoute.getServiceId()); routes.put(zuulRoute.getPath(), zuulRoute); } return routes; }} 网关日志处理代码注释已经将逻辑写的很清楚了 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576@Slf4j@Componentpublic class LogSendServiceImpl implements LogSendService { private static final String SERVICE_ID = "serviceId"; @Autowired private AmqpTemplate rabbitTemplate; /** * 1. 获取 requestContext 中的请求信息 * 2. 如果返回状态不是OK,则获取返回信息中的错误信息 * 3. 发送到MQ * * @param requestContext 上下文对象 */ @Override public void send(RequestContext requestContext) { HttpServletRequest request = requestContext.getRequest(); String requestUri = request.getRequestURI(); String method = request.getMethod(); SysLog sysLog = new SysLog(); sysLog.setType(CommonConstant.STATUS_NORMAL); sysLog.setRemoteAddr(HttpUtil.getClientIP(request)); sysLog.setRequestUri(URLUtil.getPath(requestUri)); sysLog.setMethod(method); sysLog.setUserAgent(request.getHeader("user-agent")); sysLog.setParams(HttpUtil.toParams(request.getParameterMap())); Long startTime = (Long) requestContext.get("startTime"); sysLog.setTime(System.currentTimeMillis() - startTime); if (requestContext.get(SERVICE_ID) != null) { sysLog.setServiceId(requestContext.get(SERVICE_ID).toString()); } //正常发送服务异常解析 if (requestContext.getResponseStatusCode() == HttpStatus.SC_INTERNAL_SERVER_ERROR && requestContext.getResponseDataStream() != null) { InputStream inputStream = requestContext.getResponseDataStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); InputStream stream1 = null; InputStream stream2; byte[] buffer = IoUtil.readBytes(inputStream); try { baos.write(buffer); baos.flush(); stream1 = new ByteArrayInputStream(baos.toByteArray()); stream2 = new ByteArrayInputStream(baos.toByteArray()); String resp = IoUtil.read(stream1, CommonConstant.UTF8); sysLog.setType(CommonConstant.STATUS_LOCK); sysLog.setException(resp); requestContext.setResponseDataStream(stream2); } catch (IOException e) { log.error("响应流解析异常:", e); throw new RuntimeException(e); } finally { IoUtil.close(stream1); IoUtil.close(baos); IoUtil.close(inputStream); } } //网关内部异常 Throwable throwable = requestContext.getThrowable(); if (throwable != null) { log.error("网关异常", throwable); sysLog.setException(throwable.getMessage()); } //保存发往MQ(只保存授权) Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && StrUtil.isNotBlank(authentication.getName())) { LogVO logVo = new LogVO(); sysLog.setCreateBy(authentication.getName()); logVo.setSysLog(sysLog); logVo.setUsername(authentication.getName()); rabbitTemplate.convertAndSend(MqQueueConstant.LOG_QUEUE, logVo); } }} 多维度限流限流降级处理器ZuulRateLimiterErrorHandler重写zuul中默认的限流处理器DefaultRateLimiterErrorHandler,使之记录日志内容 12345678910111213141516171819@Bean public RateLimiterErrorHandler rateLimitErrorHandler() { return new DefaultRateLimiterErrorHandler() { @Override public void handleSaveError(String key, Exception e) { log.error("保存key:[{}]异常", key, e); } @Override public void handleFetchError(String key, Exception e) { log.error("路由失败:[{}]异常", key); } @Override public void handleError(String msg, Exception e) { log.error("限流异常:[{}]", msg, e); } }; } 与spring security oAuth方法整合单点登陆授权拒绝处理器 PigAccessDeniedHandler重写Srping security oAuth 提供单点登录验证拒绝OAuth2AccessDeniedHandler接口,使用R包装失败信息到PigDeniedException 12345678910@Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException authException) throws IOException, ServletException { log.info("授权失败,禁止访问 {}", request.getRequestURI()); response.setCharacterEncoding(CommonConstant.UTF8); response.setContentType(CommonConstant.CONTENT_TYPE); R<String> result = new R<>(new PigDeniedException("授权失败,禁止访问")); response.setStatus(HttpStatus.SC_FORBIDDEN); PrintWriter printWriter = response.getWriter(); printWriter.append(objectMapper.writeValueAsString(result)); } 菜单管理MenuService1234567891011@FeignClient(name = "pig-upms-service", fallback = MenuServiceFallbackImpl.class)public interface MenuService { /** * 通过角色名查询菜单 * * @param role 角色名称 * @return 菜单列表 */ @GetMapping(value = "/menu/findMenuByRole/{role}") Set<MenuVO> findMenuByRole(@PathVariable("role") String role);} 使用feign连接pig系统的菜单微服务 菜单权限123456789101112131415161718192021222324252627282930313233343536373839@Service("permissionService")public class PermissionServiceImpl implements PermissionService { @Autowired private MenuService menuService; private AntPathMatcher antPathMatcher = new AntPathMatcher(); @Override public boolean hasPermission(HttpServletRequest request, Authentication authentication) { //ele-admin options 跨域配置,现在处理是通过前端配置代理,不使用这种方式,存在风险// if (HttpMethod.OPTIONS.name().equalsIgnoreCase(request.getMethod())) {// return true;// } Object principal = authentication.getPrincipal(); List<SimpleGrantedAuthority> authorityList = (List<SimpleGrantedAuthority>) authentication.getAuthorities(); AtomicBoolean hasPermission = new AtomicBoolean(false); if (principal != null) { if (CollUtil.isEmpty(authorityList)) { log.warn("角色列表为空:{}", authentication.getPrincipal()); return false; } Set<MenuVO> urls = new HashSet<>(); authorityList.stream().filter(authority -> !StrUtil.equals(authority.getAuthority(), "ROLE_USER")) .forEach(authority -> { Set<MenuVO> menuVOSet = menuService.findMenuByRole(authority.getAuthority()); CollUtil.addAll(urls, menuVOSet); }); urls.stream().filter(menu -> StrUtil.isNotEmpty(menu.getUrl()) && antPathMatcher.match(menu.getUrl(), request.getRequestURI()) && request.getMethod().equalsIgnoreCase(menu.getMethod())) .findFirst().ifPresent(menuVO -> hasPermission.set(true)); } return hasPermission.get(); }} 网关总结pig这个系统是个很好的框架,本次体验的是pig的zuul网关模块,此模块与feign,ribbon,spring security,Eurasia进行整合,完成或部分完成了动态路由,灰度发布,菜单权限管理,服务限流,网关日志处理,非常值得学习! UPMs权限管理系统模块百度了一下,UPMS是User Permissions Management System,通用用户权限管理系统 数据库设计部门表 部门关系表 字典表1234567891011121314151617181920212223242526272829303132333435363738394041424344/** * 编号 */ @TableId(value="id", type= IdType.AUTO) private Integer id; /** * 数据值 */ private String value; /** * 标签名 */ private String label; /** * 类型 */ private String type; /** * 描述 */ private String description; /** * 排序(升序) */ private BigDecimal sort; /** * 创建时间 */ @TableField("create_time") private Date createTime; /** * 更新时间 */ @TableField("update_time") private Date updateTime; /** * 备注信息 */ private String remarks; /** * 删除标记 */ @TableField("del_flag") private String delFlag; 日志表1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465@Datapublic class SysLog implements Serializable { private static final long serialVersionUID = 1L; /** * 编号 */ @TableId(type = IdType.ID_WORKER) @JsonSerialize(using = ToStringSerializer.class) private Long id; /** * 日志类型 */ private String type; /** * 日志标题 */ private String title; /** * 创建者 */ private String createBy; /** * 创建时间 */ private Date createTime; /** * 更新时间 */ private Date updateTime; /** * 操作IP地址 */ private String remoteAddr; /** * 用户代理 */ private String userAgent; /** * 请求URI */ private String requestUri; /** * 操作方式 */ private String method; /** * 操作提交的数据 */ private String params; /** * 执行时间 */ private Long time; /** * 删除标记 */ private String delFlag; /** * 异常信息 */ private String exception; /** * 服务ID */ private String serviceId; }} 菜单权限表 角色表 角色与部门对应关系略 角色与菜单权限对应关系略 用户表12345678910111213141516171819202122232425262728293031323334353637383940414243444546/** * 主键ID */@TableId(value = "user_id", type = IdType.AUTO)private Integer userId;/** * 用户名 */private String username;private String password;/** * 随机盐 */@JsonIgnoreprivate String salt;/** * 创建时间 */@TableField("create_time")private Date createTime;/** * 修改时间 */@TableField("update_time")private Date updateTime;/** * 0-正常,1-删除 */@TableField("del_flag")private String delFlag;/** * 简介 */private String phone;/** * 头像 */private String avatar;/** * 部门ID */@TableField("dept_id")private Integer deptId; 动态路由配置表 业务逻辑 全是基于mybatis plus的CRUD,有点多。大部分干这行的都懂,我就不详细展开了。 验证码创建ValidateCodeController可以找到创建验证码相关代码123456789101112131415161718192021/** * 创建验证码 * * @param request request * @throws Exception */ @GetMapping(SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX + "/{randomStr}") public void createCode(@PathVariable String randomStr, HttpServletRequest request, HttpServletResponse response) throws Exception { Assert.isBlank(randomStr, "机器码不能为空"); response.setHeader("Cache-Control", "no-store, no-cache"); response.setContentType("image/jpeg"); //生成文字验证码 String text = producer.createText(); //生成图片验证码 BufferedImage image = producer.createImage(text); userService.saveImageCode(randomStr, text); ServletOutputStream out = response.getOutputStream(); ImageIO.write(image, "JPEG", out); IOUtils.closeQuietly(out); } 其中的 producer是使用Kaptcha,下面是配置类 123456789101112131415161718192021222324252627@Configurationpublic class KaptchaConfig { private static final String KAPTCHA_BORDER = "kaptcha.border"; private static final String KAPTCHA_TEXTPRODUCER_FONT_COLOR = "kaptcha.textproducer.font.color"; private static final String KAPTCHA_TEXTPRODUCER_CHAR_SPACE = "kaptcha.textproducer.char.space"; private static final String KAPTCHA_IMAGE_WIDTH = "kaptcha.image.width"; private static final String KAPTCHA_IMAGE_HEIGHT = "kaptcha.image.height"; private static final String KAPTCHA_TEXTPRODUCER_CHAR_LENGTH = "kaptcha.textproducer.char.length"; private static final Object KAPTCHA_IMAGE_FONT_SIZE = "kaptcha.textproducer.font.size"; @Bean public DefaultKaptcha producer() { Properties properties = new Properties(); properties.put(KAPTCHA_BORDER, SecurityConstants.DEFAULT_IMAGE_BORDER); properties.put(KAPTCHA_TEXTPRODUCER_FONT_COLOR, SecurityConstants.DEFAULT_COLOR_FONT); properties.put(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, SecurityConstants.DEFAULT_CHAR_SPACE); properties.put(KAPTCHA_IMAGE_WIDTH, SecurityConstants.DEFAULT_IMAGE_WIDTH); properties.put(KAPTCHA_IMAGE_HEIGHT, SecurityConstants.DEFAULT_IMAGE_HEIGHT); properties.put(KAPTCHA_IMAGE_FONT_SIZE, SecurityConstants.DEFAULT_IMAGE_FONT_SIZE); properties.put(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, SecurityConstants.DEFAULT_IMAGE_LENGTH); Config config = new Config(properties); DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); defaultKaptcha.setConfig(config); return defaultKaptcha; }} 发送手机验证码大体逻辑为,先查询验证码redis缓存,没有缓存则说明验证码缓存没有失效,返回错误。 查到没有验证码,则根据手机号码从数据库获得用户信息,生成一个4位的验证码,使用rabbbitmq队列把短信验证码保存到队列,同时加上手机验证码的redis缓存 1234567891011121314151617181920212223242526272829303132333435363738394041424344/** * 发送验证码 * <p> * 1. 先去redis 查询是否 60S内已经发送 * 2. 未发送: 判断手机号是否存 ? false :产生4位数字 手机号-验证码 * 3. 发往消息中心-》发送信息 * 4. 保存redis * * @param mobile 手机号 * @return true、false */ @Override public R<Boolean> sendSmsCode(String mobile) { Object tempCode = redisTemplate.opsForValue().get(SecurityConstants.DEFAULT_CODE_KEY + mobile); if (tempCode != null) { log.error("用户:{}验证码未失效{}", mobile, tempCode); return new R<>(false, "验证码未失效,请失效后再次申请"); } SysUser params = new SysUser(); params.setPhone(mobile); List<SysUser> userList = this.selectList(new EntityWrapper<>(params)); if (CollectionUtil.isEmpty(userList)) { log.error("根据用户手机号{}查询用户为空", mobile); return new R<>(false, "手机号不存在"); } String code = RandomUtil.randomNumbers(4); JSONObject contextJson = new JSONObject(); contextJson.put("code", code); contextJson.put("product", "Pig4Cloud"); log.info("短信发送请求消息中心 -> 手机号:{} -> 验证码:{}", mobile, code); rabbitTemplate.convertAndSend(MqQueueConstant.MOBILE_CODE_QUEUE, new MobileMsgTemplate( mobile, contextJson.toJSONString(), CommonConstant.ALIYUN_SMS, EnumSmsChannelTemplate.LOGIN_NAME_LOGIN.getSignName(), EnumSmsChannelTemplate.LOGIN_NAME_LOGIN.getTemplate() )); redisTemplate.opsForValue().set(SecurityConstants.DEFAULT_CODE_KEY + mobile, code, SecurityConstants.DEFAULT_IMAGE_EXPIRE, TimeUnit.SECONDS); return new R<>(true); } 树形节点工具栏1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889public class TreeUtil { /** * 两层循环实现建树 * * @param treeNodes 传入的树节点列表 * @return */ public static <T extends TreeNode> List<T> bulid(List<T> treeNodes, Object root) { List<T> trees = new ArrayList<T>(); for (T treeNode : treeNodes) { if (root.equals(treeNode.getParentId())) { trees.add(treeNode); } for (T it : treeNodes) { if (it.getParentId() == treeNode.getId()) { if (treeNode.getChildren() == null) { treeNode.setChildren(new ArrayList<TreeNode>()); } treeNode.add(it); } } } return trees; } /** * 使用递归方法建树 * * @param treeNodes * @return */ public static <T extends TreeNode> List<T> buildByRecursive(List<T> treeNodes, Object root) { List<T> trees = new ArrayList<T>(); for (T treeNode : treeNodes) { if (root.equals(treeNode.getParentId())) { trees.add(findChildren(treeNode, treeNodes)); } } return trees; } /** * 递归查找子节点 * * @param treeNodes * @return */ public static <T extends TreeNode> T findChildren(T treeNode, List<T> treeNodes) { for (T it : treeNodes) { if (treeNode.getId() == it.getParentId()) { if (treeNode.getChildren() == null) { treeNode.setChildren(new ArrayList<TreeNode>()); } treeNode.add(findChildren(it, treeNodes)); } } return treeNode; } /** * 通过sysMenu创建树形节点 * * @param menus * @param root * @return */ public static List<MenuTree> bulidTree(List<SysMenu> menus, int root) { List<MenuTree> trees = new ArrayList<MenuTree>(); MenuTree node; for (SysMenu menu : menus) { node = new MenuTree(); node.setId(menu.getMenuId()); node.setParentId(menu.getParentId()); node.setName(menu.getName()); node.setUrl(menu.getUrl()); node.setPath(menu.getPath()); node.setCode(menu.getPermission()); node.setLabel(menu.getName()); node.setComponent(menu.getComponent()); node.setIcon(menu.getIcon()); trees.add(node); } return TreeUtil.bulid(trees, root); }} 生成avue模板类123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100public class PigResourcesGenerator { public static void main(String[] args) { String outputDir = "/Users/lengleng/work/temp"; final String viewOutputDir = outputDir + "/view/"; AutoGenerator mpg = new AutoGenerator(); // 全局配置 GlobalConfig gc = new GlobalConfig(); gc.setOutputDir(outputDir); gc.setFileOverride(true); gc.setActiveRecord(true); // XML 二级缓存 gc.setEnableCache(false); // XML ResultMap gc.setBaseResultMap(true); // XML columList gc.setBaseColumnList(true); gc.setAuthor("lengleng"); mpg.setGlobalConfig(gc); // 数据源配置 DataSourceConfig dsc = new DataSourceConfig(); dsc.setDbType(DbType.MYSQL); dsc.setDriverName("com.mysql.jdbc.Driver"); dsc.setUsername("root"); dsc.setPassword("lengleng"); dsc.setUrl("jdbc:mysql://139.224.200.249:3309/pig?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false"); mpg.setDataSource(dsc); // 策略配置 StrategyConfig strategy = new StrategyConfig(); // strategy.setCapitalMode(true);// 全局大写命名 ORACLE 注意 strategy.setSuperControllerClass("com.github.pig.common.web.BaseController"); // 表名生成策略 strategy.setNaming(NamingStrategy.underline_to_camel); mpg.setStrategy(strategy); // 包配置 PackageConfig pc = new PackageConfig(); pc.setParent("com.github.pig.admin"); pc.setController("controller"); mpg.setPackageInfo(pc); // 注入自定义配置,可以在 VM 中使用 cfg.abc 设置的值 InjectionConfig cfg = new InjectionConfig() { @Override public void initMap() { } }; // 生成的模版路径,不存在时需要先新建 File viewDir = new File(viewOutputDir); if (!viewDir.exists()) { viewDir.mkdirs(); } List<FileOutConfig> focList = new ArrayList<FileOutConfig>(); focList.add(new FileOutConfig("/templates/listvue.vue.vm") { @Override public String outputFile(TableInfo tableInfo) { return getGeneratorViewPath(viewOutputDir, tableInfo, ".vue"); } }); cfg.setFileOutConfigList(focList); mpg.setCfg(cfg); //生成controller相关 mpg.execute(); } /** * 获取配置文件 * * @return 配置Props */ private static Properties getProperties() { // 读取配置文件 Resource resource = new ClassPathResource("/config/application.properties"); Properties props = new Properties(); try { props = PropertiesLoaderUtils.loadProperties(resource); } catch (IOException e) { e.printStackTrace(); } return props; } /** * 页面生成的文件名 */ private static String getGeneratorViewPath(String viewOutputDir, TableInfo tableInfo, String suffixPath) { String name = StringUtils.firstToLowerCase(tableInfo.getEntityName()); String path = viewOutputDir + "/" + name + "/index" + suffixPath; File viewDir = new File(path).getParentFile(); if (!viewDir.exists()) { viewDir.mkdirs(); } return path; }} velocity模板 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788package $!{package.Controller};import java.util.Map;import java.util.Date;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.*;import com.github.pig.common.constant.CommonConstant;import com.baomidou.mybatisplus.mapper.EntityWrapper;import com.baomidou.mybatisplus.plugins.Page;import com.github.pig.common.util.Query;import com.github.pig.common.util.R;import $!{package.Entity}.$!{entity};import $!{package.Service}.$!{entity}Service;#if($!{superControllerClassPackage})import $!{superControllerClassPackage};#end/** * <p> * $!{table.comment} 前端控制器 * </p> * * @author $!{author} * @since $!{date} */@RestController@RequestMapping("/$!{table.entityPath}")public class $!{table.controllerName} extends $!{superControllerClass} { @Autowired private $!{entity}Service $!{table.entityPath}Service; /** * 通过ID查询 * * @param id ID * @return $!{entity} */ @GetMapping("/{id}") public R<$!{entity}> get(@PathVariable Integer id) { return new R<>($!{table.entityPath}Service.selectById(id)); } /** * 分页查询信息 * * @param params 分页对象 * @return 分页对象 */ @RequestMapping("/page") public Page page(@RequestParam Map<String, Object> params) { params.put(CommonConstant.DEL_FLAG, CommonConstant.STATUS_NORMAL); return $!{table.entityPath}Service.selectPage(new Query<>(params), new EntityWrapper<>()); } /** * 添加 * @param $!{table.entityPath} 实体 * @return success/false */ @PostMapping public R<Boolean> add(@RequestBody $!{entity} $!{table.entityPath}) { return new R<>($!{table.entityPath}Service.insert($!{table.entityPath})); } /** * 删除 * @param id ID * @return success/false */ @DeleteMapping("/{id}") public R<Boolean> delete(@PathVariable Integer id) { $!{entity} $!{table.entityPath} = new $!{entity}(); $!{table.entityPath}.setId(id); $!{table.entityPath}.setUpdateTime(new Date()); $!{table.entityPath}.setDelFlag(CommonConstant.STATUS_DEL); return new R<>($!{table.entityPath}Service.updateById($!{table.entityPath})); } /** * 编辑 * @param $!{table.entityPath} 实体 * @return success/false */ @PutMapping public R<Boolean> edit(@RequestBody $!{entity} $!{table.entityPath}) { $!{table.entityPath}.setUpdateTime(new Date()); return new R<>($!{table.entityPath}Service.updateById($!{table.entityPath})); }} 缓存在部分实现类中,我们看到了作者使用了spring cache相关的注解。现在我们回忆一下相关缓存注解的含义: @Cacheable:用来定义缓存的。常用到是value,key;分别用来指明缓存的名称和方法中参数,对于value你也可以使用cacheName,在查看源代码是我们可以看到:两者是指的同一个东西。 @CacheEvict:用来清理缓存。常用有cacheNames,allEntries(默认值false);分别代表了要清除的缓存名称和是否全部清除(true代表全部清除)。 @CachePut:用来更新缓存,用它来注解的方法都会被执行,执行完后结果被添加到缓存中。该方法不能和@Cacheable同时在同一个方法上使用。 后台跑批定时任务模块Elastic-Job是ddframe中dd-job的作业模块中分离出来的分布式弹性作业框架。去掉了和dd-job中的监控和ddframe接入规范部分。该项目基于成熟的开源产品Quartz和Zookeeper及其客户端Curator进行二次开发。主要功能如下: 定时任务: 基于成熟的定时任务作业框架Quartz cron表达式执行定时任务。 作业注册中心: 基于Zookeeper和其客户端Curator实现的全局作业注册控制中心。用于注册,控制和协调分布式作业执行。 作业分片: 将一个任务分片成为多个小任务项在多服务器上同时执行。 弹性扩容缩容: 运行中的作业服务器崩溃,或新增加n台作业服务器,作业框架将在下次作业执行前重新分片,不影响当前作业执行。 支持多种作业执行模式: 支持OneOff,Perpetual和SequencePerpetual三种作业模式。 失效转移: 运行中的作业服务器崩溃不会导致重新分片,只会在下次作业启动时分片。启用失效转移功能可以在本次作业执行过程中,监测其他作业服务器空闲,抓取未完成的孤儿分片项执行。 运行时状态收集: 监控作业运行时状态,统计最近一段时间处理的数据成功和失败数量,记录作业上次运行开始时间,结束时间和下次运行时间。 作业停止,恢复和禁用:用于操作作业启停,并可以禁止某作业运行(上线时常用)。 被错过执行的作业重触发:自动记录错过执行的作业,并在上次作业完成后自动触发。可参考Quartz的misfire。 多线程快速处理数据:使用多线程处理抓取到的数据,提升吞吐量。 幂等性:重复作业任务项判定,不重复执行已运行的作业任务项。由于开启幂等性需要监听作业运行状态,对瞬时反复运行的作业对性能有较大影响。 容错处理:作业服务器与Zookeeper服务器通信失败则立即停止作业运行,防止作业注册中心将失效的分片分项配给其他作业服务器,而当前作业服务器仍在执行任务,导致重复执行。 Spring支持:支持spring容器,自定义命名空间,支持占位符。 运维平台:提供运维界面,可以管理作业和注册中心。 配置作者直接使用了开源项目的配置,我顺着他的pom文件找到了这家的github,地址如下 https://github.com/xjzrc/elastic-job-lite-spring-boot-starter 工作流作业配置1234567891011121314@ElasticJobConfig(cron = "0 0 0/1 * * ? ", shardingTotalCount = 3, shardingItemParameters = "0=Beijing,1=Shanghai,2=Guangzhou")public class PigDataflowJob implements DataflowJob<Integer> { @Override public List<Integer> fetchData(ShardingContext shardingContext) { return null; } @Override public void processData(ShardingContext shardingContext, List<Integer> list) { }} 测试代码1234567891011121314151617@Slf4j@ElasticJobConfig(cron = "0 0 0/1 * * ?", shardingTotalCount = 3, shardingItemParameters = "0=pig1,1=pig2,2=pig3", startedTimeoutMilliseconds = 5000L, completedTimeoutMilliseconds = 10000L, eventTraceRdbDataSource = "dataSource")public class PigSimpleJob implements SimpleJob { /** * 业务执行逻辑 * * @param shardingContext 分片信息 */ @Override public void execute(ShardingContext shardingContext) { log.info("shardingContext:{}", shardingContext); }} 开源版对这个支持有限,等到拿到收费版我在做分析。 消息中心这里的消息中心主要是集成了钉钉服务和阿里大鱼短息服务 钉钉配置钉钉是相当简单了,只需要一个webhook信息就够了。 webhook是一种web回调或者http的push API,是向APP或者其他应用提供实时信息的一种方式。Webhook在数据产生时立即发送数据,也就是你能实时收到数据。这一种不同于典型的API,需要用了实时性需要足够快的轮询。这无论是对生产还是对消费者都是高效的,唯一的缺点是初始建立困难。Webhook有时也被称为反向API,因为他提供了API规则,你需要设计要使用的API。Webhook将向你的应用发起http请求,典型的是post请求,应用程序由请求驱动。 123456789@Data@Configuration@ConfigurationProperties(prefix = "sms.dingtalk")public class DingTalkPropertiesConfig { /** * webhook */ private String webhook;} 消息模板123456789101112131415161718192021222324252627282930313233343536373839404142434445/** * @author lengleng * @date 2018/1/15 * 钉钉消息模板 * msgtype : text * text : {"content":"服务: pig-upms-service 状态:UP"} */@Data@ToStringpublic class DingTalkMsgTemplate implements Serializable { private String msgtype; private TextBean text; public String getMsgtype() { return msgtype; } public void setMsgtype(String msgtype) { this.msgtype = msgtype; } public TextBean getText() { return text; } public void setText(TextBean text) { this.text = text; } public static class TextBean { /** * content : 服务: pig-upms-service 状态:UP */ private String content; public String getContent() { return content; } public void setContent(String content) { this.content = content; } }} 监听使用队列时时监听 12345678910111213141516@Slf4j@Component@RabbitListener(queues = MqQueueConstant.DINGTALK_SERVICE_STATUS_CHANGE)public class DingTalkServiceChangeReceiveListener { @Autowired private DingTalkMessageHandler dingTalkMessageHandler; @RabbitHandler public void receive(String text) { long startTime = System.currentTimeMillis(); log.info("消息中心接收到钉钉发送请求-> 内容:{} ", text); dingTalkMessageHandler.process(text); long useTime = System.currentTimeMillis() - startTime; log.info("调用 钉钉网关处理完毕,耗时 {}毫秒", useTime); }} 发送使用队列发送 12345678910111213141516171819202122232425262728@Slf4j@Componentpublic class DingTalkMessageHandler { @Autowired private DingTalkPropertiesConfig dingTalkPropertiesConfig; /** * 业务处理 * * @param text 消息 */ public boolean process(String text) { String webhook = dingTalkPropertiesConfig.getWebhook(); if (StrUtil.isBlank(webhook)) { log.error("钉钉配置错误,webhook为空"); return false; } DingTalkMsgTemplate dingTalkMsgTemplate = new DingTalkMsgTemplate(); dingTalkMsgTemplate.setMsgtype("text"); DingTalkMsgTemplate.TextBean textBean = new DingTalkMsgTemplate.TextBean(); textBean.setContent(text); dingTalkMsgTemplate.setText(textBean); String result = HttpUtil.post(webhook, JSONObject.toJSONString(dingTalkMsgTemplate)); log.info("钉钉提醒成功,报文响应:{}", result); return true; }} 阿里大鱼短息服务配置1234567891011121314151617181920@Data@Configuration@ConditionalOnExpression("!'${sms.aliyun}'.isEmpty()")@ConfigurationProperties(prefix = "sms.aliyun")public class SmsAliyunPropertiesConfig { /** * 应用ID */ private String accessKey; /** * 应用秘钥 */ private String secretKey; /** * 短信模板配置 */ private Map<String, String> channels;} 监听123456789101112131415161718192021222324@Slf4j@Component@RabbitListener(queues = MqQueueConstant.MOBILE_SERVICE_STATUS_CHANGE)public class MobileServiceChangeReceiveListener { @Autowired private Map<String, SmsMessageHandler> messageHandlerMap; @RabbitHandler public void receive(MobileMsgTemplate mobileMsgTemplate) { long startTime = System.currentTimeMillis(); log.info("消息中心接收到短信发送请求-> 手机号:{} -> 信息体:{} ", mobileMsgTemplate.getMobile(), mobileMsgTemplate.getContext()); String channel = mobileMsgTemplate.getChannel(); SmsMessageHandler messageHandler = messageHandlerMap.get(channel); if (messageHandler == null) { log.error("没有找到指定的路由通道,不进行发送处理完毕!"); return; } messageHandler.execute(mobileMsgTemplate); long useTime = System.currentTimeMillis() - startTime; log.info("调用 {} 短信网关处理完毕,耗时 {}毫秒", mobileMsgTemplate.getType(), useTime); }} 发送不错的模板 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576@Slf4j@Component(CommonConstant.ALIYUN_SMS)public class SmsAliyunMessageHandler extends AbstractMessageHandler { @Autowired private SmsAliyunPropertiesConfig smsAliyunPropertiesConfig; private static final String PRODUCT = "Dysmsapi"; private static final String DOMAIN = "dysmsapi.aliyuncs.com"; /** * 数据校验 * * @param mobileMsgTemplate 消息 */ @Override public void check(MobileMsgTemplate mobileMsgTemplate) { Assert.isBlank(mobileMsgTemplate.getMobile(), "手机号不能为空"); Assert.isBlank(mobileMsgTemplate.getContext(), "短信内容不能为空"); } /** * 业务处理 * * @param mobileMsgTemplate 消息 */ @Override public boolean process(MobileMsgTemplate mobileMsgTemplate) { //可自助调整超时时间 System.setProperty("sun.net.client.defaultConnectTimeout", "10000"); System.setProperty("sun.net.client.defaultReadTimeout", "10000"); //初始化acsClient,暂不支持region化 IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", smsAliyunPropertiesConfig.getAccessKey(), smsAliyunPropertiesConfig.getSecretKey()); try { DefaultProfile.addEndpoint("cn-hou", "cn-hangzhou", PRODUCT, DOMAIN); } catch (ClientException e) { log.error("初始化SDK 异常", e); e.printStackTrace(); } IAcsClient acsClient = new DefaultAcsClient(profile); //组装请求对象-具体描述见控制台-文档部分内容 SendSmsRequest request = new SendSmsRequest(); //必填:待发送手机号 request.setPhoneNumbers(mobileMsgTemplate.getMobile()); //必填:短信签名-可在短信控制台中找到 request.setSignName(mobileMsgTemplate.getSignName()); //必填:短信模板-可在短信控制台中找到 request.setTemplateCode(smsAliyunPropertiesConfig.getChannels().get(mobileMsgTemplate.getTemplate())); //可选:模板中的变量替换JSON串,如模板内容为"亲爱的${name},您的验证码为${code}" request.setTemplateParam(mobileMsgTemplate.getContext()); request.setOutId(mobileMsgTemplate.getMobile()); //hint 此处可能会抛出异常,注意catch try { SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request); log.info("短信发送完毕,手机号:{},返回状态:{}", mobileMsgTemplate.getMobile(), sendSmsResponse.getCode()); } catch (ClientException e) { log.error("发送异常"); e.printStackTrace(); } return true; } /** * 失败处理 * * @param mobileMsgTemplate 消息 */ @Override public void fail(MobileMsgTemplate mobileMsgTemplate) { log.error("短信发送失败 -> 网关:{} -> 手机号:{}", mobileMsgTemplate.getType(), mobileMsgTemplate.getMobile()); }} 资源认证服务器 (单点登陆功能)由于作者在认证中心使用了spring security oauth框架,所以需要在微服务的客户端实现一个资源认证服务器,来完成SSO需求。 配置暴露监控信息 1234567891011121314@Configuration@EnableResourceServerpublic class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .csrf().disable(); }} 接口123456789@EnableOAuth2Sso@SpringBootApplicationpublic class PigSsoClientDemoApplication { public static void main(String[] args) { SpringApplication.run(PigSsoClientDemoApplication.class, args); }} 监控模块springboot admin配置RemindingNotifier会在应用上线或宕掉的时候发送提醒,也就是把notifications发送给其他的notifier,notifier的实现很有意思,不深究了,从类关系可以知道,我们可以以这么几种方式发送notifications:Pagerduty、Hipchat 、Slack 、Mail、 Reminder 12345678910111213141516171819202122232425@Configuration public static class NotifierConfig { @Bean @Primary public RemindingNotifier remindingNotifier() { RemindingNotifier notifier = new RemindingNotifier(filteringNotifier(loggerNotifier())); notifier.setReminderPeriod(TimeUnit.SECONDS.toMillis(10)); return notifier; } @Scheduled(fixedRate = 1_000L) public void remind() { remindingNotifier().sendReminders(); } @Bean public FilteringNotifier filteringNotifier(Notifier delegate) { return new FilteringNotifier(delegate); } @Bean public LoggingNotifier loggerNotifier() { return new LoggingNotifier(); } } 短信服务下线通知继承AbstractStatusChangeNotifier,将短信服务注册到spring boot admin中。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758@Slf4jpublic class StatusChangeNotifier extends AbstractStatusChangeNotifier { private RabbitTemplate rabbitTemplate; private MonitorPropertiesConfig monitorMobilePropertiesConfig; public StatusChangeNotifier(MonitorPropertiesConfig monitorMobilePropertiesConfig, RabbitTemplate rabbitTemplate) { this.rabbitTemplate = rabbitTemplate; this.monitorMobilePropertiesConfig = monitorMobilePropertiesConfig; } /** * 通知逻辑 * * @param event 事件 * @throws Exception 异常 */ @Override protected void doNotify(ClientApplicationEvent event) { if (event instanceof ClientApplicationStatusChangedEvent) { log.info("Application {} ({}) is {}", event.getApplication().getName(), event.getApplication().getId(), ((ClientApplicationStatusChangedEvent) event).getTo().getStatus()); String text = String.format("应用:%s 服务ID:%s 状态改变为:%s,时间:%s" , event.getApplication().getName() , event.getApplication().getId() , ((ClientApplicationStatusChangedEvent) event).getTo().getStatus() , DateUtil.date(event.getTimestamp()).toString()); JSONObject contextJson = new JSONObject(); contextJson.put("name", event.getApplication().getName()); contextJson.put("seid", event.getApplication().getId()); contextJson.put("time", DateUtil.date(event.getTimestamp()).toString()); //开启短信通知 if (monitorMobilePropertiesConfig.getMobile().getEnabled()) { log.info("开始短信通知,内容:{}", text); rabbitTemplate.convertAndSend(MqQueueConstant.MOBILE_SERVICE_STATUS_CHANGE, new MobileMsgTemplate( CollUtil.join(monitorMobilePropertiesConfig.getMobile().getMobiles(), ","), contextJson.toJSONString(), CommonConstant.ALIYUN_SMS, EnumSmsChannelTemplate.SERVICE_STATUS_CHANGE.getSignName(), EnumSmsChannelTemplate.SERVICE_STATUS_CHANGE.getTemplate() )); } if (monitorMobilePropertiesConfig.getDingTalk().getEnabled()) { log.info("开始钉钉通知,内容:{}", text); rabbitTemplate.convertAndSend(MqQueueConstant.DINGTALK_SERVICE_STATUS_CHANGE, text); } } else { log.info("Application {} ({}) {}", event.getApplication().getName(), event.getApplication().getId(), event.getType()); } }} zipkin 链路追踪由于zipkin是侵入式,因此这部分组件没有代码,只有相关依赖。下面分享一下作者的yaml DB1234567891011121314151617181920server: port: 5003# datasoure默认使用JDBCspring: datasource: driver-class-name: com.mysql.jdbc.Driver username: root password: ENC(gc16brBHPNq27HsjaULgKGq00Rz6ZUji) url: jdbc:mysql://127.0.0.1:3309/pig?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=falsezipkin: collector: rabbitmq: addresses: 127.0.0.1:5682 password: lengleng username: pig queue: zipkin storage: type: mysql ELK12345678910111213141516171819202122232425262728293031323334353637server: port: 5002zipkin: collector: rabbitmq: addresses: 127.0.0.1:5682 password: lengleng username: pig queue: zipkin storage: type: elasticsearch elasticsearch: hosts: 127.0.0.1:9200 cluster: elasticsearch index: zipkin max-requests: 64 index-shards: 5 index-replicas: 1 续1s时间全片结束,觉得我写的不错?想要了解更多精彩新姿势?赶快打开我的👉个人博客 👈吧! 谢谢你那么可爱,还一直关注着我~❤😝]]></content>
<tags>
<tag>源码</tag>
<tag>java</tag>
<tag>技术</tag>
<tag>学好分布式架构</tag>
</tags>
</entry>
<entry>
<title><![CDATA[开源项目Cloud-Admin分析与学习]]></title>
<url>%2F2018%2F12%2F10%2F%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AECloud-Admin%E5%88%86%E6%9E%90%E4%B8%8E%E5%AD%A6%E4%B9%A0%2F</url>
<content type="text"><![CDATA[前一段时间在“感性认识JWT”一博文中分享了,很火的开源项目Cloud-Admin中鉴权中心和网关的实现。今天再来看看其他各个部分源码 提示老规矩,放开源项目地址:https://gitee.com/minull/ace-security 目录结构 架构下面是官方提供的架构模型。 项目的运行步骤 先启动rabbitmq、redis、mysql以及consul注册中心 运行数据库脚本:依次运行数据库:ace-admin/db/init.sql、ace-auth-server/db/init.sql、ace-trace 修改配置数据库配置:ace-admin/src/main/resources/application.yml、ace-gate/src/main/resources/application.yml 按顺序运行main类:CenterBootstrap(ace-center)、AuthBootstrap(ace-auth-server)、AdminBootstrap(ace-admin)、GatewayServerBootstrap(ace-gateway-v2) Admin模块数据库设计先看看数据库设计,admin模块负责所有权限的管理。第一张表base_element,定义了各个资源的code,类型,uri,每一个特定资源对应一种请求路径,如图👇 第二张,base_group定义了角色和请求路径的关系。👇 第三张,base_group_type定义了类型👇 第四张,base_menu定义了菜单👇 第五张,记录了网关日志相关信息 第六张,用户表 业务逻辑接口这部分作者写的有些乱,将很多业务逻辑的有关代码放到接口层了(吐槽)。这部分代码没什么好说的,就是根据上面的数据库CRUD。接口的路径分为 123456789/api/user/validate/element/**"/gateLog/**"/group/**"/groupType/**"/menu/**"/user/**"/api/permissions"/api/user/un/**" 这几类,并且走每一层都会走鉴权中心来鉴别,具体逻辑是使用springboot的addInterceptors()方法添加两层拦截器组成一个拦截器链,如下👇 1234567@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(getServiceAuthRestInterceptor()). addPathPatterns(getIncludePathPatterns()).addPathPatterns("/api/user/validate"); registry.addInterceptor(getUserAuthRestInterceptor()). addPathPatterns(getIncludePathPatterns()); } 第一层拦截器是鉴权中心的ServiceAuthRestInterceptor拦截器,判断访问的客户端是否有权限访问; 1234567891011121314151617181920212223//第一层拦截器 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HandlerMethod handlerMethod = (HandlerMethod) handler; // 配置该注解,说明不进行服务拦截 IgnoreClientToken annotation = handlerMethod.getBeanType().getAnnotation(IgnoreClientToken.class); if (annotation == null) { annotation = handlerMethod.getMethodAnnotation(IgnoreClientToken.class); } if(annotation!=null) { return super.preHandle(request, response, handler); } String token = request.getHeader(serviceAuthConfig.getTokenHeader()); IJWTInfo infoFromToken = serviceAuthUtil.getInfoFromToken(token); String uniqueName = infoFromToken.getUniqueName(); for(String client:serviceAuthUtil.getAllowedClient()){ if(client.equals(uniqueName)){ return super.preHandle(request, response, handler); } } throw new ClientForbiddenException("Client is Forbidden!"); } 第二层拦截器是鉴权中心的UserAuthRestInterceptor拦截器,拦截非法用户。 123456789101112131415161718192021222324252627@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HandlerMethod handlerMethod = (HandlerMethod) handler; // 配置该注解,说明不进行用户拦截 IgnoreUserToken annotation = handlerMethod.getBeanType().getAnnotation(IgnoreUserToken.class); if (annotation == null) { annotation = handlerMethod.getMethodAnnotation(IgnoreUserToken.class); } if (annotation != null) { return super.preHandle(request, response, handler); } String token = request.getHeader(userAuthConfig.getTokenHeader()); if (StringUtils.isEmpty(token)) { if (request.getCookies() != null) { for (Cookie cookie : request.getCookies()) { if (cookie.getName().equals(userAuthConfig.getTokenHeader())) { token = cookie.getValue(); } } } } IJWTInfo infoFromToken = userAuthUtil.getInfoFromToken(token); BaseContextHandler.setUsername(infoFromToken.getUniqueName()); BaseContextHandler.setName(infoFromToken.getName()); BaseContextHandler.setUserID(infoFromToken.getId()); return super.preHandle(request, response, handler); } 缓存中心在上面的admin模块中,作者在user接口上,使用了自定义的缓存注解@Cache,用来保存用户的权限信息,减小数据库的访问压力👇 123456789101112131415161718192021222324@RestController@RequestMapping("api")public class UserRest { @Autowired private PermissionService permissionService; @Cache(key="permission") @RequestMapping(value = "/permissions", method = RequestMethod.GET) public @ResponseBody List<PermissionInfo> getAllPermission(){ return permissionService.getAllPermission(); } @Cache(key="permission:u{1}") @RequestMapping(value = "/user/un/{username}/permissions", method = RequestMethod.GET) public @ResponseBody List<PermissionInfo> getPermissionByUsername(@PathVariable("username") String username){ return permissionService.getPermissionByUsername(username); } @RequestMapping(value = "/user/validate", method = RequestMethod.POST) public @ResponseBody UserInfo validate(@RequestBody Map<String,String> body){ return permissionService.validate(body.get("username"),body.get("password")); }} 下面我们就分析一下缓存中心的设计。 目录结构 缓存中心是作者通过maven方式添加的,并没有通过直接项目代码展现。因此我使用idea的反编码工具进行分析 入口入口为EnableAceCache,开启这个就可以自动配置缓存相关事项 缓存实体先看缓存实体,作者定义了key,描述信息desc,以及过期时间 123456789public class CacheBean { private String key = ""; private String desc = ""; @JsonFormat( timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss" ) private Date expireTime; ..................} 配置类一共有两个配置 RedisConfig先看RedisConfig,使用@PostConstruct注解,意思是会在服务器加载Servlet的时候,将服务端yml中有关redis的配置加载到JedisPool中,这个方法只会被服务器调用一次。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677@Configurationpublic class RedisConfig { @Autowired private Environment env; private JedisPool pool; private String maxActive; private String maxIdle; private String maxWait; private String host; private String password; private String timeout; private String database; private String port; private String enable; private String sysName; public RedisConfig() { } @PostConstruct public void init() { PropertiesLoaderUtils prop = new PropertiesLoaderUtils(new String[]{"application.properties"}); this.host = prop.getProperty("redis.host"); if (StringUtils.isBlank(this.host)) { this.host = this.env.getProperty("redis.host"); this.maxActive = this.env.getProperty("redis.pool.maxActive"); this.maxIdle = this.env.getProperty("redis.pool.maxIdle"); this.maxWait = this.env.getProperty("redis.pool.maxWait"); this.password = this.env.getProperty("redis.password"); this.timeout = this.env.getProperty("redis.timeout"); this.database = this.env.getProperty("redis.database"); this.port = this.env.getProperty("redis.port"); this.sysName = this.env.getProperty("redis.sysName"); this.enable = this.env.getProperty("redis.enable"); } else { this.maxActive = prop.getProperty("redis.pool.maxActive"); this.maxIdle = prop.getProperty("redis.pool.maxIdle"); this.maxWait = prop.getProperty("redis.pool.maxWait"); this.password = prop.getProperty("redis.password"); this.timeout = prop.getProperty("redis.timeout"); this.database = prop.getProperty("redis.database"); this.port = prop.getProperty("redis.port"); this.sysName = prop.getProperty("redis.sysName"); this.enable = prop.getProperty("redis.enable"); } } @Bean public JedisPoolConfig constructJedisPoolConfig() { JedisPoolConfig config = new JedisPoolConfig(); config.setMaxTotal(Integer.parseInt(this.maxActive)); config.setMaxIdle(Integer.parseInt(this.maxIdle)); config.setMaxWaitMillis((long)Integer.parseInt(this.maxWait)); config.setTestOnBorrow(true); return config; } @Bean( name = {"pool"} ) public JedisPool constructJedisPool() { String ip = this.host; int port = Integer.parseInt(this.port); String password = this.password; int timeout = Integer.parseInt(this.timeout); int database = Integer.parseInt(this.database); if (null == this.pool) { if (StringUtils.isBlank(password)) { this.pool = new JedisPool(this.constructJedisPoolConfig(), ip, port, timeout); } else { this.pool = new JedisPool(this.constructJedisPoolConfig(), ip, port, timeout, password, database); } } return this.pool; } CacheWebConfig第二个配置,就是使用springboot拦截器,将作者自己写的缓存管理中心视图界面展示出来(这个操作太神奇了,第一次看到)👇 1234public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(new String[]{"/static/cache/**"}).addResourceLocations(new String[]{"classpath:/META-INF/static/"}); super.addResourceHandlers(registry); } 使用缓存RedisServiceImpl和CacheRedisRedisServiceImpl使用了JedisPool那几个方法实现了增删改查操作,代码省略。 CacheRedis则RedisServiceImpl是加入一些增删改查逻辑,譬如什么是什么设置缓存。 切面加入缓存核心方法核心方法interceptor如下,使用ProceedingJoinPoint拿到被@Cache标记的的方法中的参数,用getKey()方法拿到具体缓存的key,使用CacheRedis的get()方法查找对应的key,如果key找不到则用CacheRedis的set()方法添加新的缓存。 123456789101112131415161718192021222324252627@Around("aspect()&&@annotation(anno)") public Object interceptor(ProceedingJoinPoint invocation, Cache anno) throws Throwable { MethodSignature signature = (MethodSignature)invocation.getSignature(); Method method = signature.getMethod(); Object result = null; Class<?>[] parameterTypes = method.getParameterTypes(); Object[] arguments = invocation.getArgs(); String key = ""; String value = ""; try { key = this.getKey(anno, parameterTypes, arguments); value = this.cacheAPI.get(key); Type returnType = method.getGenericReturnType(); result = this.getResult(anno, result, value, returnType); } catch (Exception var14) { this.log.error("获取缓存失败:" + key, var14); } finally { if (result == null) { result = invocation.proceed(); if (StringUtils.isNotBlank(key)) { this.cacheAPI.set(key, result, anno.expire()); } } } return result; } 取得keygetKey()方法的逻辑为,判断key生成器是否是默认生成器(可以使用多种生成器),然后根据默认生成器的规则生成一个key。 123456789101112131415private String getKey(Cache anno, Class<?>[] parameterTypes, Object[] arguments) throws InstantiationException, IllegalAccessException { String generatorClsName = anno.generator().getName(); IKeyGenerator keyGenerator = null; if (anno.generator().equals(DefaultKeyGenerator.class)) { keyGenerator = this.keyParser; } else if (this.generatorMap.contains(generatorClsName)) { keyGenerator = (IKeyGenerator)this.generatorMap.get(generatorClsName); } else { keyGenerator = (IKeyGenerator)anno.generator().newInstance(); this.generatorMap.put(generatorClsName, keyGenerator); } String key = keyGenerator.getKey(anno.key(), anno.scope(), parameterTypes, arguments); return key; } key生成器默认key生成器的代码如下(生成规则我看不懂😭), 12345678910111213141516171819202122232425262728293031323334353637public String buildKey(String key, CacheScope scope, Class<?>[] parameterTypes, Object[] arguments) { boolean isFirst = true; if (key.indexOf("{") > 0) { key = key.replace("{", ":{"); Pattern pattern = Pattern.compile("\\d+\\.?[\\w]*"); Matcher matcher = pattern.matcher(key); while(matcher.find()) { String tmp = matcher.group(); String[] express = matcher.group().split("\\."); String i = express[0]; int index = Integer.parseInt(i) - 1; Object value = arguments[index]; if (parameterTypes[index].isAssignableFrom(List.class)) { List result = (List)arguments[index]; value = result.get(0); } if (value == null || value.equals("null")) { value = ""; } if (express.length > 1) { String field = express[1]; value = ReflectionUtils.getFieldValue(value, field); } if (isFirst) { key = key.replace("{" + tmp + "}", value.toString()); } else { key = key.replace("{" + tmp + "}", "_" + value.toString()); } } } return key; } key解析因为作者的key生成器抖了很多机灵,因此,拿到key以后,要将生成key和value之前的数值找到才能进行比对,下面时解析key的代码👇大概逻辑是根据不同的value类型返回json 12345678910111213141516public Object parse(String value, Type type, Class... origins) { Object result = null; if (type instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType)type; Type rawType = parameterizedType.getRawType(); if (((Class)rawType).isAssignableFrom(List.class)) { result = JSON.parseArray(value, (Class)parameterizedType.getActualTypeArguments()[0]); } } else if (origins == null) { result = JSON.parseObject(value, (Class)type); } else { result = JSON.parseObject(value, origins[0]); } return result; } 切面清除缓存大概意思和上面差不多,只要服务端上加入@CacheClear注解就可以清除对应缓存。 1234567891011121314151617181920212223242526@Around("aspect()&&@annotation(anno)") public Object interceptor(ProceedingJoinPoint invocation, CacheClear anno) throws Throwable { MethodSignature signature = (MethodSignature)invocation.getSignature(); Method method = signature.getMethod(); Class<?>[] parameterTypes = method.getParameterTypes(); Object[] arguments = invocation.getArgs(); String key = ""; if (StringUtils.isNotBlank(anno.key())) { key = this.getKey(anno, anno.key(), CacheScope.application, parameterTypes, arguments); this.cacheAPI.remove(key); } else if (StringUtils.isNotBlank(anno.pre())) { key = this.getKey(anno, anno.pre(), CacheScope.application, parameterTypes, arguments); this.cacheAPI.removeByPre(key); } else if (anno.keys().length > 1) { String[] arr$ = anno.keys(); int len$ = arr$.length; for(int i$ = 0; i$ < len$; ++i$) { String tmp = arr$[i$]; tmp = this.getKey(anno, tmp, CacheScope.application, parameterTypes, arguments); this.cacheAPI.removeByPre(tmp); } } return invocation.proceed(); }]]></content>
<tags>
<tag>源码</tag>
<tag>技术</tag>
<tag>学好分布式架构</tag>
</tags>
</entry>
<entry>
<title><![CDATA[微服务核心架构梳理]]></title>
<url>%2F2018%2F12%2F08%2F%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%A0%B8%E5%BF%83%E6%9E%B6%E6%9E%84%E6%A2%B3%E7%90%86%2F</url>
<content type="text"><![CDATA[在公司学习了接近一个月。 一个月内,从0开始开始接触分布式微服务架构,给了我不小的收获。今天,我来从头到尾梳理一下,有关微服务架构的核心内容(全是干货)。 下文,你将看到业界主流微服务框架的核心原理,包括服务发现,网关,配置中心,监控等组件,功能和架构原理的简单介绍。感谢阅读!😋 想要解锁更多新姿势?请访问我的博客。😏 Hello,Microservices什么是微服务微服务Microservices之父,马丁.福勒,对微服务大概的概述如下: 就目前而言,对于微服务业界并没有一个统一的、标准的定义(While there is no precise definition of this architectural style ) 。但通在其常而言,微服务架构是一种架构模式或者说是一种架构风格,它提倡将单一应用程序划分成一组小的服务,每个服务运行独立的自己的进程中,服务之间互相协调、互相配合,为用户提供最终价值。服务之间采用轻量级的通信机制互相沟通(通常是基于 HTTP 的 RESTful API ) 。每个服务都围绕着具体业务进行构建,并且能够被独立地部署到生产环境、类生产环境等。另外,应尽量避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具对其进行构建,可以有一个非常轻量级的集中式管理来协调这些服务。可以使用不同的语言来编写服务,也可以使用不同的数据存储。 根据马丁.福勒的描述,我总结了一下几点: (字差,勿嫌) 小服务小服务,没有特定的标准或者规范,但他在总体规范上一定是小的。 进程独立每一组服务都是独立运行的,可能我这个服务运行在tomcat容器,而另一个服务运行在jetty上。可以通过进程方式,不断的横向扩展整个服务。 通信过去的协议都是很重的,就像ESB,就像SOAP,轻通信,着意味着相比过去更智能更轻量的服务相互调用,就所谓smart endpoints and dumb pipes,这些endpoint都是解耦的,完成一个业务通信调用串起这些micro service就像是linux系统中通过管道串起一系列命令业务。 过去的业务,我们通常会考虑各种各样的依赖关系,考虑系统耦合带来的问题。微服务,可以让开发者更专注于业务的逻辑开发。 部署不止业务要独立,部署也要独立。不过这也意味着,传统的开发流程会出现一定程度的改变,开发的适合也要有一定的运维指责 管理传统的企业级SOA服务往往很大,不易于管理,耦合性高,团队开发成本比较大。微服务,可以让团队各思其政的选择技术实现,不同的service可以根据各自的需要选择不同的技术栈来实现其业务逻辑。 微服务的利与弊为什么用微服务呢?因为好玩? 不是的。下面是我从网络上找到说的比较全的优点: 优点每个服务足够内聚,足够小,代码容易理解这样能聚焦一个指定的业务功能或业务需求 开发简单、开发效率提高,一个服务可能就是专一的只干一件事。 微服务能够被小团队单独开发,这个小团队是 2 到 5 人的开发人员组成。 微服务是松藕合的,是有功能意义的服务,无论是在开发阶段或部署阶段都是独立的。 微服务能使用不同的语言开发。 易于和第三方集成,微服务允许容易且灵活的方式集成自动部署,通过持续集成工具,如Jenkins,Hudson,bamboo。 微服务易于被一个开发人员理解,修改和维护,这样小团队能够更关注自己的工作成果。无需- - 通过合作才能体现价值。微服务允许你利用融合最新技术。 微服务只是业务逻辑的代码,不会和 HTML,CSS或其他界面组件混合。 每个微服务都有自己的存储能力,可以有自己的数据库。也可以有统一数据库。 总的来说,微服务的优势,就是在于,面对大的系统,可以有效的减少复杂程度,使服务架构的逻辑更清晰明了。 但是这样也会带来很多问题,就譬如分布式环境下的数据一致性,测试的复杂性,运维的复杂性。 什么组织适合使用微服务?微服务带了种种优点,种种弊端,那么什么组织适合使用微服务? 墨菲定律(设计系统)和康威定律(系统划分)康威定律,是一个五十多年前就被提出来的微服务概念。在康威的这篇文章中,最有名的一句话就是: Organizations which design systems are constrained to produce designs which are copies of the communication structures of these organizations. - Melvin Conway(1967) 中文直译大概的意思就是:设计系统的组织,其产生的设计等同于组织之内、组织之间的沟通结构。看看下面的图片(来源于互联网,侵删),再想想Apple的产品、微软的产品设计,就能形象生动的理解这句话。 感兴趣的各位可以研究一下 架构演化架构是不断演化出来的,微服务也是这样,当从各大科技公司,规模大到一定程度,完全需要演化成更进一步管理的技术架构体系。 (字差,勿嫌) 传统的团队,都是面向过程化的,产品想完了去找策划,策划完了找开发,接着顺着一步一步找。我们做技术都是为了产品的,一旦过程出来了什么问题,回溯寻找问题会非常耗时。 (字差,勿嫌) 使用了微服务架构体系,团队组织方式需要转变成跨职能团队,即每个团队都有产品专家,策划专家,开发专家,运维专家,他们使用API方式发布他们的功能,而平台使用他们的功能发布产品 微服务技术架构体系下面我分享一下大部分公司都使用的微服务技术架构体系。 (图差,勿嫌) 服务发现主流的服务发现,分为三种 第一种,开发人员开发了程序以后,会找运维配一个域名,服务的话通过dns就能找到我们对应的服务 缺点是,由于服务没有负载均衡功能,对负载均衡服务,可能会有相当大的性能问题。 第二种,是目前普遍的做法。可以参考我上篇博客分析的zuul网关,每一个服务都通过服务端内置的功能注册到注册中心,服务消费者不断轮询注册中心发现对应的服务,使用内置负载均衡调用服务。 缺点是,对多语言环境不是很好,你需要单独给消费者的客户端开发服务发现和负载均衡功能。当然了,这个方法通常都是用在spring cloud上的。 第三种,是将客户端和负载均衡放在同一个主机,而不是同一个进程内。 这种方法相对第一种第二种方法来说,改善了他们的缺点,但是会极大增加运维成本。 网关微服务的网关是什么?我们可以联系生活实际想一下。每一个大的公司,都会有一偏属于自己的建筑区,而这建筑区内,都有不少的门卫。如果有外来人员进入公司,会先和门卫打好招呼,才能进去。 将生活实际联系到微服务上,就不难理解网关的意思了。 网关有什么用 反向路由:很多时候,公司不想让外部人员看到我们公司的内部,就需要网关来进行反向路由。即将外部请求转换成内部具体服务条用 安全认证:网络中会有很多恶意访问,譬如爬虫,譬如黑客攻击,网关维护安全功能。 限流熔断:参考我学好分布式zookepper的博客,当请求很多服务不堪重负,会让我们的服务自动关闭,导致不能用服务。限流熔断可以有效的避免这类问题 日志监控:所有的外面的请求都会经过网关,这样我们就可以使用网关来记录日志信息 灰度发布,蓝绿部署。是指能够平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。 开源网关Zuul架构 zuul网关核心其实是一个servlet,所有请求都会经过zuul servlet传到zuulFilter Runner,然后分发到三种过滤器。 先说说架构图左半部分,分别是使用Groovy实现的前置路由过滤器,路由过滤器,后置路由过滤器。 一般请求都会先经过前置路由过滤器处理,一般的自定义java封装逻辑也会在这里实现。 路由过滤器,实现的是找到对应的微服务进行调用。 调用完了,响应回来,会经过后置路由过滤器,通过后置路由过滤器我们可以封装日志审计的处理。 可以说zuul网关最大的特色就是它三层过滤器。 架构图右半部分,是zuul网关设计的自定义过滤器加载机制。网关内部会有生产者消费者模型,自动的将过滤器脚本发布到zuul网关读取加载运行。 配置中心以前,开发人员把配置文件放在开发文件里面,这样会有很多隐患。譬如,配置规范不同,无法追溯配置人员。一旦需要大规模改动配置,改动时间会很长,无法追溯配置人员,从而影响整个产品,后果是我们承担不起的。 因此就有配置中心这个喽~ 现在的开源中心有百度配置中心 Disconf,spring cloud config,Apollo,今天重点说说现在应用质量不错的配置中心阿波罗。 携程开源的Apollo开源地址👉:https://github.com/ctripcorp/apollo apollo的配置中心规模比较大,本地应用会有响应的配置中心客户端,可以定时同步配置中心里的配置。如果配置中心怠机,会使用缓存来进行配置。 通讯方式关于通讯方式,一般市面也就是两种远程调用方式,我整理了一个表格: RPC REST 耦合性 强耦合 松散耦合 消息协议 TCP HTTP 通讯协议 二进制 文本XML,Json 性能 高 低于RPC 接口契约IDL thrift,protobuf,IdL Swagger 客户端 强类型客户端,一般自动生成 一般HTTP可访问,生成强类型客户端,多语言支持好 案例 Dubbo,Dubbox,motan,tars,grpc,thrift spring boot,tax-rs,dropwizard 开发者友好 客户端比较方面,二进制消息不能读 可读消息 对外开放 一般需要转成REST/文本协议 可直接对外开发 监控预警监控预警对于微服务很重要,一个可靠的监控预警体系对微服务运行至关重要。一般监控分为如下层次: 从基础设施到用户端,层层有监控,全方位,多角度,每一个层面都很重要。总体来说,微服务可分5个监控点:日志监控,Metrics监控,健康检查,调用链检查,告警系统 监控架构下面的图是大部分公司的一种监控架构图。每一个服务都有一个agent,agent收集到关键信息,会传到一些MQ中,为了解耦。同时将日志传入ELK,将Metrics传入InfluxDB时间序列库。而像nagios,可以定期向agent发起信息检查微服务。 调用链监控APM很多公司都有调用链监控,就譬如阿里有鹰眼监控,点评的Cat,大部分调用链监控(没错,我指的Zipkin)架构是这样的👇 当请求进入Web容器的时候,会经过创建Tracer,连接spans(模拟潜在的分布式工作的延迟,该模块还包含在系统网络间传递跟踪上下文信息的工具包,如通过http headers)。Spans有一个上下文,其中包含tracer标识符,将其放在表示分布式操作的树的正确位置。当我们把图中的各种span放到后端的时候,我们的服务调用链会动态的生成调用链。 下面是一些市场上用的比较多的调用链监控: 1、Pinpointgithub地址:GitHub - naver/pinpoint: Pinpoint is an open source APM (Application Performance Management) tool for large-scale distributed systems written in Java.对java领域的性能分析有兴趣的朋友都应该看看这个开源项目,这个是一个韩国团队开源出来的,通过JavaAgent的机制来做字节码代码植入,实现加入traceid和抓取性能数据的目的。NewRelic、Oneapm之类的工具在java平台上的性能分析也是类似的机制。 2、SkyWalkinggithub地址:wu-sheng/sky-walking这是国内一位叫吴晟的兄弟开源的,也是一个对JAVA分布式应用程序集群的业务运行情况进行追踪、告警和分析的系统,在github上也有400多颗星了。功能相对pinpoint还是稍弱一些,插件还没那么丰富,不过也很难得了。 3、Zipkin官网:OpenZipkin · A distributed tracing systemgithub地址:GitHub - openzipkin/zipkin: Zipkin is a distributed tracing system这个是twitter开源出来的,也是参考Dapper的体系来做的。 Zipkin的java应用端是通过一个叫Brave的组件来实现对应用内部的性能分析数据采集。Brave的github地址:https://github.com/openzipkin/brave这个组件通过实现一系列的java拦截器,来做到对http/servlet请求、数据库访问的调用过程跟踪。然后通过在spring之类的配置文件里加入这些拦截器,完成对java应用的性能数据采集。 4、CATgithub地址:GitHub - dianping/cat: Central Application Tracking这个是大众点评开源出来的,实现的功能也还是蛮丰富的,国内也有一些公司在用了。不过他实现跟踪的手段,是要在代码里硬编码写一些“埋点”,也就是侵入式的。这样做有利有弊,好处是可以在自己需要的地方加埋点,比较有针对性;坏处是必须改动现有系统,很多开发团队不愿意。 5、Xhprof/Xhgui这两个工具的组合,是针对PHP应用提供APM能力的工具,也是非侵入式的。Xhprof github地址:GitHub - preinheimer/xhprof: XHGUI is a GUI for the XHProf PHP extension, using a database backend, and pretty graphs to make it easy to use and interpret.Xhgui github地址:GitHub - perftools/xhgui: A graphical interface for XHProf data built on MongoDB我对PHP不熟,不过网上介绍这两个工具的资料还是蛮多的。 熔断、隔离、限流、降级面对巨大的突发流量下,大型公司一般会采用一系列的熔断(系统自动将服务关闭防止让出现的问题最大化)、隔离(将服务和服务隔离,防止一个服务挂了其他服务不能访问)、限流(单位时间内之允许一定数量用户访问)、降级(当整个微服务架构整体的负载超出了预设的上限阈值或即将到来的流量预计将会超过预设的阈值时,为了保证重要或基本的服务能正常运行,我们可以将一些 不重要或 不紧急 的服务或任务进行服务的 延迟使用 或 暂停使用)措施。 下面介绍一下hystrix的运行流程(没找到架构图不好意思): 每一个微服务调用时,都会使用hystrix的command方式(上图的左上角那个),然后使用command同步的,或者是响应式的,或者是异步的,判断电路是否熔断(顺着图从左往右看), 如果断路则走降级fallback; 如果这个线闭合着,但是线程资源没了,队列满了,则走限流措施(看图的第5步); 如果走完了,执行成功了,则走run()方法,获取response,但是这个过程如果出错了,则继续走降级fallback. 同时,看图最上面有一个后缀是health的,这是一个计算整个链路是否健康的组件,每一步操作都被它记录着。 容器与服务编排引擎从物理机到虚拟机,从虚拟机到容器;从物理集群到open stack,open stack到kubernetes;科技不断的变化,我们的认知也没刷新。 我们从容器开始说起,它首先是一个相对独立的运行环境,在这一点有点类似于虚拟机,但是不像虚拟机那样彻底。 虚拟机会将虚拟硬件、内核(即操作系统)以及用户空间打包在新虚拟机当中,虚拟机能够利用“虚拟机管理程序”运行在物理设备之上。虚拟机依赖于hypervisor,其通常被安装在“裸金属”系统硬件之上,这导致hypervisor在某些方面被认为是一种操作系统。一旦 hypervisor安装完成, 就可以从系统可用计算资源当中分配虚拟机实例了,每台虚拟机都能够获得唯一的操作系统和负载(应用程序)。简言之,虚拟机先需要虚拟一个物理环境,然后构建一个完整的操作系统,再搭建一层Runtime,然后供应用程序运行。 对于容器环境来说,不需要安装主机操作系统,直接将容器层(比如LXC或libcontainer)安装在主机操作系统(通常是Linux变种)之上。在安装完容器层之后,就可以从系统可用计算资源当中分配容器实例了,并且企业应用可以被部署在容器当中。但是,每个容器化应用都会共享相同的操作系统(单个主机操作系统)。容器可以看成一个装好了一组特定应用的虚拟机,它直接利用了宿主机的内核,抽象层比虚拟机更少,更加轻量化,启动速度极快。 相比于虚拟机,容器拥有更高的资源使用效率,因为它并不需要为每个应用分配单独的操作系统——实例规模更小、创建和迁移速度也更快。这意味相比于虚拟机,单个操作系统能够承载更多的容器。云提供商十分热衷于容器技术,因为在相同的硬件设备当中,可以部署数量更多的容器实例。此外,容器易于迁移,但是只能被迁移到具有兼容操作系统内核的其他服务器当中,这样就会给迁移选择带来限制。因为容器不像虚拟机那样同样对内核或者虚拟硬件进行打包,所以每套容器都拥有自己的隔离化用户空间,从而使得多套容器能够运行在同一主机系统之上。我们可以看到全部操作系统层级的架构都可实现跨容器共享,惟一需要独立构建的就是二进制文件与库。正因为如此,容器才拥有极为出色的轻量化特性。 我们最常用的容器是daocker,网址如下👉https://www.docker.com/ 容器编排过去虚拟机可以通过云平台open stack管理虚拟化,容器时代如何管理容器呢?这就要看看容器编排引擎了。 Apache mesosmesos是基于master,slave架构,框架决定如何利用资源,master负责管理机器,slave会定期的将机器情况报告给master,master再将信息给框架。master是高可用的,因为zk,也有leader的存在。下面是架构图👇 kuberneteskubernetes是最近十分火热的开源容器编排引擎,具体可以参考kubernetes中文文档 Kubernetes设计理念和功能其实就是一个类似Linux的分层架构,先说说每一个Kubernetes节点内部,kubelet管理全局全局pod,而每一个pod承载着一个或多个容器,kube-proxy负责网络代理和负载均衡 。 Kubernetes节点外部,则是对应的控制管理服务器,负责统一管理各个节点调度分配与运行。 服务网格化。。。待更新 资料与文献马丁.福勒对微服务的描述 微服务架构的理论基础 - 康威定律 调用链选型之Zipkin,Pinpoint,SkyWalking,CAT 结束此片完了~ 想要了解更多精彩新姿势? 请访问我的个人博客 本篇为原创内容,已在个人博客率先发表,随后看心情可能会在CSDN,segmentfault,掘金,简书,开源中国同步发出。如有雷同,缘分呢兄弟。赶快加个好友,咱们两个想个号码, 买个彩票,先挣他个几百万😝]]></content>
<tags>
<tag>技术</tag>
<tag>学好分布式架构</tag>
<tag>架构</tag>
</tags>
</entry>
<entry>
<title><![CDATA[阿里巴巴Java开发手册]]></title>
<url>%2F2018%2F12%2F07%2F%E9%98%BF%E9%87%8C%E5%B7%B4%E5%B7%B4Java%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C%2F</url>
<content type="text"><![CDATA[分析一下广泛流传的阿里巴巴Java开发手册,当作在线文档,转侵删。。]]></content>
<tags>
<tag>java</tag>
<tag>分享</tag>
</tags>
</entry>
<entry>
<title><![CDATA[阅读阿里开发手册发现的ConcurrentModificationException异常]]></title>
<url>%2F2018%2F12%2F06%2FConcurrentModificationException%E5%BC%82%E5%B8%B8%2F</url>
<content type="text"><![CDATA[今天阅读阿里巴巴Java开发手册(终极版).pdf,偶然发现一条强制规范: 【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator方式,如果并发操作,需要对 Iterator 对象加锁。 这引发了我的注意力。 按照他说的做了一遍,果然有这个问题。下面我们就探索一下,到底为什么会出这个问题吧! 想要解锁更多新姿势?请访问https://tengshe789.github.io/ 实验代码下面是推荐使用的Iterator方式来remove/add操作 12345678910public static List<String> test1(List<String> list){ Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String item = iterator.next(); if (item.equals(NUM2)) { iterator.remove(); } } return list; } 不推荐使用的方法: 12345678public static List<String> test2(List<String> list){ for (String item : list) { if (NUM2.equals(item)) { list.remove(item); } } return list; } 我的main方法: 1234567891011121314public static final String NUM1 = "1"; public static final String NUM2 = "2"; public static void main(String[] args) { List<String> list = new ArrayList<String>(); list.add("1"); list.add("2"); test2(list); list.forEach(item -> { System.out.println("结果是 " + item); }); } 实验结果果然,将判断条件设置成NUM2时,就出异常了,可是这是为什么呢? 一探究竟我们从出错日志中查找答案。 123456789101112@SuppressWarnings("unchecked") public E next() { checkForComodification(); int i = cursor; if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i + 1; return (E) elementData[lastRet = i]; } 来到foreach中走的next(),第一步需要走checkForComodification() 1234final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } 这边又出现一个成员变量modCount,我们顺着来找找看。 123456789101112131415161718192021222324252627/** * The number of times this list has been <i>structurally modified</i>. * Structural modifications are those that change the size of the * list, or otherwise perturb it in such a fashion that iterations in * progress may yield incorrect results. * * <p>This field is used by the iterator and list iterator implementation * returned by the {@code iterator} and {@code listIterator} methods. * If the value of this field changes unexpectedly, the iterator (or list * iterator) will throw a {@code ConcurrentModificationException} in * response to the {@code next}, {@code remove}, {@code previous}, * {@code set} or {@code add} operations. This provides * <i>fail-fast</i> behavior, rather than non-deterministic behavior in * the face of concurrent modification during iteration. * * <p><b>Use of this field by subclasses is optional.</b> If a subclass * wishes to provide fail-fast iterators (and list iterators), then it * merely has to increment this field in its {@code add(int, E)} and * {@code remove(int)} methods (and any other methods that it overrides * that result in structural modifications to the list). A single call to * {@code add(int, E)} or {@code remove(int)} must add no more than * one to this field, or the iterators (and list iterators) will throw * bogus {@code ConcurrentModificationExceptions}. If an implementation * does not wish to provide fail-fast iterators, this field may be * ignored. */ protected transient int transient = 0; 哦,缘来乳此,list是线程不安全的,因此如果在使用迭代器的过程中有其他线程修改了list,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略(见jdk注释)。 modCount它代表该List对象被修改的次数,每对List对象修改一次,modCount都会加Iterator类里有一个成员变量expectedModCount,它的值为创建Iterator对象的时候List的modCount值。用此变量来检验在迭代过程中List对象是否被修改了,如果被修改了则抛出java.util.ConcurrentModificationException异常。在每次调用Iterator对象的next()方法的时候都会调用checkForComodification()方法进行一次检验,checkForComodification()方法中做的工作就是比较expectedModCount和modCount的值是否相等,如果不相等,就认为还有其他对象正在对当前的List进行操作,那个就会抛出ConcurrentModificationException异常。 网上查找的关于Iterator的工作机制。Iterator是工作在一个独立的线程中,并且拥有一个 mutex锁,就是说Iterator在工作的时候,是不允许被迭代的对象被改变的。而List等是动态的,可变对象数量的数据结构,但是Iterator则是单向不可变,只能顺序读取,不能逆序操作的数据结构,当 Iterator指向的原始数据发生变化时,Iterator自己就迷失了方向。 续1s时间全片结束,觉得我写的不错?想要了解更多精彩新姿势?赶快打开我的👉个人博客 👈吧! 谢谢你那么可爱,还一直关注着我~❤😝]]></content>
<tags>
<tag>技术</tag>
</tags>
</entry>
<entry>
<title><![CDATA[分布式系统--感性认识JWT]]></title>
<url>%2F2018%2F12%2F02%2F%E6%84%9F%E6%80%A7%E8%AE%A4%E8%AF%86JWT%2F</url>
<content type="text"><![CDATA[好久没写博客了,因为最近公司要求我学spring cloud,早点将以前软件迁移到新的架构上。所以我那个拼命的学呐,总是图快,很多关键的笔记没有做好记录,现在又遗忘了很多关键的技术点,极其罪恶! 现在想一想,还是踏踏实实的走比较好。这不,今天我冒了个泡,来补一补前面我所学所忘的知识点。 想要解锁更多新姿势?请访问我的博客。 常见的认证机制今天我么聊一聊JWT。 关于JWT,相信很多人都已经看过用过,他是基于json数据结构的认证规范,简单的说就是验证用户登没登陆的玩意。这时候你可能回想,哎哟,不是又那个session么,分布式系统用redis做分布式session,那这个jwt有什么好处呢? 请听我慢慢诉说这历史! 最原始的办法–HTTP BASIC AUTHHTTP BASIC auth,别看它名字那么长那么生,你就认为这个玩意很高大上。其实原理很简单,简单的说就是每次请求API的时候,都会把用户名和密码通过restful API传给服务端。这样就可以实现一个无状态思想,即每次HTTP请求和以前都没有啥关系,只是获取目标URI,得到目标内容之后,这次连接就被杀死,没有任何痕迹。你可别一听无状态,正是现在的热门思想,就觉得很厉害。其实他的缺点还是又的,我们通过http请求发送给服务端的时候,很有可能将我们的用户名密码直接暴漏给第三方客户端,风险特别大,因此生产环境下用这个方法很少。 Session和cookiesession和cookie老生常谈了。开始时,都会在服务端全局创建session对象,session对象保存着各种关键信息,同时向客户端发送一组sessionId,成为一个cookie对象保存在浏览器中。 当认证时,cookie的数据会传入服务端与session进行匹配,进而进行数据认证。 此时,实现的是一个有状态的思想,即该服务的实例可以将一部分数据随时进行备份,并且在创建一个新的有状态服务时,可以通过备份恢复这些数据,以达到数据持久化的目的。 缺点这种认证方法基本是现在软件最常用的方法了,它有一些自己的缺点: 安全性。cookies的安全性不好,攻击者可以通过获取本地cookies进行欺骗或者利用cookies进行CSRF攻击。 跨域问题。使用cookies时,在多个域名下,会存在跨域问题。 有状态。session在一定的时间里,需要存放在服务端,因此当拥有大量用户时,也会大幅度降低服务端的性能。 状态问题。当有多台机器时,如何共享session也会是一个问题,也就是说,用户第一个访问的时候是服务器A,而第二个请求被转发给了服务器B,那服务器B如何得知其状态。 移动手机问题。现在的智能手机,包括安卓,原生不支持cookie,要使用cookie挺麻烦。 Token认证(使用jwt规范)token 即使是在计算机领域中也有不同的定义,这里我们说的token,是指 访问资源的凭据 。使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。大概的流程是 这样的: 客户端使用用户名跟密码请求登录 服务端收到请求,去验证用户名与密码 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据 Token机制,我认为其本质思想就是将session中的信息简化很多,当作cookie用,也就是客户端的“session”。 好处那Token机制相对于Cookie机制又有什么好处呢? 支持跨域访问: Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提 是传输的用户认证信息通过HTTP头传输. 无状态:Token机制本质是校验, 他得到的会话状态完全来自于客户端, Token机制在服务端不需要存储session信息,因为 Token 自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息. 更适用CDN: 可以通过内容分发网络请求你服务端的所有资料(如:javascript, HTML,图片等),而你的服务端只要提供API即可. 去耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在 你的API被调用的时候,你可以进行Token生成调用即可. 更适用于移动应用: 当你的客户端是一个原生平台(iOS, Android,Windows 8等) 时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认 证机制就会简单得多。 CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防 范。 性能: 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256 计算 的Token验证和解析要费时得多. 不需要为登录页面做特殊处理: 如果你使用Protractor 做功能测试的时候,不再需要 为登录页面做特殊处理. 基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在 多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如: Firebase,Google, Microsoft) 缺陷在哪?说了那么多token认证的好处,但他其实并没有想象的那么神,token 也并不是没有问题。 占带宽 正常情况下要比 session_id 更大,需要消耗更多流量,挤占更多带宽,假如你的网站每月有 10 万次的浏览器,就意味着要多开销几十兆的流量。听起来并不多,但日积月累也是不小一笔开销。实际上,许多人会在 JWT 中存储的信息会更多。 无论如何你需要操作数据库 在网站上使用 JWT,对于用户加载的几乎所有页面,都需要从缓存/数据库中加载用户信息,如果对于高流量的服务,你确定这个操作合适么?如果使用redis进行缓存,那么效率上也并不能比 session 更高效 无法在服务端注销,那么久很难解决劫持问题 性能问题 JWT 的卖点之一就是加密签名,由于这个特性,接收方得以验证 JWT 是否有效且被信任。但是大多数 Web 身份认证应用中,JWT 都会被存储到 Cookie 中,这就是说你有了两个层面的签名。听着似乎很牛逼,但是没有任何优势,为此,你需要花费两倍的 CPU 开销来验证签名。对于有着严格性能要求的 Web 应用,这并不理想,尤其对于单线程环境。 JWT现在我们来说说今天的主角,JWT JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用 户和服务器之间传递安全可靠的信息 组成一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。 头部(header)头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以 被表示成一个JSON对象。 1234{ "typ":"JWT", "alg":"HS256"} 这就是头部的明文内容,第一部分说明他是一个jwt,第二部分则指出签名算法用的是HS256算法。 然后将这个头部进行BASE64编码,编码后形成头部: 1eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 载荷(payload)载荷就是存放有效信息的地方,有效信息包含三个部分: (1)标准中注册的声明(建议但不强制使用) iss: jwt签发者 sub: jwt所面向的用户 aud: 接收jwt的一方 exp: jwt的过期时间,这个过期时间必须要大于签发时间 nbf: 定义在什么时间之前,该jwt都是不可用的. iat: jwt的签发时间 jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。 (2)公共的声明公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息. 但不建议添加敏感信息,因为该部分在客户端可解密. (3)私有的声明 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64 是对称解密的,意味着该部分信息可以归类为明文信息。 12345{ "sub":"1234567890", "name":"tengshe789", "admin": true} 上面就是一个简单的载荷的明文,接下来使用base64加密: 1eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9 签证(signature)jwt的第三部分是一个签证信息,这个签证信息由三部分组成: header (base64后的) payload (base64后的) secret 这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第 三部分。 1TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ 合成1eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6I kpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7Hg Q 实现JWT现在一般实现jwt,都使用Apache 的开源项目JJWT(一个提供端到端的JWT创建和验证的Java库)。 依赖123456<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt --><dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.7.0</version></dependency> 创建token的demo1234567891011public class CreateJWT { public static void main(String[] args) throws Exception{ JwtBuilder builder = Jwts.builder().setId("123") .setSubject("jwt所面向的用户") .setIssuedAt(new Date()) .signWith(SignatureAlgorithm.HS256,"tengshe789"); String s = builder.compact(); System.out.println(s); //eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMjMiLCJzdWIiOiJqd3TmiYDpnaLlkJHnmoTnlKjmiLciLCJpYXQiOjE1NDM3NTk0MjJ9.1sIlEynqqZmA4PbKI6GgiP3ljk_aiypcsUxSN6-ATIA }} 结果如图: (注意,jjwt不支持jdk11,0.9.1以后的jjwt必须实现signWith()方法才能实现) 解析Token的demo123456789101112public class ParseJWT { public static void main(String[] args) { String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMjMiLCJzdWIiOiJqd3TmiYDpnaLlkJHnmoTnlKjmiLciLCJpYXQiOjE1NDM3NTk0MjJ9.1sIlEynqqZmA4PbKI6GgiP3ljk_aiypcsUxSN6-ATIA"; Claims claims = Jwts.parser().setSigningKey("tengshe789").parseClaimsJws(token).getBody(); System.out.println("id"+claims.getId()); System.out.println("Subject"+claims.getSubject()); System.out.println("IssuedAt"+claims.getIssuedAt()); }} 结果如图: 生产中的JWT在企业级系统中,通常内部会有非常多的工具平台供大家使用,比如人力资源,代码管理,日志监控,预算申请等等。如果每一个平台都实现自己的用户体系的话无疑是巨大的浪费,所以公司内部会有一套公用的用户体系,用户只要登陆之后,就能够访问所有的系统。 这就是 单点登录(SSO: Single Sign-On) SSO 是一类解决方案的统称,而在具体的实施方面,一般有两种策略可供选择: SAML 2.0 OAuth 2.0 欲扬先抑,先说说几个重要的知识点。 Authentication VS Authorisation Authentication: 身份鉴别,鉴权,以下简称认证 认证 的作用在于认可你有权限访问系统,用于鉴别访问者是否是合法用户。负责认证的服务通常称为 Authorization Server 或者 Identity Provider,以下简称 IdP Authorisation: 授权 授权 用于决定你有访问哪些资源的权限。大多数人不会区分这两者的区别,因为站在用户的立场上。而作为系统的设计者来说,这两者是有差别的,这是不同的两个工作职责,我们可以只需要认证功能,而不需要授权功能,甚至不需要自己实现认证功能,而借助 Google 的认证系统,即用户可以用 Google 的账号进行登陆。负责提供资源(API调用)的服务称为 Resource Server 或者 Service Provider,以下简称 SP SMAL 2.0 OAuth(JWT)OAuth(开放授权)是一个开放的授权标准,允许用户让第三方应用访问该用户在 某一web服务上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。 流程可以参考如下: 简单的来说,就是你要访问一个应用服务,先找它要一个request token(请求令牌),再把这个request token发到第三方认证服务器,此时第三方认证服务器会给你一个aceess token(通行令牌), 有了aceess token你就可以使用你的应用服务了。 注意图中第4步兑换 access token 的过程中,很多第三方系统,如Google ,并不会仅仅返回 access token,还会返回额外的信息,这其中和之后更新相关的就是 refresh token。一旦 access token过期,你就可以通过 refresh token 再次请求 access token。 当然了,流程是根据你的请求方式和访问的资源类型而定的,业务很多也是不一样的,我这是简单的聊聊。 现在这种方法比较常见,常见的譬如使用QQ快速登陆,用的基本的都是这种方法。 开源项目我们用一个很火的开源项目Cloud-Admin为栗子,来分析一下jwt的应用。 Cloud-Admin是基于Spring Cloud微服务化开发平台,具有统一授权、认证后台管理系统,其中包含具备用户管理、资源权限管理、网关API管理等多个模块,支持多业务系统并行开发。 目录结构 鉴权中心功能在ace-auth与ace-gate下。 模型下面是官方提供的架构模型。 可以看到,AuthServer在架构的中心环节,要访问服务,必须需要鉴权中心的JWT鉴权。 鉴权中心服务端代码解读实体类先看实体类,这里鉴权中心定义了一组客户端实体,如下: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849@Table(name = "auth_client")@Getter@Setterpublic class Client { @Id private Integer id; private String code; private String secret; private String name; private String locked = "0"; private String description; @Column(name = "crt_time") private Date crtTime; @Column(name = "crt_user") private String crtUser; @Column(name = "crt_name") private String crtName; @Column(name = "crt_host") private String crtHost; @Column(name = "upd_time") private Date updTime; @Column(name = "upd_user") private String updUser; @Column(name = "upd_name") private String updName; @Column(name = "upd_host") private String updHost; private String attr1; private String attr2; private String attr3; private String attr4; private String attr5; private String attr6; private String attr7; private String attr8; 对应数据库: 12345678910111213141516171819202122232425CREATE TABLE `auth_client` ( `id` int(11) NOT NULL AUTO_INCREMENT, `code` varchar(255) DEFAULT NULL COMMENT '服务编码', `secret` varchar(255) DEFAULT NULL COMMENT '服务密钥', `name` varchar(255) DEFAULT NULL COMMENT '服务名', `locked` char(1) DEFAULT NULL COMMENT '是否锁定', `description` varchar(255) DEFAULT NULL COMMENT '描述', `crt_time` datetime DEFAULT NULL COMMENT '创建时间', `crt_user` varchar(255) DEFAULT NULL COMMENT '创建人', `crt_name` varchar(255) DEFAULT NULL COMMENT '创建人姓名', `crt_host` varchar(255) DEFAULT NULL COMMENT '创建主机', `upd_time` datetime DEFAULT NULL COMMENT '更新时间', `upd_user` varchar(255) DEFAULT NULL COMMENT '更新人', `upd_name` varchar(255) DEFAULT NULL COMMENT '更新姓名', `upd_host` varchar(255) DEFAULT NULL COMMENT '更新主机', `attr1` varchar(255) DEFAULT NULL, `attr2` varchar(255) DEFAULT NULL, `attr3` varchar(255) DEFAULT NULL, `attr4` varchar(255) DEFAULT NULL, `attr5` varchar(255) DEFAULT NULL, `attr6` varchar(255) DEFAULT NULL, `attr7` varchar(255) DEFAULT NULL, `attr8` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4; 这些是每组微服务客户端的信息 第二个实体类,就是客户端_服务的实体,也就是对应着那些微服务客户端能调用哪些微服务客户端: 大概对应的就是微服务间调用权限关系。 123456789101112131415161718192021222324@Table(name = "auth_client_service")public class ClientService { @Id private Integer id; @Column(name = "service_id") private String serviceId; @Column(name = "client_id") private String clientId; private String description; @Column(name = "crt_time") private Date crtTime; @Column(name = "crt_user") private String crtUser; @Column(name = "crt_name") private String crtName; @Column(name = "crt_host") private String crtHost;} 接口层我们跳着看,先看接口层 1234567891011121314151617181920212223242526272829303132@RestController@RequestMapping("jwt")@Slf4jpublic class AuthController { @Value("${jwt.token-header}") private String tokenHeader; @Autowired private AuthService authService; @RequestMapping(value = "token", method = RequestMethod.POST) public ObjectRestResponse<String> createAuthenticationToken( @RequestBody JwtAuthenticationRequest authenticationRequest) throws Exception { log.info(authenticationRequest.getUsername()+" require logging..."); final String token = authService.login(authenticationRequest); return new ObjectRestResponse<>().data(token); } @RequestMapping(value = "refresh", method = RequestMethod.GET) public ObjectRestResponse<String> refreshAndGetAuthenticationToken( HttpServletRequest request) throws Exception { String token = request.getHeader(tokenHeader); String refreshedToken = authService.refresh(token); return new ObjectRestResponse<>().data(refreshedToken); } @RequestMapping(value = "verify", method = RequestMethod.GET) public ObjectRestResponse<?> verify(String token) throws Exception { authService.validate(token); return new ObjectRestResponse<>(); }} 这里放出了三个接口 先说第一个接口,创建token。 具体逻辑如下:每一个用户登陆进来时,都会进入这个环节。根据request中用户的用户名和密码,利用feign客户端的拦截器拦截request,然后使用作者写的JwtTokenUtil里面的各种方法取出token中的key和密钥,验证token是否正确,正确则用authService.login(authenticationRequest);的方法返回出去一个新的token。 1234567public String login(JwtAuthenticationRequest authenticationRequest) throws Exception { UserInfo info = userService.validate(authenticationRequest); if (!StringUtils.isEmpty(info.getId())) { return jwtTokenUtil.generateToken(new JWTInfo(info.getUsername(), info.getId() + "", info.getName())); } throw new UserInvalidException("用户不存在或账户密码错误!"); } 下图是详细逻辑图: 鉴权中心客户端代码入口作者写了个注解的入口,使用@EnableAceAuthClient即自动开启微服务(客户端)的鉴权管理 1234567@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Import(AutoConfiguration.class)@Documented@Inheritedpublic @interface EnableAceAuthClient {} 配置接着沿着注解的入口看 123456789101112@Configuration@ComponentScan({"com.github.wxiaoqi.security.auth.client","com.github.wxiaoqi.security.auth.common.event"})public class AutoConfiguration { @Bean ServiceAuthConfig getServiceAuthConfig(){ return new ServiceAuthConfig(); } @Bean UserAuthConfig getUserAuthConfig(){ return new UserAuthConfig(); }} 注解会自动的将客户端的用户token和服务token的关键信息加载到bean中 feigin拦截器作者重写了okhttp3拦截器的方法,每一次微服务客户端请求的token都会被拦截下来,验证服务调用服务的token和用户调用服务的token是否过期,过期则返回新的token 123456789101112131415161718192021222324252627282930@Override public Response intercept(Chain chain) throws IOException { Request newRequest = null; if (chain.request().url().toString().contains("client/token")) { newRequest = chain.request() .newBuilder() .header(userAuthConfig.getTokenHeader(), BaseContextHandler.getToken()) .build(); } else { newRequest = chain.request() .newBuilder() .header(userAuthConfig.getTokenHeader(), BaseContextHandler.getToken()) .header(serviceAuthConfig.getTokenHeader(), serviceAuthUtil.getClientToken()) .build(); } Response response = chain.proceed(newRequest); if (HttpStatus.FORBIDDEN.value() == response.code()) { if (response.body().string().contains(String.valueOf(CommonConstants.EX_CLIENT_INVALID_CODE))) { log.info("Client Token Expire,Retry to request..."); serviceAuthUtil.refreshClientToken(); newRequest = chain.request() .newBuilder() .header(userAuthConfig.getTokenHeader(), BaseContextHandler.getToken()) .header(serviceAuthConfig.getTokenHeader(), serviceAuthUtil.getClientToken()) .build(); response = chain.proceed(newRequest); } } return response; } spring容器的拦截器第二道拦截器是来自spring容器的,第一道feign拦截器只是验证了两个token是否过期,但token真实的权限却没验证。接下来就要验证两个token的权限问题了。 服务调用权限代码如下: 12345678910111213141516171819202122@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HandlerMethod handlerMethod = (HandlerMethod) handler; // 配置该注解,说明不进行服务拦截 IgnoreClientToken annotation = handlerMethod.getBeanType().getAnnotation(IgnoreClientToken.class); if (annotation == null) { annotation = handlerMethod.getMethodAnnotation(IgnoreClientToken.class); } if(annotation!=null) { return super.preHandle(request, response, handler); } String token = request.getHeader(serviceAuthConfig.getTokenHeader()); IJWTInfo infoFromToken = serviceAuthUtil.getInfoFromToken(token); String uniqueName = infoFromToken.getUniqueName(); for(String client:serviceAuthUtil.getAllowedClient()){ if(client.equals(uniqueName)){ return super.preHandle(request, response, handler); } } throw new ClientForbiddenException("Client is Forbidden!"); } 用户权限: 123456789101112131415161718192021222324252627282930313233@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HandlerMethod handlerMethod = (HandlerMethod) handler; // 配置该注解,说明不进行用户拦截 IgnoreUserToken annotation = handlerMethod.getBeanType().getAnnotation(IgnoreUserToken.class); if (annotation == null) { annotation = handlerMethod.getMethodAnnotation(IgnoreUserToken.class); } if (annotation != null) { return super.preHandle(request, response, handler); } String token = request.getHeader(userAuthConfig.getTokenHeader()); if (StringUtils.isEmpty(token)) { if (request.getCookies() != null) { for (Cookie cookie : request.getCookies()) { if (cookie.getName().equals(userAuthConfig.getTokenHeader())) { token = cookie.getValue(); } } } } IJWTInfo infoFromToken = userAuthUtil.getInfoFromToken(token); BaseContextHandler.setUsername(infoFromToken.getUniqueName()); BaseContextHandler.setName(infoFromToken.getName()); BaseContextHandler.setUserID(infoFromToken.getId()); return super.preHandle(request, response, handler); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { BaseContextHandler.remove(); super.afterCompletion(request, response, handler, ex); } spring cloud gateway网关代码该框架中所有的请求都会走网关服务(ace-gatev2),通过网关,来验证token是否过期异常,验证token是否不存在,验证token是否有权限进行服务。 下面是核心代码: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546@Override public Mono<Void> filter(ServerWebExchange serverWebExchange, GatewayFilterChain gatewayFilterChain) { log.info("check token and user permission...."); LinkedHashSet requiredAttribute = serverWebExchange.getRequiredAttribute(ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR); ServerHttpRequest request = serverWebExchange.getRequest(); String requestUri = request.getPath().pathWithinApplication().value(); if (requiredAttribute != null) { Iterator<URI> iterator = requiredAttribute.iterator(); while (iterator.hasNext()){ URI next = iterator.next(); if(next.getPath().startsWith(GATE_WAY_PREFIX)){ requestUri = next.getPath().substring(GATE_WAY_PREFIX.length()); } } } final String method = request.getMethod().toString(); BaseContextHandler.setToken(null); ServerHttpRequest.Builder mutate = request.mutate(); // 不进行拦截的地址 if (isStartWith(requestUri)) { ServerHttpRequest build = mutate.build(); return gatewayFilterChain.filter(serverWebExchange.mutate().request(build).build()); } IJWTInfo user = null; try { user = getJWTUser(request, mutate); } catch (Exception e) { log.error("用户Token过期异常", e); return getVoidMono(serverWebExchange, new TokenForbiddenResponse("User Token Forbidden or Expired!")); } List<PermissionInfo> permissionIfs = userService.getAllPermissionInfo(); // 判断资源是否启用权限约束 Stream<PermissionInfo> stream = getPermissionIfs(requestUri, method, permissionIfs); List<PermissionInfo> result = stream.collect(Collectors.toList()); PermissionInfo[] permissions = result.toArray(new PermissionInfo[]{}); if (permissions.length > 0) { if (checkUserPermission(permissions, serverWebExchange, user)) { return getVoidMono(serverWebExchange, new TokenForbiddenResponse("User Forbidden!Does not has Permission!")); } } // 申请客户端密钥头 mutate.header(serviceAuthConfig.getTokenHeader(), serviceAuthUtil.getClientToken()); ServerHttpRequest build = mutate.build(); return gatewayFilterChain.filter(serverWebExchange.mutate().request(build).build()); } cloud admin总结总的来说,鉴权和网关模块就说完了。作者代码构思极其精妙,使用在大型的权限系统中,可以巧妙的减少耦合性,让服务鉴权粒度细化,方便管理。 结束此片完了~ 想要了解更多精彩新姿势?请访问我的个人博客 本篇为原创内容,已在个人博客率先发表,随后看心情可能会在CSDN,segmentfault,掘金,简书,开源中国同步发出。如有雷同,缘分呢兄弟。赶快加个好友,咱们两个想个号码, 买个彩票,先挣他个几百万😝]]></content>
<tags>
<tag>源码</tag>
<tag>java</tag>
<tag>技术</tag>
<tag>学好分布式架构</tag>
<tag>网络</tag>
</tags>
</entry>
<entry>
<title><![CDATA[光鲜下的china]]></title>
<url>%2F2018%2F11%2F03%2F%E5%88%86%E4%BA%AB%E5%BE%AE%E5%8D%9A%2F</url>
<content type="text"><![CDATA[看到一篇微博文章,分享一下。 为什么分享这篇文章呢? 其实现在,我对现在的环境还是抱有一点悲观态度的。今天和十年前的今天不一样,十年前,我小学6年级,北京奥运会,红歌洋溢,欢声笑语,那时候,谷歌没有退出中国,Facebook还能继续登陆,GFW根本不存在。那时候,我们不发达,但是我们每家安居乐业。 随后汶川大地震,没个中国人众志成城,我们没钱,但是我们有物资。每个人都会自动献出自己的一片心意。大街上大家伙自发向上的踊跃报名志愿者,全中国人民众志成城。那时候,我们不发达,但是我们自由团结。 再后来,金融危机,举世萧条,我们拥有优良的社会制度,我们也遭受着一系列的损失,但是远不如其他国家那么强烈。在这期间,我们不光自我改革,而且还援助整个世界。那时候,我们不发达,但是我们制度先进。 现在呢? 我长大了,成年了。21岁,一个值得娓娓道来的年龄,一个需要担起责任的年龄,一个初入社会的年龄。一个男人,又是一个家族的独生子,需要养老,需要找媳妇,需要实现自我责任与义务的年龄。 可是在现在的环境下,房价涨了,房价(青岛)从十年前的4000翻到现在的30000;就业少了,就业生/当届应届生 的比例一年比一年少;税收高了,高新产业税务严重,消费产品一般人消费不起来;GFW来了,世界也变小了,甚至我们生而为人-说话的权力,都被削弱了。 好失望。 不光是我,肯定也有很多和我一个年龄的人,承受着和我一样的困扰。憋在心里好难受啊,想说!想说!想把它像痘痘一样挤出来,不要让他变大,不要让他变黄。 可是,已经很少有人说话了。关于政治,关于中国左倾,很少有人出来说话了。说了,就是不懂事;说了,就是不尊老;说了,就是不爱国。 解决问题的最好方法就是解决说出问题的人。 我不敢说,我有父母,我有爱人,说话的代价太大了,我承受不起。 父母让我做公务员,我觉得很不错,因为我觉得总有和我一样的人,热爱这片土地,这爱这个国家,却又看见这世界矛盾不公平的现象。 但我放弃了。理由,就是那样。 我很笨,所以我选择学计算机,安安心心的做一个技术人员,安安心心的跟着人民群众走,坚决拥护党的正确领导。做一个乌龟,做一个书呆子,慢慢积德,没准,下辈子我就会富裕了呢! 当我沉默的时候,我觉得很充实,当我开口说话,就感到了空虚。 –鲁迅]]></content>
<tags>
<tag>日常</tag>
</tags>
</entry>
<entry>
<title><![CDATA[秋招失败了,还是要继续]]></title>
<url>%2F2018%2F11%2F03%2F%E5%8A%A0%E6%B2%B92%2F</url>
<content type="text"><![CDATA[在济南本地投了几份简历,收到了五六份offer,感觉除了一家小公司以外以外,都不是很满意。官僚作风,技术封闭,干活累,不尊重应聘者,什么都有。 人生好迷茫,做人好累啊。 今天女朋友逼我,问我意向城市,她性子急,她想根据我选的工作地点找工作。可是我太弱小了,我找不到工作,好烦。好惆怅。她和我说我们可能要分手了,工作地点不同,两方不会在一起的。我很难受,可现实就是这样,菜是原罪,而我恰好就很菜。 她是个很懒的人,也是个经常焦虑的人。今年秋招,她只去了一个面试,结果过了,工作地点在青岛即墨,一个不是很发达的地方,和我老家在一个市,但是距离非常远,工资4K,外贸专员。我不看好她的工作,主要是地点不好,不是市区,我让他放弃,可她不甘心,说怪我没定下来地点,我不靠谱。我们吵了很多次,没有结果。 超级想转行~ 有时候真的觉得,计算机方面我有爱好,但是却没有这个能力。 如果我做管理,结果会怎么样呢?如果我做营销,结果会怎么样呢?如果我做教书,结果会怎么样呢?我不知道,但我认为我会做的很好,在或者,会非常不甘心吧。 不管如何,还是慢慢的活着吧。就像我父母最开始的愿望,让我安安稳稳的活着,最好衣食无忧,也不要做个富人,受人嫉妒,做个小小的勤劳的蚂蚁就好。 这个月中旬我要去实习了,去那家,一开始给我面试很和蔼又感觉很有深度的面试官所在的公司。生活还在继续,我还是会慢慢活着,但是我永远不会停止我对自己的追求! 优秀的人不在于顺境如何过活,而是在逆境活出精彩。难熬的日子总会出现,过去了就是过去,更加坚韧。 鸡汤点到为止,要开始吃饭了,秋招只是工作生涯的开始,起跑线落户的事不是没经历过,现在开始努力,最差不过下一站春招,没再怕的。 Nothing to lose,everything will be !]]></content>
<tags>
<tag>日常</tag>
<tag>生活</tag>
<tag>吐槽</tag>
</tags>
</entry>
<entry>
<title><![CDATA[技术学习路线图]]></title>
<url>%2F2018%2F11%2F01%2F%E6%8A%80%E6%9C%AF%E5%AD%A6%E4%B9%A0%E8%B7%AF%E7%BA%BF%E5%9B%BE%2F</url>
<content type="text"><![CDATA[分享一下学习技术的思维脑图,主要是架构师图谱,java知识图谱,前端技术学习导图,大数据与云计算技能,DevOps相关图谱,留作个人备份用。素材全部搜集于网络,侵删! 架构师图谱 java知识图谱java框架图谱 Java并发知识图谱 前端技术学习导图 大数据与云计算技能 ios技能 DevOps相关图谱]]></content>
<tags>
<tag>技术</tag>
<tag>分享</tag>
</tags>
</entry>
<entry>
<title><![CDATA[未来の目標]]></title>
<url>%2F2018%2F10%2F27%2F%E6%9C%AA%E6%9D%A5%E3%81%AE%E7%9B%AE%E6%A8%99%2F</url>
<content type="text"><![CDATA[很多时候,生活并不如意;很多时候,理想并不能实现;路很长,身体很累,可还是一直要走下去。 始我是一个一个普普通通的双非学校的大学生,来自山东的二十线小城市,家境一般,生活很安逸。可能小时候受到的熏陶比较多吧,家里人比较护犊,接触的事情也很少。平时没啥习惯,最大的爱好是捣鼓计算机。 妈妈的眼界很广,我小时候三年级,妈妈教了flash动画,初中傻了吧唧的的玩国外游戏,因此英语还算可以,高中自己学diy电脑,搞机器。 一步一步的成长,到了快上大学的年纪,自己啥也不知道,只靠着父母。父母很爱我,给我选了一个网络工程专业。进了大学,什么都变了,人变了,楼变了,世界变了。这里的人温文尔雅,这里的人衣着整齐,我动心了,生活在这里好幸福。 我在慢慢懂事。 第一年,我去了学生会,我去了一家世界500强实习,同时还找了一份修电脑的兼职,又对一个女孩产生了好感。 第二年,慢慢的摸索,我继续我的老路。我认为我自己做的决定,很完善,很好,我沉浸在其中。 第三年,恋爱余温降下来了,现实也来了。青春期的烦恼接踵而至,“我在哪?我为什么要这么做?”,我总是在自己探索,我总是比别人晚发育很多。 有这样一个故事,一名记者看见农村的一个正在放羊的小孩,问他“你的理想是什么?”小孩回答说:“放羊!”“羊儿喂大了干什么?”“卖钱!”“有了卖羊儿的钱干什么?”“娶老婆!”“娶老婆干什么?”“生小孩!”“生小孩来干什么?”“放羊!”…… 放羊的小孩对世界的理解也就是:放羊挣钱娶老婆生小孩再放羊。对外面世界的了解可能趋向于了零,在他的世界里,这就是美好的一切,幸福的生活。对世界认识的局限性会导致思维的局限性,思维的局限性会导致目标的局限性,目标的局限性最终导致行为的局限性,从而影响人生中的各种选择。 我,会放羊,但是我不想一直放羊。我需要努力,我需要上进,我想赚钱,我想给我爱的人最好的。 所以,我想做程序员。 走我做过很多事情,很多没有目标。可做程序员,没有目标是不行的。现在是2018年了,我面临着非常巨大的竞争压力。这一年,正好赶上了90后的末班潮,全国学校都已经扩容完毕,数不胜数的,比我优秀的211,985大学生研究生,疯狂的挤入人才市场。可是,今年的人才供求却远不如去年前年。我,觉得我失业了。 我不甘心,我投了很多很多面试,有的长辈对我大吼大叫,说我这样是不可能找到工作的,有的长辈连看都不看,只是冷漠的说声“你走吧”。 望尽世俗常态,唯有提升自己才是根本。 所以我想努力,我想成为我自己。 学习目标在这里我定下目标,三年内,需要学会或者精通以下知识: 数据结构 队列 集合 链表、数组 字典、关联数组 栈 树 二叉树 完全二叉树 平衡二叉树 二叉查找树(BST) B,B+,B - [x]树 红黑树 LSM 树 常用算法 排序、查找算法 选择排序 冒泡排序 插入排序 快速排序 归并排序 桶排序 计数排序 希尔排序 基数排序 计数排序 二分查找 归并排序 Java 中的排序数据结构(arraylist) 布隆过滤器 字符串比较 KMP 算法 深度优先、广度优先 贪心算法 回溯算法 剪枝算法 动态规划 朴素贝叶斯 最小生成树算法 最短路径算法 并发编程 Java 并发 多线程 线程安全 一致性、事务 ACID 事务的隔离级别 MVCC 锁 Java中的锁和同步类 公平锁 & 非公平锁 悲观锁 乐观锁 & CAS ABA 问题 CopyOnWrite容器 RingBuffer 可重入锁 & 不可重入锁 互斥锁 & 共享锁 死锁 操作系统 计算机原理 CPU 内存 进程线程协程 Linux 设计模式 23种常见设计模式 MVC IOC AOP 设计模式的六大原则 微服务思想 康威定律 运维 常规监控 APM 统计分析 持续集成(CI/CD) 环境分离 Jenkins 自动化运维 运维 ; 统计 ; 技术支持 Jenkins 环境分离 自动化运维 Ansible puppet chef 测试 TDD 理论 单元测试 压力测试 全链路压测 A/B 、灰度、蓝绿测试 虚拟化 KVM Xen OpenVZ 容器技术 Docker 云技术 OpenStack DevOps 文档管理 中间件 Web Server Nginx OpenResty Tengine Apache Httpd Tomcat 架构原理 调优方案 Jetty 缓存 本地缓存 客户端缓存 服务端缓存 Web缓存 Memcached Redis 架构 回收策略 Tair 消息队列 消息总线 消息的顺序 RabbitMQ RocketMQ ActiveMQ Kafka Redis 消息推送 ZeroMQ 定时调度 单机定时调度 分布式定时调度 RPC Dubbo Thrift gRPC 数据库中间件 Sharding Jdbc 日志系统 日志搜集 配置中心 API 网关 网络 协议 OSI 七层协议 TCP/IP HTTP HTTP2.0 HTTPS 网络模型 Epoll Java NIO kqueue 连接和短连接 框架 零拷贝(Zero-copy) 序列化(二进制协议) Hessian Protobuf 数据库 基础理论 数据库设计的三大范式 MySQL 原理 InnoDB 优化 索引 聚集索引, 非聚集索引 复合索引 自适应哈希索引(AHI) explain NoSQL MongoDB Hbase 搜索引擎 搜索引擎原理 Lucene Elasticsearch Solr sphinx 性能 性能优化方法论 容量评估 CDN 网络 连接池 性能调优 大数据 流式计算 Storm Flink Kafka Stream 应用场景 Hadoop HDFS MapReduce Yarn Spark 安全 web 安全 XSS CSRF SQL 注入 Hash Dos 脚本注入 漏洞扫描工具 验证码 DDoS 防范 用户隐私信息保护 序列化漏洞 加密解密 对称加密 哈希算法 非对称加密 服务器安全 数据安全 数据备份 网络隔离 内外网分离 登录跳板机 授权、认证 RBAC OAuth2.0 双因素认证(2FA) 单点登录(SSO) 常用开源框架 开源协议 日志框架 Log4j、Log4j2 Logback ORM 网络框架 Web 框架 Spring 家族 工具框架 分布式设计 扩展性设计 稳定性 & 高可用 硬件负载均衡 软件负载均衡 限流 应用层容灾 跨机房容灾 容灾演练流程 平滑启动 数据库扩展 读写分离模式 分片模式 服务治理 服务注册与发现 服务路由控制 分布式一致 CAP 与 BASE 理论 分布式锁 分布式一致性算法 PAXOS Zab Raft Gossip 两阶段提交、多阶段提交 幂等 分布式一致方案 分布式 Leader 节点选举 TCC(Try/Confirm/Cancel) 柔性事务 分布式文件系统 唯一ID 生成 全局唯一ID 一致性Hash算法 设计思想 & 开发模式 DDD(Domain-driven Design - 领域驱动设计) 命令查询职责分离(CQRS) 贫血,充血模型 Actor 模式 响应式编程 Reactor RxJava Vert.x DODAF2.0 Serverless Service Mesh 项目管理 架构评审 重构 代码规范 代码 Review RUP 看板管理 SCRUM 敏捷开发 极限编程(XP) 结对编程 PDCA 循环质量管理 FMEA管理模式 通用业务术语 技术趋势 政策、法规 法律 严格遵守刑法253法条 架构师素质 团队管理 招聘结束有目标,就有动力。未来,我期望着你!]]></content>
<tags>
<tag>生活</tag>
</tags>
</entry>
<entry>
<title><![CDATA[论如何在论文中优美的黏贴代码]]></title>
<url>%2F2018%2F10%2F27%2F%E8%AE%BA%E5%A6%82%E4%BD%95%E5%9C%A8%E8%AE%BA%E6%96%87%E4%B8%AD%E4%BC%98%E7%BE%8E%E7%9A%84%E9%BB%8F%E8%B4%B4%E4%BB%A3%E7%A0%81%2F</url>
<content type="text"><![CDATA[又是一年论文季,学生党表示鸭梨山大。 这不,复制粘贴一个项目里的代码,搞得整个结构乱七八糟的!这该如何是好呢? 今天,我来分享一个网站,可以优雅的将我的代码复制到WORD上~ 想要解锁更多新姿势?请访问我的博客 工具方法1:打开这个网页PlanetB 方法2:谷歌搜索syntax highlight code in word documents,检索结果的第一个。当然了,这两个方法返回的是同一个网站。 步骤 将你需要插入在word中的代码完整的复制到该网站提示的文本框内,选择你的代码类型,如Java,C++,HTML等,并点击Show Highlighted。如下图: 该网页会自动将该代码生成到一个新的页面,此时ctrl+A全选,后ctrl+c复制到word中粘贴就好。如下图: 总结怎么样,是不是效果很好?各位看观老爷,喜欢就点个赞吧!]]></content>
<tags>
<tag>窍门</tag>
</tags>
</entry>
<entry>
<title><![CDATA[万物基于Spring(一)]]></title>
<url>%2F2018%2F10%2F07%2F%E4%B8%87%E7%89%A9%E5%9F%BA%E4%BA%8ESpring%2F</url>
<content type="text"><![CDATA[前面文章提到了,Springboot是基于spring为基础扩展出来的一个快递框架。深入理解springboot就需要理解spring的相关原理。所以我们就开始吧! 想要解锁更多新姿势?请访问我的个人博客https://tengshe789.github.io/(😘 Spring的概述Spring是一个开源的轻量级JavaSE(Java标准版本)/ JavaEE(Java企业版本)开发应用框架,其目的是用于简化企业级应用程序开发。 应用程序是由一组相互协作的对象组成。而在传统应用程序开发中,一个完整的应用是由一组相互协作的对象组成。所以开发一个应用除了要开发业务逻辑之外,最多的是关注如何使这些对象协作来完成所需功能,而且要低耦合、高内聚。 业务逻辑开发是不可避免的,那如果有个框架出来帮我们来创建对象及管理这些对象之间的依赖关系。可能有人说了,比如“抽象工厂、工厂方法设计模式”不也可以帮我们创建对象,“生成器模式”帮我们处理对象间的依赖关系,不也能完成这些功能吗?可是这些又需要我们创建另一些工厂类、生成器类,我们又要而外管理这些类,增加了我们的负担,如果能有种通过配置方式来创建对象,管理对象之间依赖关系,我们不需要通过工厂和生成器来创建及管理对象之间的依赖关系,这样我们是不是减少了许多工作,加速了开发,能节省出很多时间来干其他事。 Spring框架刚出来时主要就是来完成这个功能。 Spring框架除了帮我们管理对象及其依赖关系,还提供像通用日志记录、性能统计、安全控制、异常处理等面向切面的能力,还能帮我们管理最头疼的数据库事务,本身提供了一套简单的JDBC访问实现,提供与第三方数据访问框架集成,如JPA(默认使用Hibernate),与各种JavaEE技术整合(如JavaMail、任务调度等等),提供一套自己的web层框架SpringMVC、Webflex,而且还能非常简单的与第三方Web框架集成。 从这里可以认为Spring是一个超级粘合平台,除了自己提供功能外,还提供粘合其他技术和框架的能力,从而使我们可以更自由的选择到底使用什么技术进行开发。而且不管是JAVASE(C/S架构)应用程序还是JAVAEE(B/S架构)应用程序都可以使用这个平台进行开发。让我们来深入看一下Spring到底能帮我们做些什么? 历史的开始1996,Java还只是一个新兴的、初出茅庐的编程语言。人们之所以关注她仅仅是因为,可以使用Java的Applet来开发Web应用。但这些开发者很快就发现这个新兴的语言还能做更多的事情。与之前的所有语言不同,Java让模块化构建复杂的系统成为可能(当时的软件行业虽然在业务上突飞猛进,但当时开发用的是传统的面向过程开发思想,软件的开发效率一直踟蹰不前。伴随着业务复杂性的不断加深,开发也变得越发困难。其实,当时也是面向对象思想飞速发展的时期,她在80年代末被提出,成熟于90年代,现今大多数编程语言都是面向对象的,当然这是后话了)。他们为Applet而来,为组件化而留。这便是最早的Java。 同样在这一年的12月,Sun公司发布了当时还名不见经传但后来人尽皆知的JavaBean1.00-A规范。早期的JavaBean规范针对于Java,她定义了软件组件模型。这个规范规定了一整套编码策略,使简单的Java对象不仅可以被重用,而且还可以轻松地构建更为复杂的应用。尽管JavaBean最初是为重用应用组件而设计的,但当时他们却是主要用作构建窗体控件,毕竟在PC时代那才是主流。但相比于当时正如日中天的Delphi、VB和C++,他看起来还是太简易了,以至于无法胜任任何”实际的”工作。 复杂的应用通常需要诸如事物、安全、分布式等服务的支持,但JavaBean并未直接提供。所以到了1998年3月,Sun发布了EJB1.0规范,该规范把Java组件的设计理念延伸到了服务器端,并提供了许多必须的企业级服务,但他也不再像早期的JavaBean那么简单了。实际上,除了名字,EJBBean已经和JavaBean没有任何关系了。 尽管现实中有很多系统是基于EJB构建的,但EJB从来没有实现它最初的设想:简化开发。EJB的声明式编程模型的确简化了很多基础架构层面的开发,例如事务和安全;但另一方面EJB在部署描述符和配套代码实现等方面变得异常复杂。随着时间的推移,很多开发者对EJB已经不再抱有幻想,开始寻求更简洁的方法。 现在Java组件开发理念重新回归正轨。新的编程技术AOP和DI的不断出现,他们为JavaBean提供了之前EJB才能拥有的强大功能。这些技术为POJO提供了类似EJB的声明式编程模型,而没有引入任何EJB的复杂性。当简单的JavaBean足以胜任时,人们便不愿编写笨重的EJB组件了。 客观地讲,EJB的发展甚至促进了基于POJO的编程模型。引入新的理念,最新的EJB规范相比之前的规范有了前所未有的简化,但对很多开发者而言,这一切的一切都来得太迟了。到了EJB3规范发布时,其他基于POJO的开发架构已经成为事实的标准了,而Spring框架也是在这样的大环境下出现的。 Spring设计的初衷Spring是为解决企业级应用开发的复杂性而设计,她可以做很多事。但归根到底支撑Spring的仅仅是少许的基本理念,而所有地这些的基本理念都能可以追溯到一个最根本的使命:简化开发。这是一个郑重的承诺,其实许多框架都声称在某些方面做了简化。 而Spring则立志于全方面的简化Java开发。对此,她主要采取了4个关键策略: 1,基于POJO的轻量级和最小侵入性编程; 2,通过依赖注入和面向接口松耦合; 3,基于切面进行声明式编程; 4,通过切面和模板减少样板式代码; 而他主要是通过:面向Bean、依赖注入以及面向切面这三种方式来达成的。 BOP编程伊始Spring是面向Bean的编程(BeanOrientedProgramming,BOP),Bean在Spring中才是真正的主角。Bean在Spring中作用就像Object对OOP的意义一样,Spring中没有Bean也就没有Spring存在的意义。Spring提供了IOC容器通过配置文件或者注解的方式来管理对象之间的依赖关系。 控制反转(其中最常见的方式叫做依赖注入(DependencyInjection,DI),还有一种方式叫“依赖查找”(DependencyLookup,DL),她在C++、Java、PHP以及.NET中都运用。在最早的Spring中是包含有依赖注入方法和依赖查询的,但因为依赖查询使用频率过低,不久就被Spring移除了,所以在Spring中控制反转也被称作依赖注入),她的基本概念是:不创建对象,但是描述创建它们的方式。在代码中不直接与对象和服务连接,但在配置文件中描述哪一个组件需要哪一项服务。容器(在Spring框架中是IOC容器)负责将这些联系在一起。 在典型的IOC场景中,容器创建了所有对象,并设置必要的属性将它们连接在一起,决定什么时间调用方法。 依赖注入DI的基本概念Spring设计的核心org.springframework.beans包(架构核心是org.springframework.core包),它的设计目标是与JavaBean组件一起使用。这个包通常不是由用户直接使用,而是由服务器将其用作其他多数功能的底层中介。下一个最高级抽象是BeanFactory接口,它是工厂设计模式的实现,允许通过名称创建和检索对象。BeanFactory也可以管理对象之间的关系。 BeanFactory支持两个对象模型。 1,单例:模型提供了具有特定名称的对象的共享实例,可以在查询时对其进行检索。Singleton是默认的也是最常用的对象模型。对于无状态服务对象很理想。 2,原型:模型确保每次检索都会创建单独的对象。在每个用户都需要自己的对象时,原型模型最适合。 Bean工厂的概念是Spring作为IOC容器的基础。IOC则将处理事情的责任从应用程序代码转移到框架。 AOP编程理念面向切面编程,即AOP,是一种编程思想,它允许程序员对横切关注点或横切典型的职责分界线的行为(例如日志和事务管理)进行模块化。AOP的核心构造是方面(切面),它将那些影响多个类的行为封装到可重用的模块中。 AOP和IOC是补充性的技术,它们都运用模块化方式解决企业应用程序开发中的复杂问题。在典型的面向对象开发方式中,可能要将日志记录语句放在所有方法和Java类中才能实现日志功能。在AOP方式中,可以反过来将日志服务模块化,并以声明的方式将它们应用到需要日志的组件上。当然,优势就是Java类不需要知道日志服务的存在,也不需要考虑相关的代码。所以,用SpringAOP编写的应用程序代码是松散耦合的。 AOP的功能完全集成到了Spring事务管理、日志和其他各种特性的上下文中。 AOP编程的常用场景有:Authentication权限认证、Logging日志、TransctionsManager事务、LazyLoading懒加载、ContextProcess上下文处理、ErrorHandler错误跟踪(异常捕获机制)、Cache缓存。 编程思想总结 Spring思想 应用场景(特点) 一句话归纳 AOP AspectOrientedProgramming(面向切面编程) 找出多个类中有一定规律的代码,开发时拆开,运行时再合并。 面向切面编程,即面向规则编程。 解耦,专人做专事。 OOP ObjectOrientedProgramming(面向对象编程) 归纳总结生活中一切事物。 封装、继承、多态。 BOP BeanOrientedProgramming(面向Bean编程) 面向Bean(普通的java类)设计程序。 一切从Bean开始。 IOC InversionofControl(控制反转) 将new对象的动作交给Spring管理,并由Spring保存已创建的对象(IOC容器)。 转交控制权(即控制权反转)。 DI/DL DependencyInjection(依赖注入)或者DependencyLookup(依赖查找) 依赖注入、依赖查找,Spring不仅保存自己创建的对象,而且保存对象与对象之间的关系。 注入即赋值,主要三种方式构造方法、set方法、直接赋值。 先理清关系再赋值。 Spring架构设计Spring总共大约有20个模块,由1300多个不同的文件构成。而这些组件被分别整合在核心容器(CoreContainer)、AOP(AspectOrientedProgramming)和设备支持(Instrmentation)、数据访问及集成(DataAccess/Integeration)、Web、报文发送(Messaging)、Test,6个模块集合中。以下是spring总体架构图和Spring5的模块结构图: 组成Spring框架的每个模块集合或者模块都可以单独存在,也可以一个或多个模块联合实现。每个模块的组成和功能如下: 1.核心容器:由spring-beans、spring-core、spring-context和spring-expression(SpringExpressionLanguage,SpEL)4个模块组成。 spring-beans和spring-core模块是Spring框架的核心模块,包含了控制反转(InversionofControl,IOC)和依赖注入(DependencyInjection,DI)。BeanFactory接口是Spring框架中的核心接口,它是工厂模式的具体实现。BeanFactory使用控制反转对应用程序的配置和依赖性规范与实际的应用程序代码进行了分离。但BeanFactory容器实例化后并不会自动实例化Bean,只有当Bean被使用时BeanFactory容器才会对该Bean进行实例化与依赖关系的装配。 spring-context模块构架于核心模块之上,他扩展了BeanFactory,为她添加了Bean生命周期控制、框架事件体系以及资源加载透明化等功能。此外该模块还提供了许多企业级支持,如邮件访问、远程访问、任务调度等,ApplicationContext是该模块的核心接口,她是BeanFactory的超类,与BeanFactory不同,ApplicationContext容器实例化后会自动对所有的单实例Bean进行实例化与依赖关系的装配,使之处于待用状态。 spring-expression模块是统一表达式语言(EL)的扩展模块,可以查询、管理运行中的对象,同时也方便的可以调用对象方法、操作数组、集合等。它的语法类似于传统EL,但提供了额外的功能,最出色的要数函数调用和简单字符串的模板函数。这种语言的特性是基于Spring产品的需求而设计,他可以非常方便地同SpringIOC进行交互。 2.AOP和设备支持:由spring-aop、spring-aspects和spring-instrument3个模块组成。 spring-aop是Spring的另一个核心模块,是AOP主要的实现模块。作为继OOP后,对程序员影响最大的编程思想之一,AOP极大地开拓了人们对于编程的思路。在Spring中,他是以JVM的动态代理技术为基础,然后设计出了一系列的AOP横切实现,比如前置通知、返回通知、异常通知等,同时,Pointcut接口来匹配切入点,可以使用现有的切入点来设计横切面,也可以扩展相关方法根据需求进行切入。 spring-aspects模块集成自AspectJ框架,主要是为SpringAOP提供多种AOP实现方法。 spring-instrument模块是基于JAVASE中的”java.lang.instrument”进行设计的,应该算是AOP的一个支援模块,主要作用是在JVM启用时,生成一个代理类,程序员通过代理类在运行时修改类的字节,从而改变一个类的功能,实现AOP的功能。在分类里,我把他分在了AOP模块下,在Spring官方文档里对这个地方也有点含糊不清,这里是纯个人观点。 3.数据访问及集成:由spring-jdbc、spring-tx、spring-orm、spring-jms和spring-oxm5个模块组成。 spring-jdbc模块是Spring提供的JDBC抽象框架的主要实现模块,用于简化SpringJDBC。主要是提供JDBC模板方式、关系数据库对象化方式、SimpleJdbc方式、事务管理来简化JDBC编程,主要实现类是JdbcTemplate、SimpleJdbcTemplate以及NamedParameterJdbcTemplate。 spring-tx模块是SpringJDBC事务控制实现模块。使用Spring框架,它对事务做了很好的封装,通过它的AOP配置,可以灵活的配置在任何一层;但是在很多的需求和应用,直接使用JDBC事务控制还是有其优势的。其实,事务是以业务逻辑为基础的;一个完整的业务应该对应业务层里的一个方法;如果业务操作失败,则整个事务回滚;所以,事务控制是绝对应该放在业务层的;但是,持久层的设计则应该遵循一个很重要的原则:保证操作的原子性,即持久层里的每个方法都应该是不可以分割的。所以,在使用SpringJDBC事务控制时,应该注意其特殊性。 spring-orm模块是ORM框架支持模块,主要集成Hibernate,JavaPersistenceAPI(JPA)和JavaDataObjects(JDO)用于资源管理、数据访问对象(DAO)的实现和事务策略。 spring-jms模块(JavaMessagingService)能够发送和接受信息,自SpringFramework4.1以后,他还提供了对spring-messaging模块的支撑。 spring-oxm模块主要提供一个抽象层以支撑OXM(OXM是Object-to-XML-Mapping的缩写,它是一个O/M-mapper,将java对象映射成XML数据,或者将XML数据映射成java对象),例如:JAXB,Castor,XMLBeans,JiBX和XStream等。 4.Web:由spring-web、spring-webmvc、spring-websocket和spring-webflux4个模块组成。 spring-web模块为Spring提供了最基础Web支持,主要建立于核心容器之上,通过Servlet或者Listeners来初始化IOC容器,也包含一些与Web相关的支持。 spring-webmvc模块众所周知是一个的Web-Servlet模块,实现了SpringMVC(model-view-Controller)的Web应用。 spring-websocket模块主要是与Web前端的全双工通讯的协议。(资料缺乏,这是个人理解) spring-webflux是一个新的非堵塞函数式ReactiveWeb框架,可以用来建立异步的,非阻塞,事件驱动的服务,并且扩展性非常好。 5.报文发送:即spring-messaging模块。 spring-messaging是从Spring4开始新加入的一个模块,主要职责是为Spring框架集成一些基础的报文传送应用。 6.Test:即spring-test模块。 spring-test模块主要为测试提供支持的,毕竟在不需要发布(程序)到你的应用服务器或者连接到其他企业设施的情况下能够执行一些集成测试或者其他测试对于任何企业都是非常重要的。 Spirng各模块之间的依赖关系该图是Spring5的包结构,可以从中清楚看出Spring各个模块之间的依赖关系。 未完待续这是9月份写的,大部分是复制的,因为面试没有写完,所以未完待续吧! 接下来咱们介绍Spring的这个博客,是从spring-core开始,其次是spring-beans和spring-aop,随后是spring-context,再其次是spring-tx和spring-orm,最后是spring-web和其他部分。 感谢观看 参考资料大部分来自咕泡学院TOM老师的材料]]></content>
<tags>
<tag>java</tag>
<tag>技术</tag>
<tag>Spring</tag>
</tags>
</entry>
<entry>
<title><![CDATA[并发基础-缓存、内存与JVM]]></title>
<url>%2F2018%2F09%2F30%2FJava%E4%B8%8E%E5%86%85%E5%AD%98%2F</url>
<content type="text"><![CDATA[又是一年秋招季,哎呀妈呀我被虐的惨来~这不,前几阵失踪没更新博客,其实是我偷偷把时间用在复习课本了(雾 坚持在社区分享博客也很久了,由于过去的文章有很多疏漏之处,很多大佬都在评论指出我的过错,我很开心也很失望,开心的是有大家帮我指出错误,失望的鄙人学识浅薄总没法做到完美。总之,欢迎评论区各种pr~ 好,回到正题。复习的时候,无意间看到java虚拟机的有关知识点,我产生了非常浓厚的兴趣,今天我来结合计算机内存模型的相关知识,与Java内存模型、Java对象模型、JVM内存结构等相关的知识串联起来,本篇文章共1.5W字,分享给大家,感谢阅读。 想要解锁更多新姿势?请访问我的个人博客https://tengshe789.github.io/(😘 计算机内存相信每个人都有一台电脑,也有diy电脑的经历。现在一台功能强大的diy电脑大概3k就能组装起来,一个i5-8400 的cpu 869元,DDR4 内存 1200块钱,b360主板300元 散热器50元 机械硬盘200元 350w电源300元 机箱100元 ,没错,只要3k就能拿到一个性能强大的6C6T电脑。 要说一台PC中最重要的部件是什么?大家看价格也会看明白,是cpu和内存,下面我来介绍一下cpu和内存之间的关系。 cpu与内存缓存的千丝万缕cpu相关术语首先说明一下相关的cpu术语: socket:cpu插在主板上那个槽与cpu称作一个socket。 Die:核心(Die)又称为内核,是cpu的物理组成部分之一。cpu也会分为多die cpu与单die cpu,譬如我们现在强大的AMD TR-2990WX就是4die cpu,每个die里面有8个核心(core) core:也就是物理核心了。core这个词是英特尔起的,起初是为了与竞争对手AMD区别开,后面用的多了也淡了。 thread:就是硬件线程数。一个程序执行可能需要多个线程一起进行~而现在也就比较强大的超线程技术,过去的cpu往往一个cpu核心只支持一个线程,现在一些强大的cpu中,就譬如IBM 的POWER 9 ,支持8核心32个线程(平均一个核心4个线程),理论性能非常强大。 总结一下,以明星cpu AMD TR-2990WX作为栗子,这个cpu使用一个socket,一个socket里面有4个die,总共32个物理核心64个线程 cpu缓存我们都知道,cpu将要处理的数据会放到内存中保存,可是,为什么会这样,将内存缓存硬盘行不行呢? 答案当然是不行的。cpu的处理速度很强大,内存的速度虽然非常快速但是根本跟不上cpu的步伐,所以,就出现的缓存。与来自DRAM家族的内存不同,缓存SRAM与内存最大的特点是,特别快,容量小,结构复杂,成本也高。 造成内存和缓存性能差异,主要有以下原因: DRAM储存一位数据只需要一个电容加上一个晶体管,而SRAM需要6个晶体管。由于DRAM保存数据其实是在电容里面的,电容需要充放电才能进行读写操作,这就导致其读写数据就有比较大的延迟问题。 存储可以看错一个二维数组,每个存储单元都有其行地址列地址。SRAM的容量很小,其存储单元比较短(行列短),可以一次性传输到SRAM中;而DRAM,需要分别传送行列地址。 SRAM的频率和cpu频率比较接近;而DRAM的频率和cpu差距比较大。 近代的缓存通常被集成到cpu当中,为了适应性能与成本的需要,现实中的缓存往往使用金字塔型多级缓存架构。也就是当CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找。 下面是英特尔最近以来用的初代skylake架构 可以看到,每个个核心有专属的L1,L2缓存,他们共享一个L3缓存。如果cpu如果要访问内存中的数据,必须要经过L1,L2,L3,LLC(或者L4)四层缓存。 缓存一致性问题最开始的cpu,其实只是一个核心一个线程的,当时根本不需要考虑缓存一致性问题,单线程,也就是cpu核心的缓存只被一个线程访问。缓存独占,不会出现访问冲突等问题。 后来超线程技术来到我们视野,‘’单核CPU多线程’’,也就是进程中的多个线程会同时访问进程中的共享数据,CPU将某块内存加载到缓存后,不同线程在访问相同的物理地址的时候,都会映射到相同的缓存位置,这样即使发生线程的切换,缓存仍然不会失效。但由于任何时刻只能有一个线程在执行,因此不会出现缓存访问冲突。 时代不断发展,“多核CPU多线程”来了,即多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的caehe中保留一份共享内存的缓冲。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。 这就是我们说的缓存一致性问题。 目前公认最好的解决方案是英特尔的MESI协议,下面我们着重介绍。 MESI协议首先说说I/O操作的单位问题,大部分人都知道,在内存中操作I/O不是以字节为单位,而是以“块”为单位,这是为什么呢? 其实这是因为I/O操作的数据访问有空间连续性特征,即需要访问内存空间很多数据,但是I/O操作比较慢,读一个字节和读N个字节的时间基本相同。 机智的intel就规定了,cpu缓存中最小的存储单元是缓存行cache line,在x86的cpu中,一个cache line储存64字节,每一级的缓存都会被划分成许多组cache line。 缓存工作原理请看👉维基百科 接下来我们看看MESI规范,这其实是用四种缓存行状态命名的,我们定义了CPU中每个缓存行使用4种状态进行标记(使用额外的两位(bit)表示),分别是: M: 被修改(Modified) 该缓存行只被缓存在该CPU的缓存中,并且是被修改过的(dirty),即与主存中的数据不一致,该缓存行中的内存需要在未来的某个时间点(允许其它CPU读取请主存中相应内存之前)写回(write back)主存。当被写回主存之后,该缓存行的状态会变成独享(exclusive)状态。 E: 独享的(Exclusive) 该缓存行只被缓存在该CPU的缓存中,它是未被修改过的(clean),与主存中数据一致。该状态可以在任何时刻当有其它CPU读取该内存时变成共享状态(shared)。同样地,当CPU修改该缓存行中内容时,该状态可以变成Modified状态。 S: 共享的(Shared) 该状态意味着该缓存行可能被多个CPU缓存,并且各个缓存中的数据与主存数据一致(clean),当有一个CPU修改该缓存行中,其它CPU中该缓存行可以被作废(变成无效状态(Invalid))。 I: 无效的(Invalid) 该缓存是无效的(可能有其它CPU修改了该缓存行)。 然而,只是有这四种状态也会带来一定的问题。下面引用一下oracle的文档。 同时更新来自不同处理器的相同缓存代码行中的单个元素会使整个缓存代码行无效,即使这些更新在逻辑上是彼此独立的。每次对缓存代码行的单个元素进行更新时,都会将此代码行标记为无效。其他访问同一代码行中不同元素的处理器将看到该代码行已标记为无效。即使所访问的元素未被修改,也会强制它们从内存或其他位置获取该代码行的较新副本。这是因为基于缓存代码行保持缓存一致性,而不是针对单个元素的。因此,互连通信和开销方面都将有所增长。并且,正在进行缓存代码行更新的时候,禁止访问该代码行中的元素。 MESI协议,可以保证缓存的一致性,但是无法保证实时性。这种情况称为伪共享。 伪共享问题伪共享问题其实在Java中是真实存在的一个问题。假设有如下所示的java class 12345class MyObiect{ long a; long b; long c;} 按照java规范,MyObiect对象是在堆空间中分配的,a、b、c这三个变量在内存空间中是近邻,分别占8字节,长度之和为24字节。而我们的x86的缓存行是64字节,这三个变量完全有可能会在一个缓存行中,并且被两个不同的cpu核心共享! 根据MESI协议,如果不同物理核心cpu中的线程1和线程2要互斥的对这几个变量进行操作,很有可能要互相抢占资源,导致原来的并行变成串行,大大降低了系统的并发性,这就是缓存的伪共享。 解决伪共享其实解决伪共享很简单,只需要将这几个变量分别放到不同的缓存行即可。在java8中,就已经提供了普适性的解决方案,即采用@Contended注解来保证对象中的变量或者属性不在一个缓存行中~ 123456@Contendedclass VolatileObiect{ volatile long a = 1L; volatile long b = 2L; volatile long c = 3L;} 内存不一致性问题上面我说了MESI协议在多核心cpu中解决缓存一致性的问题,下面我们说说cpu的内存不一致性问题。 三种cpu架构首先,要了解三个名词: SMP(Symmetric Multi-Processor) SMP ,对称多处理系统内有许多紧耦合多处理器,在这样的系统中,所有的CPU共享全部资源,如总线,内存和I/O系统等,操作系统或管理数据库的复本只有一个,这种系统有一个最大的特点就是共享所有资源。多个CPU之间没有区别,平等地访问内存、外设、一个操作系统。操作系统管理着一个队列,每个处理器依次处理队列中的进程。如果两个处理器同时请求访问一个资源(例如同一段内存地址),由硬件、软件的锁机制去解决资源争用问题。 所谓对称多处理器结构,是指服务器中多个 CPU 对称工作,无主次或从属关系。各 CPU 共享相同的物理内存,每个 CPU 访问内存中的任何地址所需时间是相同的,因此 SMP 也被称为一致存储器访问结构 (UMA : Uniform Memory Access) 。对 SMP 服务器进行扩展的方式包括增加内存、使用更快的 CPU 、增加 CPU 、扩充 I/O( 槽口数与总线数 ) 以及添加更多的外部设备 ( 通常是磁盘存储 ) 。 SMP 服务器的主要特征是共享,系统中所有资源 (CPU 、内存、 I/O 等 ) 都是共享的。也正是由于这种特征,导致了 SMP 服务器的主要问题,那就是它的扩展能力非常有限。对于 SMP 服务器而言,每一个共享的环节都可能造成 SMP 服务器扩展时的瓶颈,而最受限制的则是内存。由于每个 CPU 必须通过相同的内存总线访问相同的内存资源,因此随着 CPU 数量的增加,内存访问冲突将迅速增加,最终会造成 CPU 资源的浪费,使 CPU 性能的有效性大大降低。实验证明, SMP 服务器 CPU 利用率最好的情况是 2 至 4 个 CPU 。 NUMA(Non-Uniform Memory Access) 由于 SMP 在扩展能力上的限制,人们开始探究如何进行有效地扩展从而构建大型系统的技术, NUMA 就是这种努力下的结果之一。利用 NUMA 技术,可以把几十个 CPU( 甚至上百个 CPU) 组合在一个服务器内。其NUMA 服务器 CPU 模块结构如图所示: NUMA 服务器的基本特征是具有多个 CPU 模块,每个 CPU 模块由多个 CPU( 如 4 个 ) 组成,并且具有独立的本地内存、 I/O 槽口等。由于其节点之间可以通过互联模块 ( 如称为 Crossbar Switch) 进行连接和信息交互,因此每个 CPU 可以访问整个系统的内存 ( 这是 NUMA 系统与 MPP 系统的重要差别 ) 。显然,访问本地内存的速度将远远高于访问远地内存 ( 系统内其它节点的内存 ) 的速度,这也是非一致存储访问 NUMA 的由来。由于这个特点,为了更好地发挥系统性能,开发应用程序时需要尽量减少不同 CPU 模块之间的信息交互。 利用 NUMA 技术,可以较好地解决原来 SMP 系统的扩展问题,在一个物理服务器内可以支持上百个 CPU 。比较典型的 NUMA 服务器的例子包括 HP 的 Superdome 、 SUN15K 、 IBMp690 等。 但 NUMA 技术同样有一定缺陷,由于访问远地内存的延时远远超过本地内存,因此当 CPU 数量增加时,系统性能无法线性增加。如 HP 公司发布 Superdome 服务器时,曾公布了它与 HP 其它 UNIX 服务器的相对性能值,结果发现, 64 路 CPU 的 Superdome (NUMA 结构 ) 的相对性能值是 20 ,而 8 路 N4000( 共享的 SMP 结构 ) 的相对性能值是 6.3 。从这个结果可以看到, 8 倍数量的 CPU 换来的只是 3 倍性能的提升。 MPP(Massive Parallel Processing) 和 NUMA 不同, MPP 提供了另外一种进行系统扩展的方式,它由多个 SMP 服务器通过一定的节点互联网络进行连接,协同工作,完成相同的任务,从用户的角度来看是一个服务器系统。其基本特征是由多个 SMP 服务器 ( 每个 SMP 服务器称节点 ) 通过节点互联网络连接而成,每个节点只访问自己的本地资源 ( 内存、存储等 ) ,是一种完全无共享 (Share Nothing) 结构,因而扩展能力最好,理论上其扩展无限制,目前的技术可实现 512 个节点互联,数千个 CPU 。目前业界对节点互联网络暂无标准,如 NCR 的 Bynet , IBM 的 SPSwitch ,它们都采用了不同的内部实现机制。但节点互联网仅供 MPP 服务器内部使用,对用户而言是透明的。 在 MPP 系统中,每个 SMP 节点也可以运行自己的操作系统、数据库等。但和 NUMA 不同的是,它不存在异地内存访问的问题。换言之,每个节点内的 CPU 不能访问另一个节点的内存。节点之间的信息交互是通过节点互联网络实现的,这个过程一般称为数据重分配 (Data Redistribution) 。 但是 MPP 服务器需要一种复杂的机制来调度和平衡各个节点的负载和并行处理过程。目前一些基于 MPP 技术的服务器往往通过系统级软件 ( 如数据库 ) 来屏蔽这种复杂性。举例来说, NCR 的 Teradata 就是基于 MPP 技术的一个关系数据库软件,基于此数据库来开发应用时,不管后台服务器由多少个节点组成,开发人员所面对的都是同一个数据库系统,而不需要考虑如何调度其中某几个节点的负载。 MPP (Massively Parallel Processing),大规模并行处理系统,这样的系统是由许多松耦合的处理单元组成的,要注意的是这里指的是处理单元而不是处理器。每个单元内的CPU都有自己私有的资源,如总线,内存,硬盘等。在每个单元内都有操作系统和管理数据库的实例复本。这种结构最大的特点在于不共享资源。 NUMA结构下的缓存一致性要知道,MESI协议解决的是传统SMP结构下缓存的一致性,为了在NUMA架构也实现缓存一致性,intel引入了MESI的一个拓展协议–MESIF,但是目前并没有什么资料,也没法研究,更多消息请查阅intel的wiki。 Java内存模型起因我们写程序,为什么要考虑内存模型呢,我们前面说了,缓存一致性问题、内存一致问题是硬件的不断升级导致的。解决问题,最简单直接的做法就是废除CPU缓存,让CPU直接和主存交互。但是,这么做虽然可以保证多线程下的并发问题。但是,这就有点时代倒退了。 所以,为了保证并发编程中可以满足原子性、可见性及有序性。有一个重要的概念,那就是——内存模型。 即为了保证共享内存的正确性(可见性、有序性、原子性),需要内存模型来定义了共享内存系统中多线程程序读写操作行为的相应规范~ JMMJava内存模型是根据英文Java Memory Model(JMM)翻译过来的。其实JMM并不像JVM内存结构一样是真实存在的。它是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。就像JSR-133: Java Memory Model and Thread Specification 中描述了,JMM是和多线程相关的,他描述了一组规则或规范,这个规范定义了一个线程对共享变量的写入时对另一个线程是可见的。 Java的多线程之间是通过共享内存进行通信的,而由于采用共享内存进行通信,在通信过程中会存在一系列如可见性、原子性、顺序性等问题,而JMM就是围绕着多线程通信以及与其相关的一系列特性而建立的模型。JMM定义了一些语法集,这些语法集映射到Java语言中就是volatile、synchronized等关键字。 在JMM中,我们把多个线程间通信的共享内存称之为主内存,而在并发编程中多个线程都维护了一个自己的本地内存(这是个抽象概念),其中保存的数据是主内存中的数据拷贝。而JMM主要是控制本地内存和主内存之间的数据交互的。 下面看看Java内存模型抽象图: 在Java中,JMM是一个非常重要的概念,正是由于有了JMM,Java的并发编程才能避免很多问题。 JMM同步八种操作JMM规定了一系列操作,为了保证数据的操作的一致数据的一致。 (1)lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态 (2)unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定 (3)read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用 (4)load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中 (5)use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎 (6)assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量 (7)store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作 (8)write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中 按照顺序如图所示: 同步规则同步规则分析: 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步会主内存中 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或者assign)的变量。即就是对一个变量实施use和store操作之前,必须先自行assign和load操作。 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现。 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量之前需要重新执行load或assign操作初始化变量的值。 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作) JMM应用了解Java多线程的朋友都知道,在Java中提供了一系列和并发处理相关的关键字,比如volatile、synchronized、final、concurrent包等。其实这些就是Java内存模型封装了底层的实现后提供给我们使用的一些关键字。 在开发多线程的代码的时候,我们可以直接使用synchronized等关键字来控制并发,从来就不需要关心底层的编译器优化、缓存一致性等问题。所以,Java内存模型,除了定义了一套规范,还提供了一系列原语,封装了底层实现后,供开发者直接使用。 并发编程要解决原子性、有序性和可见性的问题,我们就再来看下,在Java中,分别使用什么方式来保证。 原子性原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。 JMM提供保证了访问基本数据类型的原子性(其实在写一个工作内存变量到主内存是分主要两步:store、write),但是实际业务处理场景往往是需要更大的范围的原子性保证。 在Java中,为了保证原子性,提供了两个高级的字节码指令monitorenter和monitorexit,而这两个字节码,在Java中对应的关键字就是synchronized。 因此,在Java中可以使用synchronized来保证方法和代码块内的操作是原子性的。这里推荐一篇文章深入理解Java并发之synchronized实现原理。 可见性可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。 Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值的这种依赖主内存作为传递媒介的方式来实现的。 Java中的volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用volatile来保证多线程操作时变量的可见性。 除了volatile,Java中的synchronized和final、static三个关键字也可以实现可见性。下面分享一下我的读书笔记: 有序性有序性即程序执行的顺序按照代码的先后顺序执行。 在Java中,可以使用synchronized和volatile来保证多线程之间操作的有序性。实现方式有所区别: volatile关键字会禁止指令重排。synchronized关键字保证同一时刻只允许一条线程操作。 好了,这里简单的介绍完了Java并发编程中解决原子性、可见性以及有序性可以使用的关键字。读者可能发现了,好像synchronized关键字是万能的,他可以同时满足以上三种特性,这其实也是很多人滥用synchronized的原因。 但是synchronized是比较影响性能的,虽然编译器提供了很多锁优化技术,但是也不建议过度使用。 JVM我们都知道,Java代码是要运行在虚拟机上的,而虚拟机在执行Java程序的过程中会把所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途。下面我们来说说JVM运行时内存区域结构 JVM运行时内存区域结构在《Java虚拟机规范(Java SE 8)》中描述了JVM运行时内存区域结构如下: 1.程序计数器 程序计数器(Program Counter Register),也有称作为PC寄存器。想必学过汇编语言的朋友对程序计数器这个概念并不陌生,在汇编语言中,程序计数器是指CPU中的寄存器,它保存的是程序当前执行的指令的地址(也可以说保存下一条指令的所在存储单元的地址),当CPU需要执行指令时,需要从程序计数器中得到当前需要执行的指令所在存储单元的地址,然后根据得到的地址获取到指令,在得到指令之后,程序计数器便自动加1或者根据转移指针得到下一条指令的地址,如此循环,直至执行完所有的指令。 虽然JVM中的程序计数器并不像汇编语言中的程序计数器一样是物理概念上的CPU寄存器,但是JVM中的程序计数器的功能跟汇编语言中的程序计数器的功能在逻辑上是等同的,也就是说是用来指示 执行哪条指令的。 由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,因此,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个线程所私有的。 在JVM规范中规定,如果线程执行的是非native方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则程序计数器中的值是undefined。 由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。 2.Java栈 Java栈也称作虚拟机栈(Java Vitual Machine Stack),也就是我们常常所说的栈,跟C语言的数据段中的栈类似。事实上,Java栈是Java方法执行的内存模型。为什么这么说呢?下面就来解释一下其中的原因。 Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。讲到这里,大家就应该会明白为什么 在 使用 递归方法的时候容易导致栈内存溢出的现象了以及为什么栈区的空间不用程序员去管理了(当然在Java中,程序员基本不用关系到内存分配和释放的事情,因为Java有自己的垃圾回收机制),这部分空间的分配和释放都是由系统自动实施的。对于所有的程序设计语言来说,栈这部分空间对程序员来说是不透明的。下图表示了一个Java栈的模型: 局部变量表,顾名思义,想必不用解释大家应该明白它的作用了吧。就是用来存储方法中的局部变量(包括在方法中声明的非静态变量以及函数形参)。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。 操作数栈,想必学过数据结构中的栈的朋友想必对表达式求值问题不会陌生,栈最典型的一个应用就是用来对表达式求值。想想一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。 指向运行时常量池的引用,因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。 方法返回地址,当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。 由于每个线程正在执行的方法可能不同,因此每个线程都会有一个自己的Java栈,互不干扰。 3.本地方法栈 本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。在JVM规范中,并没有对本地方发展的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。 4.堆 在C语言中,堆这部分空间是唯一一个程序员可以管理的内存区域。程序员可以通过malloc函数和free函数在堆上申请和释放空间。那么在Java中是怎么样的呢? Java中的堆是用来存储对象本身的以及数组(当然,数组引用是存放在Java栈中的)。只不过和C语言中的不同,在Java中,程序员基本不用去关心空间释放的问题,Java的垃圾回收机制会自动进行处理。因此这部分空间也是Java垃圾收集器管理的主要区域。另外,堆是被所有线程共享的,在JVM中只有一个堆。 5.方法区 方法区在JVM中也是一个非常重要的区域,它与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。 在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。 在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。 在JVM规范中,没有强制要求方法区必须实现垃圾回收。很多人习惯将方法区称为“永久代”,是因为HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。不过自从JDK7之后,Hotspot虚拟机便将运行时常量池从永久代移除了。 Java对象模型的内存布局java是一种面向对象的语言,而Java对象在JVM中的存储也是有一定的结构的。而这个关于Java对象自身的存储模型称之为Java对象模型。 HotSpot虚拟机中,设计了一个OOP-Klass Model。OOP(Ordinary Object Pointer)指的是普通对象指针,而Klass用来描述对象实例的具体类型。 每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个instanceKlass,保存在方法区,用来在JVM层表示该Java类。当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,对象在内存中存储的布局可以分为3块区域:对象头(Header)、 实例数据(Instance Data)和对齐填充(Padding)。 对象头:标记字(32位虚拟机4B,64位虚拟机8B) + 类型指针(32位虚拟机4B,64位虚拟机8B)+ [数组长(对于数组对象才需要此部分信息)] 实例数据:存储的是真正有效数据,如各种字段内容,各字段的分配策略为longs/doubles、ints、shorts/chars、bytes/boolean、oops(ordinary object pointers),相同宽度的字段总是被分配到一起,便于之后取数据。父类定义的变量会出现在子类定义的变量的前面。 对齐填充:对于64位虚拟机来说,对象大小必须是8B的整数倍,不够的话需要占位填充 JVM内存垃圾收集器为了理解现有收集器,我们需要先了解一些术语。最基本的垃圾收集涉及识别不再使用的内存并使其可重用。现代收集器在几个阶段进行这一过程,对于这些阶段我们往往有如下描述: 并行- 在JVM运行时,同时存在应用程序线程和垃圾收集器线程。 并行阶段是由多个gc线程执行,即gc工作在它们之间分配。 不涉及GC线程是否需要暂停应用程序线程。 串行- 串行阶段仅在单个gc线程上执行。与之前一样,它也没有说明GC线程是否需要暂停应用程序线程。 STW - STW阶段,应用程序线程被暂停,以便gc执行其工作。 当应用程序因为GC暂停时,这通常是由于Stop The World阶段。 并发 -如果一个阶段是并发的,那么GC线程可以和应用程序线程同时进行。 并发阶段很复杂,因为它们需要在阶段完成之前处理可能使工作无效(译者注:因为是并发进行的,GC线程在完成一阶段的同时,应用线程也在工作产生操作内存,所以需要额外处理)的应用程序线程。 增量 -如果一个阶段是增量的,那么它可以运行一段时间之后由于某些条件提前终止,例如需要执行更高优先级的gc阶段,同时仍然完成生产性工作。 增量阶段与需要完全完成的阶段形成鲜明对比。 Serial收集器Serial收集器是最基本的收集器,这是一个单线程收集器,它仍然是JVM在Client模式下的默认新生代收集器。它有着优于其他收集器的地方:简单而高效(与其他收集器的单线程比较),Serial收集器由于没有线程交互的开销,专心只做垃圾收集自然也获得最高的效率。在用户桌面场景下,分配给JVM的内存不会太多,停顿时间完全可以在几十到一百多毫秒之间,只要收集不频繁,这是完全可以接受的。 ParNew收集器ParNew是Serial的多线程版本,在回收算法、对象分配原则上都是一致的。ParNew收集器是许多运行在Server模式下的默认新生代垃圾收集器,其主要在于除了Serial收集器,目前只有ParNew收集器能够与CMS收集器配合工作。 Parallel Scavenge收集器Parallel Scavenge收集器是一个新生代垃圾收集器,其使用的算法是复制算法,也是并行的多线程收集器。 Parallel Scavenge 收集器更关注可控制的吞吐量,吞吐量等于运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)。直观上,只要最大的垃圾收集停顿时间越小,吞吐量是越高的,但是GC停顿时间的缩短是以牺牲吞吐量和新生代空间作为代价的。比如原来10秒收集一次,每次停顿100毫秒,现在变成5秒收集一次,每次停顿70毫秒。停顿时间下降的同时,吞吐量也下降了。 停顿时间越短就越适合需要与用户交互的程序;而高吞吐量则可以最高效的利用CPU的时间,尽快的完成计算任务,主要适用于后台运算。 Serial Old收集器Serial Old收集器是Serial收集器的老年代版本,也是一个单线程收集器,采用“标记-整理算法”进行回收。其运行过程与Serial收集器一样。 Parallel Old收集器Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法进行垃圾回收。其通常与Parallel Scavenge收集器配合使用,“吞吐量优先”收集器是这个组合的特点,在注重吞吐量和CPU资源敏感的场合,都可以使用这个组合。 CMS 收集器CMS(Concurrent Mark Sweep)收集器是一种以获取最短停顿时间为目标的收集器,CMS收集器采用标记–清除算法,运行在老年代。主要包含以下几个步骤: 初始标记 并发标记 重新标记 并发清除 其中初始标记和重新标记仍然需要“Stop the world”。初始标记仅仅标记GC Root能直接关联的对象,并发标记就是进行GC Root Tracing过程,而重新标记则是为了修正并发标记期间,因用户程序继续运行而导致标记变动的那部分对象的标记记录。 由于整个过程中最耗时的并发标记和并发清除,收集线程和用户线程一起工作,所以总体上来说,CMS收集器回收过程是与用户线程并发执行的。虽然CMS优点是并发收集、低停顿,很大程度上已经是一个不错的垃圾收集器,但是还是有三个显著的缺点: CMS收集器对CPU资源很敏感。在并发阶段,虽然它不会导致用户线程停顿,但是会因为占用一部分线程(CPU资源)而导致应用程序变慢。 CMS收集器不能处理浮动垃圾。所谓的“浮动垃圾”,就是在并发标记阶段,由于用户程序在运行,那么自然就会有新的垃圾产生,这部分垃圾被标记过后,CMS无法在当次集中处理它们,只好在下一次GC的时候处理,这部分未处理的垃圾就称为“浮动垃圾”。也是由于在垃圾收集阶段程序还需要运行,即还需要预留足够的内存空间供用户使用,因此CMS收集器不能像其他收集器那样等到老年代几乎填满才进行收集,需要预留一部分空间提供并发收集时程序运作使用。要是CMS预留的内存空间不能满足程序的要求,这是JVM就会启动预备方案:临时启动Serial Old收集器来收集老年代,这样停顿的时间就会很长。 由于CMS使用标记–清除算法,所以在收集之后会产生大量内存碎片。当内存碎片过多时,将会给分配大对象带来困难,这是就会进行Full GC。 G1收集器G1收集器与CMS相比有很大的改进: · G1收集器采用标记–整理算法实现。 · 可以非常精确地控制停顿。 G1收集器可以实现在基本不牺牲吞吐量的情况下完成低停顿的内存回收,这是由于它极力的避免全区域的回收,G1收集器将Java堆(包括新生代和老年代)划分为多个区域(Region),并在后台维护一个优先列表,每次根据允许的时间,优先回收垃圾最多的区域 。 ZGC收集器Java 11 新加入的ZGC垃圾收集器号称可以达到10ms 以下的 GC 停顿,ZGC给Hotspot Garbage Collectors增加了两种新技术:着色指针和读屏障。下面引用国外文章说的内容: 着色指针着色指针是一种将信息存储在指针(或使用Java术语引用)中的技术。因为在64位平台上(ZGC仅支持64位平台),指针可以处理更多的内存,因此可以使用一些位来存储状态。 ZGC将限制最大支持4Tb堆(42-bits),那么会剩下22位可用,它目前使用了4位: finalizable, remap, mark0和mark1。 我们稍后解释它们的用途。 着色指针的一个问题是,当您需要取消着色时,它需要额外的工作(因为需要屏蔽信息位)。 像SPARC这样的平台有内置硬件支持指针屏蔽所以不是问题,而对于x86平台来说,ZGC团队使用了简洁的多重映射技巧。 多重映射要了解多重映射的工作原理,我们需要简要解释虚拟内存和物理内存之间的区别。 物理内存是系统可用的实际内存,通常是安装的DRAM芯片的容量。 虚拟内存是抽象的,这意味着应用程序对(通常是隔离的)物理内存有自己的视图。 操作系统负责维护虚拟内存和物理内存范围之间的映射,它通过使用页表和处理器的内存管理单元(MMU)和转换查找缓冲器(TLB)来实现这一点,后者转换应用程序请求的地址。 多重映射涉及将不同范围的虚拟内存映射到同一物理内存。 由于设计中只有一个remap,mark0和mark1在任何时间点都可以为1,因此可以使用三个映射来完成此操作。 ZGC源代码中有一个很好的图表可以说明这一点。 读屏障读屏障是每当应用程序线程从堆加载引用时运行的代码片段(即访问对象上的非原生字段non-primitive field): 1234567> void printName( Person person ) {> String name = person.name; // 这里触发读屏障> // 因为需要从heap读取引用 > // > System.out.println(name); // 这里没有直接触发读屏障> }> 在上面的代码中,String name = person.name 访问了堆上的person引用,然后将引用加载到本地的name变量。此时触发读屏障。 Systemt.out那行不会直接触发读屏障,因为没有来自堆的引用加载(name是局部变量,因此没有从堆加载引用)。 但是System和out,或者println内部可能会触发其他读屏障。 这与其他GC使用的写屏障形成对比,例如G1。读屏障的工作是检查引用的状态,并在将引用(或者甚至是不同的引用)返回给应用程序之前执行一些工作。 在ZGC中,它通过测试加载的引用来执行此任务,以查看是否设置了某些位。 如果通过了测试,则不执行任何其他工作,如果失败,则在将引用返回给应用程序之前执行某些特定于阶段的任务。 标记现在我们了解了这两种新技术是什么,让我们来看看ZG的GC循环。 GC循环的第一部分是标记。标记包括查找和标记运行中的应用程序可以访问的所有堆对象,换句话说,查找不是垃圾的对象。 ZGC的标记分为三个阶段。 第一阶段是STW,其中GC roots被标记为活对象。 GC roots类似于局部变量,通过它可以访问堆上其他对象。 如果一个对象不能通过遍历从roots开始的对象图来访问,那么应用程序也就无法访问它,则该对象被认为是垃圾。从roots访问的对象集合称为Live集。GC roots标记步骤非常短,因为roots的总数通常比较小。 该阶段完成后,应用程序恢复执行,ZGC开始下一阶段,该阶段同时遍历对象图并标记所有可访问的对象。 在此阶段期间,读屏障针使用掩码测试所有已加载的引用,该掩码确定它们是否已标记或尚未标记,如果尚未标记引用,则将其添加到队列以进行标记。 在遍历完成之后,有一个最终的,时间很短的的Stop The World阶段,这个阶段处理一些边缘情况(我们现在将它忽略),该阶段完成之后标记阶段就完成了。 重定位GC循环的下一个主要部分是重定位。重定位涉及移动活动对象以释放部分堆内存。 为什么要移动对象而不是填补空隙? 有些GC实际是这样做的,但是它导致了一个不幸的后果,即分配内存变得更加昂贵,因为当需要分配内存时,内存分配器需要找到可以放置对象的空闲空间。 相比之下,如果可以释放大块内存,那么分配内存就很简单,只需要将指针递增新对象所需的内存大小即可。 ZGC将堆分成许多页面,在此阶段开始时,它同时选择一组需要重定位活动对象的页面。选择重定位集后,会出现一个Stop The World暂停,其中ZGC重定位该集合中root对象,并将他们的引用映射到新位置。与之前的Stop The World步骤一样,此处涉及的暂停时间仅取决于root的数量以及重定位集的大小与对象的总活动集的比率,这通常相当小。所以不像很多收集器那样,暂停时间随堆增加而增加。 移动root后,下一阶段是并发重定位。 在此阶段,GC线程遍历重定位集并重新定位其包含的页中所有对象。 如果应用程序线程试图在GC重新定位对象之前加载它们,那么应用程序线程也可以重定位该对象,这可以通过读屏障(在从堆加载引用时触发) 这可确保应用程序看到的所有引用都已更新,并且应用程序不可能同时对重定位的对象进行操作。 GC线程最终将对重定位集中的所有对象重定位,然而可能仍有引用指向这些对象的旧位置。 GC可以遍历对象图并重新映射这些引用到新位置,但是这一步代价很高昂。 因此这一步与下一个标记阶段合并在一起。在下一个GC周期的标记阶段遍历对象对象图的时候,如果发现未重映射的引用,则将其重新映射,然后标记为活动状态。 JVM内存优化在《深入理解Java虚拟机》一书中讲了很多jvm优化思路,下面我来简单说说。 java内存抖动堆内存都有一定的大小,能容纳的数据是有限制的,当Java堆的大小太大时,垃圾收集会启动停止堆中不再应用的对象,来释放内存。现在,内存抖动这个术语可用于描述在极短时间内分配给对象的过程。 具体如何优化请谷歌查询~ jvm大页内存什么是内存分页?CPU是通过寻址来访问内存的。32位CPU的寻址宽度是 0~0xFFFFFFFF,即4G,也就是说可支持的物理内存最大是4G。但在实践过程中,程序需要使用4G内存,而可用物理内存小于4G,导致程序不得不降低内存占用。为了解决此类问题,现代CPU引入了MMU(Memory Management Unit,内存管理单元)。 MMU 的核心思想是利用虚拟地址替代物理地址,即CPU寻址时使用虚址,由MMU负责将虚址映射为物理地址。MMU的引入,解决了对物理内存的限制,对程序来说,就像自己在使用4G内存一样。 内存分页(Paging)是在使用MMU的基础上,提出的一种内存管理机制。它将虚拟地址和物理地址按固定大小(4K)分割成页(page)和页帧(page frame),并保证页与页帧的大小相同。这种机制,从数据结构上,保证了访问内存的高效,并使OS能支持非连续性的内存分配。在程序内存不够用时,还可以将不常用的物理内存页转移到其他存储设备上,比如磁盘,这就是虚拟内存。 要知道,虚拟地址与物理地址需要通过映射,才能使CPU正常工作。而映射就需要存储映射表。在现代CPU架构中,映射关系通常被存储在物理内存上一个被称之为页表(page table)的地方。 页表是被存储在内存中的,CPU通过总线访问内存,肯定慢于直接访问寄存器的。为了进一步优化性能,现代CPU架构引入了TLB(Translation lookaside buffer,页表寄存器缓冲),用来缓存一部分经常访问的页表内容 。 为什么要支持大内存分页?TLB是有限的,这点毫无疑问。当超出TLB的存储极限时,就会发生 TLB miss,于是OS就会命令CPU去访问内存上的页表。如果频繁的出现TLB miss,程序的性能会下降地很快。 为了让TLB可以存储更多的页地址映射关系,我们的做法是调大内存分页大小。 如果一个页4M,对比一个页4K,前者可以让TLB多存储1000个页地址映射关系,性能的提升是比较可观的。 开启JVM大页内存JVM启用时加参数 -XX:LargePageSizeInBytes=10m 如果JDK是在1.5 update5以前的,还需要加 -XX:+UseLargePages,作用是启用大内存页支持。 通过软引用和弱引用提升JVM内存使用性能强软弱虚 强引用: 只要引用存在,垃圾回收器永远不会回收 Object obj = new Object(); //可直接通过obj取得对应的对象 如obj.equels(new Object()); 而这样 obj对象对后面new Object的一个强引用,只有当obj这个引用被释放之后,对象才会被释放掉,这也是我们经常所用到的编码形式。 软引用(可以实现缓存): 非必须引用,内存溢出之前进行回收,可以通过以下代码实现1234567Object obj = new Object();SoftReference<Object> sf = new SoftReference<Object>(obj);obj = null;sf.get();//有时候会返回null 这时候sf是对obj的一个软引用,通过sf.get()方法可以取到这个对象,当然,当这个对象被标记为需要回收的对象时,则返回null;软引用主要用户实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。 弱引用(用来在回调函数中防止内存泄露): 第二次垃圾回收时回收,可以通过如下代码实现123456789Object obj = new Object();WeakReference<Object> wf = new WeakReference<Object>(obj);obj = null;wf.get();//有时候会返回nullwf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾 弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器标记。 虚引用: 垃圾回收时回收,无法通过引用取到对象值,可以通过如下代码实现 12345Object obj = new Object();PhantomReference<Object> pf = new PhantomReference<Object>(obj);obj=null;pf.get();//永远返回nullpf.isEnQueued();//返回是否从内存中已经删除 虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。虚引用主要用于检测对象是否已经从内存中删除。 优化简单来说,可以使用软引用还引用数量巨大的对象,详情请参考http://www.cnblogs.com/JavaArchitect/p/8685993.html 总结此篇文章总共1.5W字,我从计算机物理内存体系讲到了java内存模型,在通过java内存模型引出了JVM内存的相关知识点。觉得写的好的请给个赞。本篇文章我会率先发布在我的个人博客,随后会在掘金等平台相继发出。最后,非常感谢你的阅读~ 参考资料文中的各种超链接 《深入理解Java虚拟机》 《Java并发编程的艺术》 《架构解密从分布式到微服务》 SMP、NUMA、MPP体系结构介绍 ZGC原理(请用正确的姿势魔法上网观看) Stefan Karlsson和PerLiden Jfokus的演讲(请用正确的姿势魔法上网) 声明【版权申明】此片为原创内容,使用 CC BY-NC-SA 3.0授权条款,请遵守对应的义务,即被授权人有义务在所有副本中都必须包含版权声明。谢谢合作~ 想要解锁更多新姿势?请访问我的个人博客https://tengshe789.github.io/(😘]]></content>
<tags>
<tag>java</tag>
<tag>技术</tag>
<tag>并发</tag>
</tags>
</entry>
<entry>
<title><![CDATA[记一次不经思考的消费(AppStore退款)]]></title>
<url>%2F2018%2F09%2F03%2F%E8%AE%B0%E4%B8%80%E6%AC%A1%E4%B8%8D%E7%BB%8F%E6%80%9D%E8%80%83%E7%9A%84%E6%B6%88%E8%B4%B9-AppStore%E9%80%80%E6%AC%BE%2F</url>
<content type="text"><![CDATA[昨天晚上,我做了一个惊天地泣鬼神的事情 帮媳妇改完论文,突然抖了一个机灵“每天都用电脑,我难道永远的脱离使用纸和笔么?” 我还是个孩子! 说罢,打开AppStore,就是一堆翻。没错,上个月我入了一个apple pencil,几乎没怎么用。我现在的所有思维都是给我的apple pencil找老婆 一眼就看到了这个app,支持手写笔,自动谷歌同步,可以OCR,那个眼馋啊 纠结了一阵, 👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇 👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇 👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇 👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇 👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇 👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇 然后我就买了。。 打开app以后, 这啥啊,长得和OneNote一毛一样,竟然收50大洋 退退退! 果断打开果子官方网找退款通道 🤨🤨🤨🤨🤨🤨🤨🤨🤨🤨🤨🤨🤨🤨🤨🤨🤨🤨 一小时过去了 😵😵😵😵😵😵😵😵😵😵😵😵😵😵😵😵😵😵 夜还是辣么黑,我的钱包还是辣么瘪 法克Q🙂果子 我心里咆哮着 没办法,百度吧,以下是正文 退款要知道,在苹果眼中,退款是分为两种的,超过90天和没有超过90天。要阅读本片文章,你要知道,用了超过90天的app是非常难退款的。下面我来介绍苹果家没有超过90天退款app的方法 第一步打开 https://reportaproblem.apple.com/ ,并登陆你的果子账号 ※第二步※ 寻找你买过的爱啪啪,点击报告问题,选择需要报告的问题。 这其中有“我没有授权此项购买”、“并不像购买此项目”、“原本打算购买另一个项目”、“未下载项目或者无法找到项目”、“项目无法安装或下载太慢”、“项目可打开,但是未起到预期的作用”、“没有列出我的问题”等多个选项各选。 由这些选项我们可以看出,苹果对退款原因的接受度还是挺广的,这些选项几乎包含了所有的原因,尤其是“并不想购买此项目”显得格外任性。 下面给大家一个问题描述的范文: I have downloaded a XXXX(要退费程序的名称)program for iPhone (或 iPad) from the App Store, but this item dose not meet my expectation. Would you please help me refund my payment ? Thank you very much. 第三步慢慢等邮件么,我是晚上12点退的,第二天醒来就收到退款和退款邮件了~ 如何理性消费 理性! 不可能的!这辈子都无法理性!]]></content>
<tags>
<tag>窍门</tag>
<tag>生活</tag>
</tags>
</entry>
<entry>
<title><![CDATA[如何用Java与python代码解释IO模型]]></title>
<url>%2F2018%2F08%2F25%2FIO%E6%A8%A1%E5%9E%8B%2F</url>
<content type="text"><![CDATA[最近爷爷生病,我一直在陪床。陪床空隙,偶而研究一会资料。前天刚好看了点《UNIX网络编程》,比较头大。现在我来整理一下所学所得,并用于个人备忘。如果有不对,请批评。 想要解锁更多新姿势?请访问https://tengshe789.github.io/ IO模型介绍IO模型是什么?很多书籍或者百度百度百科,都没有给出明确的解释,我也不敢乱下定义。以我愚见,IO模型,是通过根据前人主观意识的思考而构成客观阐述IO复杂操作逻辑的物件。 要知道,应用程序使用系统资源的一个过程,进程无法直接操作IO设备的,因为用户进程不能直接访问磁盘,所以要通过内核的系统调用读取,这个内核读取的过程就是用户进程等待的过程,等待内核读取后将数据从内核内存复制到进程内存。因此操作系统设立一个IO模型进行规范,就非常有必要了。 为了更好地了解IO模型,我们需要事先回顾下:同步、异步、阻塞、非阻塞 同步与异步:描述的是用户线程与内核的交互方式,同步指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;而异步是指用户线程发起IO请求后仍然继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数。 阻塞与非阻塞:描述是用户线程调用内核IO操作的方式,阻塞是指IO操作需要彻底完成后才返回到用户空间;而非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成。 IO模型一共有5类: blocking-IO BIO(阻塞IO) non-blocking IO NIO(非阻塞IO) IO multiplexing IO多路复用 signal driven IO 信号驱动IO asynchronous IO AIO(异步IO) 由于signal driven IO(信号驱动IO)在实际中并不常用,所以主要介绍其余四种IO Model。 BIO(blocking io)先来看看读操作流程 从图中可以看出,用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。 对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。 而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。 也就是说,blocking IO的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block了。 JAVA 阻塞 demo下面的例子主要使用Socket通道进行编程。服务端如下: 1234567891011121314151617181920212223242526272829303132/** * @program: socketTest * @description: one thread demo for bio version * @author: tEngSHe789 * @create: 2018-08-26 21:17 **/public class Server { public static void main(String[] args) { try { ServerSocket serverSocket=new ServerSocket(8888); System.out.println("服务端Start...."); //等待客户端就绪 -> 堵塞 while (true){ Socket socket = serverSocket.accept(); System.out.println("发现客户端连接"); InputStream is=socket.getInputStream(); byte[] b =new byte[1024]; //等待客户端发送请求 -> 堵塞 while (true) { int data = is.read(b); String info=null; if (data!=-1){ info=new String(b,0,data,"GBK"); } System.out.println(info); } } } catch (IOException e) { } }} 客户端 123456789101112131415161718/** * @program: socketTest * @description: one thread demo for bio version * @author: tEngSHe789 **/public class Client { public static void main(String[] args) { try { Socket socket=new Socket("127.0.0.1",8888); OutputStream os = socket.getOutputStream(); System.out.println("正在发送数据"); os.write("这是来自客户端的信息".getBytes()); os.flush(); } catch (IOException e) { e.printStackTrace(); } }} PY 阻塞 demo服务端 12345678910111213import sockets = socket.socket()s.bind(('127.0.0.1',8888))print('服务端启动....')# 等待客户端就绪 -> 堵塞s.listen()# 等待客户端发送请求 -> 堵塞conn,addr = s.accept()msg = conn.recv(1024).decode('utf-8')print(msg)conn.close()s.close() 客户端 1234567import sockets = socket.socket()s.connect(('127.0.0.1',8888))print('客户端已启动....')s.send('正在发送数据'.encode('utf-8'))s.close() NIO(non blocking io)NIO就不一样了,recvform系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error。进程在返回之后,可以干点别的事情,然后再发起recvform系统调用。重复上面的过程,循环往复的进行recvform系统调用。这个过程通常被称之为轮询。 轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。 JAVA 与NIOJava NIO(New IO)是一个可以替代标准Java IO API的IO API(从Java 1.4开始),Java NIO提供了与标准IO不同的IO工作方式。 在java中,标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。 我们先看看Buffer类 Buffer类Java NIO中的Buffer主要用于与NIO通道进行交互,数据是从通道读入到缓冲区,从缓冲区写入通道中的。概念上,缓冲区可以看成包在一个对象内的数组,下面看一个图 这是一个新创建的容量为10的ByteBuffer逻辑图,他有四个属性来提供关于其包含的数据元素信息,分别是: 1)容量(capacity):表示Buffer最大数据容量,缓冲区容量不能为负,并且建立后不能修改。 2)限制(limit):也叫上界。第一个不应该读取或者写入的数据的索引,即位于limit后的数据不可以读写。缓冲区的限制不能为负,并且不能大于其容量(capacity)。 3)位置(position):下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制(limit)。 4)标记(mark)与重置(reset):标记是一个索引,通过Buffer中的mark()方法指定Buffer中一个特定的position,之后可以通过调用reset()方法恢复到这个position。 从这幅图可以看到,他的容量(capacity)和限制(limit)设置为10,位置设置为0,每个缓冲区容量是固定的,标记是未定义的,其他三个属性可以通过使用缓冲区解决。 缓冲区存储数据支持的数据类型支持七种数据类型,他们是:1.byteBuffer2.charBuffer3.shortBuffer4.IntBuffer5.LongBuffer6.FloatBuffer7.DubooBuffer 基本用法使用Buffer读写数据一般遵循以下四个步骤: (1) 写入数据到Buffer,一般有可以从Channel读取到缓冲区中,也可以调用put方法写入。 (2) 调用flip()方法,切换数据模式。 (3) 从Buffer中读取数据,一般从缓冲区读取数据写入到通道中,也可以调用get方法读取。 (4) 调用clear()方法或者compact()方法。 缓冲区API首先,用allocate 指定缓冲区大小1024 1ByteBuffer byteBuffer=ByteBuffer.allocate(1024); 存储或填充我们可以用put 存入数据到缓冲区 1byteBuffer.put("tengshe789".getBytes()); 当调用put时,会指出下一个元素应当被插入的位置,位置(position)指向的是下一个元素。如果指向的位置超过限制(limit),则抛出BufferOverFlowException异常。 翻转Flip将一个能够继续添加数据元素的填充状态的缓冲区翻转成一个准备读出元素的释放状态 1byteBuffer.flip(); 具体有什么用呢? 对于已经写满了缓冲区,如果将缓冲区内容传递给一个通道,以使内容能被全部写出。 但如果通道现在在缓冲区上执行get,那么它将从我们刚刚插入的有用数据之外取出未定义数据。通过翻转将位置值重新设为 0,通道就会从正确位置开始获取。 例如我们定义了一个容量是10的buffer,并填入hello,如下图所示 翻转后如下图所示 重读Rewind与 flip相似,但不影响上界属性。它只是将位置值设回 0。可以使用 rewind()后退,重读已经被翻转的缓冲区中的数据。 1byteBuffer.rewind(); 获取翻转完了,就可以用get获取缓冲区数据了 12byte[] b= new byte[byteBuffer.limit()];byteBuffer.get(b); 当调用get时,会指出下一个元素应当被索引的位置,位置(position)返回时会+1s。如果指向的位置超过限制(limit),则抛出BufferUnderFlowException异常。如果提供的索引超过范围,也会抛出IndexOutOfBoundsException异常 释放remaining可以告诉你从当前位置(position)到限制(limit)还剩的元素数目 1int count = byteBuffer.remaining(); clear将缓冲区重置为空状态 1byteBuffer.clear(); 压缩如果我们只想从缓冲区中释放一部分数据,而不是全部,然后重新填充。为了实现这一点,未读的数据元素需要下移以使第一个元素索引为 0。尽管重复这样做会效率低下,但这有时非常必要,而 API 对此为您提供了一个 compact()函数。 1byteBuffer.compact(); 标记与重置标记是一个索引,通过Buffer中的mark()方法指定Buffer中一个特定的position,之后可以通过调用reset()方法恢复到这个position。要知道缓冲区的标记在mark()函数被调用前时未定义的,如果标记未定义,调用reset()会导致InvalidMarkException异常 1byteBuffer.position(2).mark().position(4).reset(); 要注意,java.nio中的类特意被设计为支持级联调用,优雅的使用级联调用,可以产生优美易读的代码。 直接缓冲区与非直接缓冲区非直接缓冲区上面我们说了ByteBuffer,也就是缓冲区的用法,譬如用allocate() 方法指定缓冲区大小,然后进行填充或翻转操作等等等。我们所创建的缓冲区,都属于直接缓冲区。他们都是在JVM中内存中创建,在每次调用基础操作系统的一个本机IO之前或者之后,虚拟机都会将缓冲区的内容复制到中间缓冲区(或者从中间缓冲区复制内容),缓冲区的内容驻留在JVM内,因此销毁容易,但是占用JVM内存开销,处理过程中有复制操作。 非直接缓冲区写入步骤: 1.创建一个临时的直接ByteBuffer对象。2.将非直接缓冲区的内容复制到临时缓冲中。3.使用临时缓冲区执行低层次I/O操作。4.临时缓冲区对象离开作用域,并最终成为被回收的无用数据。 1234567891011121314151617181920212223242526/** * @program: UndirectBuffer * @description: 利用通道完成文件的复制(非直接缓冲区) * @author: tEngSHe789 **/public class UndirectBuffer { public static void main(String[] args) throws IOException { // 创建流 FileInputStream fis = new FileInputStream("d://blog.md"); FileOutputStream fos = new FileOutputStream("d://blog.md"); //获取管道 FileChannel in = fis.getChannel(); FileChannel out = fos.getChannel(); // 分配指定大小的缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); while (in.read(buffer) !=-1){ buffer.flip();// 准备读数据了 out.write(buffer); buffer.clear(); } out.close(); in.close(); fis.close(); fos.close(); }} 直接缓冲区直接缓冲区,是通过 allocateDirect() 方法在JVM内存外开辟内存,在每次调用基础操作系统的一个本机IO之前或者之后,虚拟机都会避免将缓冲区的内容复制到中间缓冲区(或者从中间缓冲区复制内容),缓冲区的内容驻留在物理内存内,会少一次复制过程,如果需要循环使用缓冲区,用直接缓冲区可以很大地提高性能。 虽然直接缓冲区使JVM可以进行高效的I/O操作,但它使用的内存是操作系统分配的,绕过了JVM堆栈,建立和销毁比堆栈上的缓冲区要更大的开销。 12345678910111213141516171819202122/** * @program: DirectBuffer * @description: 使用直接缓冲区完成文件的复制(内存映射文件) * @author: tEngSHe789 **/public class DirectBuffer { public static void main(String[] args) throws IOException { //创建管道 FileChannel in=FileChannel.open(Paths.get("d://blog.md"),StandardOpenOption.READ); FileChannel out=FileChannel.open(Paths.get("d://blog.md"),StandardOpenOption.WRITE ,StandardOpenOption.READ,StandardOpenOption.CREATE); // 拿到将管道内容映射到内存的直接缓冲区映射文件(一个位置在硬盘的基于内存的缓冲区) MappedByteBuffer inMappedByteBuffer = in.map(FileChannel.MapMode.READ_ONLY, 0, in.size()); MappedByteBuffer outMappedByteBuffer = out.map(FileChannel.MapMode.READ_WRITE, 0, in.size()); // 对直接缓冲区进行数据读写操作 byte[] bytes=new byte[inMappedByteBuffer.limit()]; inMappedByteBuffer.get(bytes); outMappedByteBuffer.put(bytes); in.close(); out.close(); }} 直接缓冲区与非直接缓冲区的区别 字节缓冲区要么是直接的,要么是非直接的。如果为直接字节缓冲区,则 Java 虚拟机会尽最大努力直接在此缓冲区上执行本机 I/O 操作。也就是说,在每次调用基础操作系统的一个本机 I/O 操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。 直接字节缓冲区可以通过调用此类的 allocateDirect() 工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。 直接字节缓冲区还可以通过 FileChannel 的 map() 方法 将文件区域直接映射到内存中来创建。该方法返回MappedByteBuffer 。 Java 平台的实现有助于通过 JNI 从本机代码创建直接字节缓冲区。如果以上这些缓冲区中的某个缓冲区实例指的是不可访问的内存区域,则试图访问该区域不会更改该缓冲区的内容,并且将会在访问期间或稍后的某个时间导致抛出不确定的异常。 字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其 isDirect() 方法来确定。提供此方法是为了能够在性能关键型代码中执行显式缓冲区管理。 Channel通道是java.nio的第二个创新,表示提供 IO 设备(例如:文件、套接字)的直接连接。 若需要使用 NIO 系统,需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。这其中,Channel负责传输, Buffer 负责存储。 通道是由java.nio.channels 包定义的,Channel 表示 IO 源与目标打开的连接。Channel 类似于传统的“流”。只不过 Channel本身不能直接访问数据, Channel 只能与Buffer 进行交互。 接口java.nio.channels.Channel 接口: FileChannel SocketChannel ServerSocketChannel DatagramChannel 与缓冲区不同,通道API主要由接口指定,不同操作系统上通道的实现会不一样 实现直接缓冲区与非直接缓冲区的栗子 分散读取与聚集写入通道可以有选择地实现两个新的接口: ScatteringByteChannel 和 GatheringByteChannel。 ScatteringByteChannel 有2个read方法,我们都叫她分散读取(scattering Reads),分散读取中,通道依次填充每个缓冲区。填满一个缓冲区后,它就开始填充下一个。在某种意义上,缓冲区数组就像一个大缓冲区。 GatheringByteChannel中有2个wirte方法,我们都叫她聚集写入(gathering Writes),他可以将多个缓冲区的数据聚集到通道中 分散读取与聚集写入的应用分散读取/聚集写入对于将数据划分为几个部分很有用。例如,您可能在编写一个使用消息对象的网络应用程序,每一个消息被划分为固定长度的头部和固定长度的正文。您可以创建一个刚好可以容纳头部的缓冲区和另一个刚好可以容难正文的缓冲区。当您将它们放入一个数组中并使用分散读取来向它们读入消息时,头部和正文将整齐地划分到这两个缓冲区中。 我们从缓冲区所得到的方便性对于缓冲区数组同样有效。因为每一个缓冲区都跟踪自己还可以接受多少数据,所以分散读取会自动找到有空间接受数据的第一个缓冲区。在这个缓冲区填满后,它就会移动到下一个缓冲区。 Python与NIO服务端(具体见注释) 1234567891011121314151617181920212223242526272829303132333435from socket import *import times=socket(AF_INET,SOCK_STREAM)s.bind(('127.0.0.1',8888))s.listen(5)s.setblocking(False) #设置socket的接口为非阻塞conn_l=[] # 存储和server的连接 的 连接del_l=[] # 存储和和server的断开 的 连接while True: try: # 这个过程是不阻塞的 conn,addr=s.accept() # 当没人连接的时候会报错,走exception(<- py中是except) conn_l.append(conn) except BlockingIOError: print(conn_l) for conn in conn_l: try: data=conn.recv(1024) if not data: del_l.append(conn) # 这个过程是不阻塞的 data=conn.recv(1024) # 不阻塞 if not data: # 如果拿不到data del_l.append(conn) # 在废弃列表中添加conn continue conn.send(data.upper()) except BlockingIOError: pass except ConnectionResetError: del_l.append(conn) for conn in del_l: conn_l.remove(conn) conn.close() del_l=[] 客户端 12345678910from socket import *c=socket(AF_INET,SOCK_STREAM)c.connect(('127.0.0.1',8888))while True: msg=input('>>: ') if not msg:continue c.send(msg.encode('utf-8')) data=c.recv(1024) print(data.decode('utf-8')) IO复用(IO multiplexing)I/O多路复用实际上就是用select, poll, epoll监听多个io对象,当io对象有变化(有数据)的时候就通知用户进程。有些地方也称这种IO方式为事件驱动IO(event driven IO)。与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。当然具体的可以看看这篇博客,现在先来看下I/O多路复用的流程: (1)当用户进程调用了select,那么整个进程会被block; (2)而同时,kernel会“监视”所有select负责的socket; (3)当任何一个socket中的数据准备好了,select就会返回; (4)这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。 这个图和BIO的图其实并没有太大的不同,事实上还更差一些。因为这里需要使用两个系统调用(select和recvfrom),而BIO只调用了一个系统调用(recvfrom)。但是,用select的优势在于它可以同时处理多个connection。 JAVA实现IO复用这里我们使用的是java.nio下模块来完成I/O多路复用的例子。我用到的Selector(选择器),是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。 Selector的使用Selector的创建1Selector selector = Selector.open(); 向Selector注册通道为了将Channel和Selector配合使用,必须将channel注册到selector上。通过SelectableChannel.register()方法来实现,如下: 12channel.configureBlocking(false);SelectionKey key = channel.register(selector,Selectionkey.OP_READ); register()方法的第二个参数是一个“interest集合”,意思是在通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件:Connect、Accept、Read、Write 通道触发了一个事件意思是该事件已经就绪。所以,某个channel成功连接到另一个服务器称为“连接就绪”。一个server socket channel准备好接收新进入的连接称为“接收就绪”。一个有数据可读的通道可以说是“读就绪”。等待写数据的通道可以说是“写就绪”。 这四种事件用SelectionKey的四个常量来表示: SelectionKey.OP_CONNECT可连接 SelectionKey.OP_ACCEPT可接受连接 SelectionKey.OP_READ可读 SelectionKey.OP_WRITE可写 SelectionKey当向Selector注册Channel时,register()方法会返回一个SelectionKey对象。它包含了: interest集合 ready集合 Channel Selector 附加的对象(可选) interest集合interest集合是你所选择的感兴趣的事件集合。可以通过SelectionKey读写interest集合,像这样: 123456int interestSet = selectionKey.interestOps();boolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE; 可以看到,用“位与”操作interest 集合和给定的SelectionKey常量,可以确定某个确定的事件是否在interest 集合中。 ready集合ready 集合是通道已经准备就绪的操作的集合。在一次选择(Selection)之后,你会首先访问这个ready set。Selection将在下一小节进行解释。可以这样访问ready集合: 1int readySet = selectionKey.readyOps(); 可以用像检测interest集合那样的方法,来检测channel中什么事件或操作已经就绪。但是,也可以使用以下四个方法,它们都会返回一个布尔类型: 1234selectionKey.isAcceptable();selectionKey.isConnectable();selectionKey.isReadable();selectionKey.isWritable(); 从SelectionKey访问Channel和Selector12Channel channel = selectionKey.channel();Selector selector = selectionKey.selector(); java代码12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849/** * @program: NIOServer * @description: 服务端 * @author: tEngSHe789 **/public class NIOServer { public static void main(String[] args) throws IOException { System.out.println("服务端Start...."); // 创建通道 ServerSocketChannel serverSocketChannel=ServerSocketChannel.open(); // 设置非阻塞 serverSocketChannel.configureBlocking(false); // 绑定连接 serverSocketChannel.bind(new InetSocketAddress(8888)); // 获取选择器 Selector selector=Selector.open(); // 将通道注册到选择器 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); // 轮调式获取选择“已经准备就绪”的事件 while (selector.select() > 0){ // 获取当前选择器的左右已经准备就绪的监听事件(选择key) Iterator<SelectionKey> iterator=selector.selectedKeys().iterator(); while (iterator.hasNext()){ // 获取准备就绪事件 SelectionKey selectionKey=iterator.next(); // 判断具体是什么事件 if (selectionKey.isAcceptable()){//如果是“接受就绪” SocketChannel socketChannel=serverSocketChannel.accept();// 获取连接 socketChannel.configureBlocking(false); // 设置非阻塞 //将该通道注册到服务器上 socketChannel.register(selector, SelectionKey.OP_READ); }else if (selectionKey.isReadable()){//如是“已经就绪” SocketChannel socketChannel= (SocketChannel) selectionKey.channel();//获取连接 //读数据 ByteBuffer buffer=ByteBuffer.allocate(1024); int len = 0; //分散读取 len=socketChannel.read(buffer); while (len > 0){ buffer.flip(); System.out.println(new String(buffer.array(),0,len)); buffer.clear(); } } iterator.remove(); } } }} 客户端:1234567891011121314151617181920212223242526272829/** * @program: NIOClient * @description: 客户端 * @author: tEngSHe789 **/public class NIOClient { public static void main(String[] args) throws IOException { System.out.println("客户端Start...."); // 创建通道 SocketChannel socketChannel=SocketChannel.open(new InetSocketAddress("127.0.0.1",8888)); // 设置SocketChannel接口为非阻塞 socketChannel.configureBlocking(false); //指定缓冲区大小 ByteBuffer buffer=ByteBuffer.allocate(1024); Scanner scanner=new Scanner(System.in); while (scanner.hasNext()){ String msg = scanner.next(); // 存储 buffer.put((new Date().toString()+"\n"+msg).getBytes()); // 翻转 buffer.flip(); // 聚集写入 socketChannel.write(buffer); // 释放 buffer.clear(); } socketChannel.close(); }} python实现IO复用对比java用的是Selector,可以帮我们在默认操作系统下选择最合适的select, poll, epoll这三种多路复合模型,python是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。 服务端12345678910111213141516171819202122232425262728293031from socket import *import selects=socket(AF_INET,SOCK_STREAM)s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)s.bind(('127.0.0.1',8888))s.listen(5)s.setblocking(False) #设置socket的接口为非阻塞read_l=[s,] # 数据可读通道的列表while True: # 监听的read_l中的socket对象内部如果有变化,那么这个对象就会在r_l # 第二个参数里有什么对象,w_l中就有什么对象 # 第三个参数 如果这里的对象内部出错,那会把这些对象加到x_l中 # 1 是超时时间 r_l,w_l,x_l=select.select(read_l,[],[],1) print(r_l) for ready_obj in r_l: if ready_obj == s: conn,addr=ready_obj.accept() #此时的ready_obj等于s read_l.append(conn) else: try: data=ready_obj.recv(1024) #此时的ready_obj等于conn if not data: ready_obj.close() read_l.remove(ready_obj) raise Exception('连接断开') ready_obj.send(data.upper()) except ConnectionResetError: ready_obj.close() read_l.remove(ready_obj) 客户端12345678910from socket import *c=socket(AF_INET,SOCK_STREAM)c.connect(('127.0.0.1',8888))while True: msg=input('>>>: ') if not msg:continue c.send(msg.encode('utf-8')) data=c.recv(1024) print(data.decode('utf-8')) AIO(asynchronous io)真正的异步I/O很牛逼,流程大概如下: (1)用户进程发起read操作之后,立刻就可以开始去做其它的事。 (2)而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。 (3)然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。 JavaJava中使用AIO需要用到java.nio.channels.AsynchronousChannelGroup和java.nio.channels.AsynchronousServerSocketChannel的包,由于实际项目鲜有人用,就不演示了 总结 回顾一下各个IO Model的比较,如图所示: blocking io :阻塞型io,再熟悉不过,处理accept、read、write都会阻塞用户进程 non blocking io:当通过系统调用的时候,如果没有连接或者数据到达就直接返回一个错误,用户进程不阻塞但是不断的轮询。注意这个不是java nio框架中对应的网络模型 io multiplexing:io多路复用才是nio对应的网络io模型。该模型对于用户进程也是阻塞的,优点是可以同时支持多个connetciotn。前三种都属于同步模式,既然都是同步的,如果要做到看似非阻塞,那么就需要轮询机制。相对于上一种模型,这种只是将轮询从用户进程转移到了操作系统内核,通过调用select函数,不断轮询多个connection是否ready,如果有一种ready好的,就通过事件通知用户进程,用户进程再通过事件来处理。所以在java的nio中会看到一大堆事件处理。这种模型的阻塞不是在socket层面的阻塞,而是在调动select函数的阻塞。而且相对于blocking io,还多了一次select的系统调用,其实性能会更低,所以在低吞吐量下,这种io不见得比bio+线程池的模型优越。 sign driven:极少使用,不知道 async io :java7时候开始升级,也成为nio2。实现了异步的io。前三种都是通过用户进程在主动获取(bio的阻塞,nbio的轮询和iomult的按事件获取),而aio交互很简单,用户进程调用后立即返回,用户进程不阻塞,内核当完成网络io和数据复制后,主动通知用户进程。前面说到的系统内核做的操作,除了等待网络io就绪数据到达内核,还有从系统内核复制用户空间去的过程,异步io这两者对于用户进程而言都是非阻塞的,而前三种,在数据从内核复制到用户空间这个过程,都是阻塞的。 参考资料前言说的那本书 Ron Hitchens于2002年 著的《java nio》 findumars 冬瓜蔡 彼岸船夫 NIO的/分散读取和聚集写入 并发编程网 感谢 续1s时间全片结束,觉得我写的不错?想要了解更多精彩新姿势?赶快打开我的👉个人博客 👈吧! 本文地址https://tengshe789.github.io/2018/08/25/IO%E6%A8%A1%E5%9E%8B/#more,部分觉得比较用心的会同步到掘金,简书,谢谢你那么可爱,还一直关注着我~❤😝]]></content>
<tags>
<tag>java</tag>
<tag>技术</tag>
<tag>python</tag>
<tag>操作系统</tag>
</tags>
</entry>
<entry>
<title><![CDATA[解释性语言&编译性语言,动态&静态语言?]]></title>
<url>%2F2018%2F08%2F21%2F%E8%A7%A3%E9%87%8A%E6%80%A7%E8%AF%AD%E8%A8%80-%E7%BC%96%E8%AF%91%E6%80%A7%E8%AF%AD%E8%A8%80%EF%BC%8C%E5%8A%A8%E6%80%81-%E9%9D%99%E6%80%81%E8%AF%AD%E8%A8%80%2F</url>
<content type="text"><![CDATA[你是不是每一次写完java,写完python,都有许些疑惑~ 为什么每次新建一个python项目,JetBrains PyCharm 都会让你选一个解释器,什么是解释器呢? 为什么编写Java不需要解释器呢?今天就给你们带来答案~ 想要解锁更多新姿势?请访问https://tengshe789.github.io/ 编译型和解释型的区别编译型语言先来看看编译型语言定义: 编译型语言首先是将源代码编译生成机器指令,再由机器运行机器码(二进制)。 可以这么理解,编译型相当于用中英文词典(翻译器)将一本英文书一次性翻译(编译)成一本中文书。以后查看直接就是中文了。可想而知,以后读书(执行)会非常非常方便。 所以,由于程序执行速度快,同等条件下对系统要求较低,因此像开发操作系统、大型应用程序、数据库系统等时都采用它,像C/C++、Pascal/Object Pascal(Delphi)等都是编译语言 用c语言开发了程序后,需要通过编译器把程序编译成机器语言(即计算机识别的二进制文件,因为不同的操作系统计算机识别的二进制文件是不同的),所以c语言程序进行移植后,要重新编译。(如windows编译成ext文件,linux编译成erp文件) 解释型语言再来看看解释型语言的定义: 解释型语言的源代码不是直接翻译成机器指令,而是先翻译成中间代码,再由解释器对中间代码进行解释运行。 而解释型相当于用中英文词典(翻译器)将一本英文书读一段翻译一段(解释)中文。以后查看时还是需要重新翻译。这样效率会低一些,必须依赖解释器,但是跨平台性好。 所以,一些网页脚本、服务器脚本及辅助开发接口这样的对速度要求不高、对不同系统平台间的兼容性有一定要求的程序则通常使用解释性语言,如JavaScript、VBScript、Perl、Python、Ruby、MATLAB 等等 Java编译型和解释型的定义是对立存在的,但也可以在一个语言中同时存在。 为什么我把 java 语言单独放出来呢?因为它同时兼有编译型和解释型特点。 整个流程如下: 我们编好源代码(.java 文件),编译生成字节码(.class 文件),再通过 JVM(java 虚拟机)运行生成机器指令,由机器运行机器码。注意,此处生成机器语言前的操作是解释型,每次运行都要重新解释。因此,此处表明 java 是解释型。 但是,部分 JVM(java 虚拟机)有一种 JIT(Just in time)机制,能够将部分已经解释翻译的常用机器指令保存。下次不需要解释,直接运行即可。此时 java 是编译型。 动态语言和静态语言我们常说的动态语言和静态语言,其实是指动态类型语言和静态类型语言。先来看看动态类型语言: 动态类型语言,是指数据类型的检查是在运行时做的。用动态类型语言编程时,不用给变量指定数据类型,该语言会在你第一次赋值给变量时,在内部记录数据类型。 动态类型语言的优点是不需要写多种数据类型的代码,代码相对简洁一些,方便代码阅读。缺点是不方便调试,代码命名也容易混淆; 再来看看静态类型语言的定义: 静态类型语言,是指数据类型的检查是在运行前(如编译阶段)做的。 静态类型语言的优点是方便调试,代码相对规范。缺点是需要写很多数据类型相关的代码,代码不够简洁。 完结束 此片完了~ 想要了解更多精彩新姿势?请访问我的个人博客 本篇为原创内容,已在个人博客率先发表,随后CSDN,segmentfault,掘金,简书,开源中国同步发出。如有雷同,缘分呢兄弟。赶快加个好友~]]></content>
<tags>
<tag>技术</tag>
</tags>
</entry>
<entry>
<title><![CDATA[一战中的制式步枪]]></title>
<url>%2F2018%2F08%2F19%2F%E4%B8%80%E6%88%98%E4%B8%AD%E7%9A%84%E5%88%B6%E5%BC%8F%E6%AD%A5%E6%9E%AA%2F</url>
<content type="text"><![CDATA[今天我们不聊技术了,聊一聊枪支。我本人是个狂热的军事迷,而恰好最近找到一款游戏,是一战题材的,叫‘战地1’,原汁原味的一战武器的加入,让这个本来就很强势的第一人称射击游戏增添了独特魅力。废话不多说,今天我以这个游戏中的一战视角,来介绍以下一战中的制式武器。 想要解锁更多新姿势?请访问我的博客 第一次世界大战第一次世界大战(英语:World War I、First World War或Great War,简称WWI或WW1),简称一战,是在19世纪末20世纪初,资本主义国家向其终极阶段,即帝国主义过渡时产生的广泛的不可调和矛盾、亚洲,非洲,拉丁美洲殖民地和半殖民地基本上被列强瓜分完毕,新旧殖民主义矛盾激化、各帝国主义经济发展不平衡,秩序划分不对等的背景下,为重新瓜分世界和争夺全球霸权而爆发的一场世界级帝国主义战争。战争过程主要是同盟国和协约国之间的战斗。 德意志帝国、奥匈帝国、奥斯曼帝国、保加利亚王国属同盟国阵营 大英帝国、法兰西第三共和国、俄罗斯帝国、意大利王国和美利坚合众国则属协约国阵营。 这场战争是欧洲历史上破坏性最强的战争之一。大约有6500万人参战,1000多万人丧生,2000万人受伤。战争造成了严重的经济损失。 什么是制式武器首先我这个题目叫‘一战中的制式步枪’,什么是制式步枪(武器)呢?制式武器即同一种兵器的各种技术参数相同的武器装备,并且经部队武器装备部门批准备案,有正式型号编号,正式批量列装部队使用的正规的标准武器。就像咱们中国PLA,步兵标配的95式突击步枪(Type 95),这个95式就是制式步枪。它的出现使得军警部队的武器管理,使用得以标准化,统一化。加强了武装部门的正规化。 WWI制式步枪Winchester 1895 (Russian)温彻斯特M1894杠杆步枪由传奇性人物约翰·摩西·勃朗宁设计的M1894杠杆步枪,是美国历史上第一支使用无烟发射药枪弹,并且为狩猎而专门设计的杠杆步枪。 M1895步枪采用杠杆枪机式结构,其杠杆式枪机的基本原理与M1873步枪的相同,只是结构有所不同而已。M1873步枪通过扳机护圈杠杆的前后旋转,带动肘节摆动,从而带动枪机前后运动。而在M1895步枪上,扳机护圈杠杆与扳机座直接组成了肘节结构,其结构更为简单。M1895步枪的扳机座尾端固定在机匣上,其前端与扳机护圈杠杆中部绞接在一起,而扳机护圈杠杆上部与枪机前端绞接在一起,当扳机护圈杠杆摆动时,通过其上部带动枪机前后运动。另外,M1873步枪扳机护圈杠杆的旋转轴固定在机匣上,而M1895步枪的扳机护圈杠杆则没有固定的旋转轴,它的运动轨迹比较复杂,不是简单的旋转运动。前推扳机护圈杠杆时,其尾端先向前下方摆动,拉扳机座前端向下,使肘节结构弯曲。当扳机座前端向下摆动到一定位置时,扳机护圈杠杆前端便带动枪机向后。继续向下推扳机护圈杠杆,便使肘节结构进一步弯曲,扳机护圈杠杆上端带动枪机进一步向后,直至将其向后推到位。因此,扳机护圈杠杆既有旋转运动,又有上下方向的直线运动。 M1895步枪的特色表现在三个方面。 一是采用了盒式弹仓。这种弹仓比现在枪械上采用的固定式弹仓复杂得多。目前的固定式弹仓多采用螺旋弹簧 做托弹板簧,并直接置于托弹板之下,靠簧力将弹向上抬起。而M1895步枪采 用了当时常用的片簧做托弹板簧,其托弹板呈“V”形,上端卡在枪管下方的固定槽内,下方卡在托弹杆尾端的凹槽内,靠簧力驱动托弹杆逆时针旋转,带动托弹板向上运动,使弹仓内的枪弹进入待进膛位置。 其二,采用了闭锁楔铁。前推扳机护圈杠杆,其向前下方转动,使扳机座向下方摆动,扳机座前端钩住闭锁楔铁下端使之一起向下运动,闭锁楔铁上端离开枪机尾部的凹槽而使枪机开锁。继续前推扳机护圈杠杆,则带动枪机后退,并压倒击锤;后拉扳机护圈杠杆时,则先带动枪机复进。枪机复进到位时,继续后拉扳机护圈杠杆,其向后上方转动,带动扳机座向上摆动,推动闭锁楔铁向上,其上端进入枪机尾部的凹槽而使枪机闭锁。这个闭锁楔铁的设计使枪机闭锁更加确实,保证了射击时的安全性。其三,抛壳挺兼作推弹齿。温彻斯特M1873及M1890步枪均依靠取弹器将弹壳抛出,但由于配合动作有时不太一致,使得抛壳力度不同,因而易出现问题。M1895步枪则专门设计了抛壳挺,从而解决了这一问题。在枪机前端面,抽壳钩在上,抛壳挺在下,由于抛壳挺突出于枪 机下缘,故其兼具推弹齿的功能。 一战中W 1895为什么标注的国籍是Russian呢? 因为模型版的W 1895 中,.35 WCF,.405 WCF,.30-03和.30-06口径的用于美国体育市场, 销售额在30%-40%。 其他大部分是与俄罗斯帝国军方签订的合同中卖的。 1915年,俄罗斯急需小型武器,并与许多美国主要工厂接洽。 雷明顿和新英格兰西屋公司都签订了制造Mosin-Nagant步枪的合同,温彻斯特达成协议,用标准的俄罗斯军用7.62x54R弹药筒生产30万只1895型步枪。 与Mosin-Nagant命令不同,所有温彻斯特步枪都是在俄国革命之前交付的。 Gewehr M.95Gewehr M1895 是标准后拉式枪机步枪,服务奥匈军队在第一次世界大战。除了其他一些东欧和巴尔干国家之外,这种武器大量制造,在停战后还与奥地利和匈牙利的继承国一起服役。 该武器采用革命性的直拉动作螺栓,使独特的武器具有较高的射速,补充了其良好的可靠性。通过一个5轮整体夹子(与德国Gewehr 98使用的剥离夹子相反),M1895最初用于圆头8x50mmR Mannlicher,并在第一次世界大战期间使用该弹药筒。第一次世界大战,步枪被转换成更强大的尖头“斯皮策”子弹,如8x56mmR。 Gew.98式98步枪,德国有两个98式,一个是Gew.98一个是Kar98, G98装备到35年,之后是K98。毛瑟98k卡宾枪是二战中德国狙击手的制式武器。为了避免在取下瞄准镜后不致失去抵抗能力,枪上还备有机械瞄具。在生产中,挑选最好的枪管用于装瞄准镜的步枪。这些步枪的扳机是经过修改的,其扳机力达1.8kg。 Gew.98式步枪为毛瑟步枪的改进型号,不仅吸收了早期毛瑟型号上的所有改进,主要特征是固定式双排弹仓和旋转后拉式枪机。弹仓为双排、固定式,其底板可以拆卸出来。装填时可以从顶部的抛壳口一发一发地装入,也可以通过弹夹一次装填满5发。每个桥夹装5发弹,足够装满一个弹仓,在机匣后桥上有机器加工出来的桥夹导槽。在装填后,当枪机关闭时,空的桥夹会被自动抛出。弹仓可以打开枪机清空或通过拆卸弹仓底板进行,但一般不推荐采用后一种方法。 SMLE MKIIIThe Short Magazine Lee-Enfield Mk III,或简称为SMLE MkIII,是一种英式螺栓式步枪。Mark III是Lee-Enfield的演变,顾名思义它缩短了大约5英寸或13厘米,并于1907年推出。制造的复杂性意味着一个简化的变体,SMLE MkIII 于1915年推出。一个弹匣中有10发.303 British,它的容量是其他大多数国家步枪的两倍。挥舞着步枪的英国步兵也因“mad minute”而闻名,在此期间他们可以达到每分钟30发以上的速度。 这种武器在英联邦国家非常普遍,被加拿大,拉吉和澳新军团以及英国本身使用; 开发了许多变体,包括战地1942年出现的第4号。 春田M1903M1903春田步枪 (M1903 Springfield rifle)是一种旋转后拉式枪机步枪,由春田兵工厂生产,是美军在一战及二战的制式步枪,是春田兵工厂在毛瑟兵工厂的特许下研制的,旋转后拉式枪机仿自德国m1898,可以算是以毛瑟步枪为基础的变型枪。 M1903因为缩短了枪管,所以长度比毛瑟步枪短,拉机柄改为向下弯曲便于携行。由容量5发子弹的弹仓供弹,用5发分离式弹夹装弹。M1903步枪配用的M1906步枪弹(.30-06步枪弹)是在毛瑟式无底缘弹的基础上改进而成的,早期的M1903步枪还配有杆式刺刀,后改用了匕首型刺刀。 第一次世界大战期间,当美国参战时,M1903步枪数量不足,美国将同样仿自德国毛瑟步枪枪机的一种恩菲尔德M1917命名为“M1917步枪”作补充装备,甚至一度在前线美国军队使用的M1917比M1903还多。一战结束后,美国军队只保留M1903作为制式步枪,M1917全部撤装作储备。1938年,取代M1903步枪的半自动M1加兰德步枪产量不足,因此M1903步枪仍然是美国军队装备的主要步枪,在第二次世界大战中,美国军队仍然大量装备。剩余的M1903春田步枪被赋予了新的使命,包括改装成狙击步枪,狙击步枪版本加装光学瞄准镜,具有精度上的优势,而为不妨碍瞄准镜的使用,所以拆除了机械瞄具。第二次世界大战期间,根据援助法案大量的M1903步枪装备中国军队,在中国抗战时期以及解放战争时期广泛使用。由于该枪外形、长短均与当时中国军队装备的中正式步枪(仿毛瑟1924)相似,被中国士兵称为“花旗中正式”。M1903步枪成为美军制式装备100年后,仍然在美国军队中少量出现。并将枪身金属部件镀铬处理,配白色背带,供训练与检阅使用。 Martini Henry 马蒂尼-亨利步枪纵观19世纪后期的军用武器,英国的马蒂尼-亨利步枪是不可不提的。它为英国向全世界扩张其帝国领土,发挥了非凡的作用。从冰雪覆盖的加拿大到位于撒哈拉沙漠边缘的苏丹,再到群山之中的阿富汗,到处都留下了它的足迹。历史上许多勇猛的尚武民族都被手持射程远、射速高的马蒂尼-亨利步枪的英国士兵镇压了。 马蒂尼-亨利步枪的传奇历史始于美国马萨诸塞州的枪炮商亨利·皮博迪。 1862年,皮博迪获得了后膛装填步枪的专利权,这种步枪采用起落式枪机,枪机下降时弹壳可以自动抛出,并且能够使下一发枪弹快速入膛。该枪采用简单的火枪式击锤击发方式而且必须手动待击。1860年代末至1870年代初,这种后膛装填步枪受到了西班牙、加拿大、土耳其、罗马尼亚、新西兰和墨西哥等国的青睐,并购买了数万支。1866年,一位居住在瑞士的奥地利籍设计师弗雷德里克·冯·马蒂尼修改了皮博迪的设计,用安装在枪机内部、由弹簧控制的击针代替了笨重的外置击锤,并且还重新设计了机匣、加固了抽壳钩。这些改进使得该枪的击发时间变得更短 (击发时间即为扣动扳机完成瞬间至底火发火的时间间隔)。改进后的步枪不但比皮博迪的原型枪更坚固,而且在射速上也有很大提高。但瑞士陆军还是决定采用旋转后拉式枪机的单发步枪,马蒂尼只得把他的设计提供给了其他国家。1867年,英国陆军对外宣布要寻找一种步枪来替代其0.577英寸(14. 66mm)口径的斯奈德步枪。马蒂尼携带他的步枪参加了英军的试验,尽管有很多像亨利这样的知名枪炮商参与竞争,但他的步枪却技压群雄。1868年,坐落在恩菲尔德的英国皇家轻武器制造厂接到生产几支马蒂尼步枪用于进行进一步野外试验的命令。经过大量的野外试验,1871年7月3日,马蒂尼步枪被英军正式采用。由于该枪采用了亨利设计的带有膛线的枪管,因此英国陆军官方将其命名为马蒂尼-亨利MKⅠ步枪。该枪是一种很实用的武器,盒子状的机匣使得扳机护圈位置向前了,并且杠杆一直延伸到枪托上。采用胡桃木枪托和护木,护木用两个套箍固定在枪身上,并且有一圆杆通过沟槽镶嵌在下护木的底部,枪管右侧安装了一把骑兵刺刀。 马蒂尼-亨利MKⅠ步枪操作起来非常简便,射手只需向下拉动杠杆,就会完成如下的动作:击针缩进枪机的前部;枪机回转,露出弹膛;扳机嵌入阻铁槽;该枪仅有的保险装置是位于机匣右侧的水滴状待击指示器,待击时其指在十点钟的位置上,击发后指在十二点钟的位置。机匣右侧顶部刻有V.R字样,即维多利亚王冠标志,其右下侧为军队制式标志。罗马数字Ⅰ刻在机匣右侧靠近制造商标志处。 马蒂尼-亨利步枪最初使用的枪弹口径为0.450英寸,采用博克赛式底火,卷制黄铜弹壳。后来,该枪弹改为采用几层带纸垫的卷制薄铜皮弹壳身并配以铁制弹壳底部的弹壳。 使用卷制黄铜皮弹壳的枪弹看起来很适用,但在实战中用薄材料制成的弹壳易于变形、撕裂、受潮,而且当武器连续发射温度较高时,薄铜皮易粘着在弹膛上,阻碍抽壳。这样士兵们只好强行从弹膛里撬出空弹壳,导致铁制弹壳底部撕裂,而卷制黄铜皮弹壳留在弹膛内,使得一支很好的步枪也无用武之地了。1885年,为了对付群体犯罪,英国陆军发明了装填鹿弹的枪弹配备马蒂尼-亨利步枪使用。它是一种装有11个小球的带有博克赛式底火的枪弹,直径为7mm,内装黑火药。此外,为了防止弹内小球相互碰撞发出响声,损坏金属薄片的弹壳,弹壳中的空隙用骨粉填充。 FusilModèle1886 勒贝尔M1886勒贝尔M1886(法语:FusilModèle1886)或勒贝尔步枪(法语:FusilLebel)是法国于1886年推出的手动枪机式步枪,由尼古拉斯.勒贝尔上校研制。该枪运用了由保罗.维埃那开发的火药B,亦是史上第一种最成功的无烟火药枪械。勒贝尔步枪的出现意味着那些使用黑火药的枪械已过时,各国亦开始改为专注于研制和采购发射无烟火药的枪械。 勒贝尔步枪发射8×50毫米的全全属外壳步枪子弹,载弹量为8发,并以内置弹仓供弹。除此之外,它还可配备一种长针型的重剑式刺刀以应付白刃战。在1887年,勒贝尔步枪被法国军队列装为制式步枪,并参与了多次对外战争,当中更包括第一次世界大战及第二次世界大战。在一战期间,法军曾打算以Mle1917半自动步枪大量取代勒贝尔步枪。然而,士兵们却普遍认为此枪过于笨重和太长,因此并不容易在战壕内操作和保养。基于这些问题,法军一直都未能找到合适的替代品以取代勒贝尔步枪。而在1936年推出的MAS36步枪亦因生产进度太慢并不能满足军队,所以直至法国被纳粹德国占领之前,勒贝尔步枪仍旧为法军的主力步枪。当中有少量的步枪更被纳粹德军缴获使用,并以Gewehr301(f)的名义在军中服役。直至1920年停产前,勒贝尔步枪一共被法国的国营兵工厂生产了约2,880,000支。而在二战过后的一些冲突中仍能找到勒贝尔步枪的身影。 莫辛-纳甘步枪莫辛-纳甘步枪(莫辛纳干系列俄文Винтовка Мосина,英文Mosin-Nagant,又称莫辛-纳干),即M1891莫辛-纳甘步枪,在俄国被称为“Vintovka Mosina”(莫辛步枪),是在俄国政府委托下在1880年代后期至1890年代早期研制的步枪。该枪由俄国军队在1891年正式采用,定型为1891型3线口径(7.62x54mmR)步枪。M1891步枪在招标过程中出现了争议,有两个设计能够进入官方评审的最后阶段,一个是俄国陆军上尉谢尔盖·伊凡诺维奇·莫辛(Sergei Ivanovich Mosin)的样枪,另一个是比利时的艾米尔·纳甘和李昂·纳甘两兄弟(Emil Nagant和Leon Nagant)设计的样枪。 历史莫辛出生于1849年5月5日,12岁时进入一家军事学院并在那里参了军,在1867年他进入莫斯科Alexandrovskoye军事中学(Alexandrovskoye Military High School),在1870年离开军事中学时,他为了能够调去炮兵部门而转入开依洛夫斯科伊炮兵学院(Mikhailovskoye Artillery Academy)。他在1875年毕业后被调到图拉兵工厂。莫辛当上武器设计师后的第一个工作就是对伯丹II步枪的改进,莫辛-纳甘步枪算是他的第二个设计,虽然定型的莫辛-纳甘步枪并没有完全采用他的设计。莫辛是在1883年开始设计连发步枪的设计工作,他在1884年和1885年分别提供了几种内置弹仓供弹的步枪设计给负责招标的委员会,最初的设计是10.6mm口径。但莫辛的努力成果没有受到俄罗斯军队的重视。在1886年法国采用8mm口径M1886勒贝尔步枪后(这是第一种采用无烟火药的小口径枪弹的军用武器),此举在世界各国引起了一场使用无烟发射药小口径枪弹(相对之前的弹药)的轻武器军备变革,在1887年至1889年间,大多数欧洲国家的军队都采用了类似的武器,俄国政府也决定采用一种类似的新型连发步枪,代替现役的伯丹步枪(类似于英国马蒂尼-亨利步枪的黑火药枪弹单发后装枪)。为此俄罗斯政府组织了一个委员会,从现有的毛瑟、勒贝尔、李-梅特福、曼利夏、施密特-鲁宾和克拉格-约根森等设计中进行选择。莫辛也接受委托设计了一种5发单排弹仓的7.62mm口径步枪参与招标。根据古老的俄罗斯度量衡称为3线口径【注:liniya,英文为line,旧俄罗斯度量衡,1线等于0.1英寸或2.54mm,因此,3线等于7.62mm或0.3英寸】。而比利时武器设计师李昂·纳甘则向俄罗斯军队提交了一种3.5线口径(8.89mm)步枪和500发枪弹进行测试。莫辛纳干1891步枪早期型莫辛纳干1891步枪早期型所有参与投标的武器都在1890年至1891年间由俄罗斯军队进行测试,俄罗斯军队偏爱纳甘的设计。原本对纳甘的设计有利,但出于俄罗斯国家尊严的考虑,政府对莫辛的步枪很感兴趣。由于政府和军队的意见分歧,互不相让,最后委员会用了折衷的方法:把这两种设计合并在一种步枪上,结果是把纳甘兄弟设计的供弹系统装在莫辛设计的步枪上,因此这种步枪系统被称为莫辛-纳干步枪。而参与竞争的双方都获得补偿:纳甘兄弟得到酬金(后来纳甘兄弟设计的M1895手枪也被俄罗斯军队采用),而莫辛则晋升成上校并被任命为谢斯特罗列茨克兵工厂的主管,继续改进和生产这种步枪,莫辛上校于1902年2月8日去世,安葬在图拉。在1960年,苏联设立了一个S.I.莫辛特别奖,奖励各个防务企业系统内的专家。莫辛-纳甘步枪被采用时俄国的轻武器企业还没有做好生产准备,所以第一批M1891莫辛-纳甘步枪是法国的夏特罗轻武器厂生产的。莫辛-纳甘步枪是第一次世界大战中俄国军队的主要装备。在第一次世界大战期间,外国的承包商再一次被用来生产这种步枪,当时俄国非常缺乏步枪,所以与两家美国公司签订生产合同,但这批步枪由于1917年的十月革命而没有交给后来的苏维埃政权,在美国用于训练和民间销售。M1891步枪最初有三种型号:步兵步枪、龙骑兵步枪和哥萨克步枪,步兵步枪就是标准型长步枪,后两种是配发给骑兵部队使用的骑枪(卡宾枪)。 结构特点莫辛-纳甘是最早的无烟发射药军用步枪之一,枪声清脆, 有如水珠溅落, 因此我们中国人为莫辛-纳甘起了一个颇具诗意的绰号”水连珠”。莫辛-纳甘系列步枪与毛瑟步枪系列、李-恩菲尔德步枪系列等其他同时代同类军用步枪相比,其枪机设计显得较为复杂,它的设计粗糙而且过时,整体的操作感觉也比这些步枪笨拙。但莫辛-纳甘步枪的优点是易于生产和使用简单可靠——这相对于工业基础低、士兵教育程度低的苏/俄军队来说是极其重要的,尤其是恶劣的战争时期包需提高武器产量以满足前线需要,而大量补充的战斗人员往往训练时间不足。 莫辛-纳甘步枪是一种旋转后拉式枪机、弹仓式供弹的手动步枪,是俄罗斯军队采用的第一种无烟发射药步枪。它采用整体式的弹仓,通过机匣顶部的抛壳口单发或用弹夹装填。弹仓位于枪托下的扳机护圈前方,弹仓容弹量5发,有铰链式底盖,可打开底盖以便清空弹仓或清洁维护。由于是单排设计而没有抱弹口,因此弹仓口部有一个隔断面器,上膛时隔开第二发弹,避免出现上双弹的故障。在早期的枪型中,这个装置也兼具抛壳挺的作用,但自M1891/30型开始,以后的枪型都增加了一个独立的抛壳挺。枪膛内有4条右旋转膛线。当枪机闭锁时,回转式枪机前面的两个闭锁凸笋呈水平状态。步枪是击针式击发,击针在打开枪机的过程中进入待发状态。手动保险装置是在枪机尾部凸出的一个“小帽”,向后拉时会锁住击针,而向前推时会解脱保险状态,操作时不太方便而且费力。水平伸出的拉机柄力臂较短,因此操作时需要花较大的力气,而且比起下弯式拉机柄在携行方面时较不方便,而下弯式拉机柄只有狙击型才有。从步枪上分解出枪机时不需要专门工具,只要拉开枪机,然后扣下板机就能取出枪机。在没有工具的条件下还可以进一步分解其他几个主要部件。早期的棱形刺刀的截面为矩形,后改为一字螺丝起子形,并在分解步枪时充当分解工具。早期的刺刀是可拆卸的四棱刺刀通过用管状插座套在枪口上,后期为不可卸的折叠式,而且刺刀座兼作准星座。枪托通常用桦木。 弹药与M1891步枪一起还有一种新的小口径枪弹被采用(确实是那个年代里的小口径枪弹),直到今天,7.62×54mmR枪弹在俄国军队服役已经超过了一个世纪。该枪弹采用突底缘锥形弹壳,突底缘弹壳的设计在19世纪末也已经开始显得开始过时了,但却适合基础较低的俄罗斯轻武器工业,因为突底缘弹壳对弹膛尺寸的要求相对宽松一点,这样在机器加工时允许有较大的生产公差,既节省了工时又节约了钱。M1891式枪弹的弹头是重210格令、铜镍合金被甲、铅芯的钝圆头形弹头,在德国采用了尖头弹后,俄罗斯也开始研制尖头弹,经过广泛测试后,在1908年采用了一种重148格令、铜镍被甲的铅芯尖头弹(战争时期采用覆铜钢被甲)。在二战结束后,苏联的制式步枪先后采用了中间威力型枪弹和5.45mm小口径步枪弹,但直到现在M1908式枪弹系列仍然被用作机枪和狙击步枪的弹药。 三八式歩兵铳八式步枪(日语:三八式歩兵铳;さんぱちしきほへいじゅう,英文:Sanpachi-shiki hohei-ju)为手动步枪,日本陆军于日俄战争后1907年正式采用为制式武器,三八式步枪是第二次世界大战中日本法西斯陆、海军最主要、最基本的武器,一直使用到二战结束(日本战败),用了整整40年。三八式步枪在中国一向俗称为三八大盖,由于其枪机上有一个随枪机连动的防尘盖以及机匣上刻有“三八式”字样而得名。 历史三八式步枪的原型是三十年式步枪和三五式海军步枪。三十年式步枪仿造自德国毛瑟步枪,设计师是有坂成章大佐,以取代从甲午战争以来,以法国M1874步枪仿造但是不可靠也打不准的村田枪。 这两种步枪也因其发明者有坂成章而得名有坂步枪。有坂在设计三十年式步枪时起初设定了三种口径作为研发方向:6毫米、6.5毫米以及7毫米。三十年式步枪(三十年式歩兵铳)于1897年进行制式化生产,并于1903年完成日本全军更换整装。日本陆军换发三十年式步枪后,日本海军陆战队也跟着换枪,有坂成章将设计案转将给部属南部麒次郎上尉负责进行改进。南部麒次郎只进行了小改良,将表尺改为直立式,并且加上枪机盖,并命名为三五式海军步枪(三十五年式海军铳)。1904年至1905年日军在中国东北战场上使用的三十年式步枪表露出有一种细小的沙尘进入操作机关内,导致操作不良,三十年式步枪另外有根很容易折断的撞针。上述的情形在日俄战争后有人提出讨论与进行改善,这时候已经升任小石川炮兵工厂研究所所长的南部麒次郎因此将枪机重新进行简化,同时在枪机表面增加了一个随枪机连动的防尘盖。然而增加防尘盖的立意虽然良好,不过却又为操作时增加了独特的金属噪音。其他改进包括在准星两侧增加护翼,三八式步枪机械瞄准具早期采用V型缺口照门,后期采用觇孔式照门。 南部很快地完成三十式步枪的改良后,于明治四十年(1907年)5月开始制式化在东京小石川炮兵工厂进行生产。虽然正式的制式化量产从1907年开始,不过南部麒次郎将新改良开发的步枪仍然以明治天皇年号第三十八年(1905年)命名;明治41年3月开始,新式的三八式歩枪逐渐成为部队的标准配备;到了明治43年(1910年),部队中的三十年式步枪就全面被三八式步枪取代了。 结构特点三八式步枪重3.73千克,加上30年式刺刀为4.1千克。枪身全长127.6厘米,再加上30式单刃刺刀后就可达166.3厘米,该枪一大特征就是它的长度。三八式步枪的机匣制作公差小,表面经过防腐处理,枪机在机匣内运行顺畅,机匣上面有两个排气小孔,保证射击时的安全,枪机上方是截面为随着枪机前后滑动的“n”型的防尘盖,防尘盖上有开口供直式拉机柄伸出,枪机尾部有圆帽型的转动保险装置。枪机组件的设计其部件数量比毛瑟枪还少3个零件,仅有5个零件,是当年旋转后拉式枪机步枪中结构简单的,提高可靠性和减低维护保障难度,不过零件外形复杂增加了加工难度。三八式步枪的弹仓镶嵌在枪身内,容量5发子弹,三八式步枪的弹仓还有空仓提示功能,当弹仓内最后一发枪弹射出后,枪机后拉到位时托弹板就会顶住枪机头无法向前运动提醒射手装弹。三八式步枪的枪托加工方式与的一般式步枪枪托加工方式不同,一般式步枪的枪托是用一整块木料切削而成,三八式步枪的枪托是用两块木料拼接而成的,此种方式虽然日久容易开裂但可节省木材。 三八式步枪参考数据 类别 旋转后拉式枪机,弹仓供弹 口径 6.5毫米 全枪长 1275毫米 枪管长度 797毫米 加装刺刀总长 接近1700毫米 全枪重 4.1千克 弹匣容弹量 5发 射程 460米 弹药 6.5×50mm 步枪弹 初速 765米/秒 卡爾卡諾M91/38卡尔卡诺是过去意大利常用的手动枪机操作式一系列军用步枪和卡宾枪,最初于1891年推出时,发射的是6.5×52毫米曼利彻-卡尔卡诺口径无缘式步枪子弹(Cartuccia Modello 1895)。这枝步枪是由首席技师萨尔瓦多·卡尔卡诺在都灵军兵工厂于1890年研发,并且称为M91。它先后取代了以往的10.35×47毫米RVetterli-维塔利步枪和卡宾枪以,在1892年到1945年之间生产。在第一次世界大战期间的大多数意大利部队和第二次世界大战中的意大利以及德国军队之中一些部队里,采用了M91的步枪(fucile, sing.;fucili, pl.)和卡宾枪(moschetto, sing.;moschetti, pl.)各两种的型号。在过程中,步枪也被芬兰于冬季战争中使用,以后又在叙利亚、利比亚、突尼斯和阿尔及利亚这些国家战后的各种冲突中被正规和非正规部队使用。 虽然这种步枪经常被称为“曼利彻-卡尔卡诺”(特别是在美国的说法),但不论是该命名还是“毛瑟-帕拉维西尼”(Mauser-Parravicino)也不是正确的。该枪在意大利的正式名称是Modello 1891,或是M91(“il novantuno”)。该枪的弹匣系统使用整体漏夹式弹夹条来装填,虽然最初是由费迪南德·曼利彻研发并获得了专利,但事实上卡尔卡诺步枪的弹夹条的实际形状和设计是由德国1888式委员会步枪所得出的。 直到1938年,所有的M91步枪和卡宾枪都改为发射的是6.5×52毫米曼利彻-卡尔卡诺口径无缘式Modello 1895步枪子弹,采用重量为160格令的圆头金属弹壳子弹,视乎枪管长度而令枪口初速在609.6—755 米/秒(2,000—2,477.03 英尺/秒)之间。由于采用渐进膛线(膛线越接近枪口缠距越短,效果是弹头转速越来越快),该枪精度良好。不过,至少有一个轻武器的权威指出,一些批号的6.5×52 毫米军用弹药装载的是并非一致的火药类型,经常将不同的火药类型和弹药批号混合并且制作成为单一的弹药(可能是出于战时成本控制的原因,亦可能是生产混乱所导致)。[1]在经过修改的步枪弹药上混用火药类型和弹药批号的做法为普遍其他国家的兵工厂所极力避免的事,因为这种粗劣手法通常会导致子弹初速不同和子弹与目标过度分散。 战争海报中的妇女手中的就是一把卡尔卡诺m1938折叠刺刀型: 罗斯Mk-3罗斯Mk-3是加拿大远征军的武器,第一支罗斯步枪出现于1896年,它在风和日丽的加拿大表现很好,以精度而闻名,它的设计者罗斯爵士是一个神射手,它对自己的枪要求很高,要有优秀的精度和快速的射速,而且严格控制公差,于是这把枪采用了直拉柄设计,与传统的手拉旋转式步枪步枪,这把枪仅仅需要一前一后就可以完成抛壳,退弹,闭锁的步骤,射速较当时的步枪更快 M1917 Enfield恩菲尔德M1917(M1917 Enfield,又名P17、P1917或Pattern 1917)是“美国恩菲尔德”(American Enfield)于1917至1918年间生产的.30-06口径手动步枪,装备一战时期的美国军队,美国当时命名为United States Rifle, cal .30, Model of 1917。 历史当英国加入一战时,其盟友美国急需本土生产的步枪作军备,当时的温彻斯特及雷明登获选并生产M1914步枪(P14)。其后,美国参战后需要更多步枪,美国军械署提出采用.30-06口俓弹药的新型步枪M1917(P17),并由位于康乃狄克州纽黑文的温彻斯特工厂及位于康乃迪克州Eddystone和纽约Ilion的雷明登工厂生产,而专用的M1917刺刀由其他多间小厂生产。 M1917及M1903春田步枪作为一战时美军的主要制式步枪服役。在1918年11月11日,约75%的美国远征军(American Expeditionary Force)都装备了M1917。停战后,美军把M1917作储备储起。在二战开始初期,美国把储备起来的M1917重新改造,并作训练用途及通过租借法案向多个国家作出援助(包括英国、菲律宾、法国及中国),这批M1917步枪作喷沙及烘磷酸盐处理,部份更改用桦木制的护木,其中送交英国的国土防卫军(British Home Guard)的M1917印有红色记号以避免士兵装上P14的弹药。 结构设计M1917与李-恩菲尔德同由位于英国恩菲尔德的英国皇家轻兵器工厂生产,但李-恩菲尔德(如SMLE)采用.303 British口俓弹药,而M1917采用毛瑟98枪机及美国的.30-06弹药,由于.30-06弹药的直径较细,因此内置弹仓可放6发子弹,但弹夹仍然只有5发。由于两战之间储备过剩,有些枪匠以M1917的枪机装上雷明登M30系列上作运动步枪发售,约3000把7毫米口径的M1917在1930年卖给洪都拉斯。 1.59-inch Breech-Loading Vickers Q.F. Gun, Mk II1.59-inch Breech-Loading Vickers Q.F. Gun, Mk II是英国设计在第一次世界大战的轻火炮(超重型狙击枪)。最初的目的是用于堑壕战,而是对飞机的空对空和空对地使用进行了测试。虽然它发射炮弹并且无法发射火箭弹,但它被广泛但误导地称为“ 维克斯 - 克雷福德火箭炮 ”。 (游戏中未出现,无图) Mauser 1918 T-Gewehr毛瑟13毫米反坦克步枪(德语:Tankgewehr M1918,通常简写T-Gewehr M1918)是世界上第一个反坦克步枪,它设计用于破坏装甲目标。生产了大约15,800个。 M1918和真人比你就会发现它非常非常大! 这把来自黑科技合集体第二帝国的T-Gewehr M1918 反坦克步枪当时让英军坦克部队躲在角落瑟瑟发抖,在条件允许的情况下甚至可以轻松穿透100m外的MK-IV坦克的防护装甲,同时这把武器只是比普通的毛瑟步枪稍微大一点,所以一个人也可以拿着到处走,只是比其他人会缓慢一些。 历史在第一次世界大战期间,静态堑壕战的开始出现了使用装甲板进行人身防御的兴起,以及开发和使用穿甲弹药来对抗这种情况。为此,英国和德国都使用高能步枪,例如来自非洲殖民地的大象枪。英国人在1916年9月的Flers-Courcelette战役中首次使用装甲战车(坦克),随后是法国人。到1917年6月,德国陆军面对马克四世坦克,发现标准穿甲7.92毫米K子弹不再有效。这促使德国人开发出一种大口径和高速步枪作为反坦克武器。该毛瑟公司回应与13毫米T-格韦尔并开始批量生产的内卡河畔奥伯恩多夫于1918年五月关闭进行颁发的生产线专门提出反坦克分队的第一项。 操作该步枪是一种单发螺栓动作步枪,使用改进的毛瑟动作,手动将圆形物装入腔室。武器有一个手枪式握把和两脚架,但没有减少后坐力的方法,例如柔软的臀部或枪口制动器。这可能会导致重复射击射手的问题。在铁的景点是由前片和后切线的,在100米的增量从100米到500米毕业。这支步枪由两名持枪手和弹药的机组人员操作持票人,他们都受过训练,可以开火。由于反冲的巨大钝力,它被设计成在静止位置拍摄,无论是俯卧还是从沟内。 参考资料Winchester 1895 (Russian) Gewehr M.95 卡尔卡诺 1.59-inch Breech-Loading Vickers Q.F. Gun, Mk II Mauser 1918 T-Gewehr 结束 此片完了~ 想要了解更多精彩新姿势?请访问我的个人博客]]></content>
<tags>
<tag>军事</tag>
</tags>
</entry>
<entry>
<title><![CDATA[python-常用模块]]></title>
<url>%2F2018%2F08%2F19%2Fpython-%E5%B8%B8%E7%94%A8%E6%A8%A1%E5%9D%97%2F</url>
<content type="text"><![CDATA[最近在学习python全栈相关的内容,部分知识点着实需要记忆,索性写个blog记录一下。 想要解锁更多新姿势?请访问我的博客 什么是模块一个模块就是一个包含了python定义和声明的文件,文件名就是模块名字加上.py的后缀。 常用模块collections模块在内置数据类型(dict、list、set、tuple)的基础上,collections模块还提供了几个额外的数据类型:Counter、deque、defaultdict、namedtuple和OrderedDict等。 namedtuple生成可以使用名字来访问元素内容的tuple 使用这个可以很方便的表示一个点的二维坐标 12345from collections import namedtuplePoint = namedtuple('Point',['x','y']) #namedtuple('名称', [属性list])p = Point(1,2)print(p.x)print(p.y) deque:deque是为了高效实现插入和删除操作的双向列表,适合用于队列和栈 它可以实现添加操作append() 和删除操作pop(),往头部添加操作appendleft()往头部删除popleft() CounterCounter类的目的是用来跟踪值出现的次数。 它是一个无序的容器类型,以字典的键值对形式存储,其中元素作为key,其计数作为value。计数值可以是任意的Interger(包括0和负数)。 12345from collections import Counter# 创建c = Counter({1:'p',2:'y'})# 访问 当所访问的键不存在时,返回0,而不是KeyError;否则返回它的计数。c[1] # p OrderedDict有序字典 defaultdict带有默认值的字典 有如下值集合 [11,22,33,44,55,66,77,88,99,90…],将所有大于 66 的值保存至字典的第一个key中,将小于 66 的值保存至第二个key的值中。 12345678from collections import defaultdictvalues = [11, 22, 33,44,55,66,77,88,99,90]defaultdict = defaultdict(list)for value in values: if value > 66: defaultdict['k1'].append(value) else: defaultdict['k2'].append(value) time模块在Python中,通常有这三种方式来表示时间:时间戳、元组(struct_time)、格式化的时间字符串: (1)时间戳(timestamp) :通常来说,时间戳表示的是从1970年1月1日00:00:00开始按秒计算的偏移量。我们运行“type(time.time())”,返回的是float类型。 (2)格式化的时间字符串(Format String): ‘1999-12-06’ python中时间日期格式化符号: %y 两位数的年份表示(00-99)%Y 四位数的年份表示(000-9999)%m 月份(01-12)%d 月内中的一天(0-31)%H 24小时制小时数(0-23)%I 12小时制小时数(01-12)%M 分钟数(00=59)%S 秒(00-59)%a 本地简化星期名称%A 本地完整星期名称%b 本地简化的月份名称%B 本地完整的月份名称%c 本地相应的日期表示和时间表示%j 年内的一天(001-366)%p 本地A.M.或P.M.的等价符%U 一年中的星期数(00-53)星期天为星期的开始%w 星期(0-6),星期天为星期的开始%W 一年中的星期数(00-53)星期一为星期的开始%x 本地相应的日期表示%X 本地相应的时间表示%Z 当前时区的名称%% %号本身 (3)元组(struct_time) :struct_time元组共有9个元素共九个元素:(年,月,日,时,分,秒,一年中第几周,一年中第几天等) 索引(Index) 属性(Attribute) 值(Values) 0 tm_year(年) 比如2011 1 tm_mon(月) 1 - 12 2 tm_mday(日) 1 - 31 3 tm_hour(时) 0 - 23 4 tm_min(分) 0 - 59 5 tm_sec(秒) 0 - 60 6 tm_wday(weekday) 0 - 6(0表示周一) 7 tm_yday(一年中的第几天) 1 - 366 8 tm_isdst(是否是夏令时) 默认为0 12345678910111213#导入时间模块import time# (线程)推迟指定的时间运行。单位为秒。secs = 0.1time.sleep(secs)# 时间戳time.time()# 时间字符串time.strftime("%Y-%m-%d %X")>>>time.strftime("%Y-%m-%d %H-%M-%S")# 时间元组:localtime将一个时间戳转换为当前时区的struct_timetime.localtime()time.struct_time(tm_year=2018, tm_mon=8, tm_mday=19,tm_hour=13, tm_min=59, tm_sec=37,tm_wday=0, tm_yday=205, tm_isdst=0) random模块常用方法 12345678910111213141516171819202122232425262728>>> import random#随机小数>>> random.random() # 大于0且小于1之间的小数0.7664338663654585>>> random.uniform(1,3) #大于1小于3的小数1.6270147180533838#随机整数>>> random.randint(1,5) # 大于等于1且小于等于5之间的整数>>> random.randrange(1,10,2) # 大于等于1且小于10之间的奇数#随机选择一个返回>>> random.choice([1,'23',[4,5]]) # #1或者23或者[4,5]#随机选择多个返回,返回的个数为函数的第二个参数>>> random.sample([1,'23',[4,5]],2) # #列表元素任意2个组合[[4, 5], '23']#打乱列表顺序>>> item=[1,3,5,7,9]>>> random.shuffle(item) # 打乱次序>>> item[5, 1, 3, 7, 9]>>> random.shuffle(item)>>> item[5, 9, 7, 1, 3] 一个简单的生成验证码功能 12345678910import random# 随机生成6位验证码def Identifying_code(): code = '' for i in range(6): num=random.randint(0,9) # 大于等于0且小于等于9之间的整数 alf=chr(random.randint(65,90)) # 随机大写字母,数字对应阿斯克码 add=random.choice([num,alf]) #随机选择一个返回 code="".join([code,str(add)]) return code os模块常用和不常用的方法os.getcwd()获取当前工作目录,即当前python脚本工作的目录路径os.chdir("dirname") 改变当前脚本工作目录;相当于shell下cdos.curdir返回当前目录: (‘.’)os.pardir 获取当前目录的父目录字符串名:(‘..’)os.makedirs('dirname1/dirname2')可生成多层递归目录os.removedirs('dirname1') 若目录为空,则删除,并递归到上一级目录,如若也为空,则删除,依此类推os.mkdir('dirname') 生成单级目录;相当于shell中mkdir dirnameos.rmdir('dirname') 删除单级空目录,若目录不为空则无法删除,报错;相当于shell中rmdir dirnameos.listdir('dirname') 列出指定目录下的所有文件和子目录,包括隐藏文件,并以列表方式打印os.remove() 删除一个文件os.rename("oldname","newname")重命名文件/目录os.stat('path/filename') 获取文件/目录信息os.sep 输出操作系统特定的路径分隔符,win下为”\“,Linux下为”/“os.linesep 输出当前平台使用的行终止符,win下为”\t\n”,Linux下为”\n”os.pathsep 输出用于分割文件路径的字符串 win下为;,Linux下为:os.name 输出字符串指示当前使用平台。win->’nt’; Linux->’posix’os.system("bash command") 运行shell命令,直接显示os.popen("bash command).read() 运行shell命令,获取执行结果os.environ 获取系统环境变量 os.pathos.path.abspath(path) 返回path规范化的绝对路径 os.path.split(path) 将path分割成目录和文件名二元组返回 os.path.dirname(path)返回path的目录。其实就是os.path.split(path)的第一个元素 os.path.basename(path) 返回path最后的文件名。如何path以/或\结尾,那么就会返回空值。即os.path.split(path)的第二个元素os.path.exists(path) 如果path存在,返回True;如果path不存在,返回Falseos.path.isabs(path) 如果path是绝对路径,返回Trueos.path.isfile(path) 如果path是一个存在的文件,返回True。否则返回Falseos.path.isdir(path) 如果path是一个存在的目录,则返回True。否则返回Falseos.path.join(path1[, path2[, ...]]) 将多个路径组合后返回,第一个绝对路径之前的参数将被忽略os.path.getatime(path) 返回path所指向的文件或者目录的最后访问时间os.path.getmtime(path) 返回path所指向的文件或者目录的最后修改时间os.path.getsize(path) 返回path的大小 sys模块sys模块是与python解释器交互的一个接口 方法 sys.argv 命令行参数List,第一个元素是程序本身路径 sys.exit(n) 退出程序,正常退出时exit(0),错误退出sys.exit(1) sys.version 获取Python解释程序的版本信息 sys.path 返回模块的搜索路径,初始化时使用PYTHONPATH环境变量的值 sys.platform 返回操作系统平台名称 序列化模块用于序列化的两个模块 json,用于字符串 和 python数据类型间进行转换 pickle,用于python特有的类型 和 python的数据类型间进行转换 Jsonloads和dumps 12345678910111213141516import jsondic = {'k1':'v1','k2':'v2','k3':'v3'}str_dic = json.dumps(dic) #序列化:将一个字典转换成一个字符串print(type(str_dic),str_dic) #<class 'str'> {"k3": "v3", "k1": "v1", "k2": "v2"}#注意,json转换完的字符串类型的字典中的字符串是由""表示的dic2 = json.loads(str_dic) #反序列化:将一个字符串格式的字典转换成一个字典#注意,要用json的loads功能处理的字符串类型的字典中的字符串必须由""表示print(type(dic2),dic2) #<class 'dict'> {'k1': 'v1', 'k2': 'v2', 'k3': 'v3'}list_dic = [1,['a','b','c'],3,{'k1':'v1','k2':'v2'}]str_dic = json.dumps(list_dic) #也可以处理嵌套的数据类型 print(type(str_dic),str_dic) #<class 'str'> [1, ["a", "b", "c"], 3, {"k1": "v1", "k2": "v2"}]list_dic2 = json.loads(str_dic)print(type(list_dic2),list_dic2) #<class 'list'> [1, ['a', 'b', 'c'], 3, {'k1': 'v1', 'k2': 'v2'}] load和dump 12345678910import jsonf = open('json_file','w')dic = {'k1':'v1','k2':'v2','k3':'v3'}json.dump(dic,f) #dump方法接收一个文件句柄,直接将字典转换成json字符串写入文件f.close()f = open('json_file')dic2 = json.load(f) #load方法接收一个文件句柄,直接将文件中的json字符串转换成数据结构返回f.close()print(type(dic2),dic2) shelveshelve也是python提供给我们的序列化工具,比pickle用起来更简单一些。 shelve只提供给我们一个open方法,是用key来访问的,使用起来和字典类似。 re模块123456789101112131415161718192021222324252627282930313233import reret = re.findall('s', 'sadsadsa ssadsadsa sdadsadasd') # 返回所有满足匹配条件的结果,放在列表里print(ret) #结果 : ['s', 's', 's', 's', 's', 's', 's', 's', 's', 's']ret = re.search('s', 'sadsadsa ssadsadsa sdadsadasd').group()print(ret) # 结果 : s# 函数会在字符串内查找模式匹配,只到找到第一个匹配然后返回一个包含匹配信息的对象,该对象可以# 通过调用group()方法得到匹配的字符串,如果字符串没有匹配,则返回None。ret = re.match('a', 'abc').group() # 同search,不过尽在字符串开始处进行匹配print(ret)#结果 : 'a'ret = re.split('[ab]', 'abcd') # 先按'a'分割得到''和'bcd',在对''和'bcd'分别按'b'分割print(ret) # ['', '', 'cd']ret = re.sub('\d', 'H', 'teng she 789', 1)#将数字替换成'H',参数1表示只替换1个print(ret) # teng she H89ret = re.subn('\d', 'H', 'teng she 789')#将数字替换成'H',返回元组(替换的结果,替换了多少次)print(ret) #('teng she HHH', 3)obj = re.compile('\d{3}') #将正则表达式编译成为一个 正则表达式对象,规则要匹配的是3个数字ret = obj.search('abc123eeee') #正则表达式对象调用search,参数为待匹配的字符串print(ret.group()) #结果 : 123import reret = re.finditer('\d', 'ds3sy4784a') #finditer返回一个存放匹配结果的迭代器print(ret) # <callable_iterator object at 0x000001B84AADC7F0>print(next(ret).group()) #查看第一个结果 3print(next(ret).group()) #查看第二个结果 4print([i.group() for i in ret]) #查看剩余的左右结果 ['7', '8', '4'] hashlib模块Python的hashlib提供了常见的hash算法 我们以常见的摘要算法MD5为例,计算出一个字符串的MD5值: 1234import hashlibmd5 = hashlib.md5()md5.update('how to use md5 in python hashlib?'.encode("gb2312"))print(md5.hexdigest()) 计算结果如下:d26a53750bc40b38b65a520292f69306 configparser模块该模块适用于配置文件的格式与windows ini文件类似,可以包含一个或多个节(section),每个节可以有多个参数(键=值)。 logging模块默认情况下Python的logging模块将日志打印到了标准输出中,且只显示了大于等于WARNING级别的日志,这说明默认的日志级别设置为WARNING(日志级别等级CRITICAL > ERROR > WARNING > INFO > DEBUG),默认的日志格式为日志级别:Logger名称:用户输出消息。 123456import logging logging.debug('debug message') logging.info('info message') logging.warning('warning message') logging.error('error message') logging.critical('critical message')]]></content>
<tags>
<tag>技术</tag>
<tag>python</tag>
</tags>
</entry>
<entry>
<title><![CDATA[应用软件装在C盘里会让系统变慢么?]]></title>
<url>%2F2018%2F08%2F16%2F%E5%BA%94%E7%94%A8%E8%BD%AF%E4%BB%B6%E8%A3%85%E5%9C%A8C%E7%9B%98%E9%87%8C%E4%BC%9A%E8%AE%A9%E7%B3%BB%E7%BB%9F%E5%8F%98%E6%85%A2%E4%B9%88%EF%BC%9F%2F</url>
<content type="text"><![CDATA[8012年了,🌏还在转,但是好多顽固的人🧠不转了,所以今天就有了“应用软件装在C盘里会让系统变慢么”这个话题。 为啥人们有把软件装在C盘之外的习惯?这真的是一个远古时代遗留下来的习惯。 过去的电脑的硬件配置可不如现在的这么好,当时的硬盘可能只有10G,内存则可能只有128M,把10G的硬盘划为C盘,装进去一个系统,剩下的空间就不太多了,所以就有了把软件放在别的磁盘里面的习惯 再加上过去的电脑性能比较差,技术落后,慢慢就有把软件装在c盘会卡这种误区 应用软件装在C盘里会让系统变慢么?不会的,兄弟,大曼。 现在大部分人用的机械硬盘HDD是这样的👇 硬盘最核心的部件是像光盘那样的磁盘和连着光盘的磁头 其中,磁盘是用来存放数据的,磁头是用来读取数据的。 再说说系统分区,正常的方法分区的话,C盘在磁盘盘片上的位置应该是靠近外圈的,外圈因为周长比较大,这就导致了磁头在相同时间内,读取的数据的性能也很高。 所以,对机械硬盘来说,把应用软件装在C盘,非但不会让系统变卡,反而会让应用软件运行得更快。 还有很多小伙伴用固态硬盘SSD,把应用软件装在哪个分区运行速度都差不多。 如何让笔记本开机不再卡顿请看这里https://tengshe789.github.io/2018/08/16/%E5%A6%82%E4%BD%95%E8%AE%A9%E5%BC%80%E6%9C%BA%E4%B8%8D%E5%9C%A8%E5%8D%A1%E9%A1%BF/#more 结束此片完了~ 想要了解更多精彩新姿势?请访问我的个人博客]]></content>
<tags>
<tag>技术</tag>
<tag>硬件</tag>
</tags>
</entry>
<entry>
<title><![CDATA[如何让笔记本开机不再卡顿]]></title>
<url>%2F2018%2F08%2F16%2F%E5%A6%82%E4%BD%95%E8%AE%A9%E5%BC%80%E6%9C%BA%E4%B8%8D%E5%9C%A8%E5%8D%A1%E9%A1%BF%2F</url>
<content type="text"><![CDATA[最近老有小伙伴QQ私聊我:“然哥然哥,我电脑卡,怎么办?” 我就回复,还能怎么办啊,百度去 然后对面又来话了,“然哥然哥,我不会搜,咋办啊?” 我: 那么几天我就分享一下,我设置win10 PC的小窍门 想要解锁更多新姿势?请访问我的博客 降低显示效果首先,右键单击我的电脑,选择属性,找到高级系统设置,进入后切换选项卡到高级,在视觉效果中,选中“调整为最佳性能”,然后点击应用。 删除系统遗留的无用文件然后,在开始菜单输入cmd,选择命令提示符,进入后输入命令rd C:windows.old /s,删除系统遗留的无用文件,增加C盘空间。关于C盘的空间,我建议至少要保留20%的容量(并且不小于10GB),用来给系统交换文件使用。 取消Win10的启动画面 在开始菜单输入msconfig, 然后切换选项卡到引导,勾选“无GUI引导”就可以了 禁用随系统启动时加载的服务还是在刚才的msconfig系统配置中,切换到服务选项卡,这时所有随系统启动的加载服务都会列表显示,先勾选“隐藏所有Microsoft服务”,然后查看列表中剩余的加载服务项,把你认为不需要的应用服务取消勾选即可 最有效的办法骚年,换个SSD吧 结束此片完了~ 想要了解更多精彩新姿势?请访问我的个人博客 现在百度win10优化,还有很多小技巧,我就不一一列举了,详情请看看百度第一条https://jingyan.baidu.com/article/c843ea0ba3d01377931e4a3d.html]]></content>
<tags>
<tag>Windows10</tag>
<tag>窍门</tag>
</tags>
</entry>
<entry>
<title><![CDATA[论自动下载Bing背景的小技巧]]></title>
<url>%2F2018%2F08%2F16%2F%E8%87%AA%E5%8A%A8%E4%B8%8B%E8%BD%BDBing%E8%83%8C%E6%99%AF%2F</url>
<content type="text"><![CDATA[Bing,兄弟们懂吧🤠虽然Bing的搜索引擎还差点意思,不过每次启动它的时候,总是被它的背景震撼到,甚至很多人都把它当作美图网 诺,就是这种背景,每天都会换1张,还不重复 是不是想要把它设置成壁纸?每次都要自己手动操作对懒癌患者实在是不够友好,既然现在什么都讲究AI,咱们也就来点智能化的Bing壁纸下载操作吧。 想要解锁更多新姿势?请访问我的博客。 第一步–coding先准备一个txt,将下列代码粘贴进去 123456789101112131415161718192021222324252627282930313233343536373839404142434445$x = Split-Path -Parent $MyInvocation.MyCommand.Definitioncd $xmkdir imagescd imagesmkdir jsonscd jsons$url = "http://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=10"$time = Get-Date$data = Invoke-WebRequest $url$data.Content | Out-File $time.DayOfYear$decode = ConvertFrom-Json($data)cd ..$range = 1..8$count = $range.Countfor($i=0; $i -lt $count; $i++){$temp = $decode.images.Get($i)$urlsplit = -Join("http://www.bing.com",$temp.url)echo $urlsplitInvoke-WebRequest $urlsplit -OutFile ($temp.hsh + ".jpg")}echo ok!pause 然后将文件后缀名改成.ps1,存放到预先找到的壁纸文件夹中。 第二步–running用鼠标右键选择“使用PowerShell运行”,它就会自动生成个名为“images”的文件夹,下载最近几天显示的8张Bing壁纸。 效果就是这样: 出错怎么办?–exception注意了,那些已经不用IE浏览器的小伙伴,在使用代码前一定要把IE的启动设置选项优先完成了,不然铁定会报错的。 第三步–自动化要是觉得每天都要手动一下下非常麻烦,还有个全套自动化下载的方式,按照上面的方法把下面做成.ps1文件。 123456789101112131415161718192021222324252627282930313233$x = Split-Path -Parent $MyInvocation.MyCommand.Definitioncd $xmkdir imagescd imagesmkdir jsonscd jsons$url = "http://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=10"$time = Get-Date$data = Invoke-WebRequest $url$data.Content | Out-File $time.DayOfYear$decode = ConvertFrom-Json($data)cd ..$temp = $decode.images.Get(0)$urlsplit = -Join("http://www.bing.com",$temp.url)echo $urlsplitInvoke-WebRequest $urlsplit -OutFile ($temp.hsh + ".jpg")echo ok! 然后进入“控制面板-管理工具-任务计划程序”,选择右侧的“创建任务”。 切换到“操作”选项卡下,点击“新建”, 在“程序或脚本”一栏中,填入下面这行内容 12%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe “添加参数(可选)”内,大家则需要把脚本的保存路径填进去。 点击确定后,切换到“触发器”选项卡,同样点击新建,选择“每天”,点击确定,保存任务。 这样每天固定的时间点就会自动下载一张必应壁纸了,久而久之,壁纸库不就有了么? 完结此片完了~ 想要了解更多精彩新姿势?请访问我的个人博客 本篇为原创内容,已在个人博客率先发表,随后CSDN,segmentfault,掘金,简书,开源中国同步发出。如有雷同,缘分呢兄弟。赶快加个好友,咱们两个想个号码, 买个彩票,先挣他个几百万😝]]></content>
<tags>
<tag>Windows10</tag>
<tag>窍门</tag>
</tags>
</entry>
<entry>
<title><![CDATA[学好分布式架构=2=-通信协议HTTP]]></title>
<url>%2F2018%2F08%2F16%2F%E5%88%86%E4%BA%AB%E4%B8%80%E4%B8%8B%E6%88%91%E5%AF%B9%E9%80%9A%E4%BF%A1%E5%8D%8F%E8%AE%AEHTTP%E7%9A%84%E7%90%86%E8%A7%A3%2F</url>
<content type="text"><![CDATA[我为什么不断的更新博客呢?这是一次很好的提升机会。 平时解决问题的时候可能考虑进度问题没有更深刻地去理解,但是在写博客的时候,你会不知不觉中对一些内容进行思考,并有可能和评论者一起深入,这些都是难得的机会。 可能有人会问我,我为什么不断的更新博客呢? 我觉得吧,这是一次很好的提升机会。 平时解决问题的时候可能考虑进度问题没有更深刻地去理解,但是在写博客的时候,我会不知不觉中对一些内容进行思考,可以更深入的理解问题。在学习的过程中,不断的总结,不断的思考,不断记忆,慢慢知识就巩固了。 我是一个比较慢热的人,希望我可以用这个方式不断提高自己~ 共同加油! 想要解锁更多新姿势?请访问我的博客 HTTP协议是什么?HTTP协议(HyperText Transfer Protocol,超文本传输协议):是客户端浏览器或其他程序与Web服务器之间的应用层通信协议 ,是基于请求/响应范式的。 一个客户机与服务器建立连接后,发送一个请求给服务器,请求方式的格式为,统一资源标识符、协议版本号,后边是MIME信息包括请求修饰符、客户机信息和可能的内容。 服务器接到请求后,给予相应的响应信息,其格式为一个状态行包括信息的协议版本号、一个成功或错误的代码,后边是MIME信息包括服务器信息、实体信息和可能的内容。 它在客户端和服务器端请求和相应 RT 传输资源 通过html传输 :文本、word、avi电影、其他资源 传输的媒体类型 服务端向浏览器传输MIME类型的文件,浏览器拿到MIME类型的文件,就可以解析了 。 譬如text/html、 image/jpeg 、application/xml,json格式,都属于MIME类型 URI和URL 要了解http的传输,首先要知道传输的对象是什么。 URI:web服务器中某个资源的名字。 譬如index.html URL:网络资源描述 举个栗子: ( http://)[resume.tengshe789.tech][:80]/java/index.html[?query-string] #location 这时个典型的url,url里面是什么意思呢? schema(协议): http/https/ftp. host: web服务器的ip地址或者域名 port: 服务端端口, http默认访问的端口是80 path: 资源访问路径#location query-string: 查询参数[?query-string] 方法 每个请求都会携带GET/PUT/DELETE/POST/HEAD这样的一个方法,服务器拿到方法就知道自己该做什么了。 那java中有doGet()doPost()方法,有什么区别呢? doGet:GET方法会把名值对追加在请求的URL后面。因为URL对字符数目有限制,进而限制了用在客户端请求的参数值的数目。并且请求中的参数值是可见的,因此,敏感信息不能用这种方式传递。 doPOST:POST方法通过把请求参数值放在请求体中来克服GET方法的限制,因此,可以发送的参数的数目是没有限制的。最后,通过POST请求传递的敏感信息对外部客户端是不可见的。 方法 GET POST 缓存 能被缓存 不能缓存 编码类型 application/x-www-form-urlencoded application/x-www-form-urlencoded 或 multipart/form-data。为二进制数据使用多重编码。 对数据长度的限制 是的。当发送数据时,GET 方法向 URL 添加数据;URL 的长度是受限制的(URL 的最大长度是 2048 个字符) 无限制。 对数据类型的限制 只允许 ASCII 字符 没有限制。也允许二进制数据。 安全性 与 POST 相比,GET 的安全性较差,因为所发送的数据是 URL 的一部分。在发送密码或其他敏感信息时绝不要使用 GET POST 比 GET 更安全,因为参数不会被保存在浏览器历史或 web 服务器日志中。 可见性 数据在 URL 中对所有人都是可见的。 数据不会显示在 URL 中。 报文http交换与传输的数据单元是报文。报文由从客户机到服务器的请求和从服务器到客户机的响应构成。 应答报文格式如下: 状态行 - 通用信息头 - 响应头 - 实体头 - 报文主体 状态码元由3位数字组成,表示请求是否被理解或被满足。原因分析是对原文的状态码作简短的描述,状态码用来支持自动操作,而原因分析用来供用户使用。客户机无需用来检查或显示语法。有关通用信息头,响应头和实体头方面的具体内容可以参照相关文件。 requestrequest报文格式如下: 请求行 - 通用信息头 - 请求头 - 实体头 - 报文主体 request消息结构包含三部分: (起始行、首部字段、主体) 下面抓包验证,抓包使用的是Charles这个软件。 如图所示,起始行: METHOD /path / http/version-number 首部字段: 头信息Header-Name:value 主体 返回内容optional request body 请求行以方法字段开始,后面分别是 URL 字段和 HTTP 协议版本字段,并以 CRLF 结尾。SP 是分隔符。除了在最后的 CRLF 序列中 CF 和 LF 是必需的之外,其他都可以不要。有关通用信息头,请求头和实体头方面的具体内容可以参照相关文件。 responseresponse报文格式如下: 状态行 - 通用信息头 - 响应头 - 实体头 - 报文主体 第一部分 包括 协议版本http/version-number 状态status code message 第二部分 头信息header-name:value 第三部分 body 状态码元由3位数字组成,表示请求是否被理解或被满足。原因分析是对原文的状态码作简短的描述,状态码用来支持自动操作,而原因分析用来供用户使用。客户机无需用来检查或显示语法。有关通用信息头,响应头和实体头方面的具体内容可以参照相关文件。 常见状态码http/1.1版本的协议里面定义了五种类型的状态码: 1XX 提示信息 2XX 成功 3XX 重定向 4XX 客户端错误 5XX 服务器端的错误 消息 描述 100 Continue 服务器仅接收到部分请求,但是一旦服务器并没有拒绝该请求,客户端应该继续发送其余的请求。 101 Switching Protocols 服务器转换协议:服务器将遵从客户的请求转换到另外一种协议。 消息 描述 200 OK 请求成功(其后是对GET和POST请求的应答文档。) 201 Created 请求被创建完成,同时新的资源被创建。 202 Accepted 供处理的请求已被接受,但是处理未完成。 203 Non-authoritative Information 文档已经正常地返回,但一些应答头可能不正确,因为使用的是文档的拷贝。 204 No Content 没有新文档。浏览器应该继续显示原来的文档。如果用户定期地刷新页面,而Servlet可以确定用户文档足够新,这个状态代码是很有用的。 205 Reset Content 没有新文档。但浏览器应该重置它所显示的内容。用来强制浏览器清除表单输入内容。 206 Partial Content 客户发送了一个带有Range头的GET请求,服务器完成了它。 消息 描述 300 Multiple Choices 多重选择。链接列表。用户可以选择某链接到达目的地。最多允许五个地址。 301 Moved Permanently 所请求的页面已经转移至新的url。 302 Found 所请求的页面已经临时转移至新的url。 303 See Other 所请求的页面可在别的url下被找到。 304 Not Modified 未按预期修改文档。客户端有缓冲的文档并发出了一个条件性的请求(一般是提供If-Modified-Since头表示客户只想比指定日期更新的文档)。服务器告诉客户,原来缓冲的文档还可以继续使用。 305 Use Proxy 客户请求的文档应该通过Location头所指明的代理服务器提取。 306 Unused 此代码被用于前一版本。目前已不再使用,但是代码依然被保留。 307 Temporary Redirect 被请求的页面已经临时移至新的url。 消息 描述 400 Bad Request 服务器未能理解请求。 401 Unauthorized 被请求的页面需要用户名和密码。 401.1 登录失败。 401.2 服务器配置导致登录失败。 401.3 由于 ACL 对资源的限制而未获得授权。 401.4 筛选器授权失败。 401.5 ISAPI/CGI 应用程序授权失败。 401.7 访问被 Web 服务器上的 URL 授权策略拒绝。这个错误代码为 IIS 6.0 所专用。 402 Payment Required 此代码尚无法使用。 403 Forbidden 对被请求页面的访问被禁止。 403.1 执行访问被禁止。 403.2 读访问被禁止。 403.3 写访问被禁止。 403.4 要求 SSL。 403.5 要求 SSL 128。 403.6 IP 地址被拒绝。 403.7 要求客户端证书。 403.8 站点访问被拒绝。 403.9 用户数过多。 403.10 配置无效。 403.11 密码更改。 403.12 拒绝访问映射表。 403.13 客户端证书被吊销。 403.14 拒绝目录列表。 403.15 超出客户端访问许可。 403.16 客户端证书不受信任或无效。 403.17 客户端证书已过期或尚未生效。 403.18 在当前的应用程序池中不能执行所请求的 URL。这个错误代码为 IIS 6.0 所专用。 403.19 不能为这个应用程序池中的客户端执行 CGI。这个错误代码为 IIS 6.0 所专用。 403.20 Passport 登录失败。这个错误代码为 IIS 6.0 所专用。 404 Not Found 服务器无法找到被请求的页面。 404.0 (无)–没有找到文件或目录。 404.1 无法在所请求的端口上访问 Web 站点。 404.2 Web 服务扩展锁定策略阻止本请求。 404.3 MIME 映射策略阻止本请求。 405 Method Not Allowed 请求中指定的方法不被允许。 406 Not Acceptable 服务器生成的响应无法被客户端所接受。 407 Proxy Authentication Required 用户必须首先使用代理服务器进行验证,这样请求才会被处理。 408 Request Timeout 请求超出了服务器的等待时间。 409 Conflict 由于冲突,请求无法被完成。 410 Gone 被请求的页面不可用。 411 Length Required “Content-Length” 未被定义。如果无此内容,服务器不会接受请求。 412 Precondition Failed 请求中的前提条件被服务器评估为失败。 413 Request Entity Too Large 由于所请求的实体的太大,服务器不会接受请求。 414 Request-url Too Long 由于url太长,服务器不会接受请求。当post请求被转换为带有很长的查询信息的get请求时,就会发生这种情况。 415 Unsupported Media Type 由于媒介类型不被支持,服务器不会接受请求。 416 Requested Range Not Satisfiable 服务器不能满足客户在请求中指定的Range头。 417 Expectation Failed 执行失败。 423 锁定的错误。 消息 描述 500 Internal Server Error 请求未完成。服务器遇到不可预知的情况。 500.12 应用程序正忙于在 Web 服务器上重新启动。 500.13 Web 服务器太忙。 500.15 不允许直接请求 Global.asa。 500.16 UNC 授权凭据不正确。这个错误代码为 IIS 6.0 所专用。 500.18 URL 授权存储不能打开。这个错误代码为 IIS 6.0 所专用。 500.100 内部 ASP 错误。 501 Not Implemented 请求未完成。服务器不支持所请求的功能。 502 Bad Gateway 请求未完成。服务器从上游服务器收到一个无效的响应。 502.1 CGI 应用程序超时。 · 502.2 CGI 应用程序出错。 503 Service Unavailable 请求未完成。服务器临时过载或当机。 504 Gateway Timeout 网关超时。 505 HTTP Version Not Supported 服务器不支持请求中指明的HTTP协议版本。 缓存作用有三,减少客户端请求频率,增加客户端响应速度,减少冗余数据的传输 下图是谷歌浏览器调试模式界面,在header栏中清晰的显示了缓存的最大保持时间 HTTP协议的特点 无状态(每次请求都是独立的) 通过cookie+session的机制,完成它的无状态特点 多次请求 基于TCP协议 协议版本号Http1.1和Http1.0的区别 HTTP/1.0协议使用非持久连接,即在非持久连接下,一个tcp连接只传输一个Web对象; HTTP/1.1默认使用持久连接(然而,HTTP/1.1协议的客户机和服务器可以配置成使用非持久连接)。 HTTP2.0与HTTP1.0的区别 HTTP2.0的多路复用 浏览器对同一域名下的并发连接数量有限制,一般为6个,HTTP1中的Keep-Alive用于长连接而不必重新建立连接,然而keep-alive必须等本次请求彻底完成后才能发送下一个请求,而HTTP2的请求与响应以二进制帧的形式交错进行,只需建立一次连接,即一轮三次握手,实现多路复用。 HTTP2.0压缩消息头 HTTP1的消息头很大冗余,而HTTP2.0利用HPACK对消息头进行压缩传输,假设将常用的请求GET/index.html用1表示,POST/index.html用2表示,即是将消息头中的不同的部分分别用不用的索引进行表示,且会用哈夫曼编码压缩字符串,最后封装成frame。索引表分为动态索引和静态索引,动态索引表在客户端和服务器端共同维护,静态索引采用硬编码形式。 HTTP2.0服务端推送 HTTP2.0中服务器会主动将资源推送给客户端,例如把js和css文件主动推送给客户端而不用客户端解析HTML后请求再响应。 HTTPSHTTP请求过程中,客户端与服务器之间没有任何身份确认的过程,数据全部明文传输,“裸奔”在互联网上,所以很容易遭到黑客的攻击劫持让系统瘫痪,当客户端发送请求很容易被黑客截获,如果黑客冒充目标服务器,则可返回任意信息给客户端,不被客户端所察觉,我们经常会听到“劫持”一词,所以使用直接使用HTTP传输是有风险的。 HTTPS协议(HyperText Transfer Protocol over Secure Socket Layer):可以理解为HTTP+SSL/TLS,即HTTP下加入SSL层,HTTPS的安全基础是 SSL,因此加密的详细内容就需要SSL,用于安全的HTTP数据传输。 历史 网警公司的SSL3.0 SSL(Secure Socket Layer,安全套接字层):1994年为网景所研发,SSL协议位于TCP/IP协议与各种应用层协议之间,为数据通讯提供安全支持。 ISOC这个组织 在SSL的基础上发布了升级版本 TLS1.2 TLS(Transport Layer Security,传输层安全):其前身是SSL,它最初的几个版本(SSL 1.0、SSL 2.0、SSL 3.0)由网景公司开发,1999年从3.1 开始被IETF标准化并改名,发展至今已经有 TLS 1.0、TLS 1.1、TLS 1.2 三个版本。SSL3.0和TLS1.0由于存在安全漏洞,已经很少被使用到。目前使用最广泛的是TLS 1.1、TLS 1.2。 HTTPS的工作原理假设A要给B发“我爱你” 对称加解密如果使用对称加解密 B有密钥,可以进行相应的解密。由于密钥是公开的,所有的客户端都可以拿到,如图: 若,针对不同的客户端使用不同的密钥 又会出现协商问题:由于没有公共的密钥了,服务端要给每个客户端发密钥,但协商过程是没有加密的,所以还会出现被截断的问题 非对称加解密非对称:公钥和私钥的概念 那么问题就来了: 客户端如何拿到公钥? 方案: 服务器端把公钥发送给每一个客户端 服务器端把公钥放到远程服务器,客户端可以请求到 让浏览器保存所有的公钥(不现实) 结论: 公钥被调包的问题按照上面的方案,永远存在。 第三方机构与数字证书这时候出现了通过第三方机构,使用第三方机构的私钥对我们需要传输的公钥进行加密。 数字证书是一个经证书授权中心数字签名的包含公开密钥拥有者信息以及公开密钥的文件。 数字证书里面包含的内容: 公司信息、网站信息、数字证书的算法、公钥 连接过程 如何查看公钥?浏览器有入口直接打开。 Charles也可以更改添加SSL 总体流程 客户端发起一个https请求 a) 客户端支持的加密方式 b) 客户端生成的随机数(第一个随机数) 服务端收到请求后,拿到随机数,返回 a) 证书(颁发机构(CA)、证书内容本身的数字签名(使用第三方机构的私钥加密)、证书持有者的公钥、证书签名用到的hash算法) b) 生成一个随机数,返回给客户端(第二个随机数) 客户端拿到证书以后做验证 a) 根据颁发机构找到本地的跟证书 b) 根据CA得到根证书的公钥,通过公钥对数字签名解密,得到证书的内容摘要 A c) 用证书提供的算法对证书内容进行摘要,得到摘要 B d) 通过A和B的对比,也就是验证数字签名 验证通过以后,生成一个随机数(第三个随机数),通过证书内的公钥对这个随机数加密,发送给服务器端 (随机数1+2+3)通过对称加密得到一个密钥。(会话密钥) 通过会话密钥对内容进行对称加密传输 HTTPS的优点1、SEO方面 谷歌曾在2014年8月份调整搜索引擎算法,并称“比起同等HTTP网站,采用HTTPS加密的网站在搜索结果中的排名将会更高”。 2、安全性 尽管HTTPS并非绝对安全,掌握根证书的机构、掌握加密算法的组织同样可以进行中间人形式的攻击,但HTTPS仍是现行架构下最安全的解决方案,主要有以下几个好处: (1)使用HTTPS协议可认证用户和服务器,确保数据发送到正确的客户机和服务器; (2)HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全,可防止数据在传输过程中不被窃取、改变,确保数据的完整性。 (3)HTTPS是现行架构下最安全的解决方案,虽然不是绝对安全,但它大幅增加了中间人攻击的成本。 HTTPS的缺点(1)SSL证书费用很高,以及其在服务器上的部署、更新维护非常繁琐 (2)HTTPS降低用户访问速度(多次握手) (3)网站改用HTTPS以后,由HTTP跳转到HTTPS的方式增加了用户访问耗时(多数网站采用302跳转) (4)HTTPS涉及到的安全算法会消耗CPU资源,需要增加大量机器(https访问) (5)HTTPS协议的加密范围也比较有限,在黑客攻击、拒绝服务攻击、服务器劫持等方面几乎起不到什么作用。 http与https的区别HTTP的URL以 http://开头,而HTTPS的URL以https://开头。 HTTP是不安全的,而HTTPS是安全的。 HTTP标准端口是80 ,而HTTPS的标准端口是443。 在OSI网络模型中,HTTP工作于应用层,而HTTPS工作在传输层。 HTTP无需加密,而HTTPS对传输的数据进行加密。 HTTP无需证书,而HTTPS需要认证证书。 RESTfulREST :表述性状态转移 RESTful是使用WEB标准来做一些准则和约束。 RESTful的基本概念: 在REST中,一切的内容都被认为是一种资源 每个资源都由URI唯一标识 使用统一的接口处理资源请求(POST/GET/PUT/DELETE/HEAD) 无状态 资源和URI [/]表示资源的层级关系 ?过滤资源 使用_或者-让URI的可读性更好 统一接口 GET 获取某个资源。 幂等 POST 创建一个新的资源 PUT 替换某个已有的资源(更新操作) , 幂等 DELETE 删除某个资源 PATCH/HEAD 更新部分资源 资源表述客户端通过HTTP获取资源 MIME 类型() accept: text/xml html文件 Content-Type告诉客户端资源的表述形式 资源链接 超媒体即应用状态引擎 状态转移服务器端不应该保存客户端状态。 应用状态- >服务器端不保存应用状态 参考文献百度百科 感谢! 结束 此片完了~ 想要了解更多精彩新姿势?请访问我的个人博客 本篇为原创内容,已在个人博客率先发表,随后CSDN,segmentfault,掘金,简书,开源中国同步发出。如有雷同,缘分呢兄弟。赶快加个好友~]]></content>
<tags>
<tag>技术</tag>
<tag>学好分布式架构</tag>
<tag>网络</tag>
</tags>
</entry>
<entry>
<title><![CDATA[学好分布式架构=3=-分布式事务]]></title>
<url>%2F2018%2F08%2F16%2F%E5%AD%A6%E5%A5%BD%E5%88%86%E5%B8%83%E5%BC%8F%E6%9E%B6%E6%9E%84-%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1%2F</url>
<content type="text"><![CDATA[这是第五期文章。这一期我们聊聊分布式事务 想要解锁更多新姿势?请访问我的博客 数据库单机事务要了解分布式事务,首先要明白什么是“事务”。。 可以参考事务的定义: 事务时数据库运行中的一个逻辑工作单元,工作单元内的一系列SQL命令具有原子性操作的特点,这些命令要么完全成功运行,要么完全撤销或者不执行,如果时后者,则表现为数据库内的最终数据没有发生改变。 数据库事务满足4个要求: 原子性:事务必须时原子工作单元,对其进行数据修改,要么全都执行,要么都不执行 一致性:事务在完成时,必须使所有数据都保持一致的状态,事务结束时,所有内部数据结构都必须时正确的 隔离性:由并发事务做出的修改必须和其他并发事务所做的修改隔离 持久性:事务完成后,对系统的影响时永久的 这其中原子性,需要记录操作过程和对应结果,以便于回退;而隔离性,需要产生锁;这两种要求,导致数据库事务执行代价要远高于非事务性操作 MySQL执行事务在MySQL里,事务相关的日志为redo和undo两个文件,redo log记录事务修改后的数据,undo log记录事务前的数据 下面时MySQL执行事务的简化流程: 先记录undo/redo log ,确保日志写入硬盘 更新数据记录,缓存操作并异步刷新硬盘 commit事务操作。先清理undo信息,然后释放锁资源,在redo中写入commit操作,刷新后确保redo完成并存在硬盘中,这样可以保证数据库的完整性和一致性。 可以看到,很多操作,譬如写日志以防回滚,都是在写commit之前完成的。commit指令在事务中所占时间非常少,这就时事务的一个重要特点。 X/OpenDTP事务模型分布式事务中,一个事务内的SQL往往操作很多数据库,也要保证事务的4个要求。所以就有X/OpenDTP这个规范了 X/Open DTP 定义了三个角色: AP,TM,RM AP(Application Program):也就是应用程序,可以理解为使用DTP的程序,我们crud的代码就是这个 RM(Resource Manager):资源管理器,这里可以理解为一个DBMS系统,或者消息服务器管理系统,应用程序通过资源管理器对资源进行控制。资源必须实现XA定义的接口 TM(Transaction Manager):事务管理器,负责协调和管理事务,提供给AP应用程序编程接口以及管理资源管理器 其中,AP 可以和TM 以及 RM 通信,TM 和 RM 互相之间可以通信,DTP模型里面定义了XA接口,TM 和 RM 通过XA接口进行双向通信,例如:TM通知RM提交事务或者回滚事务,RM把提交结果通知给TM。AP和RM之间则通过RM提供的Native API 进行资源控制,这个没有进行约API和规范,各个厂商自己实现自己的资源控制,比如Oracle自己的数据库驱动程序。 其中在DTP定了以下几个概念: 事务:一个事务是一个完整的工作单元,由多个独立的计算任务组成,这多个任务在逻辑上是原子的。 全局事务:对于一次性操作多个资源管理器的事务,就是全局事务 分支事务:在全局事务中,某一个资源管理器有自己独立的任务,这些任务的集合作为这个资源管理器的分支任务 控制线程:用来表示一个工作线程,主要是关联AP,TM,RM三者的一个线程,也就是事务上下文环境。简单的说,就是需要标识一个全局事务以及分支事务的关系。 两阶段提交协议下面说说这个模型中最著名的二阶段提交协议 当一个分布式事务所设计的SQL都执行完成,打了RM最后提交事务的时候,为了避免分布式系统所固有的不可靠性导致提交事务失败,TM这时候会走二阶段提交协议 两阶段提交协议:如果一个事务管理器管理着多个资源管理器,如果控制全局事务和分支事务,在DTP里面说明两阶段提交的协议 第一阶段:发起投票表决 通知所有RM先完成事务提交涉及的复杂准备工作,譬如上文提到的redo日志,尽量把提交过程中所有消耗时间的工作提前完成,确保后面事务100%成功 第二阶段:正式提交 TM将第一阶段的结果进行决策,即提交或取消事务。当且仅当所有参与的RM同意提交时,TM才通知所有RM正式提交事务,否则则取消事务。 下面引用参考文献的一幅图,详细演示了正常情况下的两阶段提交, 如果第一阶段某一个资源预提交失败,第二阶段就回滚第一阶段已经预提交成功的资源 以上是比较正常的情况,但是由于RM有权利自己根据情况提交或者回滚自己的分支事务(官方说法是:Heuristic Decision)那三么就可能出现以下种情况: 1 在TM通知RM提交事务之前,RM分支事务已经提交 2 在TM通知RM提交事务之前,RM分支事务全部回滚 3 在TM通知RM提交事务之前,RM分支事务部分回滚 互联网中的分布式事务解决方案下面介绍三种互联网中的分布式事务解决方案 第一种:不用分布式这个方案时将一个业务流程中需要在一个事务中执行多个相关业务接口包装到一个事务中,从而避免分布式事务。 第二种:eBay方案第三种: 支付宝DTS框架参考资料X/Open DTP(X/Open Distributed Transaction Processing Reference Model) 感谢 结束 此片完了~ 想要了解更多精彩新姿势?请访问我的个人博客 本篇为原创内容,已在个人博客率先发表,随后CSDN,segmentfault,掘金,简书,开源中国同步发出。如有雷同,缘分呢兄弟。赶快加个好友,咱们两个想个号码, 买个彩票,先挣他个几百万😝]]></content>
<tags>
<tag>技术</tag>
<tag>学好分布式架构</tag>
</tags>
</entry>
<entry>
<title><![CDATA[学好分布式架构=1=-TCP/IP]]></title>
<url>%2F2018%2F08%2F16%2F%E6%88%91%E8%A6%81%E5%AD%A6%E5%A5%BD%E5%88%86%E5%B8%83%E5%BC%8F-TCP-IP%2F</url>
<content type="text"><![CDATA[分布式与网络互联尤为密切,而TCP/IP(传输控制协议/互联网络协议)是网络中最基本的协议。TCP/IP结合DNS、路由相关的协议最终实现的了网络的任意两点的数据通信问题。 想要解锁更多新姿势?请访问我的博客。 协议怎么分层的呢?提到协议分层,我们很容易联想到ISO-OSI的七层协议经典架构,但是TCP/IP协议族的结构则稍有不同。 OSI七层网络模型 TCP/IP四层概念模型 对应网络协议 应用层(Application) 应用层 HTTP、TFTP, FTP, NFS, WAIS、SMTP 表示层(Presentation) Telnet, Rlogin, SNMP, Gopher 会话层(Session) SMTP, DNS 传输层(Transport) 传输层 TCP, UDP 网络层(Network) 网络层 IP, ICMP, ARP, RARP, AKP, UUCP 数据链路层(Data Link) 数据链路层 FDDI, Ethernet, Arpanet, PDN, SLIP, PPP 物理层(Physical) IEEE 802.1A, IEEE 802.2到IEEE 802.11 TCP/IP协议族按照层次由上到下,层层包装。 最上面的就是应用层了,这里面有http,ftp,等等我们熟悉的协议。 第二层则是传输层,著名的TCP和UDP协议就在这个层次,我们熟知的网页大部分是由TCP传输的,而玩的游戏大部分是由UDP传输的。 第三层是网络层,IP协议就在这里,它负责对数据加上IP地址和其他的数据以确定传输的目标。 第四层是叫数据链路层,这个层次为待传送的数据加入一个以太网协议头,并进行CRC编码,为最后的数据传输做准备。 TCP/IP四层协议与OSI七层网络模型层层相对,结构分明。 协议是什么样子的呢?打开cmd(本人windows平台),输入运行ipconfig/all 这里就可以清楚的显示出来我PC的各种网路信息,甚至我PC几个网卡都能看的一清二楚。 传输过程本地传输如果我们浏览网页,通常会在浏览器地址栏中打上3个w和网站的名字,就譬如打开百度需要输入WWW.BAIDU.COM,这个名字就叫域名。我们的浏览器会发现url中网站的域名,然后查找本地的host文件,根据host文件查找响应的dns服务器ip地址,用UDP协议向dns服务器发送dns查询命令,dns服务器获取到查询命令并从缓存或者数据库中查询改域名对应主机的ip地址,再将这个ip地址发到你浏览器上。 有点绕口,实际你可以使用cmd的nslookup来跟踪这个过程。 我们看到,使用多次命令查询,得到的ip结果总是不一样的。这是因为dns有负载均衡机制,服务器会查询并返回多个ip地址,每次查询返回的ip地址可能有所不同。 接着往下说,当我们浏览器获知这个ip地址的时候,就向这个ip地址的80端口(HTTP标准端口是80 ,而HTTPS的标准端口是443)发起TCP连接,但其实这个ip地址并不是计算机所在的局域网,而是一个广域网地址,所以子网的网关就需要通过路由转发了 路由互联网是由很多相互隔离的小网络无限延伸而成的一个大网络,路由器负责将多个网络连接,并交换路由表信息来确定一个IP报文要经过哪个路由器的哪个端口发给哪个互联的子网。 为了防止一个数据包在发送过程中形成死循环,ip包中的TTL字段每经过路由器转发一次,就会-1。当TTL为0时则被丢弃,认为网络不可达,一般默认的最大TTL是30 同样我们可以使用tracert来跟踪这个过程。tacert是根据ICMP协议来确定一个ip报文到达目标地址所经过的路由器节点 Socketsocket时一个ip地址与端口的组合,是计算机的远程通信接口,本地一个socket与远程socket连接的过程,就是三次握手的过程。 三次握手 第一次握手:客户端发送syn包(SYN = 1,序列号seq = x,SYN报文段不能携带数据,但要消耗一个初始序号)到服务器,并进入SYN_SEND状态,等待服务器确认。 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(SYN=1 ACK=1 seq=y,同样消耗一个序号),即SYN+ACK包,此时服务器进入SYN_RECV状态。 第三次握手:客户端收到服务器的SYN+ACK包 ,向服务器发送确认包ACK(ACK = 1 seq = x + 1 ack = y + 1,ACK报文段可以携带数据,但如果不携带数据则不消耗序号,这种情况下,下一个数据报文段的序号仍是seq=x+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。 各个状态名称与含义CLOSED: 表示初始状态。LISTEN: 服务器端处于监听状态,可以接受连接。SYN_RECV: 当收到客户端的ACK报文后,服务器会进入到ESTABLISHED状态。SYN_SENT: 当客户端发送SYN报文,随即它会进入到了SYN_SENT状态,并等待服务端的发送三次握手中的第2个报文。ESTABLISHED:表示连接已经建立了。 四次挥手 第一次挥手:主动关闭方发送一个FIN,用来关闭主动方到被动关闭方的数据传送,也就是主动关闭方告诉被动关闭方:我已经不会再给你发数据了(当然,在fin包之前发送出去的数据,如果没有收到对应的ack确认报文,主动关闭方依然会重发这些数据),但是,此时主动关闭方还可以接受数据。 第二次挥手:被动关闭方收到FIN包后,发送一个ACK给对方,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号)。 第三次挥手:被动关闭方发送一个FIN,用来关闭被动关闭方到主动关闭方的数据传送,也就是告诉主动关闭方,我的数据也发送完了,不会再给你发数据了。 第四次挥手:主动关闭方收到FIN后,发送一个ACK给被动关闭方,确认序号为收到序号+1,至此,完成四次挥手。 各个状态名称与含义 FIN-WAIT-1:等待远程TCP的连接中断请求。 FIN-WAIT-2:从远程TCP等待连接中断请求。 CLOSE-WAIT:等待从本地用户发来的连接中断请求。 CLOSING:等待远程TCP对连接中断的确认。 LAST-ACK:等待原来发向远程TCP的连接中断请求的确认; TIME-WAIT:等待足够的时间以确保远程TCP接收到连接中断请求的确认 附录常见端口 FTP 21 telnet 23 域名服务器 53 POP3 110 MySQL 3306 POP-2 109 SNMP 161 SSH 22 SMTP 25 HTTP 80 NTP 123 shell或者cmd 514 SQL Server 1433 结束 此片完了~ 想要了解更多精彩新姿势?请访问我的个人博客 本篇为原创内容,已在个人博客率先发表,随后CSDN,segmentfault,掘金,简书,开源中国同步发出。如有雷同,缘分呢兄弟。赶快加个好友,咱们两个想个号码, 买个彩票,先挣他个几百万😝]]></content>
<tags>
<tag>技术</tag>
<tag>学好分布式架构</tag>
<tag>网络</tag>
</tags>
</entry>
<entry>
<title><![CDATA[收到一个令我悲伤的消息]]></title>
<url>%2F2018%2F08%2F06%2F%E6%94%B6%E5%88%B0%E4%B8%80%E4%B8%AA%E4%BB%A4%E6%88%91%E6%82%B2%E4%BC%A4%E7%9A%84%E6%B6%88%E6%81%AF%2F</url>
<content type="text"><![CDATA[我的该死的VPS提供商又要破pao产lu了,气死我也。最近的vps服务都不能用了,MySQL一连一个失败。后来他们给我发了一封邮件 👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇 👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆 大概意思是他们完了,要把机器服务转给我的另一个老东家NFHosting,对,没错。就是给我提供上一个博客服务的数据中心。 该死的,我上一个博客的数据至今没找回来! 行吧,我已经无所谓了。就当考验我的运维水平了,搭建个RabbitMQ,MySQL,redis,阿帕奇,对这种单核VPS,不就一上午么?我干还不行么? 于是乎,我按照他们的流程回复了邮件。 然后,就没有然后了。这家公司的工程师没一个理我的🙃 不能自甘堕落啊!我心中燃气了雄凶大火! 于是乎,绝望的发了个repost 希望神与我同在吧。。。]]></content>
<tags>
<tag>日常</tag>
</tags>
</entry>
<entry>
<title><![CDATA[不要懒惰,加油]]></title>
<url>%2F2018%2F08%2F04%2F%E5%8A%A0%E6%B2%B9%2F</url>
<content type="text"><![CDATA[最近母亲要去朝鲜旅游,心系母亲,有点略微浮躁。好久没持续学习了,罪过罪过。 用山本耀司的话给自己打鸡血: 我从不相信什么懒洋洋的自由,我向往的自由是通过勤奋和努力实现更广阔的人生,那样的自由才是珍贵的、有价值的。我相信一万小时定律,我从来不相信天上掉馅饼的灵感和坐等的成就。做一个自由又自律的人,靠势必实现的决心认真地活着。 没有绝对正确的事情,你能做的就是更加努力,这样才能让一切变得都是最好的安排!自己啊,一定要加油!]]></content>
<tags>
<tag>日常</tag>
<tag>生活</tag>
<tag>吐槽</tag>
</tags>
</entry>
<entry>
<title><![CDATA[这是一篇优雅的Springboot2.0使用手册]]></title>
<url>%2F2018%2F08%2F04%2Fspringboot%2F</url>
<content type="text"><![CDATA[最近再研究springboot的原理😋颇有收获,现在让我分享一下springboot如何使用吧~ 想要解锁更多新姿势?请访问我的博客 啥是Springboot和书上理解的不同,我认为Springboot是一个优秀的快速搭建框架,他通过maven继承方式添加依赖来整合很多第三方工具,可以避免各种麻烦的配置,有各种内嵌容器简化Web项目,还能避免依赖的干扰,它内置tomcat,jetty容器,使用的是java app运行程序,而不是传统的用把war放在tomcat等容器中运行 和JFinal的区别JFinal是国人出品的一个web + orm 框架 ,JFinal,优点是开发迅速、代码量少、学习简单、功能强大、轻量级、易扩展。核心就是极致简洁。他没有商业机构的支持,所以宣传不到位,少有人知。 Springboot相比与JFinal最大的优点就是支持的功能非常多,可以非常方便的将spring的各种框架如springframework , spring-mvc, spring-security, spring-data-jpa, spring-cache等等集成起来进行自动化配置 ,而且生态 比较好,很多产品都对Springboot做出一定支持。 与Springcloud的区别可以这么理解,Springboot里面包含了Springcloud,Springcloud只是Springboot里面的一个组件而已。 Springcloud提供了相当完整的微服务架构。而微服务架构,本质来说就是分布式架构,意味着你要将原来是一个整体的项目拆分成一个个的小型项目,然后利用某种机制将其联合起来,例如服务治理、通信框架等基础设施。 SpringBoot和SpringMVC区别SpringBoot的Web组件,默认集成的是SpringMVC框架。 快速使用要往下看的话,注意了👇 Springboot 2.x 要求 JDK 1.8 环境及以上版本。另外,Springboot 2.x 只兼容 Spring Framework 5.0 及以上版本。 为 Springboot 2.x 提供了相关依赖构建工具是 Maven,版本需要 3.2 及以上版本。使用 Gradle 则需要 1.12 及以上版本。 建议用IntelliJ IDEA IntelliJ IDEA (简称 IDEA) 建立项目我已经好久没用Eclipse了,要知道Eclipse是创建一个maven项目在引入Springboot依赖创建的。 下面我分享一下用IDEA创建Springboot的方法。 很简单,在这个界面里面就可以创建Springboot了。接下来在添加一些组件。 大功告成! 写一个DEMO这里用我写的一个秒杀项目作为参考栗子。秒杀商城 创建一个conntroller包,编写一个样列。 123456789101112131415package cn.tengshe789.controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/demo")public class SampleController { @RequestMapping("/hello") public String index() { return "Hello World"; }} 接下来在他同级包或者上一级的包内,创建一个主方法MainApplication。方法内容; 123456789@SpringBootApplication@EnableAsync//@ComponentScan("cn.tengshe789.controller")//@EnableAutoConfigurationpublic class MainApplication { public static void main(String[] args) { SpringApplication.run(MainApplication.class, args); }} 在浏览器输入http://127.0.0.1:8080/demo/hello/,就可以启动了! SpringApplication.runSpringboot将他标识为启动类,用它启动Springboot项目 基础注解解释@RestController在上加上RestController 表示修饰该Controller所有的方法返回JSON格式,直接可以编写Restful接口。就相当于@Controller+@ResponseBody这种实现 @SpringBootApplication用在启动Springboot中,相当于@ComponentScan+@EnableAutoConfiguration+@Configuration @ComponentScan(“cn.tengshe789.controller”)控制器扫包范围。 @EnableAutoConfiguration他让 Spring Boot 根据咱应用所声明的依赖来对 Spring 框架进行自动配置。意思是,创建项目时添加的spring-boot-starter-web添加了Tomcat和Spring MVC,所以auto-configuration将假定你正在开发一个web应用并相应地对Spring进行设置。 配置文件properties规则: 1、名用大写比较规范 2、=两边别打空格 3、名值对写完后别打分号 自定义参数1name=tengshe789 多环境配置12345spring.profiles.active=preapplication-dev.properties:开发环境application-test.properties:测试环境application-prod.properties:生产环境 修改端口号12server.port=8888 server.context-path=/tengshe789 yaml规则: 使用空格 Space 缩进表示分层,不同层次之间的缩进可以使用不同的空格数目,但是同层元素一定左对齐,即前面空格数目相同(不能使用 Tab,各个系统 Tab对应的 Space 数目可能不同,导致层次混乱) ‘#’表示注释,只能单行注释,从#开始处到行尾 破折号后面跟一个空格(a dash and space)表示列表 用冒号和空格表示键值对 key: value 简单数据(scalars,标量数据)可以不使用引号括起来,包括字符串数据。用单引号或者双引号括起来的被当作字符串数据,在单引号或双引号中使用C风格的转义字符 123server: port: 8080 context-path: /springboot xmlSpringboot官方不推荐xml,略 Web开发一个项目用Springboot,十有八九就是用于Web开发。首先让我们看看Springboot怎么快速开发Web把 如何访问静态资源请在resources目录下创建static文件夹,在该位置放置一个静态资源。 目录:src/main/resources/static 启动程序后,尝试访问http://localhost:8080/img.xxx/。就可以访问了。 关于渲染Web页面在之前的快速使用的示例中,我们都是通过添加@RestController来处理请求,所以返回的内容为json对象。那么如果需要渲染html页面的时候,要如何实现呢? 模板引擎方法Springboot依然可以实现动态HTML,并且提供了多种模板引擎的默认配置支持,Springboot官方文档有如下推荐的模板引擎: · Thymeleaf · FreeMarker · Velocity · Groovy · Mustache Springboot官方建议避免使用JSP,若一定要使用JSP将无法实现Spring Boot的多种特性。 在Springboot中,默认的模板配置路径都时:src/main/resources/templates。当然也可以修改这个路径,具体如何修改,可在各模板引擎的配置属性中查询并修改。 Thymeleaf(胸腺)这里还是用我写的一个秒杀项目作为参考栗子。秒杀商城 POM1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> 配置文件在application.properties中添加: 123456789#thymeleafspring.thymeleaf.prefix=classpath:/templates/spring.thymeleaf.suffix=.htmlspring.thymeleaf.cache=falsespring.thymeleaf.servlet.content-type=text/htmlspring.thymeleaf.enabled=truespring.thymeleaf.encoding=UTF-8# 一代填 spring.thymeleaf.mode=HTML5spring.thymeleaf.mode=HTML 后台在src/main/resources/创建一个templates文件夹,新网页后缀为*.html 12345678@RequestMapping("/to_list") public String list(Model model,MiaoshaUser user) { model.addAttribute("user", user); //查询商品列表 List<GoodsVo> goodsList = goodsService.listGoodsVo(); model.addAttribute("goodsList", goodsList); return "goods_list"; } 前台这里注意Thymeleaf语法,Thymeleaf很像HTML,不同之处在标签加了一个th前缀 1234567891011121314151617181920212223242526<!DOCTYPE HTML><html xmlns:th="http://www.thymeleaf.org"><head> <title>商品列表</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <!-- jquery --> <script type="text/javascript" th:src="@{/js/jquery.min.js}"></script></head><body><div class="panel panel-default" > <div class="panel-heading">秒杀商品列表</div> <table class="table" id="goodslist"> <tr><td>商品名称</td><td>商品图片</td><td>商品原价</td><td>秒杀价</td><td>库存数量</td><td>详情</td></tr> <tr th:each="goods,goodsStat : ${goodsList}"> <td th:text="${goods.goodsName}"></td> <td ><img th:src="@{${goods.goodsImg}}" width="100" height="100" /></td> <td th:text="${goods.goodsPrice}"></td> <td th:text="${goods.miaoshaPrice}"></td> <td th:text="${goods.stockCount}"></td> <td><a th:href="'/goods_detail.htm?goodsId='+${goods.id}">详情</a></td> </tr> </table></div></body></html> Freemarker(自由标记)POM1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId></dependency> 配置文件在application.properties中添加: 12345678910111213141516#Freemarkerspring.freemarker.allow-request-override=falsespring.freemarker.cache=truespring.freemarker.check-template-location=truespring.freemarker.charset=UTF-8spring.freemarker.content-type=text/htmlspring.freemarker.expose-request-attributes=falsespring.freemarker.expose-session-attributes=falsespring.freemarker.expose-spring-macro-helpers=false#spring.freemarker.prefix=#spring.freemarker.request-context-attribute=#spring.freemarker.settings.*=spring.freemarker.suffix=.ftlspring.freemarker.template-loader-path=classpath:/templates/#comma-separated list#spring.freemarker.view-names= # whitelist of view names that can be resolved 后台在src/main/resources/创建一个templates文件夹,新网页后缀为*.ftl 1234567891011@RequestMapping("/freemarkerIndex") public String index(Map<String, Object> result) { result.put("nickname", "tEngSHe789"); result.put("old", "18"); result.put("my Blog", "HTTPS://tengshe789.github.io/"); List<String> listResult = new ArrayList<String>(); listResult.add("guanyu"); listResult.add("zhugeliang"); result.put("listResult", listResult); return "index"; } 前台123456789101112131415161718192021<!DOCTYPE html><html><head lang="en"><meta charset="UTF-8" /><title>首页</title></head><body> ${nickname}<#if old=="18"> 太假了吧哥们 <#elseif old=="21"> 你是真的21岁 <#else> 其他 </#if> <#list userlist as user> ${user} </#list></body> </html> JSP不建议用Springboot整合JSP,要的话一定要为war类型,否则会找不到页面.,而且不要把JSP页面存放在resources// jsp 不能被访问到 POM123456789101112131415161718192021<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.4.RELEASE</version></parent><dependencies> <!-- SpringBoot web 核心组件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </dependency><!-- SpringBoot 外部tomcat支持 --> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> </dependency></dependencies> 配置文件在application.properties中添加: 12spring.mvc.view.prefix=/WEB-INF/jsp/spring.mvc.view.suffix=.jsp 后台在src/main/resources/创建一个templates文件夹,新网页后缀为*.jsp 1234567@Controllerpublic class IndexController { @RequestMapping("/index") public String index() { return "index"; }} 前台略略略😝 面向流编程要了解 WebFlux ,首先了解下什么是Reactive响应式(反应式、异步、面向流)编程 ,他是一种新的编程风格,其特点是异步或并发、事件驱动、推送PUSH机制以及观察者模式的衍生。reactive应用(响应式应用)允许开发人员构建事件驱动(event-driven),可扩展性,弹性的反应系统:提供高度敏感的实时的用户体验感觉,可伸缩性和弹性的应用程序栈的支持,随时可以部署在多核和云计算架构。 Spring WebfluxSpring Boot Webflux 就是基于 Reactor 实现的。Spring Boot 2.0 包括一个新的 spring-webflux 模块。该模块包含对响应式 HTTP 和 WebSocket 客户端的支持,以及对 REST,HTML 和 WebSocket 交互等程序的支持。一般来说,Spring MVC 用于同步处理,Spring Webflux 用于异步处理。 Spring Boot Webflux 有两种编程模型实现,一种类似 Spring MVC 注解方式,另一种是使用其功能性端点方式。 WebFlux 支持的容器有 Tomcat、Jetty(Non-Blocking IO API) ,也可以像 Netty 和 Undertow 的本身就支持异步容器。在容器中 Spring WebFlux 会将输入流适配成 Mono 或者 Flux 格式进行统一处理。 POM1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId></dependency> 官方实例1234567891011121314151617181920212223@RestControllerpublic class PersonController { private final PersonRepository repository; public PersonController(PersonRepository repository) { this.repository = repository; } @PostMapping("/person") Mono<Void> create(@RequestBody Publisher<Person> personStream) { return this.repository.save(personStream).then(); } @GetMapping("/person") Flux<Person> list() { return this.repository.findAll(); } @GetMapping("/person/{id}") Mono<Person> findById(@PathVariable String id) { return this.repository.findOne(id); }} Controller层Spring Boot 2.0 这里有两条不同的线分别是: Spring Web MVC -> Spring Data Spring WebFlux -> Spring Data Reactive 如果使用 Spring Data Reactive ,原来的 Spring 针对 Spring Data (JDBC等)的事务管理会不起作用。因为原来的 Spring 事务管理(Spring Data JPA)都是基于 ThreadLocal 传递事务的,其本质是基于 阻塞 IO 模型,不是异步的。 但 Reactive 是要求异步的,不同线程里面 ThreadLocal 肯定取不到值了。自然,我们得想想如何在使用 Reactive 编程是做到事务,有一种方式是 回调 方式,一直传递 conn :newTransaction(conn ->{}) 因为每次操作数据库也是异步的,所以 connection 在 Reactive 编程中无法靠 ThreadLocal 传递了,只能放在参数上面传递。虽然会有一定的代码侵入行。进一步,也可以 kotlin 协程,去做到透明的事务管理,即把 conn 放到 协程的局部变量中去。那 Spring Data Reactive Repositories 不支持 MySQL,进一步也不支持 MySQL 事务,怎么办? 答案是,这个问题其实和第一个问题也相关。 为啥不支持 MySQL,即 JDBC 不支持。大家可以看到 JDBC 是所属 Spring Data 的。所以可以等待 Spring Data Reactive Repositories 升级 IO 模型,去支持 MySQL。也可以和上面也讲到了,如何使用 Reactive 编程支持事务。 如果应用只能使用不强依赖数据事务,依旧使用 MySQL ,可以使用下面的实现,代码如下: 12 Service 层1234567891011121314151617181920212223242526272829303132333435363738394041public interface CityService { /** * 获取城市信息列表 * * @return */ List<City> findAllCity(); /** * 根据城市 ID,查询城市信息 * * @param id * @return */ City findCityById(Long id); /** * 新增城市信息 * * @param city * @return */ Long saveCity(City city); /** * 更新城市信息 * * @param city * @return */ Long updateCity(City city); /** * 根据城市 ID,删除城市信息 * * @param id * @return */ Long deleteCity(Long id);} 具体案例在我参考博主的 Github 路由器类 Router创建一个 Route 类来定义 RESTful HTTP 路由 12 请参考聊聊 Spring Boot 2.x 那些事儿 @Async需要执行异步方法时,在方法上加上@Async之后,底层使用多线程技术 。启动加上需要@EnableAsync 数据访问整合JdbcTemplate使用这个需要spring-boot-starter-parent版本要在1.5以上 POM1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId></dependency> 配置文件在application.properties中添加: 12345# jdbc模板spring.datasource.url=jdbc:mysql://localhost:3306/testspring.datasource.username=rootspring.datasource.password=123456spring.datasource.driver-class-name=com.mysql.jdbc.Driver 后台创建一个Service 12345678@Servicepublic class UserServiceImpl implements UserService { @Autowired private JdbcTemplate jdbcTemplate; public void createUser(String name, Integer age) { jdbcTemplate.update("insert into users values(null,?,?);", name, age); }} 整合Mybatis这里用我写的一个秒杀项目作为参考栗子。秒杀商城 POM12345<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version></dependency> 配置文件在application.properties中添加: 123456#mybatismybatis.type-aliases-package=cn.tengshe789.domainmybatis.configuration.map-underscore-to-camel-case=truemybatis.configuration.default-fetch-size=100mybatis.configuration.default-statement-timeout=3000mybatis.mapperLocations = classpath:cn/tengshe789/dao/*.xml 后台创建一个Dao(Mapper 代码) 1234567@Mapper@Componentpublic interface GoodsDao { @Select("select g.*,mg.stock_count, mg.start_date, mg.end_date,mg.miaosha_price from miaosha_goods mg left join goods g on mg.goods_id = g.id") public List<GoodsVo> listGoodsVo();} 创建service 12345678910111213@Servicepublic class GoodsService { @Autowired GoodsDao goodsDao; /* * 展示商品列表 */ public List<GoodsVo> listGoodsVo() { return goodsDao.listGoodsVo(); }} Mybatis整合分页插件PageHelperPageHelper 是一款好用的开源免费的 Mybatis 第三方物理分页插件 POM12345<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.2.5</version></dependency> 配置文件在application.properties中添加: 12345678# 配置日志logging.level.cn.tengshe789.dao=DEBUG# Pagehelperpagehelper.helperDialect=mysqlpagehelper.reasonable=truepagehelper.supportMethodsArguments=truepagehelper.params=count=countSqlpagehelper.page-size-zero=true 或者在application.yml中添加: 123456789101112# 与mybatis整合mybatis: config-location: classpath:mybatis.xml mapper-locations: - classpath:cn/tengshe789/dao/*.xml# 分页配置pagehelper: helper-dialect: mysql reasonable: true support-methods-arguments: true params: count=countSql 代码实体层面12345@Datapublic class User { private Integer id; private String name;} Dao层1234public interface UserDao { @Select("SELECT * FROM USERS ") List<User> findUserList();} Service层123456789101112131415161718@Servicepublic class UserService { @Autowired private UserMapper userDao; /** * page 当前页数<br> * size 当前展示的数据<br> */ public PageInfo<User> findUserList(int page, int size) { // 开启分页插件,放在查询语句上面 PageHelper.startPage(page, size); List<User> listUser = userDao.findUserList(); // 封装分页之后的数据 PageInfo<User> pageInfoUser = new PageInfo<User>(listUser); return pageInfoUser; }} 整合SpringJPAspring-data-jpa三个步骤: 声明持久层的接口,该接口继承 Repository(或Repository的子接口,其中定义了一些常用的增删改查,以及分页相关的方法)。 在接口中声明需要的业务方法。Spring Data 将根据给定的策略生成实现代码。 在 Spring 配置文件中增加一行声明,让 Spring 为声明的接口创建代理对象。配置了 jpa:repositories 后,Spring 初始化容器时将会扫描 base-package 指定的包目录及其子目录,为继承 Repository 或其子接口的接口创建代理对象,并将代理对象注册为 Spring Bean,业务层便可以通过 Spring 自动封装的特性来直接使用该对象。 详情:JPA官方网站 POM1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId></dependency> 配置文件Springboot 默认使用hibernate作为JPA的实现 。需要在application.properties中添加: 12345678# hibernatespring.datasource.url=jdbc:mysql://localhost:3306/test?useSSL=falsespring.datasource.username=rootspring.datasource.password=rootspring.datasource.tomcat.max-active=100spring.datasource.tomcat.max-idle=200spring.datasource.tomcat.initialSize=20spring.jpa.database-platform=org.hibernate.dialect.MySQL5Dialect 代码Domain1234567891011@Data@Entity(name = "users")public class UserEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; @Column(name = "name") private String name; @Column(name = "age") private Integer age;} 注解的意思: @Entity会被spring扫描并加载, @Id注解在主键上 @Column name="call_phone" 指该字段对应的数据库的字段名,如果相同就不需要定义。数据库下划线间隔和代码中的驼峰法视为相同,如数据库字段create_time等价于Java类中的createTime,因此不需要用@Column注解。 Dao层此时需要继承Repository接口~ 12public interface UserDao extends JpaRepository<User, Integer> {} Controller123456789101112@RestControllerpublic class IndexController { @Autowired private UserDao userDao; @RequestMapping("/jpaFindUser") public Object jpaIndex(User user) { Optional<User> userOptional = userDao.findById(user.getId()); User result = userOptional.get(); return reusltUser == null ? "没有查询到数据" : result; }} 多数据源很多公司都会使用多数据库,一个数据库存放共同的配置或文件,另一个数据库是放垂直业务的数据。所以说需要一个项目中有多个数据源 这玩意原理很简单,根据不同包名,加载不同数据源。 配置文件在application.properties中添加: 12345678910# datasource1spring.datasource.test1.driver-class-name = com.mysql.jdbc.Driverspring.datasource.test1.jdbc-url =jdbc:mysql://localhost:3306/test01?useUnicode=true&characterEncoding=utf-8spring.datasource.test1.username = rootspring.datasource.test1.password = 123456# datasource2spring.datasource.test2.driver-class-name = com.mysql.jdbc.Driverspring.datasource.test2.jdbc-url =jdbc:mysql://localhost:3306/test02?useUnicode=true&characterEncoding=utf-8spring.datasource.test2.username = rootspring.datasource.test2.password = 123456 代码添加配置数据库1的 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849//DataSource01@Configuration // 注册到springboot容器中@MapperScan(basePackages = "tech.tengshe789.test01", sqlSessionFactoryRef = "test1SqlSessionFactory")public class DataSource1Config { /** * @methodDesc: 功能描述:(配置test1数据库) * @author: tEngSHe789 */ @Bean(name = "test1DataSource") @ConfigurationProperties(prefix = "spring.datasource.test1") @Primary public DataSource testDataSource() { return DataSourceBuilder.create().build(); } /** * @methodDesc: 功能描述:(test1 sql会话工厂) */ @Bean(name = "test1SqlSessionFactory") @Primary public SqlSessionFactory testSqlSessionFactory(@Qualifier("test1DataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); //加载mapper(不需要) bean.setMapperLocations( new PathMatchingResourcePatternResolver().getResources("classpath:mybatis/mapper/test1/*.xml")); return bean.getObject(); } /** * * @methodDesc: 功能描述:(test1 事物管理) */ @Bean(name = "test1TransactionManager") @Primary public DataSourceTransactionManager testTransactionManager(@Qualifier("test1DataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Bean(name = "test1SqlSessionTemplate") @Primary public SqlSessionTemplate testSqlSessionTemplate( @Qualifier("test1SqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception { return new SqlSessionTemplate(sqlSessionFactory); }} 数据库2的同理。 Dao1234public interface User1Dao { @Insert("insert into users values(null,#{name},#{age});") public int addUser(@Param("name") String name, @Param("age") Integer age);} 注意事项在多数据源的情况下,使用@Transactional注解时,应该指定事务管理者@Transactional(transactionManager = "test1TransactionManager") 事物管理怎么进行事物管理呢,简单,往下看。 声明式事务找到service实现类,加上@Transactional 注解就行,此@Transactional注解来自org.springframework.transaction.annotation包 ,不是来自javax.transaction 。而且@Transactional不仅可以注解在方法上,也可以注解在类上。当注解在类上的时候意味着此类的所有public方法都是开启事务的。如果类级别和方法级别同时使用了@Transactional注解,则使用在类级别的注解会重载方法级别的注解。 注意:Springboot提供了一个@EnableTransactionManagement注解在配置类上来开启声明式事务的支持。注解@EnableTransactionManagement是默认打开的,想要关闭事务管理,想要在程序入口将这个注解改为false 分布式事物管理啥是分布式事务呢,比如我们在执行一个业务逻辑的时候有两步分别操作A数据源和B数据源,当我们在A数据源执行数据更改后,在B数据源执行时出现运行时异常,那么我们必须要让B数据源的操作回滚,并回滚对A数据源的操作。这种情况在支付业务时常常出现,比如买票业务在最后支付失败,那之前的操作必须全部回滚,如果之前的操作分布在多个数据源中,那么这就是典型的分布式事务回滚 了解了什么是分布式事务,那分布式事务在java的解决方案就是JTA(即Java Transaction API)。 springboot官方提供了 Atomikos , Bitronix ,Narayana 的类事务管理器 类事务管理器AtomikosPOM1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jta-atomikos</artifactId></dependency> 配置文件12345678910111213141516171819202122232425# Mysql 1mysql.datasource.test1.url = jdbc:mysql://localhost:3306/test01?useUnicode=true&characterEncoding=utf-8mysql.datasource.test1.username = rootmysql.datasource.test1.password = 123456mysql.datasource.test1.minPoolSize = 3mysql.datasource.test1.maxPoolSize = 25mysql.datasource.test1.maxLifetime = 20000mysql.datasource.test1.borrowConnectionTimeout = 30mysql.datasource.test1.loginTimeout = 30mysql.datasource.test1.maintenanceInterval = 60mysql.datasource.test1.maxIdleTime = 60# Mysql 2mysql.datasource.test2.url =jdbc:mysql://localhost:3306/test02?useUnicode=true&characterEncoding=utf-8mysql.datasource.test2.username =rootmysql.datasource.test2.password =123456mysql.datasource.test2.minPoolSize = 3mysql.datasource.test2.maxPoolSize = 25mysql.datasource.test2.maxLifetime = 20000mysql.datasource.test2.borrowConnectionTimeout = 30mysql.datasource.test2.loginTimeout = 30mysql.datasource.test2.maintenanceInterval = 60mysql.datasource.test2.maxIdleTime = 60 读取配置文件信息以下是读取数据库1的配置文件 123456789101112131415@Data@ConfigurationProperties(prefix = "mysql.datasource.test1")public class DBConfig1 { private String url; private String username; private String password; private int minPoolSize; private int maxPoolSize; private int maxLifetime; private int borrowConnectionTimeout; private int loginTimeout; private int maintenanceInterval; private int maxIdleTime; private String testQuery;} 读取数据库2的配置文件略 创建数据源数据源1: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647@Configuration// basePackages 最好分开配置 如果放在同一个文件夹可能会报错@MapperScan(basePackages = "tech.tengshe789.test01", sqlSessionTemplateRef = "testSqlSessionTemplate")public class MyBatisConfig1 { // 配置数据源 @Primary @Bean(name = "testDataSource") public DataSource testDataSource(DBConfig1 testConfig) throws SQLException { MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource(); mysqlXaDataSource.setUrl(testConfig.getUrl()); mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true); mysqlXaDataSource.setPassword(testConfig.getPassword()); mysqlXaDataSource.setUser(testConfig.getUsername()); mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true); AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean(); xaDataSource.setXaDataSource(mysqlXaDataSource); xaDataSource.setUniqueResourceName("testDataSource"); xaDataSource.setMinPoolSize(testConfig.getMinPoolSize()); xaDataSource.setMaxPoolSize(testConfig.getMaxPoolSize()); xaDataSource.setMaxLifetime(testConfig.getMaxLifetime()); xaDataSource.setBorrowConnectionTimeout(testConfig.getBorrowConnectionTimeout()); xaDataSource.setLoginTimeout(testConfig.getLoginTimeout()); xaDataSource.setMaintenanceInterval(testConfig.getMaintenanceInterval()); xaDataSource.setMaxIdleTime(testConfig.getMaxIdleTime()); xaDataSource.setTestQuery(testConfig.getTestQuery()); return xaDataSource; } @Primary @Bean(name = "testSqlSessionFactory") public SqlSessionFactory testSqlSessionFactory(@Qualifier("testDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); return bean.getObject(); } @Primary @Bean(name = "testSqlSessionTemplate") public SqlSessionTemplate testSqlSessionTemplate( @Qualifier("testSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception { return new SqlSessionTemplate(sqlSessionFactory); }} 数据库2略 如何启动1234567@EnableConfigurationProperties(value = { DBConfig1.class, DBConfig2.class })@SpringBootApplicationpublic class MainApplication { public static void main(String[] args) { SpringApplication.run(MainApplication.class, args); } } 定时任务在做项目时有时候会有定时器任务的功能,比如某某时间应该做什么,多少秒应该怎么样之类的。 spring支持多种定时任务的实现。我们来介绍下使用Quartz 和Scheduler Spring ScheduleSpring Schedule 实现定时任务有两种方式 1. 使用XML配置定时任务, 2. 使用 @Scheduled 注解。 代码固定等待时间 @Scheduled(fixedDelay = 时间间隔 ) 固定间隔时间 @Scheduled(fixedRate = 时间间隔 ) 123456789101112@Componentpublic class ScheduleJobs { public final static long SECOND = 1 * 1000; FastDateFormat fdf = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss"); @Scheduled(fixedDelay = SECOND * 2) public void fixedDelayJob() throws InterruptedException { TimeUnit.SECONDS.sleep(2); System.out.println("[FixedDelayJob Execute]"+fdf.format(new Date())); }} Corn表达式 @Scheduled(cron = Corn表达式) 1234567891011@Componentpublic class ScheduleJobs { public final static long SECOND = 1 * 1000; FastDateFormat fdf = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss"); @Scheduled(cron = "0/4 * * * * ?") public void cronJob() { System.out.println("[CronJob Execute]"+fdf.format(new Date())); }} 启动要在主方法上加上@EnableScheduling QuartzPOM1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz-starter</artifactId> </dependency> 配置文件1234567891011121314# spring boot 2.x 已集成Quartz,无需自己配置spring.quartz.job-store-type=jdbcspring.quartz.properties.org.quartz.scheduler.instanceName=clusteredSchedulerspring.quartz.properties.org.quartz.scheduler.instanceId=AUTOspring.quartz.properties.org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTXspring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegatespring.quartz.properties.org.quartz.jobStore.tablePrefix=QRTZ_spring.quartz.properties.org.quartz.jobStore.isClustered=truespring.quartz.properties.org.quartz.jobStore.clusterCheckinInterval=10000spring.quartz.properties.org.quartz.jobStore.useProperties=falsespring.quartz.properties.org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPoolspring.quartz.properties.org.quartz.threadPool.threadCount=10spring.quartz.properties.org.quartz.threadPool.threadPriority=5spring.quartz.properties.org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread=true 配置类12345678910111213141516@Configurationpublic class QuartzConfig { @Bean public JobDetail uploadTaskDetail() { return JobBuilder.newJob(UploadTask.class).withIdentity("uploadTask").storeDurably().build(); } @Bean public Trigger uploadTaskTrigger() { CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("*/5 * * * * ?"); return TriggerBuilder.newTrigger().forJob(uploadTaskDetail()) .withIdentity("uploadTask") .withSchedule(scheduleBuilder) .build(); }} 实现类创建一个配置类,分别制定具体任务类和触发的规则 12345678910111213141516@Configuration@DisallowConcurrentExecutionpublic class UploadTask extends QuartzJobBean { @Resource private TencentYunService tencentYunService; @Override protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException { System.out.println("任务开始"); try { Thread.sleep(6000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("任务结束"); }} @DisallowConcurrentExecution禁止并发执行 并发执行方面,系统默认为true,即第一个任务还未执行完整,第二个任务如果到了执行时间,则会立马开启新线程执行任务,这样如果我们是从数据库读取信息,两次重复读取可能出现重复执行任务的情况,所以我们需要将这个值设置为false,这样第二个任务会往后推迟,只有在第一个任务执行完成后才会执行第二个任务 日志管理log4jPOM12345678910111213141516171819<!-- spring boot start --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <exclusions> <!-- 排除自带的logback依赖 --> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <!-- springboot-log4j --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j</artifactId> <version>1.3.8.RELEASE</version> </dependency> 配置文件文件名称log4j.properties 1234567891011121314151617181920212223242526272829303132#log4j.rootLogger=CONSOLE,info,error,DEBUGlog4j.rootLogger=info,error,CONSOLE,DEBUGlog4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout log4j.appender.CONSOLE.layout.ConversionPattern=%d{yyyy-MM-dd-HH-mm} [%t] [%c] [%p] - %m%n log4j.logger.info=infolog4j.appender.info=org.apache.log4j.DailyRollingFileAppenderlog4j.appender.info.layout=org.apache.log4j.PatternLayout log4j.appender.info.layout.ConversionPattern=%d{yyyy-MM-dd-HH-mm} [%t] [%c] [%p] - %m%n log4j.appender.info.datePattern='.'yyyy-MM-ddlog4j.appender.info.Threshold = info log4j.appender.info.append=true #log4j.appender.info.File=/home/admin/pms-api-services/logs/info/api_services_infolog4j.appender.info.File=/Users/dddd/Documents/testspace/pms-api-services/logs/info/api_services_infolog4j.logger.error=error log4j.appender.error=org.apache.log4j.DailyRollingFileAppenderlog4j.appender.error.layout=org.apache.log4j.PatternLayout log4j.appender.error.layout.ConversionPattern=%d{yyyy-MM-dd-HH-mm} [%t] [%c] [%p] - %m%n log4j.appender.error.datePattern='.'yyyy-MM-ddlog4j.appender.error.Threshold = error log4j.appender.error.append=true #log4j.appender.error.File=/home/admin/pms-api-services/logs/error/api_services_errorlog4j.appender.error.File=/Users/dddd/Documents/testspace/pms-api-services/logs/error/api_services_errorlog4j.logger.DEBUG=DEBUGlog4j.appender.DEBUG=org.apache.log4j.DailyRollingFileAppenderlog4j.appender.DEBUG.layout=org.apache.log4j.PatternLayout log4j.appender.DEBUG.layout.ConversionPattern=%d{yyyy-MM-dd-HH-mm} [%t] [%c] [%p] - %m%n log4j.appender.DEBUG.datePattern='.'yyyy-MM-ddlog4j.appender.DEBUG.Threshold = DEBUG log4j.appender.DEBUG.append=true #log4j.appender.DEBUG.File=/home/admin/pms-api-services/logs/debug/api_services_debuglog4j.appender.DEBUG.File=/Users/dddd/Documents/testspace/pms-api-services/logs/debug/api_services_debug 使用1private static final Logger logger = LoggerFactory.getLogger(IndexController.class); 使用AOP统一处理Web请求日志POM1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> 代码1234567891011121314151617181920212223242526272829303132@Aspect@Componentpublic class WebLogAspect { private static final Logger logger = LoggerFactory.getLogger(WebLogAspect.class); @Pointcut("execution(public * tech.tengshe789.controller.*.*(..))") public void webLog() { } @Before("webLog()") public void doBefore(JoinPoint joinPoint) throws Throwable { // 接收到请求,记录请求内容 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); // 记录下请求内容 logger.info("URL : " + request.getRequestURL().toString()); logger.info("HTTP_METHOD : " + request.getMethod()); logger.info("IP : " + request.getRemoteAddr()); Enumeration<String> enu = request.getParameterNames(); while (enu.hasMoreElements()) { String name = (String) enu.nextElement(); logger.info("name:{},value:{}", name, request.getParameter(name)); } } @AfterReturning(returning = "ret", pointcut = "webLog()") public void doAfterReturning(Object ret) throws Throwable { // 处理完请求,返回内容 logger.info("RESPONSE : " + ret); }} lombok 插件非常简单的办法 POM12345<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.0</version> </dependency> 代码类中添加@Slf4j 注解即可。使用是直接输入log全局变量 Lombok的其他用法123456789101112131415@Data 标签,生成getter/setter toString()等方法 @NonNull : 让你不在担忧并且爱上NullPointerException @CleanUp : 自动资源管理:不用再在finally中添加资源的close方法 @Setter/@Getter : 自动生成set和get方法 @ToString : 自动生成toString方法 @EqualsAndHashcode : 从对象的字段中生成hashCode和equals的实现 @NoArgsConstructor/@RequiredArgsConstructor/@AllArgsConstructor 自动生成构造方法 @Data : 自动生成set/get方法,toString方法,equals方法,hashCode方法,不带参数的构造方法 @Value : 用于注解final类 @Builder : 产生复杂的构建器api类@SneakyThrows : 异常处理(谨慎使用) @Synchronized : 同步方法安全的转化 @Getter(lazy=true) : @Log : 支持各种logger对象,使用时用对应的注解,如:@Log4 拦截器拦截器,在AOP(Aspect-Oriented Programming)中用于在某个方法或字段被访问之前,进行拦截,然后在之前或之后加入某些操作。拦截是AOP的一种实现策略。 (1)拦截器是基于java的反射机制的,而过滤器是基于函数回调。 (2)拦截器不依赖于servlet容器,而过滤器依赖于servlet容器。 (3)拦截器只能对Controller请求起作用,而过滤器则可以对几乎所有的请求起作用。 (4)在Controller的生命周期中,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次。 过滤器(filter)和拦截器(interceptor)是有区别的,详情 ,他们的执行顺序: 先filter 后 interceptor ->过滤器应用场景:设置编码字符、过滤铭感字符 ->拦截器应用场景:拦截未登陆用户、审计日志 自定义拦截器代码注册拦截器 123456789101112131415@Configurationpublic class WebAppConfig { @Autowired private LoginIntercept loginIntercept; @Bean public WebMvcConfigurer WebMvcConfigurer() { return new WebMvcConfigurer() { public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginIntercept).addPathPatterns("/*"); }; }; }} 创建模拟登录拦截器,验证请求是否有token参数 123456789101112131415@Slf4j@Componentpublic class LoginIntercept implements HandlerInterceptor { public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { log.info("开始拦截登录请求...."); String token = request.getParameter("token"); if (StringUtils.isEmpty(token)) { response.getWriter().println("not found token"); return false; } return true; }} 缓存在 Spring Boot中,通过@EnableCaching注解自动化配置合适的缓存管理器(CacheManager),Spring Boot根据下面的顺序去侦测缓存提供者: Generic , JCache (JSR-107), EhCache 2.x ,Hazelcast , Infinispan ,Redis ,Guava , Simple EhCachePOM1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId></dependency> 新建ehcache.xml 文件123456789101112131415<?xml version="1.0" encoding="UTF-8"?><ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd" updateCheck="false"> <diskStore path="java.io.tmpdir/Tmp_EhCache" /> <!-- 默认配置 --> <defaultCache maxElementsInMemory="5000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" memoryStoreEvictionPolicy="LRU" overflowToDisk="false" /> <cache name="baseCache" maxElementsInMemory="10000" maxElementsOnDisk="100000" /></ehcache> 配置信息介绍 name:缓存名称。 maxElementsInMemory:缓存最大个数。 eternal:对象是否永久有效,一但设置了,timeout将不起作用。 timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。 timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。 overflowToDisk:当内存中对象数量达到maxElementsInMemory时,Ehcache将会对象写到磁盘中。 diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。 maxElementsOnDisk:硬盘最大缓存个数。 diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false. diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。 memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。 clearOnFlush:内存数量最大时是否清除。 关于注解和代码使用123456@CacheConfig(cacheNames = "baseCache")public interface UserDao { @Select("select * from users where name=#{name}") @Cacheable UserEntity findName(@Param("name") String name);} 清除缓存123456@Autowiredprivate CacheManager cacheManager;@RequestMapping("/remoKey")public void remoKey() { cacheManager.getCache("baseCache").clear();} 启动主方法启动时加上@EnableCaching即可 Redis使用自带驱动器连接使用RedisTemplate 连接 POM1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> 配置文件单机 12345678910111213141516171819#redis# Redis数据库索引(默认为0)spring.redis.database=0# Redis服务器地址spring.redis.host=127.0.0.1# Redis服务器连接端口spring.redis.port=6379# Redis服务器连接密码(默认为空)spring.redis.password=# 连接池最大连接数(使用负值表示没有限制)spring.redis.pool.max-active=8# 连接池最大阻塞等待时间(使用负值表示没有限制)spring.redis.pool.max-wait=-1# 连接池中的最大空闲连接spring.redis.pool.max-idle=8# 连接池中的最小空闲连接spring.redis.pool.min-idle=0# 连接超时时间(毫秒)spring.redis.timeout=0 集群或哨兵模式 1234567891011121314151617181920212223242526272829303132333435363738#Matser的ip地址 redis.hostName=192.168.177.128#端口号 redis.port=6382#如果有密码 redis.password=#客户端超时时间单位是毫秒 默认是2000 redis.timeout=10000 #最大空闲数 redis.maxIdle=300 #连接池的最大数据库连接数。设为0表示无限制,如果是jedis 2.4以后用redis.maxTotal #redis.maxActive=600 #控制一个pool可分配多少个jedis实例,用来替换上面的redis.maxActive,如果是jedis 2.4以后用该属性 redis.maxTotal=1000 #最大建立连接等待时间。如果超过此时间将接到异常。设为-1表示无限制。 redis.maxWaitMillis=1000 #连接的最小空闲时间 默认1800000毫秒(30分钟) redis.minEvictableIdleTimeMillis=300000 #每次释放连接的最大数目,默认3 redis.numTestsPerEvictionRun=1024 #逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1 redis.timeBetweenEvictionRunsMillis=30000 #是否在从池中取出连接前进行检验,如果检验失败,则从池中去除连接并尝试取出另一个 redis.testOnBorrow=true #在空闲时检查有效性, 默认false redis.testWhileIdle=true #redis集群配置 spring.redis.cluster.nodes=192.168.177.128:7001,192.168.177.128:7002,192.168.177.128:7003,192.168.177.128:7004,192.168.177.128:7005,192.168.177.128:7006spring.redis.cluster.max-redirects=3#哨兵模式#redis.sentinel.host1=192.168.177.128#redis.sentinel.port1=26379#redis.sentinel.host2=172.20.1.231 #redis.sentinel.port2=26379 配置类12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152@Configuration@EnableCachingpublic class RedisConfig extends CachingConfigurerSupport{ @Value("${spring.redis.host}") private String host; @Value("${spring.redis.port}") private int port; @Value("${spring.redis.timeout}") private int timeout; //自定义缓存key生成策略// @Bean// public KeyGenerator keyGenerator() {// return new KeyGenerator(){// @Override// public Object generate(Object target, java.lang.reflect.Method method, Object... params) {// StringBuffer sb = new StringBuffer();// sb.append(target.getClass().getName());// sb.append(method.getName());// for(Object obj:params){// sb.append(obj.toString());// }// return sb.toString();// }// };// } //缓存管理器 @Bean public CacheManager cacheManager(@SuppressWarnings("rawtypes") RedisTemplate redisTemplate) { RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate); //设置缓存过期时间 cacheManager.setDefaultExpiration(10000); return cacheManager; } @Bean public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory){ StringRedisTemplate template = new StringRedisTemplate(factory); setSerializer(template);//设置序列化工具 template.afterPropertiesSet(); return template; } private void setSerializer(StringRedisTemplate template){ @SuppressWarnings({ "rawtypes", "unchecked" }) Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); template.setValueSerializer(jackson2JsonRedisSerializer); }} Dao123456789101112131415161718192021@Mapper@CacheConfig(cacheNames = "users")public interface UserMapper { @Insert("insert into user(name,age) values(#{name},#{age})") int addUser(@Param("name")String name,@Param("age")String age); @Select("select * from user where id =#{id}") @Cacheable(key ="#p0") User findById(@Param("id") String id); @CachePut(key = "#p0") @Update("update user set name=#{name} where id=#{id}") void updataById(@Param("id")String id,@Param("name")String name); //如果指定为 true,则方法调用后将立即清空所有缓存 @CacheEvict(key ="#p0",allEntries=true) @Delete("delete from user where id=#{id}") void deleteById(@Param("id")String id); } @Cacheable将查询结果缓存到redis中,(key=”#p0”)指定传入的第一个参数作为redis的key。 @CachePut,指定key,将更新的结果同步到redis中 @CacheEvict,指定key,删除缓存数据,allEntries=true,方法调用后将立即清除缓存 使用Jedis连接要注意,redis在5.0版本以后不支持Jedis POM1234<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency> 配置类123456789101112@Data@Component@ConfigurationProperties(prefix="redis")public class RedisConfig { private String host; private int port; private int timeout;//秒 private String password; private int poolMaxTotal; private int poolMaxIdle; private int poolMaxWait;//秒} 123456789101112131415161718@Servicepublic class RedisPoolFactory { @Autowired RedisConfig redisConfig; @Bean public JedisPool edisPoolFactory() { JedisPoolConfig poolConfig = new JedisPoolConfig(); poolConfig.setMaxIdle(redisConfig.getPoolMaxIdle()); poolConfig.setMaxTotal(redisConfig.getPoolMaxTotal()); poolConfig.setMaxWaitMillis(redisConfig.getPoolMaxWait() * 1000); JedisPool jp = new JedisPool(poolConfig, redisConfig.getHost(), redisConfig.getPort(), redisConfig.getTimeout()*1000, redisConfig.getPassword(), 0); return jp; }} 监控中心Springboot监控中心是干什么的呢?他是针对微服务的 服务状态、Http请求资源进行监控,可以看到服务器内存变化(堆内存、线程、日志管理),可以检测服务配置连接地址是否可用(模拟访问,懒加载),可以统计有多少Bean有什么单例多例,可以统计SpringMVC有多少@RequestMapping ActuatorActuator是spring boot的一个附加功能,可帮助你在应用程序生产环境时监视和管理应用程序。 可以使用HTTP的各种请求来监管,审计,收集应用的运行情况.返回的是json 缺点:没有可视化界面。 在springboot2.0中,Actuator的端点(endpoint)现在默认映射到/application,比如,/info 端点现在就是在/application/info。但你可以使用management.context-path来覆盖此默认值。 POM1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId></dependency> 配置信息1234567891011121314# Actuator 通过下面的配置启用所有的监控端点,默认情况下,这些端点是禁用的;management: endpoints: web: exposure: include: "*"spring: profiles: active: prod datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/test username: root password: 123456 Actuator访问路径通过actuator/+端点名就可以获取相应的信息。 路径 作用 /actuator/beans 显示应用程序中所有Spring bean的完整列表。 /actuator/configprops 显示所有配置信息。 /actuator/env 陈列所有的环境变量。 /actuator/mappings 显示所有@RequestMapping的url整理列表。 /actuator/health 显示应用程序运行状况信息 up表示成功 down失败 /actuator/info 查看自定义应用信息 Admin-UI分布式微服务监控中心Admin-UI底层使用actuator,实现监控信息 的界面 POM123456789101112131415161718192021222324<!--服务端--><dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-server</artifactId> <version>2.0.0</version></dependency><!--客户端--><dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-client</artifactId> <version>2.0.0</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId></dependency><dependency> <groupId>org.jolokia</groupId> <artifactId>jolokia-core</artifactId></dependency><dependency> <groupId>com.googlecode.json-simple</groupId> <artifactId>json-simple</artifactId> <version>1.1</version> application.yml配置文件123456789101112131415161718192021//服务端spring: application: name: spring-boot-admin-server//客户端spring: boot: admin: client: url: http://localhost:8080server: port: 8081 management: endpoints: web: exposure: include: "*" endpoint: health: show-details: ALWAYS 性能优化扫包优化默认情况下,我们会使用 @SpringBootApplication 注解来自动获取应用的配置信息,但这样也会给应用带来一些副作用。使用这个注解后,会触发自动配置( auto-configuration )和 组件扫描 ( component scanning ),这跟使用 @Configuration、@EnableAutoConfiguration 和 @ComponentScan 三个注解的作用是一样的。这样做给开发带来方便的同时,也会有三方面的影响: 1、会导致项目启动时间变长。当启动一个大的应用程序,或将做大量的集成测试启动应用程序时,影响会特别明显。 2、会加载一些不需要的多余的实例(beans)。 3、会增加 CPU 消耗。 针对以上三个情况,我们可以移除 @SpringBootApplication 和 @ComponentScan 两个注解来禁用组件自动扫描,然后在我们需要的 bean 上进行显式配置。 SpringBoot JVM参数调优各种参数 参数名称 含义 默认值 -Xms 初始堆大小 物理内存的1/64(<1GB) 默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制. -Xmx 最大堆大小 物理内存的1/4(<1GB) 默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制 -Xmn 年轻代大小(1.4or lator) 注意:此处的大小是(eden+ 2 survivor space).与jmap -heap中显示的New gen是不同的。 整个堆大小=年轻代大小 + 年老代大小 + 持久代大小. 增大年轻代后,将会减小年老代大小.此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8 -XX:NewSize 设置年轻代大小(for 1.3/1.4) -XX:MaxNewSize 年轻代最大值(for 1.3/1.4) -XX:PermSize 设置持久代(perm gen)初始值 物理内存的1/64 -XX:MaxPermSize 设置持久代最大值 物理内存的1/4 -Xss 每个线程的堆栈大小 JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.更具应用的线程所需内存大小进行 调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右 一般小的应用, 如果栈不是很深, 应该是128k够用的 大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。(校长) 和threadstacksize选项解释很类似,官方文档似乎没有解释,在论坛中有这样一句话:”” -Xss is translated in a VM flag named ThreadStackSize” 一般设置这个值就可以了。 -XX:ThreadStackSize Thread Stack Size (0 means use default stack size) [Sparc: 512; Solaris x86: 320 (was 256 prior in 5.0 and earlier); Sparc 64 bit: 1024; Linux amd64: 1024 (was 0 in 5.0 and earlier); all others 0.] -XX:NewRatio 年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代) -XX:NewRatio=4表示年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5 Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。 -XX:SurvivorRatio Eden区与Survivor区的大小比值 设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10 -XX:LargePageSizeInBytes 内存页的大小不可设置过大, 会影响Perm的大小 =128m -XX:+UseFastAccessorMethods 原始类型的快速优化 -XX:+DisableExplicitGC 关闭System.gc() 这个参数需要严格的测试 -XX:MaxTenuringThreshold 垃圾最大年龄 如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代. 对于年老代比较多的应用,可以提高效率.如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活 时间,增加在年轻代即被回收的概率 该参数只有在串行GC时才有效. -XX:+AggressiveOpts 加快编译 -XX:+UseBiasedLocking 锁机制的性能改善 -Xnoclassgc 禁用垃圾回收 -XX:SoftRefLRUPolicyMSPerMB 每兆堆空闲空间中SoftReference的存活时间 1s softly reachable objects will remain alive for some amount of time after the last time they were referenced. The default value is one second of lifetime per free megabyte in the heap -XX:PretenureSizeThreshold 对象超过多大是直接在旧生代分配 0 单位字节 新生代采用Parallel Scavenge GC时无效 另一种直接在旧生代分配的情况是大的数组对象,且数组中无外部引用对象. -XX:TLABWasteTargetPercent TLAB占eden区的百分比 1% -XX:+CollectGen0First FullGC时是否先YGC false 调优策略 初始化堆内存和最大堆相同 减少垃圾回收次数 内部调优 输入 -XX:+PrintGCDetails 是为了在控制台显示回收的信息 外部调优进入对应jar的目录,在CMD输入java -server -Xms32m -Xmx32m -jar springboot.jar 使用工具java visual vm 使用工具java console 将Servlet容器从Tomcat变成UndertowUndertow 是一个采用 Java 开发的灵活的高性能 Web 服务器,提供包括阻塞和基于 NIO 的非堵塞机制。Undertow 是红帽公司的开源产品,是 JBoss默认的 Web 服务器。👇 Undertow POM首先,从依赖信息里移除 Tomcat 配置 12345678910<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions></dependency> 然后添加 Undertow: 1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-undertow</artifactId></dependency> Tomcat 优化见👉Spring Boot Memory Performance 热部署热部署,就是在应用程序在不停止的情况下,自动实现新的部署 原理使用类加载器classroad来检测字节码文件,然后重新加载到jvm内存中 第一步:检测本地.class文件变动(版本号,修改时间不一样) 第二步:自动监听,实现部署 应用场景本地开发时,可以提高运行环境 Dev-toolsspring-boot-devtools 是一个为开发者服务的一个模块,其中最重要的功能就是自动应用代码更改到最新的App上面去 POM123456<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> <scope>true</scope></dependency> 原理 devtools会监听classpath下的文件变动,并且会立即重启应用(发生在保存时机),因为其采用的虚拟机机制,该项重启是很快的。 devtools可以实现页面热部署(即页面修改后会立即生效,这个可以直接在application.properties文件中配置spring.thymeleaf.cache=false来实现(注意:不同的模板配置不一样) 发布打包Jar类型打包方式1.使用mvn clean package 打包 2.使用java –jar 包名 war类型打包方式1.使用mvn celan package 打包 2.使用java –jar 包名 外部Tomcat运行1.使用mvn celan package 打包 2.将war包 放入到tomcat webapps下运行即可。 注意:springboot2.0内置tomcat8.5.25,建议使用外部Tomcat9.0版本运行即可,否则报错版本不兼容。 POM123456789101112131415161718192021222324252627<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <maimClass>com.itmayiedu.app.App</maimClass> </configuration> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build> 参考文献JdbcTemplate SpringBoot分页插件PageHelper jpa Spring For All 社区 Spring 官方教程翻译 SpringBoot使用Redis缓存 Spring Boot Admin简单使用 Spring Boot 性能优化 WebFlux 感谢以上大大们~! 广告时间:想要了解更多精彩新姿势?请访问我的博客]]></content>
<tags>
<tag>java</tag>
<tag>技术</tag>
<tag>Spring</tag>
</tags>
</entry>
<entry>
<title><![CDATA[安利软件-如何一键装B]]></title>
<url>%2F2018%2F08%2F02%2F%E5%A6%82%E4%BD%95%E4%B8%80%E9%94%AE%E8%A3%85B%2F</url>
<content type="text"><![CDATA[今天教大家如果高大上的装B! 想要解锁更多新姿势?请访问我的博客 第一步https://github.com/WWILLV/GodOfHacker 克隆软件 第二部使用 不谢哦😏 结束 此片完了~ 想要了解更多精彩新姿势?请访问我的个人博客 . 本篇为原创内容,已在个人博客率先发表,随后CSDN,segmentfault,juejin同步发出。如有雷同,缘分呢兄弟。赶快加个好友~]]></content>
<tags>
<tag>日常</tag>
<tag>Windows10</tag>
<tag>软件教学</tag>
</tags>
</entry>
<entry>
<title><![CDATA[记一次失败的redis安装]]></title>
<url>%2F2018%2F08%2F02%2F%E8%AE%B0%E4%B8%80%E6%AC%A1%E5%A4%B1%E8%B4%A5%E7%9A%84redis%E5%AE%89%E8%A3%85%2F</url>
<content type="text"><![CDATA[刚刚看见群友说VPS又搞年中特价了,看来看去,选了个年付$9.99的CN2线路的KVM VPS,心里美滋滋。然而这家的最小化centOS居然很多基础运行环境都不装,甚是头疼。接下来我来分享一下我装机的过程 想要解锁更多新姿势?请访问我的博客 安装编译环境首先,用sftp(妈蛋竟然没有内置wget)下载好redis,进入redis目录 运行 123yum -y install wgetyum -y install makeyum install gcc 装好make等环境,make 一下,报错了: 图是我从csdn截的,我和他的错误一模一样。查了查原因,是因为,redame中有这样一句话: Allocator ——— Selecting a non-default memory allocator when building Redis is done by setting the MALLOC environment variable. Redis is compiled and linked against libc malloc by default, with the exception of jemalloc being the default on Linux systems. This default was picked because jemalloc has proven to have fewer fragmentation problems than libc malloc. To force compiling against libc malloc, use: % make MALLOC=libc To compile against jemalloc on Mac OS X systems, use: % make MALLOC=jemalloc 它说,在构建Redis时选择非默认内存分配器是通过设置完成的 MALLOC环境变量。 Redis是针对libc编译和链接的 默认情况下为malloc,但jemalloc是Linux上的默认设置系统。 所以,要执行 1make MALLOC=libc 然后执行make && make install 安装redis 大功告成! 修改配置文件接下来改redis配置文件 绑定0.0.0.0,及允许任意服务器访问redis 将daemonize改成yes,及允许后台执行 访问密码requirepass设置成自己的 启动redis用刚刚的配置文件启动redis: 12redis-server ./redis.confps -ef |grep redis 然后进入utils目录下,运行生成系统服务 1./install_server.sh Redis配置文件样例123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426# Redis配置文件样例# Note on units: when memory size is needed, it is possible to specifiy# it in the usual form of 1k 5GB 4M and so forth:## 1k => 1000 bytes# 1kb => 1024 bytes# 1m => 1000000 bytes# 1mb => 1024*1024 bytes# 1g => 1000000000 bytes# 1gb => 1024*1024*1024 bytes## units are case insensitive so 1GB 1Gb 1gB are all the same.# Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程# 启用守护进程后,Redis会把pid写到一个pidfile中,在/var/run/redis.piddaemonize no# 当Redis以守护进程方式运行时,Redis默认会把pid写入/var/run/redis.pid文件,可以通过pidfile指定pidfile /var/run/redis.pid# 指定Redis监听端口,默认端口为6379# 如果指定0端口,表示Redis不监听TCP连接port 6379# 绑定的主机地址# 你可以绑定单一接口,如果没有绑定,所有接口都会监听到来的连接# bind 127.0.0.1# Specify the path for the unix socket that will be used to listen for# incoming connections. There is no default, so Redis will not listen# on a unix socket when not specified.## unixsocket /tmp/redis.sock# unixsocketperm 755# 当客户端闲置多长时间后关闭连接,如果指定为0,表示关闭该功能timeout 0# 指定日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning,默认为verbose# debug (很多信息, 对开发/测试比较有用)# verbose (many rarely useful info, but not a mess like the debug level)# notice (moderately verbose, what you want in production probably)# warning (only very important / critical messages are logged)loglevel verbose# 日志记录方式,默认为标准输出,如果配置为redis为守护进程方式运行,而这里又配置为标准输出,则日志将会发送给/dev/nulllogfile stdout# To enable logging to the system logger, just set 'syslog-enabled' to yes,# and optionally update the other syslog parameters to suit your needs.# syslog-enabled no# Specify the syslog identity.# syslog-ident redis# Specify the syslog facility. Must be USER or between LOCAL0-LOCAL7.# syslog-facility local0# 设置数据库的数量,默认数据库为0,可以使用select <dbid>命令在连接上指定数据库id# dbid是从0到‘databases’-1的数目databases 16################################ SNAPSHOTTING ################################## 指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合# Save the DB on disk:## save <seconds> <changes>## Will save the DB if both the given number of seconds and the given# number of write operations against the DB occurred.## 满足以下条件将会同步数据:# 900秒(15分钟)内有1个更改# 300秒(5分钟)内有10个更改# 60秒内有10000个更改# Note: 可以把所有“save”行注释掉,这样就取消同步操作了save 900 1save 300 10save 60 10000# 指定存储至本地数据库时是否压缩数据,默认为yes,Redis采用LZF压缩,如果为了节省CPU时间,可以关闭该选项,但会导致数据库文件变的巨大rdbcompression yes# 指定本地数据库文件名,默认值为dump.rdbdbfilename dump.rdb# 工作目录.# 指定本地数据库存放目录,文件名由上一个dbfilename配置项指定# # Also the Append Only File will be created inside this directory.# # 注意,这里只能指定一个目录,不能指定文件名dir ./################################# REPLICATION ################################## 主从复制。使用slaveof从 Redis服务器复制一个Redis实例。注意,该配置仅限于当前slave有效# so for example it is possible to configure the slave to save the DB with a# different interval, or to listen to another port, and so on.# 设置当本机为slav服务时,设置master服务的ip地址及端口,在Redis启动时,它会自动从master进行数据同步# slaveof <masterip> <masterport># 当master服务设置了密码保护时,slav服务连接master的密码# 下文的“requirepass”配置项可以指定密码# masterauth <master-password># When a slave lost the connection with the master, or when the replication# is still in progress, the slave can act in two different ways:## 1) if slave-serve-stale-data is set to 'yes' (the default) the slave will# still reply to client requests, possibly with out of data data, or the# data set may just be empty if this is the first synchronization.## 2) if slave-serve-stale data is set to 'no' the slave will reply with# an error "SYNC with master in progress" to all the kind of commands# but to INFO and SLAVEOF.#slave-serve-stale-data yes# Slaves send PINGs to server in a predefined interval. It's possible to change# this interval with the repl_ping_slave_period option. The default value is 10# seconds.## repl-ping-slave-period 10# The following option sets a timeout for both Bulk transfer I/O timeout and# master data or ping response timeout. The default value is 60 seconds.## It is important to make sure that this value is greater than the value# specified for repl-ping-slave-period otherwise a timeout will be detected# every time there is low traffic between the master and the slave.## repl-timeout 60################################## SECURITY #################################### Warning: since Redis is pretty fast an outside user can try up to# 150k passwords per second against a good box. This means that you should# use a very strong password otherwise it will be very easy to break.# 设置Redis连接密码,如果配置了连接密码,客户端在连接Redis时需要通过auth <password>命令提供密码,默认关闭# requirepass foobared# Command renaming.## It is possilbe to change the name of dangerous commands in a shared# environment. For instance the CONFIG command may be renamed into something# of hard to guess so that it will be still available for internal-use# tools but not available for general clients.## Example:## rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52## It is also possilbe to completely kill a command renaming it into# an empty string:## rename-command CONFIG ""################################### LIMITS ##################################### 设置同一时间最大客户端连接数,默认无限制,Redis可以同时打开的客户端连接数为Redis进程可以打开的最大文件描述符数,# 如果设置maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis会关闭新的连接并向客户端返回max Number of clients reached错误信息# maxclients 128# Don't use more memory than the specified amount of bytes.# When the memory limit is reached Redis will try to remove keys with an# EXPIRE set. It will try to start freeing keys that are going to expire# in little time and preserve keys with a longer time to live.# Redis will also try to remove objects from free lists if possible.## If all this fails, Redis will start to reply with errors to commands# that will use more memory, like SET, LPUSH, and so on, and will continue# to reply to most read-only commands like GET.## WARNING: maxmemory can be a good idea mainly if you want to use Redis as a# 'state' server or cache, not as a real DB. When Redis is used as a real# database the memory usage will grow over the weeks, it will be obvious if# it is going to use too much memory in the long run, and you'll have the time# to upgrade. With maxmemory after the limit is reached you'll start to get# errors for write operations, and this may even lead to DB inconsistency.# 指定Redis最大内存限制,Redis在启动时会把数据加载到内存中,达到最大内存后,Redis会先尝试清除已到期或即将到期的Key,# 当此方法处理后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。# Redis新的vm机制,会把Key存放内存,Value会存放在swap区# maxmemory <bytes># MAXMEMORY POLICY: how Redis will select what to remove when maxmemory# is reached? You can select among five behavior:# # volatile-lru -> remove the key with an expire set using an LRU algorithm# allkeys-lru -> remove any key accordingly to the LRU algorithm# volatile-random -> remove a random key with an expire set# allkeys->random -> remove a random key, any key# volatile-ttl -> remove the key with the nearest expire time (minor TTL)# noeviction -> don't expire at all, just return an error on write operations# # Note: with all the kind of policies, Redis will return an error on write# operations, when there are not suitable keys for eviction.## At the date of writing this commands are: set setnx setex append# incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd# sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby# zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby# getset mset msetnx exec sort## The default is:## maxmemory-policy volatile-lru# LRU and minimal TTL algorithms are not precise algorithms but approximated# algorithms (in order to save memory), so you can select as well the sample# size to check. For instance for default Redis will check three keys and# pick the one that was used less recently, you can change the sample size# using the following configuration directive.## maxmemory-samples 3############################## APPEND ONLY MODE ################################ # Note that you can have both the async dumps and the append only file if you# like (you have to comment the "save" statements above to disable the dumps).# Still if append only mode is enabled Redis will load the data from the# log file at startup ignoring the dump.rdb file.# 指定是否在每次更新操作后进行日志记录,Redis在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。# 因为redis本身同步数据文件是按上面save条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为no# IMPORTANT: Check the BGREWRITEAOF to check how to rewrite the append# log file in background when it gets too big.appendonly no# 指定更新日志文件名,默认为appendonly.aof# appendfilename appendonly.aof# The fsync() call tells the Operating System to actually write data on disk# instead to wait for more data in the output buffer. Some OS will really flush # data on disk, some other OS will just try to do it ASAP.# 指定更新日志条件,共有3个可选值:# no:表示等操作系统进行数据缓存同步到磁盘(快)# always:表示每次更新操作后手动调用fsync()将数据写到磁盘(慢,安全)# everysec:表示每秒同步一次(折衷,默认值)appendfsync everysec# appendfsync no# When the AOF fsync policy is set to always or everysec, and a background# saving process (a background save or AOF log background rewriting) is# performing a lot of I/O against the disk, in some Linux configurations# Redis may block too long on the fsync() call. Note that there is no fix for# this currently, as even performing fsync in a different thread will block# our synchronous write(2) call.## In order to mitigate this problem it's possible to use the following option# that will prevent fsync() from being called in the main process while a# BGSAVE or BGREWRITEAOF is in progress.## This means that while another child is saving the durability of Redis is# the same as "appendfsync none", that in pratical terms means that it is# possible to lost up to 30 seconds of log in the worst scenario (with the# default Linux settings).# # If you have latency problems turn this to "yes". Otherwise leave it as# "no" that is the safest pick from the point of view of durability.no-appendfsync-on-rewrite no# Automatic rewrite of the append only file.# Redis is able to automatically rewrite the log file implicitly calling# BGREWRITEAOF when the AOF log size will growth by the specified percentage.# # This is how it works: Redis remembers the size of the AOF file after the# latest rewrite (or if no rewrite happened since the restart, the size of# the AOF at startup is used).## This base size is compared to the current size. If the current size is# bigger than the specified percentage, the rewrite is triggered. Also# you need to specify a minimal size for the AOF file to be rewritten, this# is useful to avoid rewriting the AOF file even if the percentage increase# is reached but it is still pretty small.## Specify a precentage of zero in order to disable the automatic AOF# rewrite feature.auto-aof-rewrite-percentage 100auto-aof-rewrite-min-size 64mb################################## SLOW LOG #################################### The Redis Slow Log is a system to log queries that exceeded a specified# execution time. The execution time does not include the I/O operations# like talking with the client, sending the reply and so forth,# but just the time needed to actually execute the command (this is the only# stage of command execution where the thread is blocked and can not serve# other requests in the meantime).# # You can configure the slow log with two parameters: one tells Redis# what is the execution time, in microseconds, to exceed in order for the# command to get logged, and the other parameter is the length of the# slow log. When a new command is logged the oldest one is removed from the# queue of logged commands.# The following time is expressed in microseconds, so 1000000 is equivalent# to one second. Note that a negative number disables the slow log, while# a value of zero forces the logging of every command.slowlog-log-slower-than 10000# There is no limit to this length. Just be aware that it will consume memory.# You can reclaim memory used by the slow log with SLOWLOG RESET.slowlog-max-len 1024################################ VIRTUAL MEMORY ################################## WARNING! Virtual Memory is deprecated in Redis 2.4### The use of Virtual Memory is strongly discouraged.### WARNING! Virtual Memory is deprecated in Redis 2.4### The use of Virtual Memory is strongly discouraged.# Virtual Memory allows Redis to work with datasets bigger than the actual# amount of RAM needed to hold the whole dataset in memory.# In order to do so very used keys are taken in memory while the other keys# are swapped into a swap file, similarly to what operating systems do# with memory pages.# 指定是否启用虚拟内存机制,默认值为no,# VM机制将数据分页存放,由Redis将访问量较少的页即冷数据swap到磁盘上,访问多的页面由磁盘自动换出到内存中# 把vm-enabled设置为yes,根据需要设置好接下来的三个VM参数,就可以启动VM了vm-enabled no# vm-enabled yes# This is the path of the Redis swap file. As you can guess, swap files# can't be shared by different Redis instances, so make sure to use a swap# file for every redis process you are running. Redis will complain if the# swap file is already in use.## Redis交换文件最好的存储是SSD(固态硬盘)# 虚拟内存文件路径,默认值为/tmp/redis.swap,不可多个Redis实例共享# *** WARNING *** if you are using a shared hosting the default of putting# the swap file under /tmp is not secure. Create a dir with access granted# only to Redis user and configure Redis to create the swap file there.vm-swap-file /tmp/redis.swap# With vm-max-memory 0 the system will swap everything it can. Not a good# default, just specify the max amount of RAM you can in bytes, but it's# better to leave some margin. For instance specify an amount of RAM# that's more or less between 60 and 80% of your free RAM.# 将所有大于vm-max-memory的数据存入虚拟内存,无论vm-max-memory设置多少,所有索引数据都是内存存储的(Redis的索引数据就是keys)# 也就是说当vm-max-memory设置为0的时候,其实是所有value都存在于磁盘。默认值为0vm-max-memory 0# Redis swap文件分成了很多的page,一个对象可以保存在多个page上面,但一个page上不能被多个对象共享,vm-page-size是要根据存储的数据大小来设定的。# 建议如果存储很多小对象,page大小最后设置为32或64bytes;如果存储很大的对象,则可以使用更大的page,如果不确定,就使用默认值vm-page-size 32# 设置swap文件中的page数量由于页表(一种表示页面空闲或使用的bitmap)是存放在内存中的,在磁盘上每8个pages将消耗1byte的内存# swap空间总容量为 vm-page-size * vm-pages## With the default of 32-bytes memory pages and 134217728 pages Redis will# use a 4 GB swap file, that will use 16 MB of RAM for the page table.## It's better to use the smallest acceptable value for your application,# but the default is large in order to work in most conditions.vm-pages 134217728# Max number of VM I/O threads running at the same time.# This threads are used to read/write data from/to swap file, since they# also encode and decode objects from disk to memory or the reverse, a bigger# number of threads can help with big objects even if they can't help with# I/O itself as the physical device may not be able to couple with many# reads/writes operations at the same time.# 设置访问swap文件的I/O线程数,最后不要超过机器的核数,如果设置为0,那么所有对swap文件的操作都是串行的,可能会造成比较长时间的延迟,默认值为4vm-max-threads 4############################### ADVANCED CONFIG ################################ Hashes are encoded in a special way (much more memory efficient) when they# have at max a given numer of elements, and the biggest element does not# exceed a given threshold. You can configure this limits with the following# configuration directives.# 指定在超过一定的数量或者最大的元素超过某一临界值时,采用一种特殊的哈希算法hash-max-zipmap-entries 512hash-max-zipmap-value 64# Similarly to hashes, small lists are also encoded in a special way in order# to save a lot of space. The special representation is only used when# you are under the following limits:list-max-ziplist-entries 512list-max-ziplist-value 64# Sets have a special encoding in just one case: when a set is composed# of just strings that happens to be integers in radix 10 in the range# of 64 bit signed integers.# The following configuration setting sets the limit in the size of the# set in order to use this special memory saving encoding.set-max-intset-entries 512# Similarly to hashes and lists, sorted sets are also specially encoded in# order to save a lot of space. This encoding is only used when the length and# elements of a sorted set are below the following limits:zset-max-ziplist-entries 128zset-max-ziplist-value 64# Active rehashing uses 1 millisecond every 100 milliseconds of CPU time in# order to help rehashing the main Redis hash table (the one mapping top-level# keys to values). The hash table implementation redis uses (see dict.c)# performs a lazy rehashing: the more operation you run into an hash table# that is rhashing, the more rehashing "steps" are performed, so if the# server is idle the rehashing is never complete and some more memory is used# by the hash table.# # The default is to use this millisecond 10 times every second in order to# active rehashing the main dictionaries, freeing memory when possible.## If unsure:# use "activerehashing no" if you have hard latency requirements and it is# not a good thing in your environment that Redis can reply form time to time# to queries with 2 milliseconds delay.# 指定是否激活重置哈希,默认为开启activerehashing yes################################## INCLUDES #################################### 指定包含其他的配置文件,可以在同一主机上多个Redis实例之间使用同一份配置文件,而同时各实例又拥有自己的特定配置文件# include /path/to/local.conf# include /path/to/other.conf 完成!结束 此片完了~ 想要了解更多精彩新姿势?请访问我的个人博客 本篇为原创内容,已在个人博客率先发表,随后CSDN,segmentfault,掘金,简书,开源中国同步发出。如有雷同,缘分呢兄弟。赶快加个好友~]]></content>
<tags>
<tag>操作系统</tag>
<tag>软件教学</tag>
<tag>redis</tag>
</tags>
</entry>
<entry>
<title><![CDATA[学好分布式架构=5=-zookeeper]]></title>
<url>%2F2018%2F07%2F29%2F%E6%88%91%E8%A6%81%E5%AD%A6%E5%A5%BD%E5%88%86%E5%B8%83%E5%BC%8F-zookeeper%2F</url>
<content type="text"><![CDATA[“学好分布式架构”节目做到第四期了,👏👏👏 想要解锁更多新姿势?请访问我的博客 分布式概念(前戏)了解一个技术,先要掌握它的理论,然后循序渐进。所以,理论来了~ 什么是分布式呢?简单说,分布式系统背后是由一系列的计算机组成的,但用户感知不到背后的逻辑,就像访问单个计算机一样 最基本的特点分布性服务器的位置可能在不一个位置 并发性程序运行过程中,并发性操作是很常见的。比如同一个分布式系统中的多个节点,同时访问一个共享资源。数据库、分布式存储 无序性进程之间的消息通信,会出现顺序不一致问题 分布式环境下面临的问题分布式的特点决定了它一定会有相应的问题 网络通信分布式系统的信息通过网络来传输的,而网络本身的不可靠性,因此会涉及到一些网络通信问题 网络分区(脑裂)什么是脑裂呢? 在高可用(HA)系统中,当联系2个节点的“心跳线”断开时,本来为一整体、动作协调的HA系统,就分裂成为2个独立的个体。由于相互失去了联系,都以为是对方出了故障。两个节点上的HA软件像“裂脑人”一样,争抢“共享资源”、争起“应用服务”,就会发生严重后果——或者共享资源被瓜分、2边“服务”都起不来了;或者2边“服务”都起来了,但同时读写“共享存储”,导致数据损坏(常见如数据库轮询着的联机日志出错)。 而当网络发生异常导致分布式系统中部分节点之间的网络延时不断增大,就会产生脑裂现象,即组成分布式架构的所有节点,只有部分节点能够正常通信 三态在正常的单机环境下,一个程序调用一个接口,正常情况下只有2个状态,成功态和失败态。 但是在分布式环境下,就又多了一种状态,叫做超时态,指的就是在分布式网路通信时,因为网络故障,我的消息一直没有收到返回或者是我的消息没有发送出去。 分布式事务分布式也会有事物问题,即ACID(原子性、一致性、隔离性、持久性)。譬如,如果两台分布式架构的服务器有这相同的功能,当用户操作的时候,你要保存这两台服务器的同时发生同时成功,这就属于事物问题。 1.原子性(Atomicity) 一个原子事务要么完整执行,要么干脆不执行。这意味着,工作单元中的每项任务都必须正确执行。如果有任一任务执行失败,则整个工作单元或事务就会被终止。即此前对数据所作的任何修改都将被撤销。如果所有任务都被成功执行,事务就会被提交,即对数据所作的修改将会是永久性的。 2.一致性(Consistency) 一致性代表了底层数据存储的完整性。它必须由事务系统和应用开发人员共同来保证。事务系统通过保证事务的原子性,隔离性和持久性来满足这一要求; 应用开发人员则需要保证数据库有适当的约束(主键,引用完整性等),并且工作单元中所实现的业务逻辑不会导致数据的不一致(即,数据预期所表达的现实业务情况不相一致)。例如,在一次转账过程中,从某一账户中扣除的金额必须与另一账户中存入的金额相等。 3.隔离性(Isolation) 隔离性意味着事务必须在不干扰其他进程或事务的前提下独立执行。换言之,在事务或工作单元执行完毕之前,其所访问的数据不能受系统其他部分的影响。 4.持久性(Durability) 持久性表示在某个事务的执行过程中,对数据所作的所有改动都必须在事务成功结束前保存至某种物理存储设备。这样可以保证,所作的修改在任何系统瘫痪时不至于丢失。 中心化和去中心化数据库备份一般使用冷备或者热备 热备:一般用于保证服务正常不间断运行,用两台机器作为服务机器,一台用于实际数据库操作应用,另外一台实时的从前者中获取数据以保持数据一致.如果当前的机器熄火,备份的机器立马取代当前的机器继续提供服务 冷备:.冷备份指在数据库关闭后,进行备份,适用于所有模式的数据库 分布式架构里面,很多的架构思想采用的是去中心化。即在一个分布有众多节点的系统中,每个节点都具有高度自治的特征。节点之间彼此可以自由连接,形成新的连接单元。任何一个节点都可能成为阶段性的中心,但不具备强制性的中心控制功能。节点与节点之间的影响,会通过网络而形成非线性因果关系。这种开放式、扁平化、平等性的系统现象或结构,我们称之为去中心化 最典型的是: zookeeper / etcd 经典的CAPC一致性 (Consistency): 所有节点上的数据,时刻保持一致 A可用性(Availability):每个请求都能够收到一个响应,无论响应成功或者失败 P分区容错 (Partition-tolerance):表示系统出现脑裂以后,可能导致某些server与集群中的其他机器失去联系 无论如何,都只能保证CP或者 AP。CAP理论仅适用于原子读写的Nosql场景,不适用于数据库系统 BASE理论基于CAP理论,CAP理论并不适用于数据库事务(因为更新一些错误的数据而导致数据出现紊乱,无论什么样的数据库高可用方案都是徒劳) ,虽然XA事务可以保证数据库在分布式系统下的ACID特性,但是会带来性能方面的影响; eBay尝试了一种完全不同的套路,放宽了对事务ACID的要求。提出了BASE理论: Basically available : 数据库采用分片模式, 把100W的用户数据分布在5个实例上。如果破坏了其中一个实例,仍然可以保证 80%的用户可用 soft-state: 在基于client-server模式的系统中,server端是否有状态,决定了系统是否具备良好的水平扩展、负载均衡、故障恢复等特性。 Server端承诺会维护client端状态数据,这个状态仅仅维持一小段时间, 这段时间以后,server端就会丢弃这个状态,恢复正常状态 Eventually consistent:数据的最终一致性 是 Zookeeper(正片)zookeeper是一个开源的分布式数据存储系统,提供分布式数据一致性的解决方案。是由雅虎创建的,基于google chubby。 zookeeper是什么分布式数据一致性的解决方案 zookeeper能做什么数据的发布/订阅(配置中心:disconf) 、 负载均衡(dubbo利用了zookeeper机制实现负载均衡) 、命名服务、 master选举(kafka、hadoop、hbase)、分布式队列、分布式锁 zookeeper特性顺序一致性从同一个客户端发起的事务请求,最终会严格按照顺序被应用到zookeeper中 原子性所有的事务请求的处理结果在整个集群中的所有机器上的应用情况是一致的,也就是说,要么整个集群中的所有机器都成功应用了某一事务、要么全都不应用 可靠性一旦服务器成功应用了某一个事务数据,并且对客户端做了响应,那么这个数据在整个集群中一定是同步并且保留下来的 实时性一旦一个事务被成功应用,客户端就能够立即从服务器端读取到事务变更后的最新数据状态;(zookeeper仅仅保证在一定时间内,近实时) zookeeper集群环境zookeeper集群, 包含三种角色: leader / follower /observer observer observer 是一种特殊的zookeeper节点。可以帮助解决zookeeper的扩展性(如果大量客户端访问我们zookeeper集群,需要增加zookeeper集群机器数量。从而增加zookeeper集群的性能。 导致zookeeper写性能下降, zookeeper的数据变更需要半数以上服务器投票通过。造成网络消耗增加投票成本) observer不参与投票。 只接收投票结果。不和follower联系。 不属于zookeeper的关键部位。 安装zookeeper可以虚拟机中操作,系统centos7,zookeeper去官方网下稳定版。安装后: 第一步: 修改配置文件zoo.cfgserver.id=host:port:port id的取值范围: 1~255; 用id来标识该机器在集群中的机器序号; 2888是表示follower节点与leader节点交换信息的端口号,要定义成和系统没有冲突的端口号; 3181 表示leader选举的端口,如果leader节点挂掉了, 需要一个端口来重新选举 栗子: server.1=192.168.11.129:2888:3181 server.2=192.168.11.131:2888:3181 server.3=192.168.11.135:2888:3181 如果需要增加observer节点,zoo.cfg中,增加 ;peerType=observer server.1=192.168.11.129:2888:3181 server.2=192.168.11.135:2888:3181 server.3=192.168.111.136:2888:3181:observer 第二步:创建myid在每一个服务器的dataDir目录下创建一个myid的文件,文件就一行数据,数据内容是每台机器对应的server ID的数字 第三步:启动zookeeper 关掉防火墙 看到生成zeekeeper.out这样的一个日志,可以使用tail命令查看 Zookeeper中的一些概念Zookeeper配置解析 tickTime=2000 zookeeper中最小的时间单位长度 (ms) initLimit=10 follower节点启动后与leader节点完成数据同步的时间 syncLimit=5 leader节点和follower节点进行心跳检测的最大延时时间 dataDir=/tmp/zookeeper 表示zookeeper服务器存储快照文件的目录 dataLogDir 表示配置 zookeeper事务日志的存储路径,默认指定在dataDir目录下 clientPort 表示客户端和服务端建立连接的端口号: 2181 数据模型zookeeper的数据模型和文件系统类似,每一个节点称为:znode. 是zookeeper中的最小数据单元。每一个znode上都可以保存数据和挂载子节点。 从而构成一个层次化的属性结构 节点特性持久化节点 : 节点创建后会一直存在zookeeper服务器上,直到主动删除 持久化有序节点 :每个节点都会为它的一级子节点维护一个顺序 临时节点 : 临时节点的生命周期和客户端的会话保持一致。当客户端会话失效,该节点自动清理 临时有序节点 : 在临时节点上多勒一个顺序性特性 会话客户端和zookeeper会建立一个连接,这就是一个会话。 会话有什么过程呢? Watcher(监听)zookeeper提供了分布式数据发布/订阅,zookeeper允许客户端向服务器注册一个watcher监听。当服务器端的节点触发指定事件的时候,会触发watcher。服务端会向客户端发送一个事件通知 watcher的通知是一次性,一旦触发一次通知后,该watcher就失效 ACLzookeeper提供控制节点访问权限的功能,用于有效的保证zookeeper中数据的安全性。避免误操作而导致系统出现重大事故。 提供了:CREATE /READ/WRITE/DELETE/ADMIN Zookeeper命令操作(增删改查) create [-s] -e path data acl -s 表示节点是否有序 -e 表示是否为临时节点 (默认情况下,是持久化节点) 123create /zoo #创建zoo节点create /zoo /zoo-1 #创建zoo节点下的zoo-1ls /nodes #显示节点 get path [watch] 获得指定 path的信息 3.set path data [version] 修改节点 path对应的data version表示乐观锁,及数据库中有一个version字段去控制数据行的版本号 乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。 4.delete path [version] 删除节点 节点状态stat信息cversion = 0 子(child)节点的版本号 aclVersion = 0 表示acl的版本号,修改节点权限 dataVersion = 1 表示的是当前节点数据的版本号 czxid 节点被创建时的事务ID mzxid 节点最后一次被更新的事务ID pzxid 当前节点下的子节点最后一次被修改时的事务ID ctime = 创建时间 mtime = 修改时间 ephemeralOwner = 0x0 创建临时节点的时候,会有一个sessionId 。 该值存储的就是这个sessionid 。0X0代表无 dataLength = 3 数据值长度 numChildren = 0 子节点数 在Java中使用zookeeper引入依赖 12345 <dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.9</version></dependency> 困了,睡觉。明天再码字。未完待续。。。。。 参考 聊聊分布式事务,再说说解决方案 感谢 结束 此片完了~ 想要了解更多精彩新姿势?请访问我的个人博客 本篇为原创内容,已在个人博客率先发表,随后CSDN,segmentfault,掘金,简书,开源中国同步发出。如有雷同,缘分呢兄弟。赶快加个好友,咱们两个想个号码, 买个彩票,先挣他个几百万😝]]></content>
<tags>
<tag>java</tag>
<tag>技术</tag>
<tag>学好分布式架构</tag>
</tags>
</entry>
<entry>
<title><![CDATA[学好分布式架构=4=-webservice]]></title>
<url>%2F2018%2F07%2F28%2F%E6%88%91%E8%A6%81%E5%AD%A6%E5%A5%BD%E5%88%86%E5%B8%83%E5%BC%8F-webservice%2F</url>
<content type="text"><![CDATA[今天咱们一起探索一下webservice这个技术。 想要解锁更多新姿势?请访问我的博客 什么是webservicewebservice也可以叫xml web service webservice(在javax.jws), 轻量级的独立的通讯技术 基于web的服务:服务端提供的服务接口让客户端访问 跨平台、跨语言的整合方案 为什么要使用webservice相比RMI,它是以前比较好的跨语言调用的解决方案 就譬如说,一个电商平台,订单可以查看物流状态。,这可能就是.net实现的webservice服务接口。 webservice中的一些概念WSDL(web service definition language webservice 定义语言)webservice服务需要通过wsdl文件来说明自己有什么服务可以对外调用。并且有哪些方法、方法里面有哪些参数 wsdl基于XML(可扩展标记语言)去定义了 对应一个.wsdl的文件类型 定义了webservice的服务器端和客户端应用进行交互的传递数据和响应数据格式和方式 一个webservice对应唯一一个wsdl文档 SOAP(simple object access protocal简单对象访问协议)差不多就是http+xml,什么意思呢?webservice通过http协议发送和接收请求时, 发送的内容(请求报文)和接收的内容(响应报文)都是采用xml格式进行封装,这些特定的HTTP消息头和XML内容格式就是SOAP协议。 一条 SOAP 消息就是一个普通的 XML 文档,包含下列元素: • Envelope 元素,标识XML 文档一条 SOAP 消息 • Header 元素,包含头部信息的XML标签 • Body 元素,包含所有的调用和响应的主体信息的标签 • Fault 元素,错误信息标签。 插一句嘴,现在的呢,基于RESTful接口的都是http+json 回到主题,什么是soap? 一种简单、基于HTTP和XML的协议 soap消息:请求和响应消息 http+xml报文 SEI(webservice endpoint interface webservice的终端接口)webservice服务端用来处理请求的接口,也就是发布出去的接口。 Demo如何实现呢?写一个小demo 首先是服务端的接口 123456@WebService//SEI以及实现类public interface ISayHello { @WebMethod//sei中的方法 public String sayHello();} 具体实现类 123456789@WebServicepublic class SayHello implements ISayHello{ @Override public String sayHello() { return "hello"; }} 启动类 12345678public class Bootstrap { public static void main(String[] args) { //终端发布~ Endpoint.publish("http://localhost:8888/tech/hello",new SayHello()); System.out.println("发布成功~"); }} 接着我们启动程序。到浏览器输入url+”?wsdl”就可以查看wsdl文档了 接下来在IDEA中打开terminal或者直接进入cmd,调用wsimport工具,输入命令: 1wsimport -keep http://localhost:8888/tech/hello?wsdl 这里简介一下參数: -d 表示输出的文件夹。文件夹必须事先存在,否则导出失败。 -keep 表示导出webservice的class文件时是否也导出源码java文件。 -verbose verbose表示具体信息。 这时候生成的客户端代码 迁移到IDEA打开,新建一个demo 1234567public class Demo { public static void main(String[] args) { SayHelloService service=new SayHelloService(); SayHello sayHello= (SayHello) service.getSayHelloPort(); System.out.println(); }} demo完成~! 关于WSDL文档下面说说wsdl文档各个标签的含义 Types标签定义整服务端的数据报文 Schema标签为了实现最大的互操作性(interoperability)和平台中立性(neutrality),WSDL选用XML Schema DataTypes,简称XSD作为标准类型系统,并将它作为固有类型系统。 图中的是数据定义部分,该部分定义了两个元素,一个是sayHello,一个是sayHelloResponse: sayHello:定义了一个复杂类型,仅仅包含一个简单的字符串,将来用来描述操作的参入传入部分; sayHelloResponse:定义了一个复杂类型,仅仅包含一个简单的字符串,将来用来描述操作的返回值; Message标签定义元素数据类型 Porttype定义服务器端的SEI input/output属性:指定输入输出的数据类型 Binding标签type属性:引用PortType service标签表示服务器端的一个webservice容器 name属性:制定客户端属性 prot属性:指定客户端的容器类 address:大部分全部webservice的地址 过去的Webservice工具发个名字留作备份,因为现在早已不用复杂的webservice了 cxfceltix+xfire axis完结束 此片完了~ 想要了解更多精彩新姿势?请访问我的个人博客 本篇为原创内容,已在个人博客率先发表,随后CSDN,segmentfault,掘金,简书,开源中国同步发出。如有雷同,缘分呢兄弟。赶快加个好友,咱们两个想个号码, 买个彩票,先挣他个几百万😝]]></content>
<tags>
<tag>java</tag>
<tag>技术</tag>
<tag>学好分布式架构</tag>
</tags>
</entry>
<entry>
<title><![CDATA[学好分布式架构=6=-RMI通信框架]]></title>
<url>%2F2018%2F07%2F26%2FRMI%E9%80%9A%E4%BF%A1%E6%A1%86%E6%9E%B6%2F</url>
<content type="text"><![CDATA[分布式框架是最近几年的热门。可是要想理解分布式框架着实不易,为了努力跟上时代潮流,特此开了一个专题,起名“我要学好分布式”,通过博客来分享一下我的学习过程,加深我对分布式整体框架的理解。 想要解锁更多新姿势?请访问我的博客 什么是RPC英文就不说了。中文名远程进程调用协议。顾名思义,客户端在不知道细节的情况下,可以调用远程计算机的api,就像是调用本地方法一样。 RPC协议是一个规范。主流的PRC协议有Dubbo、Thrif、RMI、Webservice、Hessain 他又一个非常大的特点,网络协议和网络IO对于调用端和服务端来说是透明的(动态代理) 一个RPC框架包含的要素: RMIRMI(remote method invocation) , 可以认为是RPC的java版本 RMI使用的是JRMP(Java Remote Messageing Protocol), JRMP是专门为java定制的通信协议,所以他是纯java的分布式解决方案 。 RMI Demo 先写个测试用的远程接口,注意接口要抛异常 123public interface ISayHello extends Remote { public String satHello(String name) throws RemoteException;} 2.实现远程接口,并且继承:`UnicastRemoteObject` 123456789public class SayHelloImpl extends UnicastRemoteObject implements ISayHello{ protected SayHelloImpl() throws RemoteException { } public String satHello(String name) throws RemoteException { return "hello," + name; }} 3.创建服务器程序: `createRegistry`方法注册远程对象 123456789101112131415161718192021import java.net.MalformedURLException;import java.rmi.AlreadyBoundException;import java.rmi.Naming;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;public class HelloServer { public static void main(String[] args) { try { ISayHello sayHello =new SayHelloImpl(); LocateRegistry.createRegistry(8888); Naming.bind("rmi://localhost:8888/sayhello",sayHello); System.out.println("server start success"); } catch (RemoteException e) { e.printStackTrace(); } catch (MalformedURLException e) { e.printStackTrace(); } catch (AlreadyBoundException e) { e.printStackTrace(); } } 4.创建客户端程序 1234567891011121314public class HelloClient { public static void main(String[] args) { try { ISayHello iSayHello = (ISayHello) Naming.lookup("rmi://localhost:8888/sayhello"); System.out.println("hello"); } catch (NotBoundException e) { e.printStackTrace(); } catch (MalformedURLException e) { e.printStackTrace(); } catch (RemoteException e) { e.printStackTrace(); } }} RMI调用过程 流程: 1.去注册中心注册,server端启动服务。 2.注册中心联系stub(存根)。stub用于客户端 ,在j2ee中是这么说的:为屏蔽客户调用远程主机上的对象,必须提供某种方式来模拟本地对象,这种本地对象称为存根(stub),存根负责接收本地方法调用,并将它们委派给各自的具体实现对象 3.server注册对象,然后返回注册对象 4.客户端访问注册中心,(动态代理)返回stub对象 5.stub(存根)远程调用skeleton (骨架 ) 6.skeleton 调用相应接口 源码让我看看核心的注册服务的源码实现 123456789101112131415161718192021222324public RegistryImpl(final int var1) throws RemoteException { this.bindings = new Hashtable(101); //安全认证 if (var1 == 1099 && System.getSecurityManager() != null) { try { AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() { public Void run() throws RemoteException { LiveRef var1x = new LiveRef(RegistryImpl.id, var1); RegistryImpl.this.setup(new UnicastServerRef(var1x, (var0) -> { return RegistryImpl.registryFilter(var0); })); return null; } }, (AccessControlContext)null, new SocketPermission("localhost:" + var1, "listen,accept")); } catch (PrivilegedActionException var3) { throw (RemoteException)var3.getException(); } } else { //初始化远程引用UnicastServerRef对象 LiveRef var2 = new LiveRef(id, var1);//《-------------------------- this.setup(new UnicastServerRef(var2, RegistryImpl::registryFilter)); } } 点进UnicastServerRef,找出实现的关系~ 点进setup方法,用idea反编码 12345678910111213141516171819public Remote exportObject(Remote var1, Object var2, boolean var3) throws RemoteException { Class var4 = var1.getClass(); Remote var5; try { var5 = Util.createProxy(var4, this.getClientRef(), this.forceStubUse);//《-------------------- } catch (IllegalArgumentException var7) { throw new ExportException("remote object implements illegal remote interface", var7); } if (var5 instanceof RemoteStub) {//《-------------------------- this.setSkeleton(var1); } Target var6 = new Target(var1, this, var5, this.ref.getObjID(), var3);//《------------------------ this.ref.exportObject(var6); this.hashToMethod_Map = (Map)hashToMethod_Maps.get(var4); return var5; } 发现在创建代理,判断当前的var是不是远程stub,如果是就设置骨架。如果不是,就构建target对象。点开代理 1234567891011121314151617181920212223242526public static Remote createProxy(Class<?> var0, RemoteRef var1, boolean var2) throws StubNotFoundException { Class var3; try { var3 = getRemoteClass(var0);//《-------------------------- } catch (ClassNotFoundException var9) { throw new StubNotFoundException("object does not implement a remote interface: " + var0.getName()); } if (var2 || !ignoreStubClasses && stubClassExists(var3)) { return createStub(var3, var1);//《-------------------------- } else { final ClassLoader var4 = var0.getClassLoader(); final Class[] var5 = getRemoteInterfaces(var0); final RemoteObjectInvocationHandler var6 = new RemoteObjectInvocationHandler(var1); try { return (Remote)AccessController.doPrivileged(new PrivilegedAction<Remote>() { public Remote run() { return (Remote)Proxy.newProxyInstance(var4, var5, var6); } }); } catch (IllegalArgumentException var8) { throw new StubNotFoundException("unable to create proxy", var8); } } } 发现在调用远程服务,然后创建了stub。继续点开getRemoteClass()方法 123456789101112131415private static Class<?> getRemoteClass(Class<?> var0) throws ClassNotFoundException { while(var0 != null) { Class[] var1 = var0.getInterfaces();//《-------------------------- for(int var2 = var1.length - 1; var2 >= 0; --var2) { if (Remote.class.isAssignableFrom(var1[var2])) { return var0; } } var0 = var0.getSuperclass(); } throw new ClassNotFoundException("class does not implement java.rmi.Remote"); } 发现现在在创建实例 好吧,回到createProxy方法,再看看顺着往下走,看看Target var6 = new Target(var1, this, var5, this.ref.getObjID(), var3); this.ref.exportObject(var6);的出口对象方法 123public void exportObject(Target var1) throws RemoteException { this.ep.exportObject(var1); } 123456789public interface Endpoint { Channel getChannel(); void exportObject(Target var1) throws RemoteException; Transport getInboundTransport(); Transport getOutboundTransport();} 123public void exportObject(Target var1) throws RemoteException { this.transport.exportObject(var1); } 一路点下去,找到了tcp出口的方法。这是属于协议层的玩意。 12345public void exportObject(Target var1) throws RemoteException { synchronized(this) { this.listen(); ++this.exportCount; } 一路点下去,发现listen。 12345678910111213141516171819202122232425private void listen() throws RemoteException { assert Thread.holdsLock(this); TCPEndpoint var1 = this.getEndpoint(); int var2 = var1.getPort(); if (this.server == null) { if (tcpLog.isLoggable(Log.BRIEF)) { tcpLog.log(Log.BRIEF, "(port " + var2 + ") create server socket"); } try { this.server = var1.newServerSocket();//《-------------------------- Thread var3 = (Thread)AccessController.doPrivileged(new NewThreadAction(new TCPTransport.AcceptLoop(this.server), "TCP Accept-" + var2, true)); var3.start(); } catch (BindException var4) { throw new ExportException("Port already in use: " + var2, var4); } catch (IOException var5) { throw new ExportException("Listen failed on port: " + var2, var5); } } else { SecurityManager var6 = System.getSecurityManager(); if (var6 != null) { var6.checkListen(var2); } } 发现newServerSocket!!! 综上,总体流程和上图一样。 RMI缺陷1.基于java,支持语言单一 2.服务注册只能注册到我上面分析的那个源码。注册中心挂了以后就完了 3.序列化是用java原生那个方法,效率不好 4.服务端底层是bio方式,性能不好 手写个RMI?步骤: 编写服务器程序,暴露一个监听, 可以使用socket 编写客户端程序,通过ip和端口连接到指定的服务器,并且将数据做封装(序列化) 服务器端收到请求,先反序列化。再进行业务逻辑处理。把返回结果序列化返回 源码:https://github.com/tengshe789/ForRMI 把源码发布到GitHub了,在把源码粘贴太麻烦了。 结束 此片完了~ 想要了解更多精彩新姿势?请访问我的个人博客 本篇为原创内容,已在个人博客率先发表,随后CSDN,segmentfault,掘金,简书,开源中国同步发出。如有雷同,缘分呢兄弟。赶快加个好友~]]></content>
<tags>
<tag>java</tag>
<tag>技术</tag>
<tag>学好分布式架构</tag>
<tag>网络</tag>
</tags>
</entry>
<entry>
<title><![CDATA[关掉X5内核让微信变得飞快~]]></title>
<url>%2F2018%2F07%2F26%2F%E5%85%B3%E6%8E%89X5%E5%86%85%E6%A0%B8%E8%AE%A9%E5%BE%AE%E4%BF%A1%E5%8F%98%E5%BE%97%E9%A3%9E%E5%BF%AB%2F</url>
<content type="text"><![CDATA[前言我换手机了。。。 以前的小S8卖掉了(日常操作😎)等价换了个👇 新手机测试完功能,下好微信以后,发现浏览技术公众号比老手机慢了很多,纳闷。后来才想到忘了换内核了。 操作步骤WebView 是Android开发中经常会用到的功能,是一个基于webkit引擎,用于显示来自本地、服务器web页面的控件,可以很好的提升应用扩展性。有以下优点: 可以直接显示和渲染web页面 webview可以直接用html文件(网络上或本地assets中)作布局 和JavaScript交互调用 首先,我们需要将webview更新到最新。 然后打开微信,在任意聊天界面,输入 debugtbs.qq.com 点击禁用内核,重启微信后,在点击清除tbs 操作完成,结束! 此片完了~ 想要了解更多精彩新姿势?请访问我的个人博客 本篇为原创内容,已在个人博客率先发表,随后CSDN,segmentfault,掘金,简书,开源中国同步发出。如有雷同,缘分呢兄弟。赶快加个好友~]]></content>
<tags>
<tag>窍门</tag>
</tags>
</entry>
<entry>
<title><![CDATA[安利软件-如何在Windows调整多核心CPU负载并优化?]]></title>
<url>%2F2018%2F07%2F25%2F%E6%95%99%E4%BD%A0%E5%A6%82%E4%BD%95%E5%9C%A8Winddows%E7%8E%AF%E5%A2%83%E4%B8%8B%E5%A4%9A%E6%A0%B8%E5%BF%83CPU%E8%B4%9F%E8%BD%BD%E4%BC%98%E5%8C%96%2F</url>
<content type="text"><![CDATA[hexo不知道咋地了,markdown转html生成静态资源的时候,图片完全不写进html,前面几篇源码分析的图全显示不出来了。脑阔疼~ 由于暂时没有解决静态资源的问题,所以我本篇不放图,来一场没有硝烟的战争。我会尽量让此文章通俗易懂,待我解决问题时,再重新编辑一下(反正这个博客没人看)。 今天我来分享一个软件,名字是Process Lasso Pro。这个软件的作用是优化进程调度,降低高CPU占用率进程的优先级,保证前台进程和系统拥有最高的响应速度,简单说就是尽可能保证电脑在高CPU占用率的情况不卡。注意,软件如果是家用就是免费的,专业版有高级功能,不过基本用不到。 想要解锁更多新姿势?请访问我的博客 主要特性先来看看官方发的产品特性。 ProBalance:安全有效地平衡进程优先级,从而使电脑在高负荷的时候提高系统的响应速度和防止系统失速和反应滞后。我们的技术并不做额外的前台推进而是适当降低了后台进程优先级。我们多年的经验使我们的算法高度优化、独一无二。 持久的优先级和亲和力:为您的进程设置“默认”的优先级和CPU亲和力,让您可以调整它们的执行方式。 自动终止:对于您所选择的特定进程,它一旦运行,Process Lasso将自动终止它。 进程看守:当达到指定的阈值的时候,将终止、重新启动或更改CPU亲和力。 实例数量限制:将进程实例数量限制在指定的范围内。 多核优化:通过预设的CPU亲和力和ProBalance亲和力的调整,可以优化多核处理器,提高CPU运行效率。它还避免使用逻辑核心,从而在超线程CPU下提高提高某些应用程序的性能。 I/O优先级:Windows Vista及Windows 7下可以设置进程的当前的/默认的/持续的I / O优先级 电源模式自动化:设置电源模式,当某些进程正在运行时自动更改为“高性能”,当进程退出后再换到原先的模式(较新版本功能更强大,可以更改到任何电源模式)。 节约电能:或当需要时提高性能! 系统响应标:我们将以独特的算法,以惊人的准确率监测系统响应 独立的核心引擎:这个独立的后台进程可以一个普通的过程或服务形式运行。这个“核心引擎”被命名 ProcessGovernor.exe,它实际上可以强制执行进程规则和ProBalance。它根本不需要GUI(主窗口)的存在。这可以实现最小的资源利用率。 最小的资源利用:在实现最小的资源利用方面,没有任何一个程序可以与Process Lasso相比。它是用原生的C + +编写的,并不断改进以获得最大的效率。我们也提供原生的64位版本。本程序的内存使用量通常在1MB以下!(依系统而定) 安装首先,选择X64安装包,一路next到底。 GUI使用安装完毕,在主界面上,可以看到当前所有的活动进程,这个和系统的资源管理器很相似。Process Lasso在默认设置下已经可以生效,它会监视后台进程,如果某个进程超过了CPU占用率的阈值,就会对他进行限制,降低CPU对这个进程的响应优先级,让CPU着力于前台进程(如游戏等)。 右击较为占CPU使用时间的游戏、视频等的进程,勾选“归类为游戏或多媒体进程” 。这时,Process Lasso会排除对这个进程的“抑制”,并在运行这个进程时调节电源方案为“高性能”,充分调动CPU资源。 除此之外,重点就是优化多核心CPU的利用率了,还是右键单击要优化执行的进程,选中“CPU亲和力(限制CPU使用)–>总是–>选择CPU亲和力”,按照自己的需求勾选需要使用的CPU核心(一般来说,CPU 0性能最强,CPU 1性能第二强…….CPU n性能最弱),这样这个进程关闭后再运行也是按照这个设置调用CPU资源。 注意事项 不推荐对Process Lasso自身/安全软件/杀毒软件/系统关键服务进程等进行结束进程、降低内存或I/O优先级至非常低、硬节流等操作,且部分杀毒、安全软件有自我保护功能,结束进程、CPU优先级、I/O优先级 置可能会失效 菜单->选项->日志设置->勾选“退出时清除日志”“保持日志修剪”可以有效降低日志数量 建议保持Process Lasso开机启动(包括服务),部分开机加速优化软件会禁止Process Lasso启动,这将导致Process Lasso无法实时优化系统 Process Lasso在界面运行的过程中,由于有大量信息需要获取并实时显示,可能会看到Process Lasso图形界面或核心引擎进程CPU使用率达到15%~40%,这属于正常现象。隐藏或关闭此界面后Process Lasso的资源占用率将会变得非常小,不会拖慢电脑速度。 总结先喊一句口号,Process Lasso牛逼! 不过给大家举例的不过是它的皮毛而已,更多优化设置可以通过自己的尝试调整获得,希望大家可以多多研究,发现有什么有用的玩法可以私聊我。 对了,这个软件有收费Pro版和免费版,收费版有很多新功能,比如强制性对进程的CPU占用率进行动态调整,能够明显提升部分应用的执行效率。原则上我不推荐任何用户使用盗版软件的。]]></content>
<tags>
<tag>日常</tag>
<tag>技术</tag>
<tag>Windows10</tag>
<tag>软件教学</tag>
</tags>
</entry>
<entry>
<title><![CDATA[那些年数组的排序]]></title>
<url>%2F2018%2F07%2F24%2F%E9%82%A3%E4%BA%9B%E5%B9%B4%E6%95%B0%E7%BB%84%E7%9A%84%E6%8E%92%E5%BA%8F%2F</url>
<content type="text"><![CDATA[今天来回顾一下简单的排序思想,留作今后的复习和备份用。本篇是非常非常基础的,甚至都不会讲实际项目真正能用的排序方法,譬如双轴快速排序 。写的不好请多多谅解。 想要解锁更多新姿势?请访问我的博客 准备阶段相关功能函数为了保持代码的整洁,先创造好对数器和相关功能性函数。 交换器两个数组中的元素比较排序过程中,一定会有元素的交换操作。为了保持代码的整洁,先写出交换操作的函数。 12345public static void swap(int[] arr, int i, int j) { arr[i] = arr[i] ^ arr[j]; arr[j] = arr[i] ^ arr[j]; arr[i] = arr[i] ^ arr[j]; } 随机样本产生器自己编数组太麻烦了,让他自己生产吧 1234567public static int[] generateRandomArray(int maxSize, int maxValue) { int[] arr = new int[(int) ((maxSize + 1) * Math.random())]; for (int i = 0; i < arr.length; i++) { arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random()); } return arr; } 对数器对数器其实就是一个绝对正确但是复杂度不好的方法。 123public static void comparator(int[] arr) { Arrays.sort(arr); } 说说Arrays.sort()的逻辑吧。数组进入方法,先判断。如果数组的长度小于QUICKSORT_THRESHOLD(默认值是286)的话,再判断,如果数组长度小于INSERTION_SORT_THRESHOLD(值为47)的话,那么就会用插入排序 ,否则就会使用双轴快速排序。 如果大于286呢,它就会坚持数组的连续升序和连续降序性好不好,如果好的话就用归并排序,不好的话就用快速排序。 比较器比较两个数组一不一样~ 1234567891011121314151617public static boolean isEqual(int[] arr1, int[] arr2) { if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) { return false; } if (arr1 == null && arr2 == null) { return true; } if (arr1.length != arr2.length) { return false; } for (int i = 0; i < arr1.length; i++) { if (arr1[i] != arr2[i]) { return false; } } return true; } 打印器123456789public static void printArray(int[] arr) { if (arr == null) { return; } for (int i = 0; i < arr.length; i++) { System.out.print(arr[i] + " "); } System.out.println();} 复制器12345678910public static int[] copyArray(int[] arr) { if (arr == null) { return null; } int[] res = new int[arr.length]; for (int i = 0; i < arr.length; i++) { res[i] = arr[i]; } return res; } 主函数12345678910111213141516171819202122public static void main(String[] args) { int testTime = 500000; int maxSize = 100; int maxValue = 100; boolean succeed = true; for (int i = 0; i < testTime; i++) { int[] arr1 = generateRandomArray(maxSize, maxValue); int[] arr2 = copyArray(arr1); bubbleSort(arr1); comparator(arr2); if (!isEqual(arr1, arr2)) { succeed = false; break; } } System.out.println(succeed ? "牛逼,算法对了!" : "❌!"); int[] arr = generateRandomArray(maxSize, maxValue); printArray(arr); bubbleSort(arr);//测试的算法 printArray(arr);} 脑子脑阔疼 正篇基于比较的排序冒泡排序原理冒泡排序算法的原理如下: 比较相邻的元素。如果第一个比第二个大,就交换他们两个。此时这两个数,永远是后面的数大。 第一回合将每一对相邻元素做同样的工作。回合结束后,最后的元素是整个数组最大的数。 第二回合…第n回合过程中,对除了最后一个元素重复以上的步骤。 实现123456789101112public static void bubbleSort(int[] arr) { if (arr == null || arr.length < 2) { return; } for (int end = arr.length - 1; end > 0; end--) {//end最后的数 for (int i = 0; i < e; i++) { if (arr[i] > arr[i + 1]) { swap(arr, i, i + 1);//交换 } } } } 复杂度时间复杂度:O(N²) 额外空间复杂度:O(1) 选择排序原理1.第一回合,将指针指向第一个元素,将第一个元素和剩余的元素比较,最小的元素放到一号位置。 2.第二回合…第n回合过程中,指针加一。对除了第一个元素重复以上的步骤。 实现123456789101112public static void selectionSort(int[] arr) { if (arr == null || arr.length < 2) { return; } for (int i = 0; i < arr.length - 1; i++) { int minIndex = i; for (int j = i + 1; j < arr.length; j++) { minIndex = arr[j] < arr[minIndex] ? j : minIndex; } swap(arr, i, minIndex); }} 复杂度时间复杂度:O(N²) 额外空间复杂度:O(1) 插入排序的原理1.第一回合,比较第一个元素和第二个元素大小,大的放在第二个位置上 2.第二回合,将第三个元素与第二、第一个元素比较,大的放在第三个位置上 3.轮回 实现12345678910public static void insertionSort(int[] arr) { if (arr == null || arr.length < 2) { return; } for (int i = 1; i < arr.length; i++) { for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) { swap(arr, j, j + 1); } }} 复杂度时间复杂度:O(N²) 额外空间复杂度:O(1) 堆排序堆其实就是完全二叉树,看堆要首先知道大顶堆、小顶堆。 每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。 大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2] 小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2] 原理将待排序序列构造成一个大顶堆(升序采用大顶堆,降序采用小顶堆),此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了 代码123456789101112131415161718192021222324252627282930313233343536public static void heapSort(int[] arr) { if (arr == null || arr.length < 2) { return; } //0-i之间生成大根堆这种结构 for (int i = 0; i < arr.length; i++) { heapInsert(arr, i); } int size = arr.length;//定义数组大小,可以判断是否越界 swap(arr, 0, --size); while (size > 0) { heapify(arr, 0, size); swap(arr, 0, --size); } } //生成大根堆这种结构 public static void heapInsert(int[] arr, int index) { while (arr[index] > arr[(index - 1) / 2]) {//如果我这个节点比父节点大 swap(arr, index, (index - 1) / 2);//交换 index = (index - 1) / 2;//回到父位置继续 } } //将数值小的元素往下沉 public static void heapify(int[] arr, int index, int size) { int left = index * 2 + 1; while (left < size) {//左孩子在堆上,没越界 int largest = left + 1 < size && arr[left + 1] > arr[left] ? left + 1 : left;//找出左右孩子中最大的数 largest = arr[largest] > arr[index] ? largest : index;//和父比较 if (largest == index) { break; } swap(arr, largest, index); index = largest;//回到较大节点 left = index * 2 + 1; } } 复杂度如果只是建立堆的过程,时间复杂度为O(N) 时间复杂度O(N*logN) 额外空间复杂度O(1) 快速排序快速排序不是一种稳定的排序算法,也就是说,多个相同的值的相对位置也许会在算法结束时产生变动。 假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。 原理我先说说经典快排的思路吧。 将数组分成两部分,一部分是小于等于某个数的,一部分是大于等于某个数的。这两部分初始指针在数组的左(L)右(R)两头,此时L和R分别是一个边界点。 1.先定义less区域和more区域,代表比数组中某一个数更小更大的区域。初始less区域是L-1以左的部分,more区域是R以右的区域 2.第一回合,从数组左边开始。若L指针指的节点值小于某个数值,less区域向右移动一个位置 swap(arr,++less,L++);,L节点位置+1准备下一个回合;若它大于这个数值,more区域向左扩张一格,然后将这个节点放到more区域swap(arr,--more,L++);,L节点位置+1准备下一个回合;若他等于这个数值,什么也不管,只是L节点位置+1准备下一个回合。 3.重复上述过程,得到了一个数组,他的L指针右边时小于某个数的,R的右边时大于某个数的。[L,R]这个区间是等于某个数的。 4.返回这个都是相同数的数组的左边界、右边界 5.不断递归 经典快排有一个弊端。左部分和右部分的规模不一样或者有一个部分规模特别大,算法效率会变差。举个栗子,如果我有个数组[1,1,3,4,7,6,1,2,1,5,1,7],我指定的某个数字是7,那么那么排序后就变成了[1,1,1,1,1,2,3,4,5,6,7],经典快排结束后只搞定了一个一个区间(<7的区间),复杂度就从理想状态下的O(N)变成了O(N²) 然后就有了改进后的随机快排。 随机快排比经典快排多了一个选随机数的过程 swap(arr, L + (int) (Math.random() * (R - L + 1)), R);。就是随机生成某个数,这样生成的区间虽然也会出现上述经典快排的恶劣情况,但是此时的复杂度就从原来的恶劣事件变成了有概率恶劣事件,但总体期望是好的。这就变成了一个概率问题。 代码以下为随机快排 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364public static void main(String[] args) { int[] nums = {9,10,45,36,98,1,3,5,8,6,4}; quickSort(nums); for (int num : nums) { System.out.print(num+","); }}/** * 快速排序,使得整数数组 arr 有序 */public static void quickSort(int[] arr) { if (arr == null || arr.length < 2) { return; } quickSort(arr, 0, arr.length - 1);}/** * 快速排序,使得整数数组 arr 的 [L, R] 部分有序 */public static void quickSort(int[] arr, int L, int R) { if(L < R) { // 把数组中随机的一个元素与最后一个元素交换,这样以最后一个元素作为基准值实际上就是以数组中随机的一个元素作为基准值 swap(arr, new Random().nextInt(R - L + 1) + L, R); int[] p = partition(arr, L, R); quickSort(arr, L, p[0] - 1); quickSort(arr, p[1] + 1, R); }}/** * 分区的过程,整数数组 arr 的[L, R]部分上,使得: * 大于 arr[R] 的元素位于[L, R]部分的右边,但这部分数据不一定有序 * 小于 arr[R] 的元素位于[L, R]部分的左边,但这部分数据不一定有序 * 等于 arr[R] 的元素位于[L, R]部分的中间 * 返回等于部分的第一个元素的下标和最后一个下标组成的整数数组 */public static int[] partition(int[] arr, int L, int R) { int basic = arr[R]; int less = L - 1; int more = R + 1; while(L < more) { if(arr[L] < basic) { swap(arr, ++less, L++); } else if (arr[L] > basic) { swap(arr, --more, L); } else { L++; } } return new int[] { less + 1, more - 1 };}/* * 交换数组 arr 中下标为 i 和下标为 j 位置的元素 */public static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp;} 复杂度科学家数学证明,长期期望的时间复杂度为O(logN*N) 快速排序可以做到稳定性问题,非常难,要知道的可以谷歌“01 stable sort” ,反正我不会。 归并排序原理1.和上题一样,先定义左边界L右边界R数组中,然后定义一个中间值mid = (r-l)/2 2.递归,在边界内部不断的找中间值mid 实现1234567891011121314151617//归并排序public static void mergeSort(int[] arr){ if (arr==null || arr.length <2){ return; } mergeSort(arr,0,arr.length-1);}private static void mergeSort(int[] arr, int l, int r) { if (l == r){ return; } int mid = l + ((r - l) >> 1); //(r-l)/2 mergeSort(arr,l,mid); mergeSort(arr,mid+1,r); merge(arr, l, mid, r);} 复杂度时间复杂度O(N*logN) 额外空间复杂度O(N),归并排序的额外空间复杂度可以变成O(1),但是非常难,我没花时间研究,要知道的可以谷歌“归并排序 内部缓存法” 这里的时间复杂度怎么算出来的呢?有一个master定理: T(N) = a*T(N/b) + O(N^d) 其中 a >= 1 and b > 1 是常量,其表示的意义是n表示问题的规模,a表示递归的次数也就是生成的子问题数,b表示每次递归是原来的1/b之一个规模。 如下: 1) log(b,a) > d -> 复杂度为O(N^log(b,a)) 2) log(b,a) = d -> 复杂度为O(N^d * logN) 3) log(b,a) < d -> 复杂度为O(N^d) 这里,归并排序中b=2,a=2. 非基于比较的排序非基于比较的排序,与被排序的样本的实际数据状况很有关系,所以实际中并不经常使用 桶排序原理1.找到一个数组中最大数的值 2.定义(最大数+1)个桶 3.将数组的数放到对应编号相同的桶中,每放进一个数,桶里面的数值加一 4.依次从小输出这个桶,桶里的元素出现几次就输出几个桶的编号 代码1234567891011121314151617181920// only for 0~200 value public static void bucketSort(int[] arr) { if (arr == null || arr.length < 2) { return; } int max = Integer.MIN_VALUE; for (int i = 0; i < arr.length; i++) { max = Math.max(max, arr[i]); } int[] bucket = new int[max + 1]; for (int i = 0; i < arr.length; i++) { bucket[arr[i]]++; } int i = 0; for (int j = 0; j < bucket.length; j++) { while (bucket[j]-- > 0) { arr[i++] = j; } } } 复杂度时间复杂度O(N) 额外空间复杂度O(N) 结束 此片完了~ 想要了解更多精彩新姿势?请访问我的个人博客 本篇为原创内容,已在个人博客率先发表,随后CSDN,segmentfault,掘金,简书,开源中国同步发出。如有雷同,缘分呢兄弟。赶快加个好友~]]></content>
<tags>
<tag>java</tag>
<tag>技术</tag>
<tag>数据结构与算法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[关于TreeMap的个人理解]]></title>
<url>%2F2018%2F07%2F23%2FTreeMap%2F</url>
<content type="text"><![CDATA[群里的大哥说了,要想懂红黑树的应用,先要看TreeMap。 想要解锁更多新姿势?请访问https://tengshe789.github.io/ OK,现在开始: 红黑树简介红黑树又称红-黑二叉树,它首先是一颗二叉树,它具体二叉树所有的特性。同时红黑树更是一颗自平衡的排序二叉树。 一般的二叉树他们都需要满足一个基本性质–即树中的任何节点的值大于它的左子节点,且小于它的右子节点。因为按照这个基本性质使得树的检索效率大大提高。但我们知道在生成二叉树的过程是非常容易失衡的,最坏的情况就是一边倒(只有右/左子树),这样势必会导致二叉树的检索效率大大降低(O(n)),所以为了维持二叉树的平衡,大牛们提出了各种实现的算法,如:AVL,SBT,伸展树,TREAP ,红黑树等等。 平衡二叉树必须具备如下特性:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。也就是说该二叉树的任何一个等等子节点,其左右子树的高度都相近。 红黑树顾名思义就是节点是红色或者黑色的平衡二叉树,它通过颜色的约束来维持着二叉树的平衡。对于一棵有效的红黑树二叉树而言我们必须增加如下规则: 1、每个节点都只能是红色或者黑色 2、根节点是黑色 3、每个叶节点(NIL节点,空节点)是黑色的。 4、如果一个结点是红的,则它两个子节点都是黑的。也就是说在一条路径上不能出现相邻的两个红色结点。 5、从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。 这些约束强制了红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这棵树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。所以红黑树它是复杂而高效的,其检索效率O(log n)。下图为一颗典型的红黑二叉树。 关于TreeMap我没看懂啊啊啊啊,我看懂了在更新(立FLAG🚩)]]></content>
<tags>
<tag>源码</tag>
<tag>java</tag>
<tag>技术</tag>
</tags>
</entry>
<entry>
<title><![CDATA[A 死锁 の 物语(ˉ▽ˉ;)...]]></title>
<url>%2F2018%2F07%2F23%2FA-%E6%AD%BB%E9%94%81%2F</url>
<content type="text"><![CDATA[1234567891011121314151617181920212223242526272829303132333435363738394041/** * @author tengshe789 */public class DeadLock { public static String obj1 = "obj1"; public static String obj2 = "obj2"; public static void main(String[] args){ new Thread(()->{ try{ System.out.println("Lock1 running"); while(true){ synchronized(DeadLock.obj1){ System.out.println("Lock1 lock obj1"); Thread.sleep(3000);//获取obj1后先等一会儿,让Lock2有足够的时间锁住obj2 synchronized(DeadLock.obj2){ System.out.println("Lock1 lock obj2"); } } } }catch(Exception e){ e.printStackTrace(); } }).start();//first thread with lambda new Thread(()->{ try{ System.out.println("Lock2 running"); while(true){ synchronized(DeadLock.obj2){ System.out.println("Lock2 lock obj2"); Thread.sleep(3000); synchronized(DeadLock.obj1){ System.out.println("Lock2 lock obj1"); } } } }catch(Exception e){ e.printStackTrace(); } }).start();//second thread }} 结束 此片完了~ 想要了解更多精彩新姿势?请访问我的个人博客 本篇为原创内容,已在个人博客率先发表,随后CSDN,segmentfault,掘金,简书,开源中国同步发出。如有雷同,缘分呢兄弟。赶快加个好友~]]></content>
<tags>
<tag>日常</tag>
</tags>
</entry>
<entry>
<title><![CDATA[关于LinkedHashmap的个人理解]]></title>
<url>%2F2018%2F07%2F18%2F%E5%85%B3%E4%BA%8ELinkedHashmap%E7%9A%84%E4%B8%AA%E4%BA%BA%E7%90%86%E8%A7%A3%2F</url>
<content type="text"><![CDATA[大多数情况下,只要不涉及线程安全问题,Map基本都可以使用HashMap,不过HashMap有一个问题,就是迭代HashMap的顺序并不是HashMap放置的顺序。HashMap的这一缺点往往会带来困扰,因为有些场景,我们需要一个有序的Map。LinkedHashMap就可以做到,它虽然增加了时间和空间上的开销,但是通过维护一个运行于所有条目的双向链表,LinkedHashMap保证了元素迭代的顺序。想要解锁更多新姿势?请访问https://tengshe789.github.io/ 数据结构打开源码可以看到: 123456789/** * HashMap.Node subclass for normal LinkedHashMap entries. */ static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); } } LinkedHashMap继承了HashMap的Entry,并新增加了两个指针 参照一下网络上搜刮的图片,可以看出数据结构为数组 + 单链表 + 红黑树 + 双链表,比HashMap多了一个双向链表,就是利用了头节点和其余的各个节点之间通过 Entry 中的 after 和 before 指针进行关联。 构造方法看一下构造方法。 12345678910111213141516171819202122public LinkedHashMap(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor); accessOrder = false; } public LinkedHashMap(int initialCapacity) { super(initialCapacity); accessOrder = false; } public LinkedHashMap() { super(); accessOrder = false; } public LinkedHashMap(Map<? extends K, ? extends V> m) { super(); accessOrder = false; putMapEntries(m, false); } 有四个构造方法,每一个的构造方法第一句话基本都是调用父类HashMap方法。估计是用多态来实现的相关功能。比 HashMap 多了一个 accessOrder 的参数,用来指定按照 LRU 排列方式还是顺序插入的排序方式 添加看 LinkedHashMap 的 put() 方法之前先看看 HashMap 的 putVal 方法: 123456789101112131415161718192021222324252627282930313233343536373839404142434445final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; /* =============================敲黑板========================================== */ afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } 其实这篇已经是我早有准备的,第一次看源码的时候已经惊了。LinkedHashMap 自己没有重写put方法,全是照着他爹搬过来的,无赖 啊。代码中敲黑板位置,是LinkedHashMap 重写了 afterNodeAccess() 这个方法。 12345678910111213141516171819202122232425262728//将最近使用的Node e,放在链表的最末尾void afterNodeAccess(Node<K,V> e) { // move node to last LinkedHashMap.Entry<K,V> last; //仅当按照LRU原则且e不在最末尾,才执行修改链表,将e移到链表最末尾的操作 //accessOrder为true则表示按照基于访问的顺序来排列 if (accessOrder && (last = tail) != e) { //将e赋值临时节点p, b是e的前一个节点, a是e的后一个节点 LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; //节点移动过程 p.after = null; if (b == null) head = a; else b.after = a; if (a != null) a.before = b; else last = b; if (last == null) head = p; else { p.before = last; last.after = p; } tail = p; ++modCount; } 结束 此片完了~ 想要了解更多精彩新姿势?请访问我的个人博客 本篇为原创内容,已在个人博客率先发表,随后CSDN,segmentfault,juejin同步发出。如有雷同,缘分呢兄弟。赶快加个好友~]]></content>
<tags>
<tag>源码</tag>
<tag>java</tag>
<tag>技术</tag>
</tags>
</entry>
<entry>
<title><![CDATA[关于ConcurrentHashMap1.8的个人理解]]></title>
<url>%2F2018%2F07%2F17%2FConcurrentHashMap1-8%2F</url>
<content type="text"><![CDATA[ConcurrenHashMap 。下面分享一下我对ConcurrentHashMap 的理解,主要用于个人备忘。如果有不对,请批评。 HashMap“严重”的勾起了我对HashMap家族的好奇心,下面分享一下我对ConcurrentHashMap 的理解,主要用于个人备忘。如果有不对,请批评。 想要解锁更多新姿势?请访问https://tengshe789.github.io/ 总起HashMap是我们平时开发过程中用的比较多的集合,但它是非线程安全的,在涉及到多线程并发的情况,进行get操作有可能会引起死循环,导致CPU利用率接近100%。 因此需要支持线程安全的并发容器 ConcurrentHashMap 。 数据结构 重要成员变量12345/** * The array of bins. Lazily initialized upon first insertion. * Size is always a power of two. Accessed directly by iterators. */ transient volatile Node<K,V>[] table; table代表整个哈希表。 默认为null,初始化发生在第一次插入操作,默认大小为16的数组,用来存储Node节点数据,扩容时大小总是2的幂次方。 1234/** * The next table to use; non-null only while resizing. */ private transient volatile Node<K,V>[] nextTable; nextTable是一个连接表,用于哈希表扩容,默认为null,扩容时新生成的数组,其大小为原数组的两倍。 123456/** * Base counter value, used mainly when there is no contention, * but also as a fallback during table initialization * races. Updated via CAS. */ private transient volatile long baseCount; baseCount保存着整个哈希表中存储的所有的结点的个数总和,有点类似于 HashMap 的 size 属性。 这个数通过CAS算法更新 123456789/** * Table initialization and resizing control. When negative, the * table is being initialized or resized: -1 for initialization, * else -(1 + the number of active resizing threads). Otherwise, * when table is null, holds the initial table size to use upon * creation, or 0 for default. After initialization, holds the * next element count value upon which to resize the table. */ private transient volatile int sizeCtl; 初始化哈希表和扩容 rehash 的过程,都需要依赖sizeCtl。该属性有以下几种取值: 0:默认值 -1:代表哈希表正在进行初始化 大于0:相当于 HashMap 中的 threshold,表示阈值 小于-1:代表有多个线程正在进行扩容。(譬如:-N 表示有N-1个线程正在进行扩容操作 ) 构造方法1234567891011121314public ConcurrentHashMap() { }public ConcurrentHashMap(int initialCapacity) { if (initialCapacity < 0) throw new IllegalArgumentException(); int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));//MAXIMUM_CAPACITY = 1 << 30 this.sizeCtl = cap;//ConcurrentHashMap在构造函数中只会初始化sizeCtl值,并不会直接初始化table,而是延缓到第一次put操作。 }public ConcurrentHashMap(Map<? extends K, ? extends V> m) { this.sizeCtl = DEFAULT_CAPACITY;//DEFAULT_CAPACITY = 16 putAll(m); } 构造方法是三个。重点是第二个,带参的构造方法。这个带参的构造方法会调用tableSizeFor()方法,确保table的大小总是2的幂次方(假设参数为100,最终会调整成256)。算法如下: 12345678910111213/** * Returns a power of two table size for the given desired capacity. * See Hackers Delight, sec 3.2 */ private static final int tableSizeFor(int c) { int n = c - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; } PUT()方法put()调用putVal()方法,让我们看看: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576final V putVal(K key, V value, boolean onlyIfAbsent) { //对传入的参数进行合法性判断 if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode());//计算键所对应的 hash 值 int binCount = 0; for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; //如果哈希表还未初始化,那么初始化它 if (tab == null || (n = tab.length) == 0) tab = initTable(); //根据hash值计算出在table里面的位置 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //如果这个位置没有值 ,那么以CAS无锁式向该位置添加一个节点 if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin } //检测到桶结点是 ForwardingNode 类型,协助扩容(MOVED = -1; // hash for forwarding nodes) else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); //桶结点是普通的结点,锁住该桶头结点并试图在该链表的尾部添加一个节点 else { V oldVal = null; synchronized (f) { if (tabAt(tab, i) == f) { //向普通的链表中添加元素 if (fh >= 0) { binCount = 1; //遍历链表所有的结点 for (Node<K,V> e = f;; ++binCount) { K ek; //如果hash值和key值相同,则修改对应结点的value值 if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; //如果遍历到了最后一个结点,那么就证明新的节点需要插入链表尾部 if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; } } } //如果这个节点是树节点,就按照树的方式插入值 else if (f instanceof TreeBin) { Node<K,V> p; binCount = 2; if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } } if (binCount != 0) { //如果链表长度已经达到临界值8,就需要把链表转换为树结构(TREEIFY_THRESHOLD = 8) if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } //CAS 式更新baseCount,并判断是否需要扩容 addCount(1L, binCount); return null; } 其实putVal()也多多少少掉用了其他方法,让我们继续探究一下。 CAS(compare and swap)科普compare and swap,解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。 spread首先,第四行出现的int hash = spread(key.hashCode());这是传统的计算hash的方法。key的hash值高16位不变,低16位与高16位异或作为key的最终hash值。(h >>> 16,表示无符号右移16位,高位补0,任何数跟0异或都是其本身,因此key的hash值高16位不变。) 123static final int spread(int h) { return (h ^ (h >>> 16)) & HASH_BITS; } initTable第十行, tab = initTable();这个方法的亮点是,可以让put并发执行,实现table只初始化一次 。 initTable()核心思想就是,只允许一个线程对表进行初始化,如果有其他线程进来了,那么会让其他线程交出 CPU 等待下次系统调度。这样,保证了表同时只会被一个线程初始化。 12345678910111213141516171819202122232425262728private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; //如果表为空才进行初始化操作 while ((tab = table) == null || tab.length == 0) { //如果一个线程发现sizeCtl<0,意味着另外的线程执行CAS操作成功,当前线程只需要让出cpu时间片(放弃 CPU 的使用) if ((sc = sizeCtl) < 0) Thread.yield(); // lost initialization race; just spin //否则说明还未有线程对表进行初始化,那么本线程就来做这个工作 else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { if ((tab = table) == null || tab.length == 0) { //sc 大于零说明容量已经初始化了,否则使用默认容量 int n = (sc > 0) ? sc : DEFAULT_CAPACITY; @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; table = tab = nt; //计算阈值,等效于 n*0.75 sc = n - (n >>> 2); } } finally { //设置阈值 sizeCtl = sc; } break; } } return tab; } 接下来,第19行。 tab = helpTransfer(tab, f);这句话。要了解这个,首先需要知道ForwardingNode 这个节点类型。它一个用于连接两个table的节点类。它包含一个nextTable指针,用于指向下一张hash表。而且这个节点的key、value、next指针全部为null,它的hash值为MOVED(static final int MOVED = -1)。 123456789101112131415161718192021222324252627282930313233static final class ForwardingNode<K,V> extends Node<K,V> { final Node<K,V>[] nextTable; ForwardingNode(Node<K,V>[] tab) { super(MOVED, null, null, null); this.nextTable = tab; } //find的方法是从nextTable里进行查询节点,而不是以自身为头节点进行查找 Node<K,V> find(int h, Object k) { // loop to avoid arbitrarily deep recursion on forwarding nodes outer: for (Node<K,V>[] tab = nextTable;;) { Node<K,V> e; int n; if (k == null || tab == null || (n = tab.length) == 0 || (e = tabAt(tab, (n - 1) & h)) == null) return null; for (;;) { int eh; K ek; if ((eh = e.hash) == h && ((ek = e.key) == k || (ek != null && k.equals(ek)))) return e; if (eh < 0) { if (e instanceof ForwardingNode) { tab = ((ForwardingNode<K,V>)e).nextTable; continue outer; } else return e.find(h, k); } if ((e = e.next) == null) return null; } } } } helpTransfer在扩容操作中,我们需要对每个桶中的结点进行分离和转移。如果某个桶结点中所有节点都已经迁移完成了(已经被转移到新表 nextTable 中了),那么会在原 table 表的该位置挂上一个 ForwardingNode 结点,说明此桶已经完成迁移。 helpTransfer什么作用呢?是检测到当前哈希表正在扩容,然后让当前线程去协助扩容 ! 123456789101112131415161718192021final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) { Node<K,V>[] nextTab; int sc; if (tab != null && (f instanceof ForwardingNode) && (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {//新的table,nextTab已经存在前提下才能帮助扩容 int rs = resizeStamp(tab.length);//返回一个 16 位长度的扩容校验标识 while (nextTab == nextTable && table == tab && (sc = sizeCtl) < 0) {//sizeCtl 如果处于扩容状态的话 if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || transferIndex <= 0) //前 16 位是数据校验标识,后 16 位是当前正在扩容的线程总数 //这里判断校验标识是否相等,如果校验符不等或者扩容操作已经完成了,直接退出循环,不用协助它们扩容了 break; if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {//sc + 1 标识增加了一个线程进行扩容 transfer(tab, nextTab);//调用扩容方法 break; } } return nextTab; } return table; } helpTransfer精髓的是可以调用多个工作线程一起帮助进行扩容,这样的效率就会更高,而不是只有检查到要扩容的那个线程进行扩容操作,其他线程就要等待扩容操作完成才能工作。 transfer既然这里涉及到扩容的操作,我们也一起来看看扩容方法transfer() : 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) { int n = tab.length, stride; //计算单个线程允许处理的最少table桶首节点个数,不能小于 16 if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) stride = MIN_TRANSFER_STRIDE; // subdivide range //刚开始扩容,初始化 nextTab if (nextTab == null) { // initiating try { @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; nextTab = nt; } catch (Throwable ex) { // try to cope with OOME sizeCtl = Integer.MAX_VALUE; return; } nextTable = nextTab; //transferIndex 指向最后一个桶,方便从后向前遍历 transferIndex = n; } int nextn = nextTab.length; //定义 ForwardingNode 用于标记迁移完成的桶 ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab); boolean advance = true; boolean finishing = false; // to ensure sweep before committing nextTab //i 指向当前桶,bound 指向当前线程需要处理的桶结点的区间下限 for (int i = 0, bound = 0;;) { Node<K,V> f; int fh; //遍历当前线程所分配到的桶结点 while (advance) { int nextIndex, nextBound; if (--i >= bound || finishing) advance = false; //transferIndex <= 0 说明已经没有需要迁移的桶了 else if ((nextIndex = transferIndex) <= 0) { i = -1; advance = false; } //更新 transferIndex //为当前线程分配任务,处理的桶结点区间为(nextBound,nextIndex) else if (U.compareAndSwapInt (this, TRANSFERINDEX, nextIndex, nextBound = (nextIndex > stride ? nextIndex - stride : 0))) { bound = nextBound; i = nextIndex - 1; advance = false; } } //当前线程所有任务完成 if (i < 0 || i >= n || i + n >= nextn) { int sc; if (finishing) { nextTable = null; table = nextTab; sizeCtl = (n << 1) - (n >>> 1); return; } if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) { if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) return; finishing = advance = true; i = n; // recheck before commit } } //待迁移桶为空,那么在此位置 CAS 添加 ForwardingNode 结点标识该桶已经被处理过了 else if ((f = tabAt(tab, i)) == null) advance = casTabAt(tab, i, null, fwd); //如果扫描到 ForwardingNode,说明此桶已经被处理过了,跳过即可 else if ((fh = f.hash) == MOVED) advance = true; // already processed else { synchronized (f) { if (tabAt(tab, i) == f) { Node<K,V> ln, hn; //链表的迁移操作 if (fh >= 0) { int runBit = fh & n; Node<K,V> lastRun = f; //整个 for 循环为了找到整个桶中最后连续的 fh & n 不变的结点 for (Node<K,V> p = f.next; p != null; p = p.next) { int b = p.hash & n; if (b != runBit) { runBit = b; lastRun = p; } } if (runBit == 0) { ln = lastRun; hn = null; } else { hn = lastRun; ln = null; } for (Node<K,V> p = f; p != lastRun; p = p.next) { int ph = p.hash; K pk = p.key; V pv = p.val; if ((ph & n) == 0) ln = new Node<K,V>(ph, pk, pv, ln); else hn = new Node<K,V>(ph, pk, pv, hn); } setTabAt(nextTab, i, ln); setTabAt(nextTab, i + n, hn); setTabAt(tab, i, fwd); advance = true; } //红黑树的复制算法, else if (f instanceof TreeBin) { TreeBin<K,V> t = (TreeBin<K,V>)f; TreeNode<K,V> lo = null, loTail = null; TreeNode<K,V> hi = null, hiTail = null; int lc = 0, hc = 0; for (Node<K,V> e = t.first; e != null; e = e.next) { int h = e.hash; TreeNode<K,V> p = new TreeNode<K,V> (h, e.key, e.val, null, null); if ((h & n) == 0) { if ((p.prev = loTail) == null) lo = p; else loTail.next = p; loTail = p; ++lc; } else { if ((p.prev = hiTail) == null) hi = p; else hiTail.next = p; hiTail = p; ++hc; } } ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) : (hc != 0) ? new TreeBin<K,V>(lo) : t; hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) : (lc != 0) ? new TreeBin<K,V>(hi) : t; setTabAt(nextTab, i, ln); setTabAt(nextTab, i + n, hn); setTabAt(tab, i, fwd); advance = true; } } } } } } 至此,put方法讲完了 参考资料~参考资料 感谢 结束 此片完了~ 想要了解更多精彩新姿势?请访问我的个人博客 本篇为原创内容,已在个人博客率先发表,随后CSDN,segmentfault,掘金,简书,开源中国同步发出。如有雷同,缘分呢兄弟。赶快加个好友~]]></content>
<tags>
<tag>源码</tag>
<tag>java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[HashSet原理]]></title>
<url>%2F2018%2F07%2F17%2FHashSet%E5%8E%9F%E7%90%86%2F</url>
<content type="text"><![CDATA[HashSetHashSet 是一个不允许存储重复元素的集合。 HashSet实现Set接口,由哈希表(实际上是一个HashMap实例)支持。它不保证set 的迭代顺序;特别是它不保证该顺序恒久不变。此类允许使用null元素。 想要解锁更多新姿势?请访问https://tengshe789.github.io/ HashSet和HashMap比较HashSet: HashSet实现了Set接口,它不允许集合中出现重复元素。当我们提到HashSet时,第一件事就是在将对象存储在 HashSet之前,要确保重写hashCode()方法和equals()方法,这样才能比较对象的值是否相等,确保集合中没有 储存相同的对象。如果不重写上述两个方法,那么将使用下面方法默认实现: public boolean add(Object obj)方法用在Set添加元素时,如果元素值重复时返回 “false”,如果添加成功则返回”true” HashMap: HashMap实现了Map接口,Map接口对键值对进行映射。Map中不允许出现重复的键(Key)。Map接口有两个基本的实现 TreeMap和HashMap。TreeMap保存了对象的排列次序,而HashMap不能。HashMap可以有空的键值对(Key(null)-Value(null)) HashMap是非线程安全的(非Synchronize),要想实现线程安全,那么需要调用collections类的静态方法synchronizeMap()实现。 public Object put(Object Key,Object value)方法用来将元素添加到map中。 成员变量首先了解下 HashSet 的成员变量: 12345//用transient关键字标记的成员变量不参与序列化过程private transient HashMap<E,Object> map;// Dummy value to associate with an Object in the backing Mapprivate static final Object PRESENT = new Object(); 发现主要就两个变量: map :用于存放最终数据的。 PRESENT :是所有写入 map 的 value 值。 构造函数1234567public HashSet() { map = new HashMap<>();}public HashSet(int initialCapacity, float loadFactor) { map = new HashMap<>(initialCapacity, loadFactor);} 构造函数很简单,利用了 HashMap 初始化了 map 。 add123public boolean add(E e) { return map.put(e, PRESENT)==null;} 比较关键的就是这个 add() 方法。 可以看出它是将存放的对象当做了 HashMap 的健,value 都是相同的 PRESENT 。由于 HashMap 的 key 是不能重复的,所以每当有重复的值写入到 HashSet 时,value 会被覆盖,但 key 不会收到影响,这样就保证了 HashSet 中只能存放不重复的元素。 总结HashSet 的原理比较简单,几乎全部借助于 HashMap 来实现的。 所以 HashMap 会出现的问题 HashSet 依然不能避免。 想要解锁更多新姿势?请访问我的博客]]></content>
<tags>
<tag>源码</tag>
<tag>java</tag>
<tag>技术</tag>
</tags>
</entry>
<entry>
<title><![CDATA[分享一个简单又复杂的程序选择题]]></title>
<url>%2F2018%2F07%2F11%2F%E5%88%86%E4%BA%AB%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E5%8F%88%E5%A4%8D%E6%9D%82%E7%9A%84%E7%A8%8B%E5%BA%8F%E9%80%89%E6%8B%A9%E9%A2%98%2F</url>
<content type="text"><![CDATA[今天在牛客网刷题,看见一个非常简单又很复杂的题目,我现在来分享一下。 想要解锁更多新姿势?请访问我的博客 题目是这样的: 123456789101112131415161718192021222324252627282930313233343536public class Main { public static void main(String [] args){ System.out.println(new B().getValue()); } static class A{ protected int value; public A(int v) { setValue(v); } public void setValue(int value){ this.value = value; } public int getValue(){ try{ value++; return value; } catch(Exception e){ System.out.println(e.toString()); } finally { this.setValue(value); System.out.println(value); } return value; } } static class B extends A{ public B() { super(5); setValue(getValue() - 3); } @Override public void setValue(int value){ super.setValue(2 * value); } } } 题目让写出上述代码执行完成后的输出结果 这道题考的其实就是代码执行顺序的知识,比较基础的东西。首先,用IDEA打一下断点,如图: 首先走主函数,new B,进入B的构造方法 并将父类A传入5(当子类构造器中没有使用”super(参数或无参数)”指定调用父类构造器时,是默认调用父类的无参构造器,如果父类中包含有参构造器,却没有无参构造器,则在子类构造器中一定要使用“super(参数)”指定调用父类的有参构造器,不然就会报错。 ) 调用父类的setValue方法,由于子类已将其重写,所以直接到子类{ 敲黑板: 1.父类,子类之间转型的问题,当父类声明指向子类对象,会发生隐式向上转型。 2.编译时所参考的对象类型是声明对象所用的类型,而运行时则是参考对象实例化的类型。 3.当子类重写了父类的某个方法时,会将父类的方法进行隐藏,因此这边最后调到的都是子类重写的方法。 } 现在回到value=10 现在value是10,准备进入geiValue阶段 还是10 在这里返回value,此时value是11 try{}catch{}执行完一定要走finnally value是22了 终于开始打印了,打印第一个数字:22 返回value =22 回到刚刚B那里,第一次返回的value是11,将11-3=8 将8*2=16得到value 回到主方法 再走一边getValue() 进入finally,再将value*2=34 第二个数字出来了,打印34,然后返回3 到这里主函数执行完成,但是注意了。第二次返回值是17,第三次是34。到了最后又输出了getValue()第二次的返回值,也就是17 所以整个过程执行完后的输出结果是22、34、17。。。。。。 这个题有些语句有点多余,譬如finally代码块后面return永远用不上。 这道题虽然饶了很多弯,但细细品味后其实并没那么难,考点是: 1.自类继承父类,调用方法时先是调用子类中的方法,如果没有就调用父类中的方法。 2.try{ }、catch{ }、finally{ }返回值的问题,一旦try{ }中返回了某一个值,如果finally有返回值,finally中的返回值会覆盖try的返回值,如果finally没有返回值,就是try中的返回值。]]></content>
<tags>
<tag>java</tag>
<tag>错题</tag>
</tags>
</entry>
<entry>
<title><![CDATA[关于Hashmap的个人理解]]></title>
<url>%2F2018%2F07%2F08%2F%E5%85%B3%E4%BA%8EHashmap%E7%9A%84%E4%B8%AA%E4%BA%BA%E7%90%86%E8%A7%A3%2F</url>
<content type="text"><![CDATA[刚刚看到QQ群有人吹Hashmap,一想我啥都不懂,就赶快补了一波。下面分享一下我对Hashmap的理解,主要用于个人备忘。如果有不对,请批评。想要解锁更多新姿势?请访问https://tengshe789.github.io/ 总起Hashmap是散列表,存储结构是键值对形式。根据健的Hashcode值存储数据,有较快的访问速度。 它的线程是不安全的,在两个线程同时尝试扩容HashMap时,可能将一个链表形成环形的链表,所有的next都不为空,进入死循环;要想让它安全,可以用 Collections的synchronizedMap 方法使 HashMap具有线程安全的能力,或者使用ConcurrentHashMap 。 他的键值对都可以为空,映射不是有序的。 Hashmap有两个参数影响性能:初始容量,加载因子。 Hashmap存储结构JDK1.8中Hashmap是由链表、红黑树、数组实现的 12345678910111213141516171819202122232425262728293031323334353637383940//用来实现数组、链表的数据结构static class Node<K,V> implements Map.Entry<K,V> { final int hash;//保存节点的Hash final K key;//保存节点的键值 V value;//保存节点的值 Node<K,V> next;//指向链表或者红黑树的下一个节点 Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } } Hashmap构造方法HashMap有4个构造方法。 代码: 1234567891011121314151617181920212223242526272829//方法1.制定初始容量和负载因子public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); }//方法2.指定初始容量 public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }//方法三。无参构造。 HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }//方法四。将另一个 Map 中的映射拷贝一份到自己的存储结构中来,这个方法不是很常用 public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); } Hashmap变量成员12345678910111213141516//未指定容量的时候,数组的初始容量。初始容量是16//为什么不直接写16?因为速度快。计算机里面要转换二进制。//必须2的n次幂static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16//负载因子。当hashmap容量超过 容量*负载因子 时,进行扩容操作(resize())static final float DEFAULT_LOAD_FACTOR = 0.75f;//确定何时将hash冲突的链表转换成红黑树static final int TREEIFY_THRESHOLD = 8;//用来确何时将红黑树转换成链表static final int UNTREEIFY_THRESHOLD = 6;//当链表转换成红黑树时,需要判断数组容量。若数组容量太小导致hash冲突太多,则不进行红黑树操作,转而利用reseize扩容static final int MIN_TREEIFY_CAPACITY = 64; 初始容量、负载因子、阈值.一般情况下,使用无参构造方法创建 HashMap。但当我们对时间和空间复杂度有要求的时候,使用默认值有时可能达不到我们的要求,这个时候我们就需要手动调参。 在 HashMap 构造方法中,可供我们调整的参数有两个,一个是初始容量initialCapacity,另一个负载因子loadFactor。通过这两个设定这两个参数,可以进一步影响阈值大小。但初始阈值 threshold 仅由initialCapacity 经过移位操作计算得出。 名称 用途 initialCapacity HashMap 初始容量 loadFactor 负载因子 threshold 当前 HashMap 所能容纳键值对数量的最大值,超过这个值,则需扩容 默认情况下,HashMap 初始容量是16,负载因子为 0.75。 注释中有说明,阈值可由容量乘上负载因子计算而来 ,即threshold = capacity * loadFactor 123456789static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; } 这段代码有点难,根据大神的说法,这个方法的意思是,找到大于或等于 cap 的最小2的幂。我们先来看看 tableSizeFor 方法的图解 : 图中容量是229+1,计算后是230 引用一下啊大神说的: 对于 HashMap 来说,负载因子是一个很重要的参数,该参数反应了 HashMap 桶数组的使用情况(假设键值对节点均匀分布在桶数组中)。通过调节负载因子,可使 HashMap 时间和空间复杂度上有不同的表现。当我们调低负载因子时,HashMap 所能容纳的键值对数量变少。扩容时,重新将键值对存储新的桶数组里,键的键之间产生的碰撞会下降,链表长度变短。此时,HashMap 的增删改查等操作的效率将会变高,这里是典型的拿空间换时间。相反,如果增加负载因子(负载因子可以大于1),HashMap 所能容纳的键值对数量变多,空间利用率高,但碰撞率也高。这意味着链表长度变长,效率也随之降低,这种情况是拿时间换空间。至于负载因子怎么调节,这个看使用场景了。一般情况下,我们用默认值就可以了。 插入PUT过程: 对Key求hash值,然后计算下标 如果没有碰撞,就放入桶中 如果碰撞了,就以链表形式放到后面 如果链表长度超过阈值,就把链表转换成红黑树 如果链表存在则替换旧值 如果桶满了(容量*负载因子),则重新resize 1234public V put(K key, V value) { //调用核心方法 return putVal(hash(key), key, value, false, true); } putVal核心算法在putVal()中。要想理解,先要明白桶排序(Bucket Sort) 它是迄今为止最快的一种排序法,其时间复杂度仅为Ο(n),也就是线性复杂度。 桶排序核心思想是:根据数据规模n划分,m个相同大小的区间 (每个区间为一个桶,桶可理解为容器) 。将n个元素按照规定范围分布到各个桶中去 ,再对每个桶中的元素进行排序,排序方法可根据需要,选择快速排序,或者归并排序,或者插入排序 ,然后依次从每个桶中取出元素,按顺序放入到最初的输出序列中(相当于把所有的桶中的元素合并到一起) 。 下面是代码: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //n是数组长度 Node<K,V>[] tab; Node<K,V> p; int n, i; //判断桶数组是否是空 if ((tab = table) == null || (n = tab.length) == 0) //是就用resize()初始化 n = (tab = resize()).length; //根据 hash 值确定节点在数组中的插入位置 //若此位置没有元素则进行插入,注意确定插入位置所用的计算方法为 (n - 1) & hash,由于 n 一定是2的幂次,这个操作相当于hash % n if ((p = tab[i = (n - 1) & hash]) == null) //将新节点引入桶中 tab[i] = newNode(hash, key, value, null); else { //临时变量e进行记录。如果有值,说明仅仅是值的覆盖。 Node<K,V> e; K k; // 如果键的值以及节点 hash 等于链表中的第一个键值对节点时,则将 e 指向该键值对 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode)// 如果桶中的引用类型为 TreeNode,则调用红黑树的插入方法 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else {// 对链表进行遍历,并统计链表长度 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); // 链表中不包含要插入的键值对节点时,则将该节点接在链表的最后 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //临时变量e不为空时,说明已经有值进行替换了 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); //返回老值 return oldValue; } } ++modCount; //扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } HASHhash算法,高十六位与低十六进行异或运算,这样做的好处是使得到结果会尽可能不同。 1234static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } resize HashMap 的扩容机制与其他变长集合的套路不太一样,HashMap 按当前桶数组长度的2倍进行扩容,阈值也变为原来的2倍(如果计算过程中,阈值溢出归零,则按阈值公式重新计算)。扩容之后,要重新计算键值对的位置,并把它们移动到合适的位置上去。 resize总共做了3件事,分别是: 计算新桶数组的容量 newCap 和新阈值 newThr 根据计算出的 newCap 创建新的桶数组,桶数组 table 也是在这里进行初始化的 将键值对节点重新映射到新的桶数组里。如果节点是 TreeNode 类型,则需要拆分红黑树。如果是普通节点,则节点按原顺序进行分组。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394//resize()函数在size > threshold时被调用final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; //oldCap大于 0 代表原来的 table 非空 if (oldCap > 0) { // 当 table 容量超过容量最大值,则不再扩容 if (oldCap >= MAXIMUM_CAPACITY) { //阈值设为整形最大值 threshold = Integer.MAX_VALUE; return oldTab; }// 按旧容量和阈值的2倍计算新容量和阈值的大小 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold /* *oldCap 小于等于 0 且 oldThr 大于0,代表用户创建了一个 HashMap,但是使用的构造函数为 * HashMap(int initialCapacity, float loadFactor) 或 HashMap(int initialCapacity) * 或 HashMap(Map<? extends K, ? extends V> m),导致 oldTab 为 null,oldCap 为0, * oldThr 为用户指定的 HashMap的初始容量 */ newCap = oldThr; else { //设置新容量和新阈值大小 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // newThr 为 0 时,按阈值计算公式进行计算 if (newThr == 0) { //计算新阈值 float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } //准备初始化过程 threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { //遍历。把 oldTab 中的节点 reHash 到 newTab 中去 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; //判断老数组是否为空 if ((e = oldTab[j]) != null) { //不为空,设成空 oldTab[j] = null; //若节点是单个节点,直接重新分配定位 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; //若节点是 TreeNode 节点,要进行 红黑树的 rehash 操作 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //若是链表,进行链表的 rehash 操作 else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; // 遍历链表,并将链表节点按原顺序进行分组 do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; // rehash 后节点新的位置一定为原来基础上加上 oldCap newTab[j + oldCap] = hiHead; } } } } } return newTab; } 关于HashMap在什么时候时间复杂度是O(1),什么时候是O(n),什么时候又是O(logn)的问题O(1):链表的长度尽可能短,理想状态下链表长度都为1 O(n):当 Hash 冲突严重时,如果没有红黑树,那么在桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为O(N)。 O(logn):采用红黑树之后可以保证查询效率O(logn) 手写123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186/** * @author tengshe789 */public class 手写HashMap { public static class Node<K,V>{ K key; V value; Node<K,V> next; public Node(K key, V value, Node<K, V> next) { this.key = key; this.value = value; this.next = next; } public K getKey() { return this.key; } public V getValue() { return this.value; } public V setValue(V value) { this.value=value; return this.value; } } public static class HashMap<K, V>{ /*数据存储的结构==>数组+链表*/ Node<K,V>[] array=null; /* 哈希桶的长度 */ private static int defaultLength=16; /*加载因子/扩容因子*/ private static double factor=0.75D; /*集合中的元素个数*/ private int size; /*打印函数*/ public void print() { System.out.println("==============================="); if(array!=null) { Node<K, V> node=null; for (int i = 0; i < array.length; i++) { node=array[i]; System.out.print("下标["+i+"]"); //遍历链表 while(node!=null) { System.out.print("["+node.getKey()+":"+node.getValue()+"]"); if(node.next!=null) { node=node.next; }else { //到尾部元素 node=null; } } System.out.println(); } } } //put元素方法 public V put(K k, V v) { //1.懒加载机制,使用的时候进行分配 if(array==null) { array=new Node[defaultLength]; } //2.通过hash算法,计算出具体插入的位置 int index=position(k,defaultLength); //扩容。判断是否需要扩容 //扩容的准则,元素的个数 大于 桶的尺寸*加载因子 if(size > defaultLength*factor) { resize(); } //3.放入要插入的元素 Node<K, V> node=array[index]; if(node==null) { array[index]=new Node<K,V>(k,v,null); size++; }else { if(k.equals(node.getKey()) || k==node.getKey()) { return node.setValue(v); }else { array[index]=new Node<K,V>(k,v,node); size++; } } return null; } //扩容,并且重新排列元素 private void resize() { //翻倍扩容 //1.创建新的array临时变量,相当于defaultlength*2 Node<K, V>[] temp=new Node[defaultLength << 1]; //2.重新计算散列值,插入到新的array中去。 code=key % defaultLength ==> code=key % defaultLength*2 Node<K, V> node=null; for (int i = 0; i < array.length; i++) { node=array[i]; while(node!=null) { //重新散列 int index=position(node.getKey(),temp.length); //插入头部 Node<K, V> next = node.next; //3 node.next=temp[index]; //1 temp[index]=node; //2 node=next; } } //3.替换掉老array array=temp; defaultLength=temp.length; temp=null; } private int position(K k,int length) { int code=k.hashCode(); //取模算法 return code % (length-1); //求与算法 //return code & (defaultLength-1); } public V get(K k) { if(array!=null) { int index=position(k,defaultLength); Node<K, V> node=array[index]; //遍历链表 while(node!=null) { //如果key值相同返回value if(node.getKey()==k) { return node.getValue(); } else //如果key值不同则调到下一个元素 { node=node.next; } } } return null; } } public static void main(String[] args) { HashMap<String, String> map=new HashMap<String, String>(); map.put("001号", "001"); map.put("002号", "002"); map.put("003号", "003"); map.put("004号", "004"); map.put("005号", "005"); map.put("006号", "006"); map.put("007号", "007"); map.put("008号", "008"); map.put("009号", "009"); map.put("010号", "010"); map.put("011号", "011"); map.print(); System.out.println("========>"+map.get("009号")); } } 参考资料coolblog 阿里架构师带你分析HashMap源码实现原理 感谢! 以下来自n天后的我: 补充一下看到一个非常好的:点击链接,值得学习]]></content>
<tags>
<tag>源码</tag>
<tag>java</tag>
<tag>技术</tag>
</tags>
</entry>
<entry>
<title><![CDATA[多终端同步Hexo]]></title>
<url>%2F2018%2F07%2F07%2F%E5%A4%9A%E7%BB%88%E7%AB%AF%E5%90%8C%E6%AD%A5Hexo%2F</url>
<content type="text"><![CDATA[放寒假了,玩游戏! 呸呸呸 我是那种人么??? 放假了要好好学习,而且学什么要记录一下,每天写点博客记录一下自己的点点滴滴啊~!可垃圾笔记本屏幕太小了,看的眼疼,没有办法在原先本本上写MD博客 肿么办? 不慌,可以利用Git在多平台上同步Hexo! 下面我就分享一下步骤~ 准备环境我就不说了,Node.js,Git,Hexo,这些都是最基本的 下载Git下载 git OSX 版:git-scm.com/download/ma…下载 git Windows 版:git-scm.com/download/wi…下载 git Linux 版:git-scm.com/download/li… 安装GitLinux$ sudo yum install git或者$ sudo apt-get install gitMac直接在 Terminal 执行 git 命令, 如果没有会提示安装方法. 笔记本端(需要备份的一端)进入我博客的原始文件夹内,找到.gitignore(Git就会自动忽略文件总汇)文件,用记事本打开~ 添加如下 123public/.deploy*/node_modules/ #npm install产生的node_modules由于路径过长不好处理 添加完成后,右键 Git Bash,输入以下代码: 1234567git init #初始化本地仓库git add . #将必要的文件依次添加git commit -m "更新说明" #引号里面随便改哦~git branch hexo #新建hexo分支,当然,分支名字“hexo”也可以随便改哦~git checkout hexo #切换到hexo分支上git remote add origin [email protected]:tengshe789/tengshe789.github.io.git #将本地与Github项目对接,tengshe789为我的用户名git push origin hexo #push到Github项目的hexo分支上 完成后合影~ 这里有很多隐私文件夹,譬如config.yml,db.json…..我也一一上传了,因为我是个不怕暴漏隐私的人哈哈哈哈。要想避免这个情况只将source添加即可 台式机端(需要还原的一端)12345678git clone -b hexo [email protected]:tengshe789/tengshe789.github.io.git #将Github中hexo分支clone到本地cd tengshe789.github.io #切换到刚刚clone的文件夹内npm install #注意,这里一定要切换到刚刚clone的文件夹内执行,安装必要的所需组件,不用再initnpm install hexo-cli -g # 如果不同机器node版本升级了,需要重新运行这个npm install https://github.com/CodeFalling/hexo-asset-image --save #图片插件npm install --save hexo-pdf #pdf插件npm install hexo-deployer-git #部署npm install hexo-generator-restful --save #生成 restful 风格的 json 数据 https://github.com/yscoder/hexo-generator-restful 完成! 接下来写一篇博客庆祝一下~ 1hexo new post "new blog name" #新建一个.md文件,并编辑完成自己的博客内容 写完,备份一下,再同步到github! 1234git add source #每次只要更新sorcerer中的文件到Github中即可,因为只是新建了一篇新博客git commit -m "更新说明"git push origin hexo #更新分支hexo d -g #push更新完分支之后将自己写的博客对接到自己搭的博客网站上,同时同步了Github中的master 番外:笔记本怎么同步?进入我博客的原始文件夹内 1git pull origin hexo #先pull完成本地与远端的融合 Hexo异常:fatal:inunpopulatedsubmodule’.deploy_git’怎么解决实在不行,就把它删掉,然后重新生成和部署。 1`rm -rf .deploy_git``hexo g``hexo d` Git命令整理Git配置git config –global user.name “robbin”git config –global user.email “[email protected]“git config –global color.ui truegit config –global alias.co checkoutgit config –global alias.ci commitgit config –global alias.st statusgit config –global alias.br branchgit config –global core.editor “mate -w” # 设置Editor使用textmategit config -l # 列举所有配置用户的git配置文件~/.gitconfig Git常用命令查看、添加、提交、删除、找回,重置修改文件 12345678910111213141516171819202122232425git help <command> # 显示command的helpgit show # 显示某次提交的内容git show $idgit co -- <file> # 抛弃工作区修改git co . # 抛弃工作区修改git add <file> # 将工作文件修改提交到本地暂存区git add . # 将所有修改过的工作文件提交暂存区git rm <file> # 从版本库中删除文件git rm <file> --cached # 从版本库中删除文件,但不删除文件git reset <file> # 从暂存区恢复到工作文件git reset -- . # 从暂存区恢复到工作文件git reset --hard # 恢复最近一次提交过的状态,即放弃上次提交后的所有本次修改git ci <file>git ci .git ci -a # 将git add, git rm和git ci等操作都合并在一起做git ci -am "some comments"git ci --amend # 修改最后一次提交记录git revert <$id> # 恢复某次提交的状态,恢复动作本身也创建了一次提交对象git revert HEAD # 恢复最后一次提交的状态 查看文件diff1234567git diff <file> # 比较当前文件和暂存区文件差异git diffgit diff <$id1> <$id2> # 比较两次提交之间的差异git diff <branch1>..<branch2> # 在两个分支之间比较git diff --staged # 比较暂存区和版本库差异git diff --cached # 比较暂存区和版本库差异git diff --stat # 仅仅比较统计信息 查看提交记录1234567git loggit log <file> # 查看该文件每次提交记录git log -p <file> # 查看每次详细修改内容的diffgit log -p -2 # 查看最近两次详细修改内容的diffgit log --stat # 查看提交统计信息tigMac上可以使用tig代替diff和log,brew install tig Git 本地分支管理查看、切换、创建和删除分支 123456789101112131415git br -r # 查看远程分支git br <new_branch> # 创建新的分支git br -v # 查看各个分支最后提交信息git br --merged # 查看已经被合并到当前分支的分支git br --no-merged # 查看尚未被合并到当前分支的分支git co <branch> # 切换到某个分支git co -b <new_branch> # 创建新的分支,并且切换过去git co -b <new_branch> <branch> # 基于branch创建新的new_branchgit co $id # 把某次历史提交记录checkout出来,但无分支信息,切换到其他分支会自动删除git co $id -b <new_branch> # 把某次历史提交记录checkout出来,创建成一个分支git br -d <branch> # 删除某个分支git br -D <branch> # 强制删除某个分支 (未被合并的分支被删除的时候需要强制) 分支合并和rebase12345git merge <branch> # 将branch分支合并到当前分支git merge origin/master --no-ff # 不要Fast-Foward合并,这样可以生成merge提交git rebase master <branch> # 将master rebase到branch,相当于:git co <branch> && git rebase master && git co master && git merge <branch> Git补丁管理(方便在多台机器上开发同步时用)123git diff > ../sync.patch # 生成补丁git apply ../sync.patch # 打补丁git apply --check ../sync.patch # 测试补丁能否成功 Git暂存管理1234git stash # 暂存git stash list # 列所有stashgit stash apply # 恢复暂存的内容git stash drop # 删除暂存区 Git远程分支管理12345678910111213git pull # 抓取远程仓库所有分支更新并合并到本地git pull --no-ff # 抓取远程仓库所有分支更新并合并到本地,不要快进合并git fetch origin # 抓取远程仓库更新git merge origin/master # 将远程主分支合并到本地当前分支git co --track origin/branch # 跟踪某个远程分支创建相应的本地分支git co -b <local_branch> origin/<remote_branch> # 基于远程分支创建本地分支,功能同上git push # push所有分支git push origin master # 将本地主分支推到远程主分支git push -u origin master # 将本地主分支推到远程(如无远程主分支则创建,用于初始化远程仓库)git push origin <local_branch> # 创建远程分支, origin是远程仓库名git push origin <local_branch>:<remote_branch> # 创建远程分支git push origin :<remote_branch> #先删除本地分支(git br -d <branch>),然后再push删除远程分支 Git远程仓库管理12345git remote -v # 查看远程服务器地址和仓库名称git remote show origin # 查看远程服务器仓库状态git remote add origin git@github:robbin/robbin_site.git # 添加远程仓库地址git remote set-url origin [email protected]:robbin/robbin_site.git # 设置远程仓库地址(用于修改远程仓库地址)git remote rm <repository> # 删除远程仓库 创建远程仓库 12345678910111213git clone --bare robbin_site robbin_site.git # 用带版本的项目创建纯版本仓库scp -r my_project.git [email protected]:~ # 将纯仓库上传到服务器上mkdir robbin_site.git && cd robbin_site.git && git --bare init # 在服务器创建纯仓库git remote add origin [email protected]:robbin/robbin_site.git # 设置远程仓库地址git push -u origin master # 客户端首次提交git push -u origin develop # 首次将本地develop分支提交到远程develop分支,并且trackgit remote set-head origin master # 设置远程仓库的HEAD指向master分支也可以命令设置跟踪远程库和本地库git branch --set-upstream master origin/mastergit branch --set-upstream develop origin/develop HappyEnding完成,继续玩游戏! 参考链接1 参考链接2 感谢! 想要解锁更多新姿势?请访问我的博客]]></content>
<tags>
<tag>日常</tag>
<tag>技术</tag>
<tag>Windows10</tag>
<tag>Hexo</tag>
<tag>git</tag>
</tags>
</entry>
<entry>
<title><![CDATA[WIN10 1809隐藏大杀器--卓越模式]]></title>
<url>%2F2018%2F07%2F06%2FWIN10-1809%E9%9A%90%E8%97%8F%E5%A4%A7%E6%9D%80%E5%99%A8-%E5%8D%93%E8%B6%8A%E6%A8%A1%E5%BC%8F%2F</url>
<content type="text"><![CDATA[铛铛铛 开箱快递来了!新cpu到了其实也不算新cpu,毕竟这个cpu已经出了6年了。 放暑假了,总要娱乐一下。奈何家里的老电脑cpu性能不大够(二代i5 @2.9ghz),干啥啥不痛快,很是苦恼。正好,趁着i7 9900K将要发售的时候,赶快买了个E3-1240V2充值信仰! 先来回顾一下新买cpu的参数。 基本参数CPU系列 Xeon E3 v2系列制作工艺 22纳米核心代号 Ivy Bridge 性能参数核心数量 四核心线程数量 八线程CPU主频 3.4GHz动态加速频率 3.8GHzL3缓存 8MB总线规格 DMI 5GT/s热设计功耗(TDP) 69W 内存规格支持最大内存容量 32GB内存类型 DDR3 1333/1600MHz内存描述 双通道DDR3-1333/1600MHz 封装规格插槽类型 LGA 1155封装大小 37.5×37.5mm最大CPU配置 1颗 技术参数睿频加速技术 支持,2.0超线程技术 支持虚拟化技术 Intel VT-x指令集 AVX,SSE4.1,SSE4.264位处理器 支持其它技术 支持博锐技术,定向I/O虚拟化技术,增强型SpeedStep技术,按需配电技术,温度监视技术,身份保护技术,数据保护技术,平台保护技术集成显卡 不支持 这个cpu现在来看参数不怎么样,性能也大概比i3-8100高一点,但是这的确是LGA1155平台上性能比较高的了。不带集成显卡,规格主频和i7 3770k差不多,由于我这边老主板是b75,不能给i7超频,所以也就没选。 装机装机过程没有拍摄,上一组装机成功的BIOS截图吧。一次成功点亮!由于我买的1240v2是二字头,体质好,可以调频。果断把c1、c3、c6关掉,节电关掉,内存超导1600,时序降低一点设置成单核3.8ghz,最低3.6ghz 游戏测试测试一下我最近喜欢玩的游戏,惊魂5,画质全高 实际运行表明,最高帧81,最低62,可以流畅运行。而老平台最低51,最高72。流畅性有显著提升。 win10电源管理–卓越模式接下来是标题内容了~ 这个没什么好说的,Win10更新1803以来,微软不仅带来了一些新功能(和BUG),还悄悄地加入了一个“卓越性能模式”,开了它,CPU电压会比高性能模式更低,从而带来相对更低一些的温度以及稳定性,并且对内存超频也有一定帮助 开启首先,我们需要按Win键搜索并右键以管理员身份打开[ Windows Powershell ], 然后输入powercfg -duplicatescheme e9a42b02-d5df-448d-aa00-03f14749eb61: 那么恭喜你,你的电脑已经成功开启了“卓越性能模式”,在电源选项中轻松找到它啦~ 结束此片完了~ 开启卓越模式的文章网上一大堆一大堆的,这次呢重点还是主要分享一下我换cpu的故事。当然,“卓越性能”基本可以代替“高性能”模式使用,毕竟有了(看起来)更好的自然就选择更好的了。并且非常推荐喜欢超超频、玩硬件的用户使用,因为确实有性能提升,能开就开吧。]]></content>
<tags>
<tag>日常</tag>
<tag>技术</tag>
<tag>Windows10</tag>
</tags>
</entry>
<entry>
<title><![CDATA[=_=我的上一个博客被攻击瘫痪了=_=]]></title>
<url>%2F2018%2F07%2F04%2FHelloWorld%2F</url>
<content type="text"><![CDATA[刚刚结束期末考试,呼呼(_ _)゜zzZ,累死了。 一想到原来的博客没有了, 还要在搭一个,就更累了! 万事开头难,helloWorld不能不写。而且因为不是第一次写HelloWorld了,所以这次写的简单点吧。 哦,忘了交代了,我的上一个博客挂掉了, 被DDOS攻击攻击,攻击完了暂且不说,登陆密码又被暴力破解了。哎,难受,图省钱买的OpenVZ服务器,攻击后在数据中心管理员的几经确认下,被判定死亡。数据无法修复,内核被破坏,无法启动。 而我也没有设置备份(还不是因为懒!),雪上加霜! 😕😕😕😕😕😕😕😕😕😕😕😕😕😕😕😕😕 本父亲好难受! 😕😕😕😕😕😕😕😕😕😕😕😕😕😕😕😕😕 这次我学乖了,新的博客搭建在GithubPage上,黑客你们随便攻击,大不了就没有流量。待老夫有钱了,就去消费个CDN,爱咋地在地。 行,就这样吧, 想要解锁更多新姿势?请访问我的博客]]></content>
<tags>
<tag>日常</tag>
<tag>初</tag>
</tags>
</entry>
</search>