-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathsearch.xml
532 lines (532 loc) · 647 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
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title><![CDATA[2020 Happy Lantern Festival]]></title>
<url>%2F2020%2F02%2F08%2F2020Happy%20Lantern%20Festival%2F</url>
<content type="text"><![CDATA[2020 Happy Lantern Festival]]></content>
<categories>
<category>元宵节快乐</category>
</categories>
<tags>
<tag>节日</tag>
</tags>
</entry>
<entry>
<title><![CDATA[图解设计模式]]></title>
<url>%2F2020%2F01%2F16%2F%E5%9B%BE%E8%A7%A3%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%2F</url>
<content type="text"><![CDATA[图解设计模式]]></content>
<categories>
<category>设计模式</category>
</categories>
<tags>
<tag>设计模式</tag>
</tags>
</entry>
<entry>
<title><![CDATA[咖啡小知识]]></title>
<url>%2F2020%2F01%2F13%2F%E5%92%96%E5%95%A1%E5%B0%8F%E7%9F%A5%E8%AF%86%2F</url>
<content type="text"><![CDATA[[TOC] 咖啡小知识:咖啡现在已经成为越来越多上班族不可或缺的“提神续命”饮料,让我们来了解一下咖啡的小知识叭。 ①咖啡分类: 目前市场上的咖啡一般分为4大类: (1)美式咖啡: 是以一种咖啡为原料,然后再加水制成的,萃取而来浓度高,制作时间长; (2)意式咖啡: 意式咖啡 Espresso 是一种咖啡粉加倍,咖啡机高压研磨,浓缩咖啡,强烈浓郁。 (3)混合咖啡(花式咖啡): 意式咖啡加入花样而来。是以多种咖啡为原料,然后再加奶、水等制成的,如拿铁、摩卡、卡布奇诺、焦糖玛奇朵。 (4)单品咖啡 由原产地出产的单一咖啡豆磨制而成的纯正咖啡。 有蓝山咖啡、巴西咖啡、哥伦比亚咖啡等等。单品咖啡有强烈的特性口感特别或清新柔和或香醇顺滑。 适合对咖啡品质有追求的人 ②咖啡成分: 咖啡中含有少量的脂肪、蛋白质、糖、矿物质和粗纤维,另外,还含有咖啡因、绿原酸、单宁等成分。其中,咖啡因可以加速人体新陈代谢,使人保持头脑清醒;绿原酸具有抗氧化、抗炎、抗菌、抗病毒等生物活性。 对于除了咖啡和水,什么都没加的美式咖啡来说,它含有丰富的钾和大量的抗氧化物质,热量也很低,算是比较健康的饮品。 如果在咖啡基础上加入奶(如拿铁、卡布奇诺),将可以增加钙、蛋白质等营养成分的含量,但具体有多少,还要看奶的比例,一般来说,拿铁中奶的比例会比卡布奇诺高一些。 除了奶,一些糖、脂肪也经常被加到咖啡里去,例如摩卡中会加入巧克力酱和鲜奶油,焦糖玛奇朵中会加入焦糖,这些咖啡在口感上是更丰富了,但因为糖和脂肪的含量上升了,从营养的角度,却更逊色了。 一、美式咖啡: 咖啡豆研磨成粉萃取,制作时间长 二、意式咖啡:咖啡粉加倍,高压研磨,浓缩咖啡,强烈浓郁意式咖啡 Espresso 三、花式咖啡:是以多种咖啡为原料,然后再加奶、水等制成的,如拿铁、摩卡、卡布奇诺、焦糖玛奇朵。 1.一份Espresso+1份水=美式咖啡 2.一份Espresso+1.5份热牛奶+半分奶泡=拿铁(牛奶多点,奶泡少一点) 3.一份Espresso+1.5份热牛奶+半分奶泡+糖浆=某糖浆拿铁(例:焦糖拿铁、香草拿铁、抹茶拿铁、榛果拿铁) 4.一份Espresso+0.5份热牛奶+一分奶泡=卡布奇诺(牛奶少点,奶泡多一点,和拿铁相反) 5.一份Espresso+1.5份热牛奶+半分奶泡+巧克力酱+鲜奶油+可可粉等=摩卡摩卡是在拿铁的基础上加入巧克力酱,鲜奶油,或者可可粉、肉桂粉等。 6.一份Espresso+两份奶泡=玛奇朵 7.一份Espresso+两份奶泡+糖浆=焦糖玛奇朵 8.一份Espresso+鲜奶油=康宝蓝 9.一份Espresso+半份牛奶+半份奶油+少量奶泡=布雷卫 10.一份Espresso+少量爱尔兰威士忌+鲜奶油=爱尔兰咖啡 最简单的理解就是: 1、美式咖啡:现在做的美式咖啡主要是在意式浓缩咖啡的基础上兑热水,只是味道淡一些;味道特点:纯咖啡,口味淡 2、意式咖啡:espresso,咖啡粉加倍,咖啡机高压研磨,浓缩咖啡,强烈浓郁 3、花式咖啡 3.1 拿铁咖啡:意式浓缩咖啡大量加热牛奶+少量奶泡 口味特点:带有咖啡味的牛奶。牛奶多,奶泡少。 3.2 卡布奇诺:意式浓缩咖啡加入少量热牛奶,大量奶泡口味特点:咖啡味道稍浓一些,奶沫口感。牛奶少,奶泡多。 3.3 摩卡咖啡:意式浓缩咖啡加奶、泡沫奶油、再加巧克力酱,口味特点:带有巧克力味的奶咖啡,泡沫奶油口感。 3.4 玛奇朵:意式浓缩咖啡加入两份奶泡 上述4款是我平时比较喜欢喝的,平时加班提提神叭。 四、课代表总结 美式咖啡 萃取,咖啡因浓度高,制作时间长 意式咖啡: 也就是平时说Espresso,咖啡粉加倍,咖啡机高压研磨,浓缩咖啡,强烈浓郁。 花式咖啡: ( ̄▽ ̄) 1.一份Espresso+1份水=美式咖啡 2.一份Espresso+1.5份热牛奶+半分奶泡=拿铁 3.一份Espresso+1.5份热牛奶+半分奶泡+某糖浆=某糖浆拿铁(例:抹茶拿铁) 4.一份Espresso+0.5份热牛奶+一分奶泡=卡布奇诺 5.一份Espresso+1.5份热牛奶+半分奶泡+巧克力酱+鲜奶油+可可粉等=摩卡 6.一份Espresso+两份奶泡=玛琪朵 7.一份Espresso+两份奶泡+糖浆=焦糖玛奇朵 8.一份Espresso+鲜奶油=康宝蓝 9.一份Espresso+半份牛奶+半份奶油+少量奶泡=布雷卫 10.一份Espresso+少量爱尔兰威士忌+鲜奶油=爱尔兰咖啡 ( ̄▽ ̄) 在知乎看到的的图: 五、引用 1、哔哩哔哩上的咖啡小知识: 2、知乎上的咖啡小知识: https://www.zhihu.com/question/27113144 六、我了解的东西也不一定对,欢迎大家一起讨论。]]></content>
<categories>
<category>咖啡</category>
<category>生活</category>
</categories>
<tags>
<tag>生活</tag>
<tag>咖啡</tag>
</tags>
</entry>
<entry>
<title><![CDATA[如何给长辈选茶叶]]></title>
<url>%2F2020%2F01%2F12%2F%E5%A6%82%E4%BD%95%E7%BB%99%E9%95%BF%E8%BE%88%E9%80%89%E8%8C%B6%E5%8F%B6%2F</url>
<content type="text"><![CDATA[[TOC] 今天为了给长辈挑选茶叶挑了一下午,这篇文章主要是博主了解茶叶做的笔记。博主是茶叶的行外之人,请有懂茶叶的朋友推荐一些茶叶叭。 一、茶叶分类市面上的茶叶品种非常多,按照发酵程度可以分为六大类: 绿茶、白茶、黄茶、青茶、红茶和黑茶。 如:龙井为绿茶。铁观音为青茶叶。 龙井、碧螺春、铁观音、普洱 绿茶 绿茶品牌 白茶 白茶品牌 黄茶 青茶 红茶 黑茶 二、口感总结 茶叶包装分类: 灌装茶、砖茶、饼茶、袋泡茶(茶包)。 三、茶叶纪录片 央视纪录片《茶,一片树叶的故事》【共六集】 https://www.bilibili.com/video/av21397586/]]></content>
<categories>
<category>茶叶</category>
<category>生活</category>
</categories>
<tags>
<tag>茶叶</tag>
<tag>生活</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Activiti7工作流引擎]]></title>
<url>%2F2020%2F01%2F02%2FActiviti7%E5%B7%A5%E4%BD%9C%E6%B5%81%E5%BC%95%E6%93%8E%2F</url>
<content type="text"></content>
<categories>
<category>工作流</category>
</categories>
<tags>
<tag>工作流</tag>
</tags>
</entry>
<entry>
<title><![CDATA[架构与优化之JVM优化第03课Tomcat8的优化 看懂Java底层字节码 编码的优化建议]]></title>
<url>%2F2019%2F12%2F20%2F%E6%9E%B6%E6%9E%84%E4%B8%8E%E4%BC%98%E5%8C%96%E4%B9%8BJVM%E4%BC%98%E5%8C%96%E7%AC%AC03%E7%AF%87Tomcat8%E7%9A%84%E4%BC%98%E5%8C%96%20%E7%9C%8B%E6%87%82Java%E5%BA%95%E5%B1%82%E5%AD%97%E8%8A%82%E7%A0%81%20%E7%BC%96%E7%A0%81%E7%9A%84%E4%BC%98%E5%8C%96%E5%BB%BA%E8%AE%AE%2F</url>
<content type="text"><![CDATA[0.学习目标 Tomcat8的优化 看懂Java底层字节码 编码的优化建议 1、Tomcat8优化tomcat服务器在JavaEE项目中使用率非常高,所以在生产环境对tomcat的优化也变得非 常重要了。对于tomcat的优化,主要是从2个方面入手,一是,tomcat自身的配置,另一个是tomcat所运行的jvm虚拟机的调优。下面我们将从这2个方面进行讲解。 1.1、Tomcat配置优化1.1.1、部署安装tomcat8 下载并安装:https://tomcat.apache.org/download-80.cgicd /tmpwget http://mirrors.tuna.tsinghua.edu.cn/apache/tomcat/tomcat‐ 8/v8.5.34/bin/apache‐tomcat‐8.5.34.tar.gz tar ‐xvf apache‐tomcat‐8.5.34.tar.gz cd apache‐tomcat‐8.5.34/conf 修改配置文件,配置tomcat的管理用户vim tomcat‐users.xml #写入如下内容: 保存退出如果是tomcat7,配置了tomcat用户就可以登录系统了,但是tomcat8中不行,还需要修改另一个配置文件,否则访问不了,提示403vim webapps/manager/META‐INF/context.xml 将<Valve的内容注释掉<!‐‐ ‐‐> 保存退出即可启动tomcatcd /tmp/apache‐tomcat‐8.5.34/bin/./startup.sh && tail ‐f …/logs/catalina.out 打开浏览器进行测试访问http://192.168.40.133:8080/点击“Server Status”,输入用户名、密码进行登录,tomcat/tomcat进入之后即可看到服务的信息。 1.1.2、禁用AJP连接在服务状态页面中可以看到,默认状态下会启用AJP服务,并且占用8009端口。什么是AJP呢?AJP(Apache JServer Protocol)AJPv13协议是面向包的。WEB服务器和Servlet容器通过TCP连接来交互;为了节省SOCKET创建的昂贵代价,WEB服务器会尝试维护一个永久TCP连接到servlet容器,并且在多个请求和响应周期过程会重用连接。我们一般是使用Nginx+tomcat的架构,所以用不着AJP协议,所以把AJP连接器禁用。修改conf下的server.xml文件,将AJP服务禁用掉即可。 1 重启tomcat,查看效果。可以看到AJP服务以及不存在了。1.1.3、执行器(线程池)在tomcat中每一个用户请求都是一个线程,所以可以使用线程池提高性能。修改server.xml文件: <!‐‐将注释打开‐‐> <!‐‐参数说明:maxThreads:最大并发数,默认设置 200,一般建议在 500 ~ 1000,根据硬件设施和业务来判断minSpareThreads:Tomcat 初 始 化 时 创 建 的 线 程 数 , 默 认 设 置 25 prestartminSpareThreads: 在 Tomcat 初始化的时候就初始化 minSpareThreads 的参数值,如果不等于 true,minSpareThreads 的值就没啥效果了maxQueueSize,最大的等待队列数,超过则拒绝请求‐‐> <!‐‐在Connector中设置executor属性指向上面的执行器‐‐> 1234567891011121314保存退出,重启tomcat,查看效果。在页面中显示最大线程数为-1,这个是正常的,仅仅是显示的问题,实际使用的指定的值。 也有人遇到这样的问题:https://blog.csdn.net/weixin_38278878/article/details/801443971.1.4、3种运行模式tomcat的运行模式有3种:1.bio默认的模式,性能非常低下,没有经过任何优化处理和支持.2.nionio(new I/O),是Java SE 1.4及后续版本提供的一种新的I/O操作方式(即java.nio包及其子包)。Java nio是一个基于缓冲区、并能提供非阻塞I/O操作的Java API,因此nio 也被看成是non-blocking I/O的缩写。它拥有比传统I/O操作(bio)更好的并发运行性能。3.apr安装起来最困难,但是从操作系统级别来解决异步的IO问题,大幅度的提高性能.推荐使用nio,不过,在tomcat8中有最新的nio2,速度更快,建议使用nio2. 设置nio2: 12 可以看到已经设置为nio2了。 1.2、部署测试用的java web项目为了方便测试性能,我们将部署一个java web项目,这个项目本身和本套课程没有什么关系,仅仅用于测试。注意:这里在测试时,我们使用一个新的tomcat,进行测试,后面再对其进行优化 调整,再测试。1.2.1、创建dashboard数据库在资料中找到sql脚本文件dashboard.sql,在linux服务器上执行。 cat dashboard.sql | mysql ‐uroot ‐proot1创建完成后,可以看到有3张表。1.2.2、部署web应用在资料中找到itcat-dashboard-web.war,上传到linux服务器,进行部署安装。 cd /tmp/apache‐tomcat‐8.5.34/webappsrm ‐rf *mkdir ROOT cd ROOT/rz上传war包jar ‐xvf itcat‐dashboard‐web.war rm ‐rf itcat‐dashboard‐web.war 修改数据库配置文件cd /tmp/apache‐tomcat‐8.5.34/webapps/ROOT/WEB‐INF/classes vim jdbc.properties 这里根据自己的实际情况进行配置jdbc.driverClassName=com.mysql.jdbc.Driverjdbc.url=jdbc:mysql://node01:3306/dashboard? useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueri es=truejdbc.username=root jdbc.password=root1234567891011121314151617重新启动tomcat。访问首页,查看是否已经启动成功:http://192.168.40.133:8080/index 1.3、使用Apache JMeter进行测试Apache Jmeter是开源的压力测试工具,我们借助于此工具进行测试,将测试出tomcat的吞吐量等信息。 1.3.1、下载安装下载地址:http://jmeter.apache.org/download_jmeter.cgi安装:直接将下载好的zip压缩包进行解压即可。进入bin目录,找到jmeter.bat文件,双机打开即可启动。1.3.2、修改主题和语言默认的主题是黑色风格的主题并且语言是英语,这样不太方便使用,所以需要修改下主题和中文语言。主题修改完成。 接下来设置语言为简体中文。1.3.3、创建首页的测试用例第一步:保存测试用例第二步:添加线程组,使用线程模拟用户的并发1000个线程,每个线程循环10次,也就是tomcat会接收到10000个请求。第三步:添加http请求第四步:添加请求监控1.3.4、启动、进行测试1.3.5、聚合报告在聚合报告中,重点看吞吐量。 1.4、调整tomcat参数进行优化通过上面测试可以看出,tomcat在不做任何调整时,吞吐量为73次/秒。1.4.1、禁用AJP服务可以看到,禁用AJP服务后,吞吐量会有所提升。当然了,测试不一定准确,需要多测试几次才能看出是否有提升。 1.4.2、设置线程池通过设置线程池,调整线程池相关的参数进行测试tomcat的性能。1.4.2.1、最大线程数为500,初始为50 12测试结果: 吞吐量为128次/秒,性能有所提升。1.4.2.2、最大线程数为1000,初始为200 12 吞吐量为151,性能有所提升。1.4.2.3、最大线程数为5000,初始为1000是否是线程数最多,速度越快呢? 我们来测试下。 12 可以看到,虽然最大线程已经设置到5000,但是实际测试效果并不理想,并且平均的响 应时间也边长了,所以单纯靠提升线程数量是不能一直得到性能提升的。 1.4.2.4、设置最大等待队列数默认情况下,请求发送到tomcat,如果tomcat正忙,那么该请求会一直等待。这样虽然 可以保证每个请求都能请求到,但是请求时间就会边长。 有些时候,我们也不一定要求请求一定等待,可以设置最大等待队列大小,如果超过就不等待了。这样虽然有些请求是失败的,但是请求时间会虽短。典型的应用:12306。 <!‐‐最大等待数为100‐‐> 1234567 测试结果: 平均响应时间:3.1秒响应时间明显缩短错误率:49.88%错误率提升到一半,也可以理解,最大线程为500,测试的并发为1000 吞吐量:238次/秒吞吐量明显提升结论:响应时间、吞吐量这2个指标需要找到平衡才能达到更好的性能。1.4.3、设置nio2的运行模式将最大线程设置为500进行测试: <!‐‐ 设置nio2 ‐‐> 12345678 可以看到,平均响应时间有缩短,吞吐量有提升,可以得出结论:nio2的性能要高于nio。 1.5、调整JVM参数进行优化接下来,测试通过jvm参数进行优化,为了测试一致性,依然将最大线程数设置为500, 启用nio2运行模式。1.5.1、设置并行垃圾回收器 年轻代、老年代均使用并行收集器,初始堆内存64M,最大堆内存512M JAVA_OPTS=”‐XX:+UseParallelGC ‐XX:+UseParallelOldGC ‐Xms64m ‐Xmx512m ‐ XX:+PrintGCDetails ‐XX:+PrintGCTimeStamps ‐XX:+PrintGCDateStamps ‐ XX:+PrintHeapAtGC ‐Xloggc:../logs/gc.log”1 测试结果与默认的JVM参数结果接近。(执行了2次测试,结果是第二次测试的结果)1.5.2、查看gc日志文件将gc.log文件上传到gceasy.io查看gc中是否存在问题。在报告中显示,在5次GC时,系统所消耗的时间大于用户时间,这反应出的服务器的性能存在瓶颈,调度CPU等资源所消耗的时间要长一些。问题二:可以关键指标中可以看出,吞吐量表现不错,但是gc时,线程的暂停时间稍有点长。问题三:通过GC的统计可以看出:年轻代的gc有74次,次数稍有多,说明年轻代设置的大小不合适需要调整FullGC有8次,说明堆内存的大小不合适,需要调整问题四:从GC原因的可以看出,年轻代大小设置不合理,导致了多次GC。1.5.3、调整年轻代大小 JAVA_OPTS=”‐XX:+UseParallelGC ‐XX:+UseParallelOldGC ‐Xms128m ‐Xmx1024m ‐ XX:NewSize=64m ‐XX:MaxNewSize=256m ‐XX:+PrintGCDetails ‐ XX:+PrintGCTimeStamps ‐XX:+PrintGCDateStamps ‐XX:+PrintHeapAtGC ‐ Xloggc:../logs/gc.log”1将初始堆大小设置为128m,最大为1024m初始年轻代大小64m,年轻代最大256m从测试结果来看,吞吐量以及响应时间均有提升。查看gc日志: 可以看到GC次数要明显减少,说明调整是有效的。1.5.4、设置G1垃圾回收器 设置了最大停顿时间100毫秒,初始堆内存128m,最大堆内存1024m JAVA_OPTS=”‐XX:+UseG1GC ‐XX:MaxGCPauseMillis=100 ‐Xms128m ‐Xmx1024m ‐ XX:+PrintGCDetails ‐XX:+PrintGCTimeStamps ‐XX:+PrintGCDateStamps ‐ XX:+PrintHeapAtGC ‐Xloggc:../logs/gc.log”1测试结果: 可以看到,吞吐量有所提升,评价响应时间也有所缩短。 1.5.5、小结通过上述的测试,可以总结出,对tomcat性能优化就是需要不断的进行调整参数,然后 测试结果,可能会调优也可能会调差,这时就需要借助于gc的可视化工具来看gc的情 况。再帮我我们做出决策应该调整哪些参数。 2、JVM字节码前面我们通过tomcat本身的参数以及jvm的参数对tomcat做了优化,其实要想将应用程 序跑的更快、效率更高,除了对tomcat容器以及jvm优化外,应用程序代码本身如果写 的效率不高的,那么也是不行的,所以,对于程序本身的优化也就很重要了。 对于程序本身的优化,可以借鉴很多前辈们的经验,但是有些时候,在从源码角度方面 分析的话,不好鉴别出哪个效率高,如对字符串拼接的操作,是直接“+”号拼接效率高还 是使用StringBuilder效率高?这个时候,就需要通过查看编译好的class文件中字节码,就可以找到答案。我们都知道,java编写应用,需要先通过javac命令编译成class文件,再通过jvm执行,jvm执行时是需要将class文件中的字节码载入到jvm进行运行的。 2.1、通过javap命令查看class文件的字节码内容首先,看一个简单的Test1类的代码: package cn.itcast.jvm;public class Test1 {public static void main(String[] args) {int a = 2; int b = 5; int c = b ‐ a;System.out.println(c);}}1234567通过javap命令查看class文件中的字节码内容: javap ‐v Test1.class > Test1.txtjavap用法: javap 其中, 可能的选项包括:‐help ‐‐help ‐?‐version‐v ‐verbose‐l‐public‐protected‐package‐p ‐private‐c‐s‐sysinfo ‐constants‐classpath ‐cp ‐bootclasspath 12345678910111213141516171819查看Test1.txt文件,内容如下: Classfile /F:/code/itcast‐jvm/itcast‐jvm‐ test/target/classes/cn/itcast/jvm/Test1.classLast modified 2018‐9‐27; size 577 bytesMD5 checksum 4214859db3543c0c783ec8a216a4795f Compiled from “Test1.java”public class cn.itcast.jvm.Test1 minor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPERConstant pool: 1 = Methodref #5.#23 // java/lang/Object.”“: ()V2 = Fieldref #24.#25 // java/lang/System.out:Ljava/io/PrintStream;3 = Methodref #26.#27 // java/io/PrintStream.println: (I)V28 = Utf8 cn/itcast/jvm/Test129 = Utf8 java/lang/Object30 = Utf8 java/lang/System31 = Utf8 out32 = Utf8 Ljava/io/PrintStream;33 = Utf8 java/io/PrintStream34 = Utf8 println35 = Utf8 (I)V{public cn.itcast.jvm.Test1(); descriptor: ()Vflags: ACC_PUBLIC Code:stack=1, locals=1, args_size=1 0: aload_01: invokespecial #1 // Methodjava/lang/Object.”“:()V4: return LineNumberTable:line 3: 0 LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lcn/itcast/jvm/Test1; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=4, args_size=1 0: iconst_21: istore_12: iconst_53: istore_24: iload_25: iload_16: isub7: istore_38: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;11: iload_3 12: invokevirtual #3 // Methodjava/io/PrintStream.println:(I)V15: return LineNumberTable:line 6: 0line 7: 2line 8: 4line 9: 8line 10: 15LocalVariableTable:Start0 Length16 Slot0 Nameargs Signature[Ljava/lang/String;2 14 1 a I4 12 2 b I8 8 3 c I}SourceFile: “Test1.java”123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263内容大致分为4个部分:第一部分:显示了生成这个class的java源文件、版本信息、生成时间等。第二部分:显示了该类中所涉及到常量池,共35个常量。第三部分:显示该类的构造器,编译器自动插入的。 第四部分:显示了main方的信息。(这个是需要我们重点关注的) 2.2、常量池官网文档: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4-140 Constant Type Value 说明CONSTANT_Class 7 类或接口的符号引用CONSTANT_Fieldref 9 字段的符号引用CONSTANT_Methodref 10 类中方法的符号引用CONSTANT_InterfaceMethodref 11 接口中方法的符号引用CONSTANT_String 8 字符串类型常量CONSTANT_Integer 3 整形常量CONSTANT_Float 4 浮点型常量CONSTANT_Long 5 长整型常量CONSTANT_Double 6 双精度浮点型常量CONSTANT_NameAndType 12 字段或方法的符号引用CONSTANT_Utf8 1 UTF-8编码的字符串CONSTANT_MethodHandle 15 表示方法句柄CONSTANT_MethodType 16 标志方法类型CONSTANT_InvokeDynamic 18 表示一个动态方法调用点1234567891011121314152.3、描述符2.3.1、字段描述符官网:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.2 FieldType termTypeInterpretationB byte signed byte Cchar Unicode character code point in the Basic Multilingual Plane, encoded with UTF-16D double double-precision floating-point valueF float single-precision floating-point valueI int integerJ long long integerLClassName; reference an instance of class ClassNameS short signed shortZ boolean true or false[ reference one array dimension1234567891011121314152.3.2、方法描述符官网:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.3 示例: The method descriptor for the method: Object m(int i, double d, Thread t) {…}1is: (IDLjava/lang/Thread;)Ljava/lang/Object;12.4、解读方法字节码 public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)V //方法描述,V表示该方法的放回值为voidflags: ACC_PUBLIC, ACC_STATIC // 方法修饰符,public、static的Code:// stack=2,操作栈的大小为2、locals=4,本地变量表大小,args_size=1, 参数 的个数 stack=2, locals=4, args_size=10: iconst_2 //将数字2值压入操作栈,位于栈的最上面1: istore_1 //从操作栈中弹出一个元素(数字2),放入到本地变量表中,位 于下标为1的位置(下标为0的是this)2: iconst_5 //将数字5值压入操作栈,位于栈的最上面3: istore_2 //从操作栈中弹出一个元素(5),放入到本地变量表中,位于第下标为2个位置4: iload_2 //将本地变量表中下标为2的位置元素压入操作栈(5)5: iload_1 //将本地变量表中下标为1的位置元素压入操作栈(2)6: isub //操作栈中的2个数字相减7: istore_3 // 将相减的结果压入到本地本地变量表中,位于下标为3的位置// 通过#2号找到对应的常量,即可找到对应的引用8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;11: iload_3 //将本地变量表中下标为3的位置元素压入操作栈(3)// 通过#3号找到对应的常量,即可找到对应的引用,进行方法调用12: invokevirtual #3 // Method java/io/PrintStream.println:(I)V15: return //返回LineNumberTable: //行号的列表 line 6: 0line 7: 2line 8: 4line 9: 8line 10: 15 LocalVariableTable: // 本地变量表Start0 Length16 Slot0 Nameargs Signature[Ljava/lang/String;2 14 1 a I4 12 2 b I8 8 3 c I} SourceFile: “Test1.java” 123456789101112131415161718192021222324252627282930313233343536373839404142434445462.4.1、图解 2.5、研究 i++ 与 ++i 的不同我们都知道,i++表示,先返回再+1,++i表示,先+1再返回。它的底层是怎么样的呢? 我们一起探究下。 编写测试代码: public class Test2 { public static void main(String[] args) {new Test2().method1(); new Test2().method2();}public void method1(){int i = 1; int a = i++;System.out.println(a); //打印1}public void method2(){int i = 1; int a = ++i;System.out.println(a);//打印2}}1234567891011121314152.5.1、查看class字节码 Classfile /F:/code/itcast‐jvm/itcast‐jvm‐ test/target/classes/cn/itcast/jvm/Test2.classMD5 checksum 901660fc11c43b6daadd0942150960ed Compiled from “Test2.java”public class cn.itcast.jvm.Test2 minor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPERConstant pool: 1 = Methodref #8.#27 // java/lang/Object.”“: ()V2 = Class #28 // cn/itcast/jvm/Test23 = Methodref #2.#27 // cn/itcast/jvm/Test2.”“:()V 4 = Methodref #2.#29 // cn/itcast/jvm/Test2.method1: ()V5 = Methodref #2.#30 // cn/itcast/jvm/Test2.method2: ()V6 = Fieldref #31.#32 // java/lang/System.out:Ljava/io/PrintStream;7 = Methodref #33.#34 // java/io/PrintStream.println: (I)V26 = Utf8 Test2.java27 = NameAndType #9:#10 // ““:()V28 = Utf8 cn/itcast/jvm/Test229 = NameAndType #20:#10 // method1:()V30 = NameAndType #24:#10 // method2:()V31 = Class #36 // java/lang/System32 = NameAndType #37:#38 // out:Ljava/io/PrintStream;33 = Class #39 // java/io/PrintStream34 = NameAndType #40:#41 // println:(I)V35 = Utf8 java/lang/Object36 = Utf8 java/lang/System37 = Utf8 out38 = Utf8 Ljava/io/PrintStream;39 = Utf8 java/io/PrintStream40 = Utf8 println41 = Utf8 (I)V{public cn.itcast.jvm.Test2(); descriptor: ()Vflags: ACC_PUBLIC Code:stack=1, locals=1, args_size=1 0: aload_01: invokespecial #1 // Method java/lang/Object.”“:()V4: return LineNumberTable:line 3: 0 LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lcn/itcast/jvm/Test2; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=1, args_size=10: new #2 // class cn/itcast/jvm/Test23: dup 4: invokespecial #3 // Method ““:()V7: invokevirtual #4 // Method method1:()V10: new #2 // class cn/itcast/jvm/Test213: dup14: invokespecial #3 // Method ““:()V17: invokevirtual #5 // Method method2:()V20: return LineNumberTable:line 6: 0line 7: 10line 8: 20 LocalVariableTable:Start Length Slot Name Signature0 21 0 args [Ljava/lang/String; public void method1(); descriptor: ()V flags: ACC_PUBLIC Code:stack=2, locals=3, args_size=1 0: iconst_11: istore_12: iload_13: iinc 1, 16: istore_27: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;10: iload_211: invokevirtual #7 // Method java/io/PrintStream.println:(I)V14: return LineNumberTable:line 11: 0line 12: 2line 13: 7line 14: 14 LocalVariableTable:Start Length Slot Name Signature0 15 0 this Lcn/itcast/jvm/Test2; 2 13 1 i I7 8 2 a I public void method2(); descriptor: ()V flags: ACC_PUBLIC Code:stack=2, locals=3, args_size=1 0: iconst_11: istore_12: iinc 1, 15: iload_16: istore_27: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;10: iload_211: invokevirtual #7 // Method java/io/PrintStream.println:(I)V14: return LineNumberTable:line 17: 0line 18: 2line 19: 7line 20: 14 LocalVariableTable:Start Length Slot Name Signature0 15 0 this Lcn/itcast/jvm/Test2; 2 13 1 i I7 8 2 a I}SourceFile: “Test2.java” 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798992.5.2、对比i++: 0: iconst_1 //将数字1压入到操作栈1: istore_1 //将数字1从操作栈弹出,压入到本地变量表中,下标为12: iload_1 //从本地变量表中获取下标为1的数据,压入到操作栈中3: iinc 1, 1 // 将本地变量中的1,再+16: istore_2 // 将数字1从操作栈弹出,压入到本地变量表中,下标为2 7: getstatic #6 // Fieldjava/lang/System.out:Ljava/io/PrintStream;10: iload_2 //从本地变量表中获取下标为2的数据,压入到操作栈中11: invokevirtual #7 // Method java/io/PrintStream.println:(I)V14: return123456789++i: 0: iconst_1 //将数字1压入到操作栈1: istore_1 //将数字1从操作栈弹出,压入到本地变量表中,下标为12: iinc 1, 1// 将本地变量中的1,再+15: iload_1 //从本地变量表中获取下标为1的数据(2),压入到操作栈中6: istore_2 //将数字2从操作栈弹出,压入到本地变量表中,下标为27: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;10: iload_2 //从本地变量表中获取下标为2的数据(2),压入到操作栈中11: invokevirtual #7 // Method java/io/PrintStream.println:(I)V14: return123456789区别: i++只是在本地变量中对数字做了相加,并没有将数据压入到操作栈将前面拿到的数字1,再次从操作栈中拿到,压入到本地变量中++i将本地变量中的数字做了相加,并且将数据压入到操作栈将操作栈中的数据,再次压入到本地变量中小结:可以通过查看字节码的方式对代码的底层做研究,探究其原理。 2.6、字符串拼接字符串的拼接在开发过程中使用是非常频繁的,常用的方式有三种: +号拼接: str+“456”StringBuilder拼接StringBuffer拼接 StringBuffer是保证线程安全的,效率是比较低的,我们更多的是使用场景是不会涉及到 线程安全的问题的,所以更多的时候会选择StringBuilder,效率会高一些。那么,问题来了,StringBuilder和“+”号拼接,哪个效率高呢?接下来我们通过字节码的 方式进行探究。 首先,编写个示例: package cn.itcast.jvm;public class Test3 {public static void main(String[] args) {new Test3().m1();new Test3().m2();public void m1(){String s1 = “123”; String s2 = “456”; String s3 = s1 + s2; System.out.println(s3);} public void m2(){String s1 = “123”; String s2 = “456”;StringBuilder sb = new StringBuilder(); sb.append(s1);sb.append(s2);String s3 = sb.toString(); System.out.println(s3);}}} 123456789101112131415161718查看Test3.class的字节码 Classfile /F:/code/itcast‐jvm/itcast‐jvm‐ test/target/classes/cn/itcast/jvm/Test3.classMD5 checksum b3f7629e7e37768b9b5581be01df40d6 Compiled from “Test3.java”public class cn.itcast.jvm.Test3 minor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPERConstant pool: 1 = Methodref #14.#36 // java/lang/Object.”“: ()V2 = Class #37 // cn/itcast/jvm/Test33 = Methodref #2.#36 // cn/itcast/jvm/Test3.”“:()V 4 = Methodref #2.#38 // cn/itcast/jvm/Test3.m1:()V5 = Methodref #2.#39 // cn/itcast/jvm/Test3.m2:()V6 = String #40 // 1237 = String #41 // 4568 = Class #42 // java/lang/StringBuilder9 = Methodref #8.#36 // java/lang/StringBuilder.”“:()V 10 = Methodref #8.#43 // java/lang/StringBuilder.append: (Ljava/lang/String;)Ljava/lang/StringBuilder;11 = Methodref #8.#44 // java/lang/StringBuilder.toString:()Ljava/lang/String;12 = Fieldref #45.#46 // java/lang/System.out:Ljava/io/PrintStream;13 = Methodref #47.#48 // java/io/PrintStream.println: (Ljava/lang/String;)V14 = Class #49 // java/lang/Object15 = Utf8 16 = Utf8 ()V17 = Utf8 Code18 = Utf8 LineNumberTable19 = Utf8 LocalVariableTable20 = Utf8 this21 = Utf8 Lcn/itcast/jvm/Test3;22 = Utf8 main23 = Utf8 ([Ljava/lang/String;)V24 = Utf8 args25 = Utf8 [Ljava/lang/String;26 = Utf8 m127 = Utf8 s128 = Utf8 Ljava/lang/String;29 = Utf8 s230 = Utf8 s331 = Utf8 m232 = Utf8 sb33 = Utf8 Ljava/lang/StringBuilder;34 = Utf8 SourceFile35 = Utf8 Test3.java36 = NameAndType #15:#16 // ““:()V37 = Utf8 cn/itcast/jvm/Test338 = NameAndType #26:#16 // m1:()V39 = NameAndType #31:#16 // m2:()V40 = Utf8 12341 = Utf8 45642 = Utf8 java/lang/StringBuilder43 = NameAndType #50:#51 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 44 = NameAndType #52:#53 // toString:()Ljava/lang/String; 45 = Class #54 // java/lang/System46 = NameAndType #55:#56 // out:Ljava/io/PrintStream;47 = Class #57 // java/io/PrintStream48 = NameAndType #58:#59 // println:(Ljava/lang/String;)V #49 = Utf8 java/lang/Object #50 = Utf8 append #51 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder; #52 = Utf8 toString #53 = Utf8 ()Ljava/lang/String; #54 = Utf8 java/lang/System #55 = Utf8 out #56 = Utf8 Ljava/io/PrintStream; #57 = Utf8 java/io/PrintStream #58 = Utf8 println #59 = Utf8 (Ljava/lang/String;)V {public cn.itcast.jvm.Test3();descriptor: ()V flags: ACC_PUBLIC Code:stack=1, locals=1, args_size=1 0: aload_01: invokespecial #1 // Method java/lang/Object.”“:()V4: return LineNumberTable:line 3: 0 LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lcn/itcast/jvm/Test3; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=1, args_size=10: new #2 // class cn/itcast/jvm/Test33: dup4: invokespecial #3 // Method ““:()V7: invokevirtual #4 // Method m1:()V10: new #2 // class cn/itcast/jvm/Test313: dup14: invokespecial #3 // Method ““:()V17: invokevirtual #5 // Method m2:()V20: return LineNumberTable:line 6: 0line 7: 10line 8: 20 LocalVariableTable:Start Length Slot Name Signature0 21 0 args [Ljava/lang/String; public void m1(); descriptor: ()Vflags: ACC_PUBLICCode:stack=2, locals=4, args_size=1 0: ldc #6 // String 1232: astore_13: ldc #7 // String 4565: astore_26: new #8 // classjava/lang/StringBuilder 9: dup10: invokespecial #9 // Method java/lang/StringBuilder.”“:()V13: aload_114: invokevirtual #10 // Method java/lang/StringBuilder.append: (Ljava/lang/String;)Ljava/lang/StringBuilder;17: aload_218: invokevirtual #10 // Method java/lang/StringBuilder.append: (Ljava/lang/String;)Ljava/lang/StringBuilder;21: invokevirtual #11 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;24: astore_325: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream;28: aload_329: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V32: return LineNumberTable:line 11: 0line 12: 3line 13: 6line 14: 25line 15: 32 LocalVariableTable:Start Length Slot Name Signature0 33 0 this Lcn/itcast/jvm/Test3;3 30 1 s1 Ljava/lang/String;6 27 2 s2 Ljava/lang/String;25 8 3 s3 Ljava/lang/String;public void m2(); descriptor: ()V flags: ACC_PUBLIC Code:stack=2, locals=5, args_size=10: ldc #6 // String 1232: astore_13: ldc #7 // String 4565: astore_26: new #8 // class java/lang/StringBuilder9: dup10: invokespecial #9 // Method java/lang/StringBuilder.”“:()V13: astore_314: aload_315: aload_116: invokevirtual #10 // Method java/lang/StringBuilder.append: (Ljava/lang/String;)Ljava/lang/StringBuilder;19: pop20: aload_321: aload_222: invokevirtual #10 // Method java/lang/StringBuilder.append: (Ljava/lang/String;)Ljava/lang/StringBuilder;25: pop26: aload_327: invokevirtual #11 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;30: astore 432: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream;35: aload 437: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V40: return LineNumberTable:line 18: 0line 19: 3 line 20: 6LocalVariableTable:}SourceFile: “Test3.java” 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165从解字节码中可以看出,m1()方法源码中是使用+号拼接,但是在字节码中也被编译成了StringBuilder方式。所以,可以得出结论,字符串拼接,+号和StringBuilder是相等的,效率一样。接下来,我们再看一个案例: package cn.itcast.jvm; public class Test4 {public static void main(String[] args) { new Test4().m1();new Test4().m2();} public void m1(){ String str = “”;for (int i = 0; i < 5; i++) { str = str + i;}System.out.println(str);} public void m2(){StringBuilder sb = new StringBuilder(); for (int i = 0; i < 5; i++) {sb.append(i);}System.out.println(sb.toString());}}12345678910111213141516171819m1() 与 m2() 哪个方法的效率高? 依然是通过字节码的方式进行探究。 Classfile /F:/code/itcast‐jvm/itcast‐jvm‐ test/target/classes/cn/itcast/jvm/Test4.classMD5 checksum f87a55446b8b6cd88b6e54bd5edcc9dc Compiled from “Test4.java”public class cn.itcast.jvm.Test4 minor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPERConstant pool: 1 = Methodref #14.#39 // java/lang/Object.”“: ()V2 = Class #40 // cn/itcast/jvm/Test43 = Methodref #2.#39 // cn/itcast/jvm/Test4.”“:()V 4 = Methodref #2.#41 // cn/itcast/jvm/Test4.m1:()V5 = Methodref #2.#42 // cn/itcast/jvm/Test4.m2:()V6 = String #43 //7 = Class #44 // java/lang/StringBuilder8 = Methodref #7.#39 // java/lang/StringBuilder.”“:()V 9 = Methodref #7.#45 // java/lang/StringBuilder.append: (Ljava/lang/String;)Ljava/lang/StringBuilder;10 = Methodref #7.#46 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;11 = Methodref #7.#47 // java/lang/StringBuilder.toString:()Ljava/lang/String;12 = Fieldref #48.#49 // java/lang/System.out:Ljava/io/PrintStream;13 = Methodref #50.#51 // java/io/PrintStream.println: (Ljava/lang/String;)V14 = Class #52 // java/lang/Object15 = Utf8 16 = Utf8 ()V17 = Utf8 Code18 = Utf8 LineNumberTable19 = Utf8 LocalVariableTable20 = Utf8 this21 = Utf8 Lcn/itcast/jvm/Test4;22 = Utf8 main23 = Utf8 ([Ljava/lang/String;)V24 = Utf8 args25 = Utf8 [Ljava/lang/String;26 = Utf8 m127 = Utf8 i28 = Utf8 I29 = Utf8 str30 = Utf8 Ljava/lang/String;31 = Utf8 StackMapTable32 = Class #53 // java/lang/String33 = Utf8 m234 = Utf8 sb35 = Utf8 Ljava/lang/StringBuilder;36 = Class #44 // java/lang/StringBuilder37 = Utf8 SourceFile38 = Utf8 Test4.java39 = NameAndType #15:#16 // ““:()V40 = Utf8 cn/itcast/jvm/Test441 = NameAndType #26:#16 // m1:()V42 = NameAndType #33:#16 // m2:()V43 = Utf844 = Utf8 java/lang/StringBuilder45 = NameAndType #54:#55 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 46 = NameAndType #54:#56 // append: (I)Ljava/lang/StringBuilder;47 = NameAndType #57:#58 // toString:()Ljava/lang/String; 48 = Class #59 // java/lang/System49 = NameAndType #60:#61 // out:Ljava/io/PrintStream;50 = Class #62 // java/io/PrintStream51 = NameAndType #63:#64 // println:(Ljava/lang/String;)V 52 = Utf8 java/lang/Object53 = Utf8 java/lang/String54 = Utf8 append55 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;56 = Utf8 (I)Ljava/lang/StringBuilder;57 = Utf8 toString58 = Utf8 ()Ljava/lang/String;59 = Utf8 java/lang/System60 = Utf8 out61 = Utf8 Ljava/io/PrintStream;62 = Utf8 java/io/PrintStream63 = Utf8 println64 = Utf8 (Ljava/lang/String;)V{public cn.itcast.jvm.Test4(); descriptor: ()Vflags: ACC_PUBLIC Code:stack=1, locals=1, args_size=1 0: aload_01: invokespecial #1 // Methodjava/lang/Object.”“:()V4: return LineNumberTable:line 3: 0 LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lcn/itcast/jvm/Test4; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=1, args_size=10: new #2 // class cn/itcast/jvm/Test43: dup4: invokespecial #3 // Method ““:()V7: invokevirtual #4 // Method m1:()V10: new #2 // class cn/itcast/jvm/Test413: dup14: invokespecial #3 // Method ““:()V17: invokevirtual #5 // Method m2:()V20: return LineNumberTable:line 6: 0line 7: 10 line 8: 20 LocalVariableTable:Start Length Slot Name Signature0 21 0 args [Ljava/lang/String;public void m1(); descriptor: ()V flags: ACC_PUBLIC Code:stack=2, locals=3, args_size=10: ldc #6 // String2: astore_1 // 将空字符串压入到本地变量表中的下标为1的位置3: iconst_0 // 将数字0压入操作栈顶4: istore_2 // 将栈顶数字0压入到本地变量表中的下标为2的位置5: iload_2 // 将本地变量中下标为2的数字0压入操作栈顶6: iconst_5 // 将数字5压入操作栈顶7: if_icmpge 35 //比较栈顶两int型数值大小,当结果大于等于0时跳 转到35 10: new #7 // class java/lang/StringBuilder13: dup //复制栈顶数值并将复制值压入栈顶(数字5)14: invokespecial #8 // Method java/lang/StringBuilder.”“:()V17: aload_118: invokevirtual #9 // Method java/lang/StringBuilder.append: (Ljava/lang/String;)Ljava/lang/StringBuilder;21: iload_2 //将本地变量中下标为2的数字0压入操作栈顶22: invokevirtual #10 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;25: invokevirtual #11 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;28: astore_129: iinc 2, 132: goto 535: getstatic #12 // Fieldjava/lang/System.out:Ljava/io/PrintStream; 38: aload_139: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V42: return LineNumberTable: line 11: 0line 12: 3line 13: 10line 12: 29line 15: 35line 16: 42LocalVariableTable:Start Length Slot Name Signature5 30 2 i I0 43 0 this Lcn/itcast/jvm/Test4;3 40 1 str Ljava/lang/String;StackMapTable: number_of_entries = 2 frame_type = 253 / append /offset_delta = 5locals = [ class java/lang/String, int ] frame_type = 250 / chop /offset_delta = 29 public void m2(); descriptor: ()V flags: ACC_PUBLIC Code:stack=2, locals=3, args_size=10: new #7 // class java/lang/StringBuilder3: dup4: invokespecial #8 // Method java/lang/StringBuilder.”“:()V7: astore_18: iconst_09: istore_210: iload_211: iconst_512: if_icmpge 2715: aload_116: iload_217: invokevirtual #10 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;20: pop21: iinc 2, 124: goto 1027: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream;30: aload_131: invokevirtual #11 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;34: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V37: return LineNumberTable:line 19: 0line 20: 8line 21: 15line 20: 21line 23: 27line 24: 37 LocalVariableTable:Start Length Slot Name Signature 10 17 2 i I0 38 0 this Lcn/itcast/jvm/Test4;8 30 1 sb Ljava/lang/StringBuilder;StackMapTable: number_of_entries = 2 frame_type = 253 / append /offset_delta = 10locals = [ class java/lang/StringBuilder, int ] frame_type = 250 / chop /offset_delta = 16}SourceFile: “Test4.java” 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195可以看到,m1()方法中的循环体内,每一次循环都会创建StringBuilder对象,效率低于m2()方法。 2.7、小结使用字节码的方式可以很好查看代码底层的执行,从而可以看出哪些实现效率高,哪些 实现效率低。可以更好的对我们的代码做优化。让程序执行效率更高。 3、代码优化优化,不仅仅是在运行环境进行优化,还需要在代码本身做优化,如果代码本身存在性 能问题,那么在其他方面再怎么优化也不可能达到效果最优的。 3.1、尽可能使用局部变量调用方法时传递的参数以及在调用中创建的临时变量都保存在栈中速度较快,其他变 量,如静态变量、实例变量等,都在堆中创建,速度较慢。另外,栈中创建的变量,随 着方法的运行结束,这些内容就没了,不需要额外的垃圾回收。 3.2、尽量减少对变量的重复计算明确一个概念,对方法的调用,即使方法中只有一句语句,也是有消耗的。所以例如下 面的操作: for (int i = 0; i < list.size(); i++){…}12建议替换为: int length = list.size();for (int i = 0, i < length; i++){…} 1234这样,在list.size()很大的时候,就减少了很多的消耗。 3.3、尽量采用懒加载的策略,即在需要的时候才创建String str = “aaa”;if (i == 1){list.add(str);}//建议替换成if (i == 1){String str = “aaa”; list.add(str);}123456783.4、异常不应该用来控制程序流程异常对性能不利。抛出异常首先要创建一个新的对象,Throwable接口的构造函数调用 名为fillInStackTrace()的本地同步方 法,fillInStackTrace()方法检查堆栈,收集调用跟踪信息。只要有异常被抛出,Java虚拟机就必须调整调用堆栈,因为在处理过程中创建 了一个新的对象。异常只能用于错误处理,不应该用来控制程序流程。 3.5、不要将数组声明为public static final因为这毫无意义,这样只是定义了引用为static final,数组的内容还是可以随意改变的, 将数组声明为public更是一个安全漏洞,这意味着这个数组可以被外部类所改变。 3.6、不要创建一些不使用的对象,不要导入一些不使用的类这毫无意义,如果代码中出现”The value of the local variable i is not used”、“The import java.util is never used”,那么请删除这些无用的内容 3.7、程序运行过程中避免使用反射反射是Java提供给用户一个很强大的功能,功能强大往往意味着效率不高。不建议在程序运行过程中使用尤其是频繁使用反射机制,特别是 Method的invoke方法。如果确实有必要,一种建议性的做法是将那些需要通过反射加载的类在项目启动的时候 通过反射实例化出一个对象并放入内存。 3.8、使用数据库连接池和线程池这两个池都是用于重用对象的,前者可以避免频繁地打开和关闭连接,后者可以避免频 繁地创建和销毁线程。 3.9、容器初始化时尽可能指定长度容器初始化时尽可能指定长度,如:new ArrayList<>(10); new HashMap<>(32); 避免容器长度不足时,扩容带来的性能损耗。 3.10、ArrayList随机遍历快,LinkedList添加删除快3.11、使用Entry遍历MapMap map = new HashMap<>();for (Map.Entry entry : map.entrySet()) { String key = entry.getKey();String value = entry.getValue();}1234避免使用这种方式: Map map = new HashMap<>(); for (String key : map.keySet()) {String value = map.get(key);}1233.12、不要手动调用System.gc();3.13、String尽量少用正则表达式正则表达式虽然功能强大,但是其效率较低,除非是有需要,否则尽可能少用。 replace() 不支持正则replaceAll() 支持正则如果仅仅是字符的替换建议使用replace()。 3.14、日志的输出要注意级别// 当 前 的 日 志 级 别 是 error LOGGER.info(“保存出错!” + user);13.15、对资源的close()建议分开操作try{XXX.close();YYY.close();}catch (Exception e){…}// 建议改为try{XXX.close();}catch (Exception e){…}try{YYY.close();}catch (Exception e){…}]]></content>
<categories>
<category>JVM</category>
</categories>
<tags>
<tag>JVM</tag>
</tags>
</entry>
<entry>
<title><![CDATA[架构与优化之JVM优化第02篇垃圾回收]]></title>
<url>%2F2019%2F12%2F20%2F%E6%9E%B6%E6%9E%84%E4%B8%8E%E4%BC%98%E5%8C%96%E4%B9%8BJVM%E4%BC%98%E5%8C%96%E7%AC%AC02%E7%AF%87%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%2F</url>
<content type="text"><![CDATA[0.学习目标 了解什么是垃圾回收 掌握垃圾会回收的常见算法 学习串行、并行、并发、G1垃圾收集器 学习GC日志的可视化查看 1、什么是垃圾回收?程序的运行必然需要申请内存资源,无效的对象资源如果不及时处理就会一直占有内存 资源,最终将导致内存溢出,所以对内存资源的管理是非常重要了。 通俗的理解java对象的这一辈子 我是一个普通的java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后被回收。 GC策略解决了哪些问题? 既然是要进行自动GC,那必然会有相应的策略,而这些策略解决了哪些问题呢,粗略的来说,主要有以下几点。 1、哪些对象可以被回收。( 根搜索算法解决) 2、何时回收这些对象。 3、采用什么样的方式回收。 根搜索算法 由于引用计数算法的缺陷,所以JVM一般会采用一种新的算法,叫做根搜索算法。它的处理方式就是,设立若干种根对象,当任何一个根对象到某一个对象均不可达时,则认为这个对象是可以被回收的。 就拿上图来说,ObjectD和ObjectE是互相关联的,但是由于GC roots到这两个对象不可达,所以最终D和E还是会被当做GC的对象,上图若是采用引用计数法,则A-E五个对象都不会被回收。 说到GC roots(GC根),在JAVA语言中,可以当做GC roots的对象有以下几种: 1、虚拟机栈中的引用的对象。 2、方法区中的类静态属性引用的对象。 3、方法区中的常量引用的对象。 4、本地方法栈中JNI的引用的对象。 第一和第四种都是指的方法的本地变量表,第二种表达的意思比较清晰,第三种主要指的是声明为final的常量值。 HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1,为啥默认会是这个比例,接下来我们会聊到。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。 因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。 在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。 1.1、C/C++语言的垃圾回收在C/C++语言中,没有自动垃圾回收机制,是通过new关键字申请内存资源,通过delete 关键字释放内存资源。 如果,程序员在某些位置没有写delete进行释放,那么申请的对象将一直占用内存资源, 最终可能会导致内存溢出。 1.2、Java语言的垃圾回收为了让程序员更专注于代码的实现,而不用过多的考虑内存释放的问题,所以,在Java语言中,有了自动的垃圾回收机制,也就是我们熟悉的GC。 有了垃圾回收机制后,程序员只需要关心内存的申请即可,内存的释放由系统自动识别 完成。换句话说,自动的垃圾回收的算法就会变得非常重要了,如果因为算法的不合理,导致 内存资源一直没有释放,同样也可能会导致内存溢出的。 当然,除了Java语言,C#、Python等语言也都有自动的垃圾回收机制。 2、垃圾回收的常见算法自动化的管理内存资源,垃圾回收机制必须要有一套算法来进行计算,哪些是有效的对 象,哪些是无效的对象,对于无效的对象就要进行回收处理。 常见的垃圾回收算法有:引用计数法、标记清除法、标记压缩法、复制算法、分代算法 等。 2.1、引用计数法引用计数是历史最悠久的一种算法,最早George E. Collins在1960的时候首次提出,50年后的今天,该算法依然被很多编程语言使用。 2.1.1、原理假设有一个对象A,任何一个对象对A的引用,那么对象A的引用计数器+1,当引用失败 时,对象A的引用计数器就-1,如果对象A的计数器的值为0,就说明对象A没有引用了, 可以被回收。 2.1.2、优缺点优点: 实时性较高,无需等到内存不够的时候,才开始回收,运行时根据对象的计数器是否为0,就可以直接回收。 在垃圾回收过程中,应用无需挂起。如果申请内存时,内存不足,则立刻报outofmember 错误。 区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象。 缺点: 每次对象被引用时,都需要去更新计数器,有一点时间开销。 浪费CPU资源,即使内存够用,仍然在运行时进行计数器的统计。 无法解决循环引用问题。(最大的缺点) 什么是循环引用? 123456789101112131415161718class TestA { public TestB b;}class TestB { public TestA a;}public class Main { public static void main(String[] args) { A a = new A(); B b = new B(); a.b = b; b.a = a; a = null; b = null; }} 虽然a和b都为null,但是由于a和b存在循环引用,这样a和b永远都不会被回收。 123456789101112131415161718192021public class Object { Object field = null; public static void main(String[] args) { Thread thread = new Thread(new Runnable() { public void run() { Object objectA = new Object(); Object objectB = new Object();//1 objectA.field = objectB; objectB.field = objectA;//2 //to do something objectA = null; objectB = null;//3 } }); thread.start(); while (true); } } 这段代码看起来有点刻意为之,但其实在实际编程过程当中,是经常出现的,比如两个一对一关系的数据库对象,各自保持着对方的引用。最后一个无限循环只是为了保持JVM不退出,没什么实际意义。 对于我们现在使用的GC来说,当thread线程运行结束后,会将objectA和objectB全部作为待回收的对象。而如果我们的GC采用上面所说的引用计数算法,则这两个对象永远不会被回收,即便我们在使用后显示的将对象归为空值也毫无作用。 这里LZ大致解释一下,在代码中LZ标注了1、2、3三个数字,当第1个地方的语句执行完以后,两个对象的引用计数全部为1。当第2个地方的语句执行完以后,两个对象的引用计数就全部变成了2。当第3个地方的语句执行完以后,也就是将二者全部归为空值以后,二者的引用计数仍然为1。根据引用计数算法的回收规则,引用计数没有归0的时候是不会被回收的。 2.2、标记清除法(五分钟让你彻底明白标记/清除算法) 首先,我们通过根搜索算法知道,它可以解决我们应该回收哪些对象的问题,但是它显然还不能承担垃圾搜集的重任,因为我们在程序(程序也就是指我们运行在JVM上的JAVA程序)运行期间如果想进行垃圾回收,就必须让GC线程与程序当中的线程互相配合,才能在不影响程序运行的前提下,顺利的将垃圾进行回收。 为了达到这个目的,标记/清除算法就应运而生了。它的做法是当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被成为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。 标记清除算法,是将垃圾回收分为2个阶段,分别是标记和清除。 标记:从根节点开始标记引用的对象。( 标记的过程其实就是,遍历所有的GC Roots,然后将所有GC Roots可达的对象标记为存活的对象。 ) 清除:未被标记引用的对象就是垃圾对象,可以被清理。( 清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。 ) 通俗的话解释一下标记/清除算法: 就是当程序运行期间,若可以使用的内存被耗尽的时候,GC线程就会被触发并将程序暂停,随后将依旧存活的对象标记一遍,最终再将堆中所有没被标记的对象全部清除掉,接下来便让程序恢复运行。 这张图代表的是程序运行期间所有对象的状态,它们的标志位全部是0(也就是未标记, 以下默认0就是未标记,1为已标记),假设这会儿有效内存空间耗尽了,JVM将会停止应用程序的运行并开启GC线程,然后开始进行标记工作,按照根(root)搜索算法,标记完以后, 对象的状态如下图。 可以看到,按照根(root)搜索算法,所有从root对象可达的对象就被标记为了存活的对象,此时已经完成了第一阶段标记。接下来,就要执行第二阶段清除了,那么清除完以后,剩下的对象以及对象的状态如下图所示。 可以看到,没有被标记的对象将会回收清除掉,而被标记的对象将会留下,并且会将标记位重新归0。接下来就不用说了,唤醒停止的程序线程,让程序继续运行即可。 为什么非要停止程序的运行呢? 这个其实也不难理解,LZ举个最简单的例子,假设我们的程序与GC线程是一起运行的,各位试想这样一种场景。 假设我们刚标记完图中最右边的那个对象,暂且记为A,结果此时在程序当中又new了一个新对象B,且A对象可以到达B对象。但是由于此时A对象已经标记结束,B对象此时的标记位依然是0,因为它错过了标记阶段。因此当接下来轮到清除阶段的时候,新对象B将会被苦逼的清除掉。如此一来,不难想象结果,GC线程将会导致程序无法正常工作。 上面的结果当然令人无法接受,我们刚new了一个对象,结果经过一次GC,忽然变成null了,这还怎么玩? 2.2.2、优缺点可以看到,标记清除算法解决了引用计数算法中的循环引用的问题,没有从root节点引 用的对象都会被回收。 同样,标记清除算法也是有缺点的:1、效率较低,标记和清除两个动作都需要遍历所有的对象,并且在GC时,需要停止应用程序,对于交互性要求比较高的应用而言这个体验是非常差的。 (递归与全堆对象遍历),而且在进行GC的时候,需要停止应用程序,这会导致用户体验非常差劲,尤其对于交互式的应用程序来说简直是无法接受。试想一下,如果你玩一个网站,这个网站一个小时就挂五分钟,你还玩吗? 2、 第二点主要的缺点,则是这种方式清理出来的空闲内存是不连续的通过标记清除算法清理出来的内存,碎片化较为严重,因为被回收的对象可能存在于内存的各个角落,所以清理出来的内存是不连贯的。 第二点主要的缺点,则是这种方式清理出来的空闲内存是不连续的,这点不难理解,我们的死亡对象都是随即的出现在内存的各个角落的,现在把它们清除之后,内存的布局自然会乱七八糟。而为了应付这一点,JVM就不得不维持一个内存的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找。 2.3、标记压缩算法标记压缩算法是在标记清除算法的基础之上,做了优化改进的算法。和标记清除算法一 样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的清理未标 记的对象,而是将存活的对象压缩到内存的一端,然后清理边界以外的垃圾,从而解决 了碎片化的问题。 2.3.1、原理 2.3.2、优缺点优缺点同标记清除算法,解决了标记清除算法的碎片化的问题,同时,标记压缩算法多了一步,对象移动内存位置的步骤,其效率也有有一定的影响。 2.4、复制算法复制算法的核心就是,将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收。如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之,则不适合。 2.4.1、JVM中年轻代内存空间 1.在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor 区“To”是空的。 2.紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍 存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过- XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对 象会被复制到“To”区域。 3.经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他 们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。 4.GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。 2.4.2、优缺点优点: 在垃圾对象多的情况下,效率较高清理后,内存无碎片缺点: 在垃圾对象少的情况下,不适用,如:老年代内存分配的2块内存空间,在同一个时刻,只能使用一半,内存使用率较低 2.5、分代算法前面介绍了多种回收算法,每一种算法都有自己的优点也有缺点,谁都不能替代谁,所 以根据垃圾回收对象的特点进行选择,才是明智的选择。分代算法其实就是这样的,根据回收对象的特点进行选择,在jvm中,年轻代适合使用复 制算法,老年代适合使用标记清除或标记压缩算法。 3、垃圾收集器以及内存分配前面我们讲了垃圾回收的算法,还需要有具体的实现,在jvm中,实现了多种垃圾收集 器,包括:串行垃圾收集器、并行垃圾收集器、CMS(并发)垃圾收集器、G1垃圾收集器,接下来,我们一个个的了解学习。 3.1、串行垃圾收集器串行垃圾收集器,是指使用单线程进行垃圾回收,垃圾回收时,只有一个线程在工作, 并且java应用中的所有线程都要暂停,等待垃圾回收的完成。这种现象称之为STW(Stop-The-World)。对于交互性较强的应用而言,这种垃圾收集器是不能够接受的。一般在Javaweb应用中是不会采用该收集器的。3.1.1、编写测试代码 package cn.itcast.jvm; import java.util.ArrayList; import java.util.List; import java.util.Properties; import java.util.Random; public class TestGC { public static void main(String[] args) throws Exception { List list = new ArrayList();while (true){int sleep = new Random().nextInt(100); if(System.currentTimeMillis() % 2 ==0){list.clear();}else{for (int i = 0; i < 10000; i++) {Properties properties = new Properties(); properties.put(“key_”+i, “value_” +System.currentTimeMillis() + i);list.add(properties);}} // System.out.println(“list大小为:” + list.size()); Thread.sleep(sleep);}}} 3.1.2、设置垃圾回收为串行收集器在程序运行参数中添加2个参数,如下:-XX:+UseSerialGC指定年轻代和老年代都使用串行垃圾收集器-XX:+PrintGCDetails打印垃圾回收的详细信息 为了测试GC,将堆的初始和最大内存都设置为16M‐XX:+UseSerialGC ‐XX:+PrintGCDetails ‐Xms16m ‐Xmx16m 启动程序,可以看到下面信息: [GC (Allocation Failure) [DefNew: 4416K‐>512K(4928K), 0.0046102 secs]4416K‐>1973K(15872K), 0.0046533 secs] [Times: user=0.00 sys=0.00,real=0.00 secs] [Full GC (Allocation Failure) [Tenured: 10944K‐>3107K(10944K), 0.0085637secs] 15871K‐>3107K(15872K), [Metaspace: 3496K‐>3496K(1056768K)],0.0085974 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] GC日志信息解读:年轻代的内存GC前后的大小:DefNew表示使用的是串行垃圾收集器。4416K->512K(4928K)表示,年轻代GC前,占有4416K内存,GC后,占有512K内存,总大小4928K 0.0046102 secs表示,GC所用的时间,单位为毫秒。4416K->1973K(15872K)表示,GC前,堆内存占有4416K,GC后,占有1973K,总大小为15872K Full GC表示,内存空间全部进行GC 3.2、并行垃圾收集器并行垃圾收集器在串行垃圾收集器的基础之上做了改进,将单线程改为了多线程进行垃 圾回收,这样可以缩短垃圾回收的时间。(这里是指,并行能力较强的机器)当然了,并行垃圾收集器在收集的过程中也会暂停应用程序,这个和串行垃圾回收器是 一样的,只是并行执行,速度更快些,暂停的时间更短一些。 3.2.1、ParNew垃圾收集器ParNew垃圾收集器是工作在年轻代上的,只是将串行的垃圾收集器改为了并行。通过-XX:+UseParNewGC参数设置年轻代使用ParNew回收器,老年代使用的依然是串行收集器。 测试: 参数‐XX:+UseParNewGC ‐XX:+PrintGCDetails ‐Xms16m ‐Xmx16m 打印出的信息[GC (Allocation Failure) [ParNew: 4416K‐>512K(4928K), 0.0032106 secs] 4416K‐>1988K(15872K), 0.0032697 secs] [Times: user=0.00 sys=0.00,real=0.00 secs] 由以上信息可以看出, 致。 使用的是ParNew收集器。其他信息和串行收集器一 3.2.2、ParallelGC垃圾收集器ParallelGC收集器工作机制和ParNewGC收集器一样,只是在此基础之上,新增了两个和 系统吞吐量相关的参数,使得其使用起来更加的灵活和高效。 相关参数如下: -XX:+UseParallelGC年轻代使用ParallelGC垃圾回收器,老年代使用串行回收器。-XX:+UseParallelOldGC年轻代使用ParallelGC垃圾回收器,老年代使用ParallelOldGC垃圾回收器。-XX:MaxGCPauseMillis设置最大的垃圾收集时的停顿时间,单位为毫秒需要注意的时,ParallelGC为了达到设置的停顿时间,可能会调整堆大小或其他 的参数,如果堆的大小设置的较小,就会导致GC工作变得很频繁,反而可能会 影响到性能。该参数使用需谨慎。-XX:GCTimeRatio设置垃圾回收时间占程序运行时间的百分比,公式为1/(1+n)。它的值为0~100之间的数字,默认值为99,也就是垃圾回收时间不能超过1%-XX:UseAdaptiveSizePolicy自适应GC模式,垃圾回收器将自动调整年轻代、老年代等参数,达到吞吐量、堆大小、停顿时间之间的平衡。一般用于,手动调整参数比较困难的场景,让收集器自动进行调整。测试: 参数‐XX:+UseParallelGC ‐XX:+UseParallelOldGC ‐XX:MaxGCPauseMillis=100 ‐ XX:+PrintGCDetails ‐Xms16m ‐Xmx16m 打印的信息[GC (Allocation Failure) [PSYoungGen: 4096K‐>480K(4608K)] 4096K‐ 1840K(15872K), 0.0034307 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (Ergonomics) [PSYoungGen: 505K‐>0K(4608K)] [ParOldGen: 10332K‐ 10751K(11264K)] 10837K‐>10751K(15872K), [Metaspace: 3491K‐3491K(1056768K)], 0.0793622 secs] [Times: user=0.13 sys=0.00, real=0.08secs] 有以上信息可以看出,年轻代和老年代都使用了ParallelGC垃圾回收器。 3.3、CMS垃圾收集器CMS全称 Concurrent Mark Sweep,是一款并发的、使用标记-清除算法的垃圾回收器, 该回收器是针对老年代垃圾回收的,通过参数-XX:+UseConcMarkSweepGC进行设置。CMS垃圾回收器的执行过程如下:初始化标记(CMS-initial-mark) ,标记root,会导致stw;并发标记(CMS-concurrent-mark),与用户线程同时运行;预清理(CMS-concurrent-preclean),与用户线程同时运行; 重新标记(CMS-remark) ,会导致stw;并发清除(CMS-concurrent-sweep),与用户线程同时运行;调整堆大小,设置CMS在清理之后进行内存压缩,目的是清理内存中的碎片;并发重置状态等待下次CMS的触发(CMS-concurrent-reset),与用户线程同时运行;3.3.1、测试 设置启动参数‐XX:+UseConcMarkSweepGC ‐XX:+PrintGCDetails ‐Xms16m ‐Xmx16m 运行日志[GC (Allocation Failure) [ParNew: 4926K‐>512K(4928K), 0.0041843 secs] 9424K‐>6736K(15872K), 0.0042168 secs] [Times: user=0.00 sys=0.00,real=0.00 secs] 第一步,初始标记[GC (CMS Initial Mark) [1 CMS‐initial‐mark: 6224K(10944K)] 6824K(15872K), 0.0004209 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 第二步,并发标记[CMS‐concurrent‐mark‐start][CMS‐concurrent‐mark: 0.002/0.002 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 第三步,预处理[CMS‐concurrent‐preclean‐start][CMS‐concurrent‐preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 第四步,重新标记[GC (CMS Final Remark) [YG occupancy: 1657 K (4928 K)][Rescan (parallel) , 0.0005811 secs][weak refs processing, 0.0000136 secs][class unloading, 0.0003671 secs][scrub symbol table, 0.0006813 secs][scrub string table, 0.0001216 secs][1 CMS‐remark: 6224K(10944K)] 7881K(15872K), 0.0018324secs] [Times: user=0.00 sys=0.00, real=0.00 secs] #第五步,并发清理[CMS‐concurrent‐sweep‐start][CMS‐concurrent‐sweep: 0.004/0.004 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 第六步,重置[CMS‐concurrent‐reset‐start][CMS‐concurrent‐reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 由以上日志信息,可以看出CMS执行的过程。 3.4、G1垃圾收集器(重点)G1垃圾收集器是在jdk1.7中正式使用的全新的垃圾收集器,oracle官方计划在jdk9中将 G1变成默认的垃圾收集器,以替代CMS。G1的设计原则就是简化JVM性能调优,开发人员只需要简单的三步即可完成调优:1.第一步,开启G1垃圾收集器2.第二步,设置堆的最大内存3.第三步,设置最大的停顿时间G1中提供了三种模式垃圾回收模式,Young GC、Mixed GC 和 Full GC,在不同的条件下被触发。 3.4.1、原理G1垃圾收集器相对比其他收集器而言,最大的区别在于它取消了年轻代、老年代的物理 划分,取而代之的是将堆划分为若干个区域(Region),这些区域中包含了有逻辑上的 年轻代、老年代区域。 这样做的好处就是,我们再也不用单独的空间对每个代进行设置了,不用担心每个代内 存是否足够。在G1划分的区域中,年轻代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间,G1收集器通过将对象从一个区域复制到另外一个区 域,完成了清理工作。 这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有cms内存碎片问题的存在了。在G1中,有一种特殊的区域,叫Humongous区域。如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在老年代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。3.4.2、Young GCYoung GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分 数据会直接晋升到年老代空间。Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。 最终Eden空间的数据为空,GC停止工作,应用线程继续执行。 3.4.2.1、Remembered Set(已记忆集合)在GC年轻代的对象时,我们如何找到年轻代中对象的根对象呢?根对象可能是在年轻代中,也可以在老年代中,那么老年代中的所有对象都是根么? 如果全量扫描老年代,那么这样扫描下来会耗费大量的时间。于是,G1引进了RSet的概念。它的全称是Remembered Set,其作用是跟踪指向某个堆内的对象引用。每个Region初始化时,会初始化一个RSet,该集合用来记录并跟踪其它Region指向该Region中对象的引用,每个Region默认按照512Kb划分成多个Card,所以RSet需要记录 的东西应该是 xx Region的 xx Card。3.4.3、Mixed GC当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即Mixed GC,该算法并不是一个Old GC,除了回收整个Young Region,还会回收一部分的Old Region,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些old region进行收集,从而可以对垃圾回收的耗时时间进行控制。也要注意的是Mixed GC 并不是 Full GC。MixedGC什么时候触发? 由参数 -XX:InitiatingHeapOccupancyPercent=n 决定。默认:45%,该参数的意思是:当老年代大小占整个堆大小百分比达到该阀值时触发。它的GC步骤分2步:1.全局并发标记(global concurrent marking)2.拷贝存活对象(evacuation)3.4.3.1、全局并发标记全局并发标记,执行过程分为五个步骤:初始标记(initial mark,STW)标记从根节点直接可达的对象,这个阶段会执行一次年轻代GC,会产生全局停顿。根区域扫描(root region scan)G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。并发标记(Concurrent Marking)G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行, 可以被 STW 年轻代垃圾回收中断。重新标记(Remark,STW)该阶段是 STW 回收,因为程序在运行,针对上一次的标记进行修正。清除垃圾(Cleanup,STW)清点和重置标记状态,该阶段会STW,这个阶段并不会实际上去做垃圾的收集, 等待evacuation阶段来回收。3.4.3.2、拷贝存活对象Evacuation阶段是全暂停的。该阶段把一部分Region里的活对象拷贝到另一部分Region中,从而实现垃圾的回收清理。 3.4.4、G1收集器相关参数-XX:+UseG1GC使用 G1 垃圾收集器-XX:MaxGCPauseMillis设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到),默认值是 200 毫秒。-XX:G1HeapRegionSize=n设置的 G1 区域的大小。值是 2 的幂,范围是 1 MB 到 32 MB 之间。目标是根据最小的 Java 堆大小划分出约 2048 个区域。默认是堆内存的1/2000。-XX:ParallelGCThreads=n设置 STW 工作线程数的值。将 n 的值设置为逻辑处理器的数量。n 的值与逻辑处理器的数量相同,最多为 8。-XX:ConcGCThreads=n设置并行标记的线程数。将 n 设置为并行垃圾回收线程数 (ParallelGCThreads)的 1/4 左右。-XX:InitiatingHeapOccupancyPercent=n设置触发标记周期的 Java 堆占用率阈值。默认占用率是整个 Java 堆的 45%。3.4.5、测试 ‐XX:+UseG1GC ‐XX:MaxGCPauseMillis=100 ‐XX:+PrintGCDetails ‐Xmx256m 日志[GC pause (G1 Evacuation Pause) (young), 0.0044882 secs] [Parallel Time: 3.7 ms, GC Workers: 3][GC Worker Start (ms): Min: 14763.7, Avg: 14763.8, Max: 14763.8, Diff: 0.1] 扫描根节点[Ext Root Scanning (ms): Min: 0.2, Avg: 0.3, Max: 0.3, Diff: 0.1,Sum: 0.8] 更新RS区域所消耗的时间[Update RS (ms): Min: 1.8, Avg: 1.9, Max: 1.9, Diff: 0.2, Sum: 5.6][Processed Buffers: Min: 1, Avg: 1.7, Max: 3, Diff: 2, Sum: 5][Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0][Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0,Sum: 0.0] 对象拷贝[Object Copy (ms): Min: 1.1, Avg: 1.2, Max: 1.3, Diff: 0.2, Sum: 3.6] 0.2] 3] [Termination (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: [Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0][GC Worker Total (ms): Min: 3.4, Avg: 3.4, Max: 3.5, Diff: 0.1,Sum: 10.3][GC Worker End (ms): Min: 14767.2, Avg: 14767.2, Max: 14767.3,Diff: 0.1][Code Root Fixup: 0.0 ms] [Code Root Purge: 0.0 ms][Clear CT: 0.0 ms] #清空CardTable[Other: 0.7 ms][Choose CSet: 0.0 ms] #选取CSet[Ref Proc: 0.5 ms] #弱引用、软引用的处理耗时[Ref Enq: 0.0 ms] #弱引用、软引用的入队耗时[Redirty Cards: 0.0 ms][Humongous Register: 0.0 ms] #大对象区域注册耗时[Humongous Reclaim: 0.0 ms] #大对象区域回收耗时 [Free CSet: 0.0 ms][Eden: 7168.0K(7168.0K)‐>0.0B(13.0M) Survivors: 2048.0K‐>2048.0K Heap: 55.5M(192.0M)‐>48.5M(192.0M)] #年轻代的大小统计[Times: user=0.00 sys=0.00, real=0.00 secs] 3.4.6、对于G1垃圾收集器优化建议 年轻代大小避免使用 -Xmn 选项或 -XX:NewRatio 等其他相关选项显式设置年轻代大小。固定年轻代的大小会覆盖暂停时间目标。 暂停时间目标不要太过严苛G1 GC 的吞吐量目标是 90% 的应用程序时间和 10%的垃圾回收时间。评估 G1 GC 的吞吐量时,暂停时间目标不要太严苛。目标太过严苛表示您愿意承受更多的垃圾回收开销,而这会直接影响到吞吐量。 4、可视化GC日志分析工具4.1、GC日志输出参数前面通过-XX:+PrintGCDetails可以对GC日志进行打印,我们就可以在控制台查看,这样 虽然可以查看GC的信息,但是并不直观,可以借助于第三方的GC日志分析工具进行查 看。 在日志打印输出涉及到的参数如下: ‐XX:+PrintGC 输出GC日志‐XX:+PrintGCDetails 输出GC的详细日志‐XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)‐XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013‐05‐ 04T21:53:59.234+0800)‐XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息‐Xloggc:../logs/gc.log 日志文件的输出路径 测试: ‐XX:+UseG1GC ‐XX:MaxGCPauseMillis=100 ‐Xmx256m ‐XX:+PrintGCDetails ‐ XX:+PrintGCTimeStamps ‐XX:+PrintGCDateStamps ‐XX:+PrintHeapAtGC ‐ Xloggc:F://test//gc.log1运行后就可以在E盘下生成gc.log文件。如下: Java HotSpot(TM) 64‐Bit Server VM (25.144‐b01) for windows‐amd64 JRE (1.8.0_144‐b01), built on Jul 21 2017 21:57:33 by “java_re” with MS VC++ 10.0 (VS2010)Memory: 4k page, physical 12582392k(1939600k free), swap 17300984k(5567740k free)CommandLine flags: ‐XX:InitialHeapSize=201318272 ‐XX:MaxGCPauseMillis=100‐XX:MaxHeapSize=268435456 ‐XX:+PrintGC ‐XX:+PrintGCDateStamps ‐ XX:+PrintGCDetails ‐XX:+PrintGCTimeStamps ‐XX:+PrintHeapAtGC ‐ XX:+UseCompressedClassPointers ‐XX:+UseCompressedOops ‐XX:+UseG1GC ‐XX:‐ UseLargePagesIndividualAllocation{Heap before GC invocations=0 (full 0):garbage‐first heap total 196608K, used 9216K [0x00000000f0000000, 0x00000000f0100600, 0x0000000100000000)region size 1024K, 9 young (9216K), 0 survivors (0K)Metaspace used 3491K, capacity 4500K, committed 4864K, reserved 1056768Kclass space used 381K, capacity 388K, committed 512K, reserved 1048576K2018‐09‐24T23:06:02.230+0800: 0.379: [GC pause (G1 Evacuation Pause) (young), 0.0031038 secs][Parallel Time: 2.8 ms, GC Workers: 3][GC Worker Start (ms): Min: 378.6, Avg: 378.8, Max: 379.0, Diff:0.3][Ext Root Scanning (ms): Min: 0.0, Avg: 0.4, Max: 0.8, Diff: 0.8,Sum: 1.3][Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0][Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0][Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0][Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1,Sum: 0.1][Object Copy (ms): Min: 1.8, Avg: 1.9, Max: 1.9, Diff: 0.1, Sum: 5.6] 0.0] 3] [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: [Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: [GC Worker Other (ms): Min: 0.0, Avg: 0.2, Max: 0.6, Diff: 0.6, Sum: 0.6] [GC Worker Total (ms): Min: 2.4, Avg: 2.5, Max: 2.7, Diff: 0.3,Sum: 7.6][GC Worker End (ms): Min: 381.4, Avg: 381.4, Max: 381.4, Diff: 0.0] [Code Root Fixup: 0.0 ms][Code Root Purge: 0.0 ms] [Clear CT: 0.0 ms] [Other: 0.2 ms][Choose CSet: 0.0 ms] [Ref Proc: 0.1 ms] [Ref Enq: 0.0 ms][Redirty Cards: 0.0 ms] [Humongous Register: 0.0 ms] [Humongous Reclaim: 0.0 ms] [Free CSet: 0.0 ms][Eden: 9216.0K(9216.0K)‐>0.0B(7168.0K) Survivors: 0.0B‐>2048.0K Heap: 9216.0K(192.0M)‐>1888.0K(192.0M)]Heap after GC invocations=1 (full 0):garbage‐first heap total 196608K, used 1888K [0x00000000f0000000, 0x00000000f0100600, 0x0000000100000000)region size 1024K, 2 young (2048K), 2 survivors (2048K)Metaspace used 3491K, capacity 4500K, committed 4864K, reserved 1056768Kclass space used 381K, capacity 388K, committed 512K, reserved 1048576K}[Times: user=0.00 sys=0.00, real=0.00 secs]{Heap before GC invocations=1 (full 0):garbage‐first heap total 196608K, used 9056K [0x00000000f0000000, 0x00000000f0100600, 0x0000000100000000)region size 1024K, 9 young (9216K), 2 survivors (2048K)Metaspace used 3492K, capacity 4500K, committed 4864K, reserved 1056768Kclass space used 381K, capacity 388K, committed 512K, reserved 1048576K2018‐09‐24T23:06:02.310+0800: 0.458: [GC pause (G1 Evacuation Pause) (young), 0.0070126 secs]。。。。。。。。。。。。。。。。。。。 4.2、GC Easy 可视化工具GC Easy是一款在线的可视化工具,易用、功能强大,网站: http://gceasy.io/上传后,点击“Analyze”按钮,即可查看报告]]></content>
<categories>
<category>JVM</category>
</categories>
<tags>
<tag>JVM</tag>
</tags>
</entry>
<entry>
<title><![CDATA[架构与优化之JVM优化第01篇JVM基础]]></title>
<url>%2F2019%2F12%2F19%2F%E6%9E%B6%E6%9E%84%E4%B8%8E%E4%BC%98%E5%8C%96%E4%B9%8BJVM%E4%BC%98%E5%8C%96%E7%AC%AC01%E7%AF%87JVM%E5%9F%BA%E7%A1%80%2F</url>
<content type="text"><![CDATA[0.学习目标 了解下我们为什么要学习JVM优化 掌握jvm的运行参数以及参数的设置 掌握jvm的内存模型(堆内存) 掌握jamp命令的使用以及通过MAT工具进行分析 掌握定位分析内存溢出的方法 掌握jstack命令的使用 掌握VisualJVM工具的使用 1、我们为什么要对jvm做优化?在本地开发环境中我们很少会遇到需要对jvm进行优化的需求,但是到了生产环境,我们 可能将有下面的需求: 运行的应用“卡住了”,日志不输出,程序没有反应 服务器的CPU负载突然升高 在多线程应用下,如何分配线程的数量? …… 我们将对jvm有更深入的学习,我们不仅要让程序能跑起来,而且是可以 跑的更快!可以分析解决在生产环境中所遇到的各种“棘手”的问题。 2、jvm的运行参数在jvm中有很多的参数可以进行设置,这样可以让jvm在各种环境中都能够高效的运行。 绝大部分的参数保持默认即可。 2.1、三种参数类型jvm的参数类型分为三类,分别是: 标准参数 -help -version -X参数 (非标准参数) -Xint -Xcomp -XX参数(使用率较高) -XX:newSize -XX:+UseSerialGC 2.2、标准参数 jvm的标准参数,一般都是很稳定的,在未来的JVM版本中不会改变,可以使用java -help检索出所有的标准参数。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455[root@node01 ~]# java ‐help用法: java [‐options] class [args...](执行类)或 java [‐options] ‐jar jarfile [args...] (执行 jar 文件)其中选项包括:‐d32 使用 32 位数据模型 (如果可用)‐d64 使用 64 位数据模型 (如果可用)‐server 选择 "server" VM默认 VM 是 server,因为您是在服务器类计算机上运行。‐cp <目录和 zip/jar 文件的类搜索路径>‐classpath <目录和 zip/jar 文件的类搜索路径>用 : 分隔的目录, JAR 档案和 ZIP 档案列表, 用于搜索类文件。‐D<名称>=<值>设置系统属性‐verbose:[class|gc|jni]启用详细输出‐version 输出产品版本并退出‐version:<值>警告: 此功能已过时, 将在未来发行版中删除。需要指定的版本才能运行‐showversion 输出产品版本并继续‐jre‐restrict‐search | ‐no‐jre‐restrict‐search警告: 此功能已过时, 将在未来发行版中删除。在版本搜索中包括/排除用户专用 JRE‐? ‐help 输出此帮助消息‐X 输出非标准选项的帮助‐ea[:<packagename>...|:<classname>]‐enableassertions[:<packagename>...|:<classname>]按指定的粒度启用断言‐da[:<packagename>...|:<classname>]‐disableassertions[:<packagename>...|:<classname>]禁用具有指定粒度的断言‐esa | ‐enablesystemassertions启用系统断言‐dsa | ‐disablesystemassertions禁用系统断言‐agentlib:<libname>[=<选项>]加载本机代理库 <libname>, 例如 ‐agentlib:hprof另请参阅 ‐agentlib:jdwp=help 和 ‐agentlib:hprof=help‐agentpath:<pathname>[=<选项>]按完整路径名加载本机代理库‐javaagent:<jarpath>[=<选项>]加载 Java 编程语言代理, 请参阅 java.lang.instrument‐splash:<imagepath>使用指定的图像显示启动屏幕 2.2.1、实战 实战1:查看jvm版本 123456[root@node01 ~]# java ‐versionjava version "1.8.0_141"Java(TM) SE Runtime Environment (build 1.8.0_141‐b15)Java HotSpot(TM) 64‐Bit Server VM (build 25.141‐b15, mixed mode)# ‐showversion参数是表示,先打印版本信息,再执行后面的命令,在调试时非常有用,后面会使用到。 实战2:通过-D设置系统属性参数 12345678910111213public class TestJVM { public static void main(String[] args) { String str = System.getProperty("str"); if(str == null){ System.out.println("itcast"); }else{ System.out.println(str); } System.gc(); }} 进行编译、测试: 12345678#编译[root@node01 test]# javac TestJVM.java#测试[root@node01 test]# java TestJVM itcast[root@node01 test]# java ‐Dstr=123 TestJVM123 2.2.2、-server与-client参数可以通过-server或-client设置jvm的运行参数。 它们的区别是Server VM的初始堆空间会大一些,默认使用的是并行垃圾回收器,启动慢运行快。 Client VM相对来讲会保守一些,初始堆空间会小一些,使用串行的垃圾回收器,它的目标是为了让JVM的启动速度更快,但运行速度会比Serverm模式慢些。 JVM在启动的时候会根据硬件和操作系统自动选择使用Server还是Client类型的 JVM。 32位操作系统 如果是Windows系统,不论硬件配置如何,都默认使用Client类型的JVM。 如果是其他操作系统上,机器配置有2GB以上的内存同时有2个以上CPU的话默认使用server模式,否则使用client模式。 64位操作系统 只有server类型,不支持client类型。 测试: 1234567891011[root@node01 test]# java ‐client ‐showversion TestJVMjava version "1.8.0_141"Java(TM) SE Runtime Environment (build 1.8.0_141‐b15)Java HotSpot(TM) 64‐Bit Server VM (build 25.141‐b15, mixed mode)itcast[root@node01 test]# java ‐server ‐showversion TestJVMjava version "1.8.0_141"Java(TM) SE Runtime Environment (build 1.8.0_141‐b15)Java HotSpot(TM) 64‐Bit Server VM (build 25.141‐b15, mixed mode)itcast#由于机器是64位系统,所以不支持client模式 2.3、-X参数jvm的-X参数是非标准参数,在不同版本的jvm中,参数可能会有所不同,可以通过java - X查看非标准参数。 12345678910111213141516171819202122232425262728293031323334[root@node01 test]# java ‐X‐Xmixed 混合模式执行 (默认)‐Xint 仅解释模式执行‐Xbootclasspath:<用 : 分隔的目录和 zip/jar 文件>设置搜索路径以引导类和资源‐Xbootclasspath/a:<用 : 分隔的目录和 zip/jar 文件>附加在引导类路径末尾‐Xbootclasspath/p:<用 : 分隔的目录和 zip/jar 文件>置于引导类路径之前‐Xdiag 显示附加诊断消息‐Xnoclassgc 禁用类垃圾收集‐Xincgc 启用增量垃圾收集‐Xloggc:<file> 将 GC 状态记录在文件中 (带时间戳)‐Xbatch 禁用后台编译‐Xms<size> 设置初始 Java 堆大小‐Xmx<size> 设置最大 Java 堆大小‐Xss<size> 设置 Java 线程堆栈大小‐Xprof 输出 cpu 配置文件数据‐Xfuture 启用最严格的检查, 预期将来的默认值‐Xrs 减少 Java/VM 对操作系统信号的使用 (请参阅文档)‐Xcheck:jni 对 JNI 函数执行其他检查‐Xshare:off 不尝试使用共享类数据‐Xshare:auto 在可能的情况下使用共享类数据 (默认)‐Xshare:on 要求使用共享类数据, 否则将失败。‐XshowSettings 显示所有设置并继续‐XshowSettings:all显示所有设置并继续‐XshowSettings:vm 显示所有与 vm 相关的设置并继续‐XshowSettings:properties显示所有属性设置并继续‐XshowSettings:locale显示所有与区域设置相关的设置并继续‐X 选项是非标准选项, 如有更改, 恕不另行通知。 2.3.1、-Xint、-Xcomp、-Xmixed 在解释模式(interpreted mode)下,-Xint标记会强制JVM执行所有的字节码,当然这会降低运行速度,通常低10倍或更多。 -Xcomp参数与它(-Xint)正好相反,JVM在第一次使用时会把所有的字节码编译成本地代码,从而带来最大程度的优化。 然而,很多应用在使用-Xcomp也会有一些性能损失,当然这比使用-Xint损失的少,原因是— xcomp没有让JVM启用JIT编译器的全部功能。JIT编译器可以对是否需要编译做判断,如果所有代码都进行编译的话,对于一些只执行一次的代码就没有意义了。 -Xmixed是混合模式,将解释模式与编译模式进行混合使用,由jvm自己决定,这是jvm默认的模式,也是推荐使用的模式。 示例:强制设置运行模式 123456789101112131415161718#强制设置为解释模式[root@node01 test]# java ‐showversion ‐Xint TestJVM java version "1.8.0_141"Java(TM) SE Runtime Environment (build 1.8.0_141‐b15)Java HotSpot(TM) 64‐Bit Server VM (build 25.141‐b15, interpreted mode) itcast#强制设置为编译模式[root@node01 test]# java ‐showversion ‐Xcomp TestJVM java version "1.8.0_141"Java(TM) SE Runtime Environment (build 1.8.0_141‐b15)Java HotSpot(TM) 64‐Bit Server VM (build 25.141‐b15, compiled mode)itcast#注意:编译模式下,第一次执行会比解释模式下执行慢一些,注意观察。#默认的混合模式[root@node01 test]# java ‐showversion TestJVM java version "1.8.0_141"Java(TM) SE Runtime Environment (build 1.8.0_141‐b15)Java HotSpot(TM) 64‐Bit Server VM (build 25.141‐b15, mixed mode) itcast 2.4、-XX参数-XX参数也是非标准参数,主要用于jvm的调优和debug操作。-XX参数的使用有2种方式,一种是boolean类型,一种是非boolean类型: boolean类型格式:-XX:[±]如:-XX:+DisableExplicitGC 表示禁用手动调用gc操作,也就是说调用System.gc()无效非boolean类型格式:-XX:如:-XX:NewRatio=1 表示新生代和老年代的比值用法: 123456[root@node01 test]# java ‐showversion ‐XX:+DisableExplicitGC TestJVMjava version "1.8.0_141"Java(TM) SE Runtime Environment (build 1.8.0_141‐b15)Java HotSpot(TM) 64‐Bit Server VM (build 25.141‐b15, mixed mode)itcast 2.5、-Xms与-Xmx参数Xms与-Xmx分别是设置jvm的堆内存的初始大小和最大大小。-Xmx2048m:等价于-XX:MaxHeapSize,设置JVM最大堆内存为2048M。-Xms512m:等价于-XX:InitialHeapSize,设置JVM初始堆内存为512M。适当的调整jvm的内存大小,可以充分利用服务器资源,让程序跑的更快。示例:[root@node01 test]# java ‐Xms512m ‐Xmx2048m TestJVM itcast 123[root@node01 test]# java ‐Xms512m ‐Xmx2048m TestJVM itcast 2.6、查看jvm的运行参数有些时候我们需要查看jvm的运行参数,这个需求可能会存在2种情况: 第一,运行java命令时打印出运行参数; 第二,查看正在运行的java进程的参数; 2.6.1、运行java命令时打印参数运行java命令时打印参数,需要添加-XX:+PrintFlagsFinal参数即可。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364[root@node01 test]# java ‐XX:+PrintFlagsFinal ‐version [Global flags]uintx AdaptiveSizeDecrementScaleFactor = 4{product}uintx AdaptiveSizeMajorGCDecayTimeScale = 10{product}uintx AdaptiveSizePausePolicy = 0{product}uintx AdaptiveSizePolicyCollectionCostMargin = 50{product}uintx AdaptiveSizePolicyInitializingSteps = 20{product}uintx AdaptiveSizePolicyOutputInterval = 0{product}uintx AdaptiveSizePolicyWeight = 10{product}uintx AdaptiveSizeThroughPutPolicy = 0{product}uintx AdaptiveTimeWeight = 25{product}bool AdjustConcurrency = false{product}bool AggressiveOpts = false{product}intx AliasLevel = 3{C2 product}bool AlignVector = true{C2 product}intx AllocateInstancePrefetchLines = 1{product}intx AllocatePrefetchDistance = 256{product}intx AllocatePrefetchInstr = 0{product}…………………………略…………………………………………bool UseXmmI2D = false{C1 product}intx ValueSearchLimit = 1000{C2 product}bool VerifyMergedCPBytecodes = true{product}bool VerifySharedSpaces = false{product}intx WorkAroundNPTLTimedWaitHang = 1{product}uintx YoungGenerationSizeIncrement = 20{product}uintx YoungGenerationSizeSupplement = 80{product}uintx YoungGenerationSizeSupplementDecay = 8{product}uintx YoungPLABSize = 4096{product}bool ZeroTLAB = false{product}intx hashCode = 5{product} java version "1.8.0_141"Java(TM) SE Runtime Environment (build 1.8.0_141‐b15)Java HotSpot(TM) 64‐Bit Server VM (build 25.141‐b15, mixed mode) 由上述的信息可以看出,参数有boolean类型和数字类型,值的操作符是=或:=,分别代 表默认值和被修改的值。 示例: 123456789101112131415161718192021222324252627282930java ‐XX:+PrintFlagsFinal ‐XX:+VerifySharedSpaces ‐versionintx ValueMapInitialSize = 11{C1 product}intx ValueMapMaxLoopSize = 8{C1 product}intx ValueSearchLimit = 1000{C2 product}bool VerifyMergedCPBytecodes = true{product}bool VerifySharedSpaces := true{product}intx WorkAroundNPTLTimedWaitHang = 1{product}uintx YoungGenerationSizeIncrement = 20{product}uintx YoungGenerationSizeSupplement = 80{product}uintx YoungGenerationSizeSupplementDecay = 8{product}uintx YoungPLABSize = 4096{product}bool ZeroTLAB = false{product}intx hashCode = 5{product} java version "1.8.0_141"Java(TM) SE Runtime Environment (build 1.8.0_141‐b15)Java HotSpot(TM) 64‐Bit Server VM (build 25.141‐b15, mixed mode) #可以看到VerifySharedSpaces这个参数已经被修改了。 2.6.2、查看正在运行的jvm参数如果想要查看正在运行的jvm就需要借助于jinfo命令查看。首先,启动一个tomcat用于测试,来观察下运行的jvm参数。 1234567cd /tmp/rz 上传tar ‐xvf apache‐tomcat‐7.0.57.tar.gz cd apache‐tomcat‐7.0.57cd bin/./startup.sh#http://122.51.193.216:8080/ 进行访问 访问成功: 123456789101112131415#查看所有的参数,用法:jinfo ‐flags <进程id>#通过jps 或者 jps ‐l 查看java进程[root@node01 bin]# jps 6346 Jps6219 Bootstrap [root@node01 bin]# jps ‐l 6358 sun.tools.jps.Jps6219 org.apache.catalina.startup.Bootstrap [root@node01 bin]#[root@node01 bin]# jinfo ‐flags 6219 Attaching to process ID 6219, please wait... Debugger attached successfully.Server compiler detected. JVM version is 25.141‐b15Non‐default VM flags: ‐XX:CICompilerCount=2 ‐XX:InitialHeapSize=31457280‐XX:MaxHeapSize=488636416 ‐XX:MaxNewSize=162529280 ‐ XX:MinHeapDeltaBytes=524288 ‐XX:NewSize=10485760 ‐XX:OldSize=20971520 ‐ XX:+UseCompressedClassPointers ‐XX:+UseCompressedOops ‐ XX:+UseFastUnorderedTimeStamps ‐XX:+UseParallelGCCommand line: ‐Djava.util.logging.config.file=/tmp/apache‐tomcat‐ 7.0.57/conf/logging.properties ‐ Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager ‐ Djava.endorsed.dirs=/tmp/apache‐tomcat‐7.0.57/endorsed ‐ Dcatalina.base=/tmp/apache‐tomcat‐7.0.57 ‐Dcatalina.home=/tmp/apache‐ tomcat‐7.0.57 ‐Djava.io.tmpdir=/tmp/apache‐tomcat‐7.0.57/temp#查看某一参数的值,用法:jinfo ‐flag <参数名> <进程id> [root@node01 bin]# jinfo ‐flag MaxHeapSize 6219‐XX:MaxHeapSize=488636416 3、jvm的内存模型jvm的内存模型在1.7和1.8有较大的区别,虽然本套课程是以1.8为例进行讲解,但是我们 也是需要对1.7的内存模型有所了解,所以接下里,我们将先学习1.7再学习1.8的内存模 型。 3.1、jdk1.7的堆内存模型 Young 年轻区(代)Young区被划分为三部分,Eden区和两个大小严格相同的Survivor区,其中, Survivor区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制 对象用,在Eden区间变满的时候, GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到Tenured区间。 Tenured 年老区Tenured区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young 复制转移一定的次数以后,对象就会被转移到Tenured区,一般如果系统中用了application级别的缓存,缓存中的对象往往会被转移到这一区间。 Perm 永久区Perm代主要保存class,method,filed对象,这部份的空间一般不会溢出,除非一次性 加载了很多的类,不过在涉及到热部署的应用服务器的时候,有时候会遇到java.lang.OutOfMemoryError : PermGen space 的错误,造成这个错误的很大原因就有可能是每次都重新部署,但是重新部署后,类的class没有被卸载掉,这样就造 成了大量的class对象保存在了perm中,这种情况下,一般重新启动应用服务器可以 解决问题。 Virtual区: 最大内存和初始内存的差值,就是Virtual区。 3.2、jdk1.8的堆内存模型 由上图可以看出,jdk1.8的内存模型是由2部分组成,年轻代 + 年老代。 年轻代:Eden + 2*Survivor年老代:OldGen在jdk1.8中变化最大的Perm区,用Metaspace(元数据空间)进行了替换。 需要特别说明的是:Metaspace所占用的内存空间不是在虚拟机内部,而是在本地内存 空间中,这也是与1.7的永久代最大的区别所在。 其中:CodeCache存放的是一些类和class。CCS代表的是一些压缩指针。 3.3、为什么要废弃1.7中的永久区?官网给出了解释:http://openjdk.java.net/jeps/122 12345This is part of the JRockit and Hotspot convergence effort. JRockitcustomers do not need to configure the permanent generation (since JRockit does not have a permanent generation) and are accustomed to not configuring the permanent generation.移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。 现实使用中,由于永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryError: PermGen。基于此,将永久区废弃,而改用元空间,改为了使用本地内存空间。 3.4、通过jstat命令进行查看堆内存使用情况jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。命令的格式如下: jstat [-命令选项] [vmid] [间隔时间/毫秒] [查询次数]3.4.1、查看class加载统计 1234567[root@node01 ~]# jps7080 Jps6219 Bootstrap[root@node01 ~]# jstat ‐class 6219Loaded Bytes Unloaded Bytes Time3273 7122.3 0 0.0 3.98 说明: Loaded:加载class的数量Bytes:所占用空间大小Unloaded:未加载数量Bytes:未加载占用空间Time:时间3.4.2、查看编译统计 123[root@node01 ~]# jstat ‐compiler 6219Compiled Failed Invalid Time FailedType FailedMethod 2376 1 0 8.04 1org/apache/tomcat/util/IntrospectionUtils setProperty 说明: Compiled:编译数量。Failed:失败数量Invalid:不可用数量Time:时间FailedType:失败类型FailedMethod:失败的方法3.4.3、垃圾回收统计 12345[root@node01 ~]# jstat ‐gc 6219S0C S1C S0U S1U EC EU OC OU MCMU CCSC CCSU YGC YGCT FGC FGCT GCT 9216.0 8704.0 0.0 6127.3 62976.0 3560.4 33792.0 20434.923808.0 23196.1 2560.0 2361.6 7 1.078 1 0.244 1.323 #也可以指定打印的间隔和次数,每1秒中打印一次,共打印5次 123456789101112[root@node01 ~]# jstat ‐gc 6219 1000 5S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT9216.0 8704.0 0.0 6127.3 62976.0 3917.3 33792.0 20434.923808.0 23196.1 2560.0 2361.6 7 1.078 1 0.244 1.3239216.0 8704.0 0.0 6127.3 62976.0 3917.3 33792.0 20434.923808.0 23196.1 2560.0 2361.6 7 1.078 1 0.244 1.3239216.0 8704.0 0.0 6127.3 62976.0 3917.3 33792.0 20434.923808.0 23196.1 2560.0 2361.6 7 1.078 1 0.244 1.3239216.0 8704.0 0.0 6127.3 62976.0 3917.3 33792.0 20434.923808.0 23196.1 2560.0 2361.6 7 1.078 1 0.244 1.3239216.0 8704.0 0.0 6127.3 62976.0 3917.3 33792.0 20434.923808.0 23196.1 2560.0 2361.6 7 1.078 1 0.244 1.323 说明: S0C:第一个Survivor区的大小(KB)S1C:第二个Survivor区的大小(KB)S0U:第一个Survivor区的使用大小(KB)S1U:第二个Survivor区的使用大小(KB) EC:Eden区的大小(KB)EU:Eden区的使用大小(KB)OC:Old 区 大 小 (KB)OU:Old 使 用 大 小 (KB)MC:方法区大小(KB)MU:方法区使用大小(KB)CCSC:压缩类空间大小(KB)CCSU:压缩类空间使用大小(KB)YGC:年轻代垃圾回收次数YGCT:年轻代垃圾回收消耗时间FGC:老年代垃圾回收次数FGCT:老年代垃圾回收消耗时间GCT:垃圾回收消耗总时间 4、jmap的使用以及内存溢出分析前面通过jstat可以对jvm堆的内存进行统计分析,而jmap可以获取到更加详细的内容, 如:内存使用情况的汇总、对内存溢出的定位与分析。 4.1、查看内存使用情况123456789101112131415161718192021222324252627282930313233[root@# jmap ‐heap 6219Attaching to process ID 6219, please wait... Debugger attached successfully.Server compiler detected.JVM version is 25.141‐b15using thread‐local object allocation. Parallel GC with 2 thread(s)Heap Configuration: #堆内存配置信息MinHeapFreeRatio = 0MaxHeapFreeRatio = 100MaxHeapSize = 488636416 (466.0MB)NewSize = 10485760 (10.0MB)MaxNewSize = 162529280 (155.0MB)OldSize = 20971520 (20.0MB)NewRatio = 2SurvivorRatio = 8MetaspaceSize = 21807104 (20.796875MB) CompressedClassSpaceSize = 1073741824 (1024.0MB) MaxMetaspaceSize = 17592186044415 MBG1HeapRegionSize = 0 (0.0MB)Heap Usage: # 堆内存的使用情况PS Young Generation #年轻代Eden Space:capacity = 123731968 (118.0MB)used = 1384736 (1.320587158203125MB) free = 122347232 (116.67941284179688MB) 1.1191416594941737% usedFrom Space:capacity = 9437184 (9.0MB) used = 0 (0.0MB)free = 9437184 (9.0MB)0.0% used To Space:capacity = 9437184 (9.0MB) used = 0 (0.0MB)free = 9437184 (9.0MB)0.0% usedPS Old Generation #年老代capacity = 28311552 (27.0MB)used = 13698672 (13.064071655273438MB) free = 14612880 (13.935928344726562MB) 48.38545057508681% used13648 interned Strings occupying 1866368 bytes. 4.2、查看内存中对象数量及大小123456789101112131415161718192021222324252627282930313233343536373839404142434445464748#查看所有对象,包括活跃以及非活跃的jmap ‐histo <pid> | more#查看活跃对象jmap ‐histo:live <pid> | more[root@node01 ~]# jmap ‐histo:live 6219 | more num #instances #bytes class name‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐ 1: 37437 7914608 [C2: 34916 837984 java.lang.String3: 884 654848 [B4: 17188 550016 java.util.HashMap$Node5: 3674 424968 java.lang.Class6: 6322 395512 [Ljava.lang.Object;7: 3738 328944 java.lang.reflect.Method8: 1028 208048 [Ljava.util.HashMap$Node;9: 2247 144264 [I10: 4305 137760java.util.concurrent.ConcurrentHashMap$Node11: 1270 109080 [Ljava.lang.String;12: 64 84128[Ljava.util.concurrent.ConcurrentHashMap$Node;13: 1714 82272 java.util.HashMap14: 3285 70072 [Ljava.lang.Class;15: 2888 69312 java.util.ArrayList16: 3983 63728 java.lang.Object17: 1271 61008org.apache.tomcat.util.digester.CallMethodRule18: 1518 60720 java.util.LinkedHashMap$Entry19: 1671 53472com.sun.org.apache.xerces.internal.xni.QName20: 88 50880 [Ljava.util.WeakHashMap$Entry;21: 618 49440 java.lang.reflect.Constructor22: 1545 49440 java.util.Hashtable$Entry23: 1027 41080 java.util.TreeMap$Entry24: 846 40608org.apache.tomcat.util.modeler.AttributeInfo 25: 142 38032 [S26: 946 37840 java.lang.ref.SoftReference27: 226 36816 [[C。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。#对象说明B byteC charD doubleF floatI intJ longZ boolean[ 数组,如[I表示int[][L+类名 其他对象 4.3、将内存使用情况dump到文件中有些时候我们需要将jvm当前内存中的情况dump到文件中,然后对它进行分析,jmap也 是支持dump到文件中的。 1234#用法:jmap ‐dump:format=b,file=dumpFileName <pid>#示例jmap ‐dump:format=b,file=/tmp/dump.dat 6219 可以看到已经在/tmp下生成了dump.dat的文件。 4.4、通过jhat对dump文件进行分析在上一小节中,我们将jvm的内存dump到文件中,这个文件是一个二进制的文件,不方 便查看,这时我们可以借助于jhat工具进行查看。 1234567891011#用法:jhat ‐port <port> <file>#示例:[root@node01 tmp]# jhat ‐port 9999 /tmp/dump.dat Reading from /tmp/dump.dat...Dump file created Mon Sep 10 01:04:21 CST 2018 Snapshot read, resolving...Resolving 204094 objects...Chasing references, expect 40 dots........................................Eliminating duplicate references........................................Snapshot resolved.Started HTTP server on port 7000 Server is ready. 打开浏览器进行访问:http://192.168.40.133:7000/ 在最后面有OQL查询功能。 如:查询字符长度大于100的内容。 4.5、通过MAT工具对dump文件进行分析4.5.1、MAT工具介绍MAT(Memory Analyzer Tool),一个基于Eclipse的内存分析工具,是一个快速、功能丰富的JAVA heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗。使用内存分析工具从众多的对象中进行分析,快速的计算出在内存中对象的占用大小,看看是谁阻止 了垃圾收集器的回收工作,并可以通过报表直观的查看到可能造成这种结果的对象。 官网地址:https://www.eclipse.org/mat/ 4.5.2、下载安装下载地址:https://www.eclipse.org/mat/downloads.php 将下载得到的MemoryAnalyzer-1.8.0.20180604-win32.win32.x86_64.zip进行解压 4.5.3、使用 查看对象以及它的依赖: 查看可能存在内存泄露 的分析: 5、实战:内存溢出的定位与分析内存溢出在实际的生产环境中经常会遇到,比如,不断的将数据写入到一个集合中,出现了死循环,读取超大的文件等等,都可能会造成内存溢出。如果出现了内存溢出,首先我们需要定位到发生内存溢出的环节,并且进行分析,是正常还是非正常情况,如果是正常的需求,就应该考虑加大内存的设置,如果是非正常需求,那么就要对代码进行修改,修复这个bug。首先,我们得先学会如何定位问题,然后再进行分析。如何定位问题呢,我们需要借助于jmap与MAT工具进行定位分析。接下来,我们模拟内存溢出的场景。 5.1、模拟内存溢出编写代码,向List集合中添加100万个字符串,每个字符串由1000个UUID组成。如果程 序能够正常执行,最后打印ok。 12345678910111213141516171819202122package cn.itcast.jvm;import java.util.ArrayList;import java.util.List;import java.util.UUID;public class TestJvmOutOfMemory { // 实现,向集合中添加100万个字符串,每个字符串由1000个UUID组成 public static void main(String[] args) { List<String> list = new ArrayList<>(); for (int i = 0; i < 1000000; i++) { String str = ""; for (int j = 0; j < 1000; j++) { str += UUID.randomUUID().toString(); } list.add(str); } System.out.println("ok"); }} 为了演示效果,我们将设置执行的参数,这里使用的是Idea编辑器。 12#参数如下:‐Xms8m ‐Xmx8m ‐XX:+HeapDumpOnOutOfMemoryError 5.2、运行测试测试结果如下: 1234567java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid5348.hprof ...Heap dump file created [8137186 bytes in 0.032 secs]Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:3332)at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuil der.java:124)at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)at java.lang.StringBuilder.append(StringBuilder.java:136)at cn.itcast.jvm.TestJvmOutOfMemory.main(TestJvmOutOfMemory.java:14) Process finished with exit code 1 可以看到,当发生内存溢出时,会dump文件到java_pid5348.hprof。 5.3、导入到MAT工具中进行分析 可以看到,有91.03%的内存由Object[]数组占有,所以比较可疑。分析:这个可疑是正确的,因为已经有超过90%的内存都被它占有,这是非常有可能出现内存溢出的。 查看详情: 可以看到集合中存储了大量的uuid字符串。 6、jstack的使用有些时候我们需要查看下jvm中的线程执行情况,比如,发现服务器的CPU的负载突然增高了、出现了死锁、死循环等,我们该如何分析呢? 由于程序是正常运行的,没有任何的输出,从日志方面也看不出什么问题,所以就需要 看下jvm的内部线程的执行情况,然后再进行分析查找出原因。这个时候,就需要借助于jstack命令了,jstack的作用是将正在运行的jvm的线程情况进 行快照,并且打印出来: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175#用法:jstack <pid>[root@node01 bin]# jstack 2203Full thread dump Java HotSpot(TM) 64‐Bit Server VM (25.141‐b15 mixed mode):"Attach Listener" #24 daemon prio=9 os_prio=0 tid=0x00007fabb4001000 nid=0x906 waiting on condition [0x0000000000000000]java.lang.Thread.State: RUNNABLE"http‐bio‐8080‐exec‐5" #23 daemon prio=5 os_prio=0 tid=0x00007fabb057c000 nid=0x8e1 waiting on condition [0x00007fabd05b8000]java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method)‐ parking to wait for <0x00000000f8508360> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) atjava.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awa it(AbstractQueuedSynchronizer.java:2039)at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:44 2)at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:104) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:32) atjava.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1 074)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.jav a:624)at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)at java.lang.Thread.run(Thread.java:748)"http‐bio‐8080‐exec‐4" #22 daemon prio=5 os_prio=0 tid=0x00007fab9c113800nid=0x8e0 waiting on condition [0x00007fabd06b9000] java.lang.Thread.State: WAITING (parking)at sun.misc.Unsafe.park(Native Method)‐ parking to wait for <0x00000000f8508360> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) atjava.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awa it(AbstractQueuedSynchronizer.java:2039)at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:44 2)at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:104) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:32) atjava.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1 074)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.jav a:624)at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)at java.lang.Thread.run(Thread.java:748)"http‐bio‐8080‐exec‐3" #21 daemon prio=5 os_prio=0 tid=0x0000000001aeb800 nid=0x8df waiting on condition [0x00007fabd09ba000]java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method)‐ parking to wait for <0x00000000f8508360> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) atjava.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awa it(AbstractQueuedSynchronizer.java:2039)atjava.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:44 2)at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:104) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:32) atjava.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1 074)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.jav a:624)at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)at java.lang.Thread.run(Thread.java:748)"http‐bio‐8080‐exec‐2" #20 daemon prio=5 os_prio=0 tid=0x0000000001aea000 nid=0x8de waiting on condition [0x00007fabd0abb000]java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method)‐ parking to wait for <0x00000000f8508360> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) atjava.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awa it(AbstractQueuedSynchronizer.java:2039)at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:44 2)at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:104) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:32) atjava.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1 074)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)atjava.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.jav a:624)at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)at java.lang.Thread.run(Thread.java:748)"http‐bio‐8080‐exec‐1" #19 daemon prio=5 os_prio=0 tid=0x0000000001ae8800 nid=0x8dd waiting on condition [0x00007fabd0bbc000]java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method)‐ parking to wait for <0x00000000f8508360> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) atjava.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awa it(AbstractQueuedSynchronizer.java:2039)at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:44 2)at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:104) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:32) atjava.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1 074)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.jav a:624)at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)at java.lang.Thread.run(Thread.java:748)"ajp‐bio‐8009‐AsyncTimeout" #17 daemon prio=5 os_prio=0 tid=0x00007fabe8128000 nid=0x8d0 waiting on condition [0x00007fabd0ece000]java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method)at org.apache.tomcat.util.net.JIoEndpoint$AsyncTimeout.run(JIoEndpoint.java: 152)at java.lang.Thread.run(Thread.java:748)"ajp‐bio‐8009‐Acceptor‐0" #16 daemon prio=5 os_prio=0 tid=0x00007fabe82d4000 nid=0x8cf runnable [0x00007fabd0fcf000]java.lang.Thread.State: RUNNABLEat java.net.PlainSocketImpl.socketAccept(Native Method) atjava.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:409) at java.net.ServerSocket.implAccept(ServerSocket.java:545)at java.net.ServerSocket.accept(ServerSocket.java:513) atorg.apache.tomcat.util.net.DefaultServerSocketFactory.acceptSocket(Defaul tServerSocketFactory.java:60)at org.apache.tomcat.util.net.JIoEndpoint$Acceptor.run(JIoEndpoint.java:220)at java.lang.Thread.run(Thread.java:748)"http‐bio‐8080‐AsyncTimeout" #15 daemon prio=5 os_prio=0 tid=0x00007fabe82d1800 nid=0x8ce waiting on condition [0x00007fabd10d0000]java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method)at org.apache.tomcat.util.net.JIoEndpoint$AsyncTimeout.run(JIoEndpoint.java: 152)at java.lang.Thread.run(Thread.java:748)"http‐bio‐8080‐Acceptor‐0" #14 daemon prio=5 os_prio=0 tid=0x00007fabe82d0000 nid=0x8cd runnable [0x00007fabd11d1000]java.lang.Thread.State: RUNNABLEat java.net.PlainSocketImpl.socketAccept(Native Method) atjava.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:409) at java.net.ServerSocket.implAccept(ServerSocket.java:545)at java.net.ServerSocket.accept(ServerSocket.java:513)atorg.apache.tomcat.util.net.DefaultServerSocketFactory.acceptSocket(Defaul tServerSocketFactory.java:60)at org.apache.tomcat.util.net.JIoEndpoint$Acceptor.run(JIoEndpoint.java:220)at java.lang.Thread.run(Thread.java:748)"ContainerBackgroundProcessor[StandardEngine[Catalina]]" #13 daemon prio=5 os_prio=0 tid=0x00007fabe82ce000 nid=0x8cc waiting on condition [0x00007fabd12d2000]java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method)at org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.run(C ontainerBase.java:1513)at java.lang.Thread.run(Thread.java:748)"GC Daemon" #10 daemon prio=2 os_prio=0 tid=0x00007fabe83b4000 nid=0x8b3 in Object.wait() [0x00007fabd1c2f000]java.lang.Thread.State: TIMED_WAITING (on object monitor) at java.lang.Object.wait(Native Method)‐waiting on <0x00000000e315c2d0> (a sun.misc.GC$LatencyLock) at sun.misc.GC$Daemon.run(GC.java:117)‐locked <0x00000000e315c2d0> (a sun.misc.GC$LatencyLock)"Service Thread" #7 daemon prio=9 os_prio=0 tid=0x00007fabe80c3800 nid=0x8a5 runnable [0x0000000000000000]java.lang.Thread.State: RUNNABLE"C1 CompilerThread1" #6 daemon prio=9 os_prio=0 tid=0x00007fabe80b6800 nid=0x8a4 waiting on condition [0x0000000000000000]java.lang.Thread.State: RUNNABLE"C2 CompilerThread0" #5 daemon prio=9 os_prio=0 tid=0x00007fabe80b3800 nid=0x8a3 waiting on condition [0x0000000000000000]java.lang.Thread.State: RUNNABLE"Signal Dispatcher" #4 daemon prio=9 os_prio=0 tid=0x00007fabe80b2000 nid=0x8a2 runnable [0x0000000000000000]java.lang.Thread.State: RUNNABLE"Finalizer" #3 daemon prio=8 os_prio=0 tid=0x00007fabe807f000 nid=0x8a1in Object.wait() [0x00007fabd2a67000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method)‐ waiting on <0x00000000e3162918> (a java.lang.ref.ReferenceQueue$Lock)at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)‐ locked <0x00000000e3162918> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)"Reference Handler" #2 daemon prio=10 os_prio=0 tid=0x00007fabe807a800 nid=0x8a0 in Object.wait() [0x00007fabd2b68000]java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method)‐waiting on <0x00000000e3162958> (a java.lang.ref.Reference$Lock) at java.lang.Object.wait(Object.java:502)at java.lang.ref.Reference.tryHandlePending(Reference.java:191)‐locked <0x00000000e3162958> (a java.lang.ref.Reference$Lock)at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)"main" #1 prio=5 os_prio=0 tid=0x00007fabe8009000 nid=0x89c runnable [0x00007fabed210000]java.lang.Thread.State: RUNNABLEat java.net.PlainSocketImpl.socketAccept(Native Method) atjava.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:409) at java.net.ServerSocket.implAccept(ServerSocket.java:545)at java.net.ServerSocket.accept(ServerSocket.java:513) atorg.apache.catalina.core.StandardServer.await(StandardServer.java:453) at org.apache.catalina.startup.Catalina.await(Catalina.java:777) at org.apache.catalina.startup.Catalina.start(Catalina.java:723) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorI mpl.java:43)at java.lang.reflect.Method.invoke(Method.java:498)at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:321)at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:455) 12345678910"VM Thread" os_prio=0 tid=0x00007fabe8073000 nid=0x89f runnable"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00007fabe801e000nid=0x89d runnable"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00007fabe8020000nid=0x89e runnable"VM Periodic Task Thread" os_prio=0 tid=0x00007fabe80d6800 nid=0x8a6waiting on conditionJNI global references: 43 6.1、线程的状态 在Java中线程的状态一共被分成6种: 初始态(NEW)创建一个Thread对象,但还未调用start()启动线程时,线程处于初始态。 运行态(RUNNABLE),在Java中,运行态包括 就绪态 和 运行态。 就绪态 该状态下的线程已经获得执行所需的所有资源,只要CPU分配执行权就能运行。 所有就绪态的线程存放在就绪队列中。 运行态 获得CPU执行权,正在执行的线程。 由于一个CPU同一时刻只能执行一条线程,因此每个CPU每个时刻只有一条运行态的线程。 阻塞态(BLOCKED) 当一条正在执行的线程请求某一资源失败时,就会进入阻塞态。 而在Java中,阻塞态专指请求锁失败时进入的状态。 由一个阻塞队列存放所有阻塞态的线程。 处于阻塞态的线程会不断请求资源,一旦请求成功,就会进入就绪队列,等待执行。 等待态(WAITING) 当前线程中调用wait、join、park函数时,当前线程就会进入等待态。 也有一个等待队列存放所有等待态的线程。 线程处于等待态表示它需要等待其他线程的指示才能继续运行。 进入等待态的线程会释放CPU执行权,并释放资源(如:锁) 超时等待态(TIMED_WAITING) 当运行中的线程调用sleep(time)、wait、join、parkNanos、parkUntil时,就会进入该状态; 它和等待态一样,并不是因为请求不到资源,而是主动进入,并且进入后需要其他线程唤醒; 进入该状态后释放CPU执行权 和 占有的资源。 与等待态的区别:到了超时时间后自动进入阻塞队列,开始竞争锁。 终止态(TERMINATED) 线程执行结束后的状态。 6.2、实战:死锁问题如果在生产环境发生了死锁,我们将看到的是部署的程序没有任何反应了,这个时候我 们可以借助jstack进行分析,下面我们实战下查找死锁的原因。 6.2.1、构造死锁编写代码,启动2个线程,Thread1拿到了obj1锁,准备去拿obj2锁时,obj2已经被Thread2锁定,所以发送了死锁。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455package cn.itcast.jvm;public class TestDeadLock { private static Object obj1 = new Object(); private static Object obj2 = new Object(); public static void main(String[] args) { new Thread(new Thread1()).start(); new Thread(new Thread2()).start(); } private static class Thread1 implements Runnable{ @Override public void run() { synchronized (obj1){ System.out.println("Thread1 拿到了 obj1 的锁!"); try { // 停顿2秒的意义在于,让Thread2线程拿到obj2的锁 Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (obj2){ System.out.println("Thread1 拿到了 obj2 的锁!"); } } } } private static class Thread2 implements Runnable{ @Override public void run() { synchronized (obj2){ System.out.println("Thread2 拿到了 obj2 的锁!"); try { // 停顿2秒的意义在于,让Thread1线程拿到obj1的锁 Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (obj1){ System.out.println("Thread2 拿到了 obj1 的锁!"); } } } }} 6.2.2、在linux上运行 123456789101112131415[root@node01 test]# javac TestDeadLock.java[root@node01 test]# ll总用量 28‐rw‐r‐‐r‐‐. 1 root root 184 9月 11 10:39 TestDeadLock$1.class‐rw‐r‐‐r‐‐. 1 root root 843 9月 11 10:39 TestDeadLock.class‐rw‐r‐‐r‐‐. 1 root root 1567 9月 11 10:39 TestDeadLock.java‐rw‐r‐‐r‐‐. 1 root root 1078 9月 11 10:39 TestDeadLock$Thread1.class‐rw‐r‐‐r‐‐. 1 root root 1078 9月 11 10:39 TestDeadLock$Thread2.class‐rw‐r‐‐r‐‐. 1 root root 573 9月 9 10:21 TestJVM.class‐rw‐r‐‐r‐‐. 1 root root 261 9月 9 10:21 TestJVM.java [root@node01 test]# java TestDeadLockThread1 拿到了 obj1 的锁!Thread2 拿到了 obj2 的锁!#这里发生了死锁,程序一直将等待下去 6.2.3、使用jstack进行分析 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374[root@node01 ~]# jstack 3256Full thread dump Java HotSpot(TM) 64‐Bit Server VM (25.141‐b15 mixed mode):"Attach Listener" #11 daemon prio=9 os_prio=0 tid=0x00007f5bfc001000 nid=0xcff waiting on condition [0x0000000000000000]java.lang.Thread.State: RUNNABLE"DestroyJavaVM" #10 prio=5 os_prio=0 tid=0x00007f5c2c008800 nid=0xcb9 waiting on condition [0x0000000000000000]java.lang.Thread.State: RUNNABLE"Thread‐1" #9 prio=5 os_prio=0 tid=0x00007f5c2c0e9000 nid=0xcc5 waiting for monitor entry [0x00007f5c1c7f6000]java.lang.Thread.State: BLOCKED (on object monitor) at TestDeadLock$Thread2.run(TestDeadLock.java:47)‐waiting to lock <0x00000000f655dc40> (a java.lang.Object)‐locked <0x00000000f655dc50> (a java.lang.Object) at java.lang.Thread.run(Thread.java:748)"Thread‐0" #8 prio=5 os_prio=0 tid=0x00007f5c2c0e7000 nid=0xcc4 waiting for monitor entry [0x00007f5c1c8f7000]java.lang.Thread.State: BLOCKED (on object monitor) at TestDeadLock$Thread1.run(TestDeadLock.java:27)‐waiting to lock <0x00000000f655dc50> (a java.lang.Object)‐locked <0x00000000f655dc40> (a java.lang.Object) at java.lang.Thread.run(Thread.java:748)"Service Thread" #7 daemon prio=9 os_prio=0 tid=0x00007f5c2c0d3000 nid=0xcc2 runnable [0x0000000000000000]java.lang.Thread.State: RUNNABLE"C1 CompilerThread1" #6 daemon prio=9 os_prio=0 tid=0x00007f5c2c0b6000 nid=0xcc1 waiting on condition [0x0000000000000000]java.lang.Thread.State: RUNNABLE"C2 CompilerThread0" #5 daemon prio=9 os_prio=0 tid=0x00007f5c2c0b3000 nid=0xcc0 waiting on condition [0x0000000000000000]java.lang.Thread.State: RUNNABLE"Signal Dispatcher" #4 daemon prio=9 os_prio=0 tid=0x00007f5c2c0b1800 nid=0xcbf runnable [0x0000000000000000]java.lang.Thread.State: RUNNABLE"Finalizer" #3 daemon prio=8 os_prio=0 tid=0x00007f5c2c07e800 nid=0xcbe in Object.wait() [0x00007f5c1cdfc000]java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method)‐ waiting on <0x00000000f6508ec8> (a java.lang.ref.ReferenceQueue$Lock)at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)‐ locked <0x00000000f6508ec8> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)"Reference Handler" #2 daemon prio=10 os_prio=0 tid=0x00007f5c2c07a000 nid=0xcbd in Object.wait() [0x00007f5c1cefd000]java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method)‐waiting on <0x00000000f6506b68> (a java.lang.ref.Reference$Lock) at java.lang.Object.wait(Object.java:502)at java.lang.ref.Reference.tryHandlePending(Reference.java:191)‐locked <0x00000000f6506b68> (a java.lang.ref.Reference$Lock)at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153) "VM Thread" os_prio=0 tid=0x00007f5c2c072800 nid=0xcbc runnable"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00007f5c2c01d800 nid=0xcba runnable"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00007f5c2c01f800 nid=0xcbb runnable"VM Periodic Task Thread" os_prio=0 tid=0x00007f5c2c0d6800 nid=0xcc3 waiting on conditionJNI global references: 6Found one Java‐level deadlock:============================="Thread‐1":waiting to lock monitor 0x00007f5c080062c8 (object 0x00000000f655dc40, a java.lang.Object),which is held by "Thread‐0" "Thread‐0":waiting to lock monitor 0x00007f5c08004e28 (object 0x00000000f655dc50, a java.lang.Object),which is held by "Thread‐1"Java stack information for the threads listed above:==================================================="Thread‐1":at TestDeadLock$Thread2.run(TestDeadLock.java:47)‐waiting to lock <0x00000000f655dc40> (a java.lang.Object)‐locked <0x00000000f655dc50> (a java.lang.Object) at java.lang.Thread.run(Thread.java:748)"Thread‐0":at TestDeadLock$Thread1.run(TestDeadLock.java:27)‐waiting to lock <0x00000000f655dc50> (a java.lang.Object)‐locked <0x00000000f655dc40> (a java.lang.Object) at java.lang.Thread.run(Thread.java:748)Found 1 deadlock. 在输出的信息中,已经看到,发现了1个死锁,关键看这个: 12345678"Thread‐1":at TestDeadLock$Thread2.run(TestDeadLock.java:47)‐waiting to lock <0x00000000f655dc40> (a java.lang.Object)‐locked <0x00000000f655dc50> (a java.lang.Object) at java.lang.Thread.run(Thread.java:748)"Thread‐0":at TestDeadLock$Thread1.run(TestDeadLock.java:27)‐waiting to lock <0x00000000f655dc50> (a java.lang.Object)‐locked <0x00000000f655dc40> (a java.lang.Object) at java.lang.Thread.run(Thread.java:748) 可以清晰的看到: Thread2获取了 的锁,等待获取 这个锁 Thread1获取了 的锁,等待获取 这个锁 由此可见,发生了死锁 7、VisualVM工具的使用VisualVM,能够监控线程,内存情况,查看方法的CPU时间和内存中的对 象,已被GC的对象,反向查看分配的堆栈(如100个String对象分别由哪几个对象分配出来的)。 VisualVM使用简单,几乎0配置,功能还是比较丰富的,几乎囊括了其它JDK自带命令的所有功能。 内存信息线程信息 Dump堆(本地进程) Dump线程(本地进程) 打开堆Dump。堆Dump可以用jmap来生成。打开线程Dump 生成应用快照(包含内存信息、线程信息等等) 性能分析。CPU分析(各个方法调用时间,检查哪些方法耗时多),内存分析(各类对象占用的内存,检查哪些类占用内存多) …… 7.1、启动在jdk的安装目录的bin目录下,找到jvisualvm.exe,双击打开即可。 7.2、查看本地进程 7.3、查看CPU、内存、类、线程运行信息 7.4、查看线程详情 也可以点击右上角Dump按钮,将线程的信息导出,其实就是执行的jstack命令。 发现,显示的内容是一样的。 7.5、抽样器抽样器可以对CPU、内存在一段时间内进行抽样,以供分析。 7.6、监控远程的jvmVisualJVM不仅是可以监控本地jvm进程,还可以监控远程的jvm进程,需要借助于JMX技术实现。 7.6.1、什么是JMX?JMX(Java Management Extensions,即Java管理扩展)是一个为应用程序、设备、系统等植入管理功能的框架。JMX可以跨越一系列异构操作系统平台、系统体系结构和网络传输协议,灵活的开发无缝集成的系统、网络和服务管理应用。 7.6.2、监控远程的tomcat想要监控远程的tomcat,就需要在远程的tomcat进行对JMX配置,方法如下: 123456#在tomcat的bin目录下,修改catalina.sh,添加如下的参数JAVA_OPTS="‐Dcom.sun.management.jmxremote ‐Dcom.sun.management.jmxremote.port=9999 ‐ Dcom.sun.management.jmxremote.authenticate=false ‐ Dcom.sun.management.jmxremote.ssl=false"#这几个参数的意思是:#‐Dcom.sun.management.jmxremote :允许使用JMX远程管理#‐Dcom.sun.management.jmxremote.port=9999 :JMX远程连接端口#‐Dcom.sun.management.jmxremote.authenticate=false :不进行身份认证,任何用户都可以连接#‐Dcom.sun.management.jmxremote.ssl=false :不使用ssl 保存退出,重启tomcat。 7.6.3、使用VisualJVM连接远程tomcat添加远程主机: 在一个主机下可能会有很多的jvm需要监控,所以接下来要在该主机上添加需要监控的 jvm: 图片中的9999 是前面在远程tomcat中JMX配置的远程连接端口 。 连接成功。使用方法和前面就一样了,就可以和监控本地jvm进程一样,监控远程的 tomcat进程。]]></content>
<categories>
<category>JVM</category>
</categories>
<tags>
<tag>JVM</tag>
</tags>
</entry>
<entry>
<title><![CDATA[消息中间件之rabbitmq及数据同步]]></title>
<url>%2F2019%2F12%2F18%2F%E6%B6%88%E6%81%AF%E4%B8%AD%E9%97%B4%E4%BB%B6%E4%B9%8BRabbitMQ%E5%8F%8A%E6%95%B0%E6%8D%AE%E5%90%8C%E6%AD%A5%2F</url>
<content type="text"><![CDATA[0.学习目标 了解常见的MQ产品 了解RabbitMQ的5种消息模型 会使用Spring AMQP 利用MQ实现搜索和静态页的数据同步 1.RabbitMQ1.1.搜索与商品服务的问题目前我们已经完成了商品详情和搜索系统的开发。我们思考一下,是否存在问题? 商品的原始数据保存在数据库中,增删改查都在数据库中完成。 搜索服务数据来源是索引库,如果数据库商品发生变化,索引库数据不能及时更新。 商品详情做了页面静态化,静态页面数据也不会随着数据库商品发生变化。 如果我们在后台修改了商品的价格,搜索页面和商品详情页显示的依然是旧的价格,这样显然不对。该如何解决? 这里有两种解决方案: 方案1:每当后台对商品做增删改操作,同时要修改索引库数据及静态页面 方案2:搜索服务和商品页面服务对外提供操作接口,后台在商品增删改后,调用接口 以上两种方式都有同一个严重问题:就是代码耦合,后台服务中需要嵌入搜索和商品页面服务,违背了微服务的独立原则。 所以,我们会通过另外一种方式来解决这个问题:消息队列 1.2.消息队列(MQ)1.2.1.什么是消息队列消息队列,即MQ,Message Queue。 消息队列是典型的:生产者、消费者模型。生产者不断向消息队列中生产消息,消费者不断的从队列中获取消息。因为消息的生产和消费都是异步的,而且只关心消息的发送和接收,没有业务逻辑的侵入,这样就实现了生产者和消费者的解耦。 结合前面所说的问题: 商品服务对商品增删改以后,无需去操作索引库或静态页面,只是发送一条消息,也不关心消息被谁接收。 搜索服务和静态页面服务接收消息,分别去处理索引库和静态页面。 如果以后有其它系统也依赖商品服务的数据,同样监听消息即可,商品服务无需任何代码修改。 1.2.2.AMQP和JMSMQ是消息通信的模型,并不是具体实现。现在实现MQ的有两种主流方式:AMQP、JMS。 两者间的区别和联系: JMS是定义了统一的接口,来对消息操作进行统一;AMQP是通过规定协议来统一数据交互的格式 JMS限定了必须使用Java语言;AMQP只是协议,不规定实现方式,因此是跨语言的。 JMS规定了两种消息模型;而AMQP的消息模型更加丰富 1.2.3.常见MQ产品 ActiveMQ:基于JMS RabbitMQ:基于AMQP协议,erlang语言开发,稳定性好 RocketMQ:基于JMS,阿里巴巴产品,目前交由Apache基金会 Kafka:分布式消息系统,高吞吐量 1.2.4.RabbitMQRabbitMQ是基于AMQP的一款消息管理系统 官网: http://www.rabbitmq.com/ 官方教程:http://www.rabbitmq.com/getstarted.html 1.3.下载和安装1.3.1.下载官网下载地址:http://www.rabbitmq.com/download.html 目前最新版本是:3.7.5 我们的课程中使用的是:3.4.1版本 课前资料提供了安装包: 1.3.2.安装详见课前资料中的: 2.五种消息模型RabbitMQ提供了6种消息模型,但是第6种其实是RPC,并不是MQ,因此不予学习。那么也就剩下5种。 但是其实3、4、5这三种都属于订阅模型,只不过进行路由的方式不同。 我们通过一个demo工程来了解下RabbitMQ的工作方式: 导入工程: 导入后: 依赖: 123456789101112131415161718192021222324252627282930<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>cn.itcast.rabbitmq</groupId> <artifactId>itcast-rabbitmq</artifactId> <version>0.0.1-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.2.RELEASE</version> </parent> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.3.2</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> </dependencies></project> 我们抽取一个建立RabbitMQ连接的工具类,方便其他程序获取连接: 12345678910111213141516171819202122public class ConnectionUtil { /** * 建立与RabbitMQ的连接 * @return * @throws Exception */ public static Connection getConnection() throws Exception { //定义连接工厂 ConnectionFactory factory = new ConnectionFactory(); //设置服务地址 factory.setHost("192.168.56.101"); //端口 factory.setPort(5672); //设置账号信息,用户名、密码、vhost factory.setVirtualHost("/leyou"); factory.setUsername("leyou"); factory.setPassword("leyou"); // 通过工程获取连接 Connection connection = factory.newConnection(); return connection; }} 2.1.基本消息模型官方介绍: RabbitMQ是一个消息代理:它接受和转发消息。 你可以把它想象成一个邮局:当你把邮件放在邮箱里时,你可以确定邮差先生最终会把邮件发送给你的收件人。 在这个比喻中,RabbitMQ是邮政信箱,邮局和邮递员。 RabbitMQ与邮局的主要区别是它不处理纸张,而是接受,存储和转发数据消息的二进制数据块。 P(producer/ publisher):生产者,一个发送消息的用户应用程序。 C(consumer):消费者,消费和接收有类似的意思,消费者是一个主要用来等待接收消息的用户应用程序 队列(红色区域):rabbitmq内部类似于邮箱的一个概念。虽然消息流经rabbitmq和你的应用程序,但是它们只能存储在队列中。队列只受主机的内存和磁盘限制,实质上是一个大的消息缓冲区。许多生产者可以发送消息到一个队列,许多消费者可以尝试从一个队列接收数据。 总之: 生产者将消息发送到队列,消费者从队列中获取消息,队列是存储消息的缓冲区。 我们将用Java编写两个程序;发送单个消息的生产者,以及接收消息并将其打印出来的消费者。我们将详细介绍Java API中的一些细节,这是一个消息传递的“Hello World”。 我们将调用我们的消息发布者(发送者)Send和我们的消息消费者(接收者)Recv。发布者将连接到RabbitMQ,发送一条消息,然后退出。 2.1.1.生产者发送消息123456789101112131415161718192021222324public class Send { private final static String QUEUE_NAME = "simple_queue"; public static void main(String[] argv) throws Exception { // 获取到连接以及mq通道 Connection connection = ConnectionUtil.getConnection(); // 从连接中创建通道,这是完成大部分API的地方。 Channel channel = connection.createChannel(); // 声明(创建)队列,必须声明队列才能够发送消息,我们可以把消息发送到队列中。 // 声明一个队列是幂等的 - 只有当它不存在时才会被创建 channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 消息内容 String message = "Hello World!"; channel.basicPublish("", QUEUE_NAME, null, message.getBytes()); System.out.println(" [x] Sent '" + message + "'"); //关闭通道和连接 channel.close(); connection.close(); }} 控制台: 2.1.2.管理工具中查看消息进入队列页面,可以看到新建了一个队列:simple_queue 点击队列名称,进入详情页,可以查看消息: 在控制台查看消息并不会将消息消费,所以消息还在。 2.1.3.消费者获取消息12345678910111213141516171819202122232425public class Recv { private final static String QUEUE_NAME = "simple_queue"; public static void main(String[] argv) throws Exception { // 获取到连接 Connection connection = ConnectionUtil.getConnection(); // 创建通道 Channel channel = connection.createChannel(); // 声明队列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 定义队列的消费者 DefaultConsumer consumer = new DefaultConsumer(channel) { // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用 @Override public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throws IOException { // body 即消息体 String msg = new String(body); System.out.println(" [x] received : " + msg + "!"); } }; // 监听队列,第二个参数:是否自动进行消息确认。 channel.basicConsume(QUEUE_NAME, true, consumer); }} 控制台: 这个时候,队列中的消息就没了: 我们发现,消费者已经获取了消息,但是程序没有停止,一直在监听队列中是否有新的消息。一旦有新的消息进入队列,就会立即打印. 2.1.4.消息确认机制(ACK)通过刚才的案例可以看出,消息一旦被消费者接收,队列中的消息就会被删除。 那么问题来了:RabbitMQ怎么知道消息被接收了呢? 如果消费者领取消息后,还没执行操作就挂掉了呢?或者抛出了异常?消息消费失败,但是RabbitMQ无从得知,这样消息就丢失了! 因此,RabbitMQ有一个ACK机制。当消费者获取消息后,会向RabbitMQ发送回执ACK,告知消息已经被接收。不过这种回执ACK分两种情况: 自动ACK:消息一旦被接收,消费者自动发送ACK 手动ACK:消息接收后,不会发送ACK,需要手动调用 大家觉得哪种更好呢? 这需要看消息的重要性: 如果消息不太重要,丢失也没有影响,那么自动ACK会比较方便 如果消息非常重要,不容丢失。那么最好在消费完成后手动ACK,否则接收消息后就自动ACK,RabbitMQ就会把消息从队列中删除。如果此时消费者宕机,那么消息就丢失了。 我们之前的测试都是自动ACK的,如果要手动ACK,需要改动我们的代码: 123456789101112131415161718192021222324252627public class Recv2 { private final static String QUEUE_NAME = "simple_queue"; public static void main(String[] argv) throws Exception { // 获取到连接 Connection connection = ConnectionUtil.getConnection(); // 创建通道 final Channel channel = connection.createChannel(); // 声明队列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 定义队列的消费者 DefaultConsumer consumer = new DefaultConsumer(channel) { // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用 @Override public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throws IOException { // body 即消息体 String msg = new String(body); System.out.println(" [x] received : " + msg + "!"); // 手动进行ACK channel.basicAck(envelope.getDeliveryTag(), false); } }; // 监听队列,第二个参数false,手动进行ACK channel.basicConsume(QUEUE_NAME, false, consumer); }} 注意到最后一行代码: 1channel.basicConsume(QUEUE_NAME, false, consumer); 如果第二个参数为true,则会自动进行ACK;如果为false,则需要手动ACK。方法的声明: 2.1.4.1.自动ACK存在的问题修改消费者,添加异常,如下: 生产者不做任何修改,直接运行,消息发送成功: 运行消费者,程序抛出异常。但是消息依然被消费: 管理界面: 2.1.4.2.演示手动ACK修改消费者,把自动改成手动(去掉之前制造的异常) 生产者不变,再次运行: 运行消费者 但是,查看管理界面,发现: 停掉消费者的程序,发现: 这是因为虽然我们设置了手动ACK,但是代码中并没有进行消息确认!所以消息并未被真正消费掉。 当我们关掉这个消费者,消息的状态再次称为Ready 修改代码手动ACK: 执行: 消息消费成功! 2.2.work消息模型工作队列或者竞争消费者模式 在第一篇教程中,我们编写了一个程序,从一个命名队列中发送并接受消息。在这里,我们将创建一个工作队列,在多个工作者之间分配耗时任务。 工作队列,又称任务队列。主要思想就是避免执行资源密集型任务时,必须等待它执行完成。相反我们稍后完成任务,我们将任务封装为消息并将其发送到队列。 在后台运行的工作进程将获取任务并最终执行作业。当你运行许多消费者时,任务将在他们之间共享,但是一个消息只能被一个消费者获取。 这个概念在Web应用程序中特别有用,因为在短的HTTP请求窗口中无法处理复杂的任务。 接下来我们来模拟这个流程: P:生产者:任务的发布者 C1:消费者,领取任务并且完成任务,假设完成速度较快 C2:消费者2:领取任务并完成任务,假设完成速度慢 面试题:避免消息堆积? 1)采用workqueue,多个消费者监听同一队列。 2)接收到消息以后,而是通过线程池,异步消费。 2.2.1.生产者生产者与案例1中的几乎一样: 123456789101112131415161718192021222324public class Send { private final static String QUEUE_NAME = "test_work_queue"; public static void main(String[] argv) throws Exception { // 获取到连接 Connection connection = ConnectionUtil.getConnection(); // 获取通道 Channel channel = connection.createChannel(); // 声明队列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 循环发布任务 for (int i = 0; i < 50; i++) { // 消息内容 String message = "task .. " + i; channel.basicPublish("", QUEUE_NAME, null, message.getBytes()); System.out.println(" [x] Sent '" + message + "'"); Thread.sleep(i * 2); } // 关闭通道和连接 channel.close(); connection.close(); }} 不过这里我们是循环发送50条消息。 2.2.2.消费者1 2.2.3.消费者2 与消费者1基本类似,就是没有设置消费耗时时间。 这里是模拟有些消费者快,有些比较慢。 接下来,两个消费者一同启动,然后发送50条消息: 可以发现,两个消费者各自消费了25条消息,而且各不相同,这就实现了任务的分发。 2.2.4.能者多劳刚才的实现有问题吗? 消费者1比消费者2的效率要低,一次任务的耗时较长 然而两人最终消费的消息数量是一样的 消费者2大量时间处于空闲状态,消费者1一直忙碌 现在的状态属于是把任务平均分配,正确的做法应该是消费越快的人,消费的越多。 怎么实现呢? 我们可以使用basicQos方法和prefetchCount = 1设置。 这告诉RabbitMQ一次不要向工作人员发送多于一条消息。 或者换句话说,不要向工作人员发送新消息,直到它处理并确认了前一个消息。 相反,它会将其分派给不是仍然忙碌的下一个工作人员。 再次测试: 2.3.订阅模型分类在之前的模式中,我们创建了一个工作队列。 工作队列背后的假设是:每个任务只被传递给一个工作人员。 在这一部分,我们将做一些完全不同的事情 - 我们将会传递一个信息给多个消费者。 这种模式被称为“发布/订阅”。 订阅模型示意图: 解读: 1、1个生产者,多个消费者 2、每一个消费者都有自己的一个队列 3、生产者没有将消息直接发送到队列,而是发送到了交换机 4、每个队列都要绑定到交换机 5、生产者发送的消息,经过交换机到达队列,实现一个消息被多个消费者获取的目的 X(Exchanges):交换机一方面:接收生产者发送的消息。另一方面:知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。 Exchange类型有以下几种: Fanout:广播,将消息交给所有绑定到交换机的队列 Direct:定向,把消息交给符合指定routing key 的队列 Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列 我们这里先学习 Fanout:即广播模式 Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失! 2.4.订阅模型-FanoutFanout,也称为广播。 流程图: 在广播模式下,消息发送流程是这样的: 1) 可以有多个消费者 2) 每个消费者有自己的queue(队列) 3) 每个队列都要绑定到Exchange(交换机) 4) 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定。 5) 交换机把消息发送给绑定过的所有队列 6) 队列的消费者都能拿到消息。实现一条消息被多个消费者消费 2.4.1.生产者两个变化: 1) 声明Exchange,不再声明Queue 2) 发送消息到Exchange,不再发送到Queue 1234567891011121314151617181920212223public class Send { private final static String EXCHANGE_NAME = "fanout_exchange_test"; public static void main(String[] argv) throws Exception { // 获取到连接 Connection connection = ConnectionUtil.getConnection(); // 获取通道 Channel channel = connection.createChannel(); // 声明exchange,指定类型为fanout channel.exchangeDeclare(EXCHANGE_NAME, "fanout"); // 消息内容 String message = "Hello everyone"; // 发布消息到Exchange channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes()); System.out.println(" [生产者] Sent '" + message + "'"); channel.close(); connection.close(); }} 2.4.2.消费者112345678910111213141516171819202122232425262728293031public class Recv { private final static String QUEUE_NAME = "fanout_exchange_queue_1"; private final static String EXCHANGE_NAME = "fanout_exchange_test"; public static void main(String[] argv) throws Exception { // 获取到连接 Connection connection = ConnectionUtil.getConnection(); // 获取通道 Channel channel = connection.createChannel(); // 声明队列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 绑定队列到交换机 channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ""); // 定义队列的消费者 DefaultConsumer consumer = new DefaultConsumer(channel) { // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用 @Override public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throws IOException { // body 即消息体 String msg = new String(body); System.out.println(" [消费者1] received : " + msg + "!"); } }; // 监听队列,自动返回完成 channel.basicConsume(QUEUE_NAME, true, consumer); }} 要注意代码中:队列需要和交换机绑定 2.4.3.消费者212345678910111213141516171819202122232425262728293031public class Recv2 { private final static String QUEUE_NAME = "fanout_exchange_queue_2"; private final static String EXCHANGE_NAME = "fanout_exchange_test"; public static void main(String[] argv) throws Exception { // 获取到连接 Connection connection = ConnectionUtil.getConnection(); // 获取通道 Channel channel = connection.createChannel(); // 声明队列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 绑定队列到交换机 channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ""); // 定义队列的消费者 DefaultConsumer consumer = new DefaultConsumer(channel) { // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用 @Override public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throws IOException { // body 即消息体 String msg = new String(body); System.out.println(" [消费者2] received : " + msg + "!"); } }; // 监听队列,手动返回完成 channel.basicConsume(QUEUE_NAME, true, consumer); }} 2.4.4.测试我们运行两个消费者,然后发送1条消息: 2.5.订阅模型-Direct有选择性的接收消息 在订阅模式中,生产者发布消息,所有消费者都可以获取所有消息。 在路由模式中,我们将添加一个功能 - 我们将只能订阅一部分消息。 例如,我们只能将重要的错误消息引导到日志文件(以节省磁盘空间),同时仍然能够在控制台上打印所有日志消息。 但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。 在Direct模型下,队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key) 消息的发送方在向Exchange发送消息时,也必须指定消息的routing key。 P:生产者,向Exchange发送消息,发送消息时,会指定一个routing key。 X:Exchange(交换机),接收生产者的消息,然后把消息递交给 与routing key完全匹配的队列 C1:消费者,其所在队列指定了需要routing key 为 error 的消息 C2:消费者,其所在队列指定了需要routing key 为 info、error、warning 的消息 2.5.1.生产者此处我们模拟商品的增删改,发送消息的RoutingKey分别是:insert、update、delete 1234567891011121314151617181920public class Send { private final static String EXCHANGE_NAME = "direct_exchange_test"; public static void main(String[] argv) throws Exception { // 获取到连接 Connection connection = ConnectionUtil.getConnection(); // 获取通道 Channel channel = connection.createChannel(); // 声明exchange,指定类型为direct channel.exchangeDeclare(EXCHANGE_NAME, "direct"); // 消息内容 String message = "商品新增了, id = 1001"; // 发送消息,并且指定routing key 为:insert ,代表新增商品 channel.basicPublish(EXCHANGE_NAME, "insert", null, message.getBytes()); System.out.println(" [商品服务:] Sent '" + message + "'"); channel.close(); connection.close(); }} 2.5.2.消费者1我们此处假设消费者1只接收两种类型的消息:更新商品和删除商品。 12345678910111213141516171819202122232425262728293031public class Recv { private final static String QUEUE_NAME = "direct_exchange_queue_1"; private final static String EXCHANGE_NAME = "direct_exchange_test"; public static void main(String[] argv) throws Exception { // 获取到连接 Connection connection = ConnectionUtil.getConnection(); // 获取通道 Channel channel = connection.createChannel(); // 声明队列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 绑定队列到交换机,同时指定需要订阅的routing key。假设此处需要update和delete消息 channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "update"); channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "delete"); // 定义队列的消费者 DefaultConsumer consumer = new DefaultConsumer(channel) { // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用 @Override public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throws IOException { // body 即消息体 String msg = new String(body); System.out.println(" [消费者1] received : " + msg + "!"); } }; // 监听队列,自动ACK channel.basicConsume(QUEUE_NAME, true, consumer); }} 2.5.3.消费者2我们此处假设消费者2接收所有类型的消息:新增商品,更新商品和删除商品。 1234567891011121314151617181920212223242526272829303132public class Recv2 { private final static String QUEUE_NAME = "direct_exchange_queue_2"; private final static String EXCHANGE_NAME = "direct_exchange_test"; public static void main(String[] argv) throws Exception { // 获取到连接 Connection connection = ConnectionUtil.getConnection(); // 获取通道 Channel channel = connection.createChannel(); // 声明队列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 绑定队列到交换机,同时指定需要订阅的routing key。订阅 insert、update、delete channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "insert"); channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "update"); channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "delete"); // 定义队列的消费者 DefaultConsumer consumer = new DefaultConsumer(channel) { // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用 @Override public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throws IOException { // body 即消息体 String msg = new String(body); System.out.println(" [消费者2] received : " + msg + "!"); } }; // 监听队列,自动ACK channel.basicConsume(QUEUE_NAME, true, consumer); }} 2.5.4.测试我们分别发送增、删、改的RoutingKey,发现结果: 2.6.订阅模型-TopicTopic类型的Exchange与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key 的时候使用通配符! Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert 通配符规则: `#`:匹配一个或多个词 `*`:匹配不多不少恰好1个词 举例: `audit.#`:能够匹配`audit.irs.corporate` 或者 `audit.irs` `audit.*`:只能匹配`audit.irs` 在这个例子中,我们将发送所有描述动物的消息。消息将使用由三个字(两个点)组成的routing key发送。路由关键字中的第一个单词将描述速度,第二个颜色和第三个种类:“..”。 我们创建了三个绑定:Q1绑定了绑定键“ .orange.”,Q2绑定了“..rabbit”和“lazy.#”。 Q1匹配所有的橙色动物。 Q2匹配关于兔子以及懒惰动物的消息。 练习,生产者发送如下消息,会进入那个队列: quick.orange.rabbit Q1 Q2 lazy.orange.elephant quick.orange.fox lazy.pink.rabbit quick.brown.fox quick.orange.male.rabbit orange 2.6.1.生产者使用topic类型的Exchange,发送消息的routing key有3种: item.isnert、item.update、item.delete: 1234567891011121314151617181920public class Send { private final static String EXCHANGE_NAME = "topic_exchange_test"; public static void main(String[] argv) throws Exception { // 获取到连接 Connection connection = ConnectionUtil.getConnection(); // 获取通道 Channel channel = connection.createChannel(); // 声明exchange,指定类型为topic channel.exchangeDeclare(EXCHANGE_NAME, "topic"); // 消息内容 String message = "新增商品 : id = 1001"; // 发送消息,并且指定routing key 为:insert ,代表新增商品 channel.basicPublish(EXCHANGE_NAME, "item.insert", null, message.getBytes()); System.out.println(" [商品服务:] Sent '" + message + "'"); channel.close(); connection.close(); }} 2.6.2.消费者1我们此处假设消费者1只接收两种类型的消息:更新商品和删除商品 12345678910111213141516171819202122232425262728293031public class Recv { private final static String QUEUE_NAME = "topic_exchange_queue_1"; private final static String EXCHANGE_NAME = "topic_exchange_test"; public static void main(String[] argv) throws Exception { // 获取到连接 Connection connection = ConnectionUtil.getConnection(); // 获取通道 Channel channel = connection.createChannel(); // 声明队列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 绑定队列到交换机,同时指定需要订阅的routing key。需要 update、delete channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.update"); channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.delete"); // 定义队列的消费者 DefaultConsumer consumer = new DefaultConsumer(channel) { // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用 @Override public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throws IOException { // body 即消息体 String msg = new String(body); System.out.println(" [消费者1] received : " + msg + "!"); } }; // 监听队列,自动ACK channel.basicConsume(QUEUE_NAME, true, consumer); }} 2.6.3.消费者2我们此处假设消费者2接收所有类型的消息:新增商品,更新商品和删除商品。 123456789101112131415161718192021222324252627282930313233/** * 消费者2 */public class Recv2 { private final static String QUEUE_NAME = "topic_exchange_queue_2"; private final static String EXCHANGE_NAME = "topic_exchange_test"; public static void main(String[] argv) throws Exception { // 获取到连接 Connection connection = ConnectionUtil.getConnection(); // 获取通道 Channel channel = connection.createChannel(); // 声明队列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 绑定队列到交换机,同时指定需要订阅的routing key。订阅 insert、update、delete channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.*"); // 定义队列的消费者 DefaultConsumer consumer = new DefaultConsumer(channel) { // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用 @Override public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throws IOException { // body 即消息体 String msg = new String(body); System.out.println(" [消费者2] received : " + msg + "!"); } }; // 监听队列,自动ACK channel.basicConsume(QUEUE_NAME, true, consumer); }} 2.7.持久化如何避免消息丢失? 1) 消费者的ACK机制。可以防止消费者丢失消息。 2) 但是,如果在消费者消费之前,MQ就宕机了,消息就没了。 是可以将消息进行持久化呢? 要将消息持久化,前提是:队列、Exchange都持久化 2.7.1.交换机持久化 2.7.2.队列持久化 2.7.3.消息持久化 3.Spring AMQP3.1.简介Sprin有很多不同的项目,其中就有对AMQP的支持: Spring AMQP的页面:http://spring.io/projects/spring-amqp 注意这里一段描述: Spring-amqp是对AMQP协议的抽象实现,而spring-rabbit 是对协议的具体实现,也是目前的唯一实现。底层使用的就是RabbitMQ。 3.2.依赖和配置添加AMQP的启动器: 1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId></dependency> 在application.yml中添加RabbitMQ地址: 123456spring: rabbitmq: host: 192.168.56.101 username: leyou password: leyou virtual-host: /leyou 3.3.监听者在SpringAmqp中,对消息的消费者进行了封装和抽象,一个普通的JavaBean中的普通方法,只要通过简单的注解,就可以成为一个消费者。 123456789101112131415@Componentpublic class Listener { @RabbitListener(bindings = @QueueBinding( value = @Queue(value = "spring.test.queue", durable = "true"), exchange = @Exchange( value = "spring.test.exchange", ignoreDeclarationExceptions = "true", type = ExchangeTypes.TOPIC ), key = {"#.#"})) public void listen(String msg){ System.out.println("接收到消息:" + msg); }} @Componet:类上的注解,注册到Spring容器 @RabbitListener:方法上的注解,声明这个方法是一个消费者方法,需要指定下面的属性: bindings:指定绑定关系,可以有多个。值是@QueueBinding的数组。@QueueBinding包含下面属性: value:这个消费者关联的队列。值是@Queue,代表一个队列 exchange:队列所绑定的交换机,值是@Exchange类型 key:队列和交换机绑定的RoutingKey 类似listen这样的方法在一个类中可以写多个,就代表多个消费者。 3.4.AmqpTemplateSpring最擅长的事情就是封装,把他人的框架进行封装和整合。 Spring为AMQP提供了统一的消息处理模板:AmqpTemplate,非常方便的发送消息,其发送方法: 红框圈起来的是比较常用的3个方法,分别是: 指定交换机、RoutingKey和消息体 指定消息 指定RoutingKey和消息,会向默认的交换机发送消息 3.5.测试代码123456789101112131415@RunWith(SpringRunner.class)@SpringBootTest(classes = Application.class)public class MqDemo { @Autowired private AmqpTemplate amqpTemplate; @Test public void testSend() throws InterruptedException { String msg = "hello, Spring boot amqp"; this.amqpTemplate.convertAndSend("spring.test.exchange","a.b", msg); // 等待10秒后再结束 Thread.sleep(10000); }} 运行后查看日志: 3.项目改造接下来,我们就改造项目,实现搜索服务、商品静态页的数据同步。 3.1.思路分析 发送方:商品微服务 什么时候发? 当商品服务对商品进行写操作:增、删、改的时候,需要发送一条消息,通知其它服务。 发送什么内容? 对商品的增删改时其它服务可能需要新的商品数据,但是如果消息内容中包含全部商品信息,数据量太大,而且并不是每个服务都需要全部的信息。因此我们只发送商品id,其它服务可以根据id查询自己需要的信息。 接收方:搜索微服务、静态页微服务 接收消息后如何处理? 搜索微服务: 增/改:添加新的数据到索引库 删:删除索引库数据 静态页微服务: 增/改:创建新的静态页 删:删除原来的静态页 3.2.商品服务发送消息我们先在商品微服务leyou-item-service中实现发送消息。 3.2.1.引入依赖1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId></dependency> 3.2.2.配置文件我们在application.yml中添加一些有关RabbitMQ的配置: 123456789spring: rabbitmq: host: 192.168.56.101 username: leyou password: leyou virtual-host: /leyou template: exchange: leyou.item.exchange publisher-confirms: true template:有关AmqpTemplate的配置 exchange:缺省的交换机名称,此处配置后,发送消息如果不指定交换机就会使用这个 publisher-confirms:生产者确认机制,确保消息会正确发送,如果发送失败会有错误回执,从而触发重试 3.2.3.改造GoodsService在GoodsService中封装一个发送消息到mq的方法:(需要注入AmqpTemplate模板) 12345678private void sendMessage(Long id, String type){ // 发送消息 try { this.amqpTemplate.convertAndSend("item." + type, id); } catch (Exception e) { logger.error("{}商品消息发送异常,商品id:{}", type, id, e); }} 这里没有指定交换机,因此默认发送到了配置中的:leyou.item.exchange 注意:这里要把所有异常都try起来,不能让消息的发送影响到正常的业务逻辑 然后在新增的时候调用: 修改的时候调用: 3.3.搜索服务接收消息搜索服务接收到消息后要做的事情: 增:添加新的数据到索引库 删:删除索引库数据 改:修改索引库数据 因为索引库的新增和修改方法是合二为一的,因此我们可以将这两类消息一同处理,删除另外处理。 3.3.1.引入依赖1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId></dependency> 3.3.2.添加配置123456spring: rabbitmq: host: 192.168.56.101 username: leyou password: leyou virtual-host: /leyou 这里只是接收消息而不发送,所以不用配置template相关内容。 3.3.3.编写监听器 代码: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647@Componentpublic class GoodsListener { @Autowired private SearchService searchService; /** * 处理insert和update的消息 * * @param id * @throws Exception */ @RabbitListener(bindings = @QueueBinding( value = @Queue(value = "leyou.create.index.queue", durable = "true"), exchange = @Exchange( value = "leyou.item.exchange", ignoreDeclarationExceptions = "true", type = ExchangeTypes.TOPIC), key = {"item.insert", "item.update"})) public void listenCreate(Long id) throws Exception { if (id == null) { return; } // 创建或更新索引 this.searchService.createIndex(id); } /** * 处理delete的消息 * * @param id */ @RabbitListener(bindings = @QueueBinding( value = @Queue(value = "leyou.delete.index.queue", durable = "true"), exchange = @Exchange( value = "leyou.item.exchange", ignoreDeclarationExceptions = "true", type = ExchangeTypes.TOPIC), key = "item.delete")) public void listenDelete(Long id) { if (id == null) { return; } // 删除索引 this.searchService.deleteIndex(id); }} 3.3.4.编写创建和删除索引方法这里因为要创建和删除索引,我们需要在SearchService中拓展两个方法,创建和删除索引: 12345678910111213public void createIndex(Long id) throws IOException { Spu spu = this.goodsClient.querySpuById(id); // 构建商品 Goods goods = this.buildGoods(spu); // 保存数据到索引库 this.goodsRepository.save(goods);}public void deleteIndex(Long id) { this.goodsRepository.deleteById(id);} 创建索引的方法可以从之前导入数据的测试类中拷贝和改造。 3.4.静态页服务接收消息商品静态页服务接收到消息后的处理: 增:创建新的静态页 删:删除原来的静态页 改:创建新的静态页并覆盖原来的 不过,我们编写的创建静态页的方法也具备覆盖以前页面的功能,因此:增和改的消息可以放在一个方法中处理,删除消息放在另一个方法处理。 3.4.1.引入依赖1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId></dependency> 3.4.2.添加配置123456spring: rabbitmq: host: 192.168.56.101 username: leyou password: leyou virtual-host: /leyou 这里只是接收消息而不发送,所以不用配置template相关内容。 3.4.3.编写监听器 代码: 123456789101112131415161718192021222324252627282930313233343536@Componentpublic class GoodsListener { @Autowired private GoodsHtmlService goodsHtmlService; @RabbitListener(bindings = @QueueBinding( value = @Queue(value = "leyou.create.web.queue", durable = "true"), exchange = @Exchange( value = "leyou.item.exchange", ignoreDeclarationExceptions = "true", type = ExchangeTypes.TOPIC), key = {"item.insert", "item.update"})) public void listenCreate(Long id) throws Exception { if (id == null) { return; } // 创建页面 goodsHtmlService.createHtml(id); } @RabbitListener(bindings = @QueueBinding( value = @Queue(value = "leyou.delete.web.queue", durable = "true"), exchange = @Exchange( value = "leyou.item.exchange", ignoreDeclarationExceptions = "true", type = ExchangeTypes.TOPIC), key = "item.delete")) public void listenDelete(Long id) { if (id == null) { return; } // 删除页面 goodsHtmlService.deleteHtml(id); }} 3.4.4.添加删除页面方法1234public void deleteHtml(Long id) { File file = new File("C:\\project\\nginx-1.14.0\\html\\item\\", id + ".html"); file.deleteOnExit();} 3.5.测试3.5.1.查看RabbitMQ控制台重新启动项目,并且登录RabbitMQ管理界面:http://192.168.56.101:15672 可以看到,交换机已经创建出来了: 队列也已经创建完毕: 并且队列都已经绑定到交换机: 3.5.2.修改数据试一试在后台修改商品数据的价格,分别在搜索及商品详情页查看是否统一。]]></content>
<categories>
<category>消息中间件</category>
</categories>
<tags>
<tag>消息中间件</tag>
<tag>rabbitmq</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深入理解Java虚拟机之破坏双亲委派加载机制]]></title>
<url>%2F2019%2F12%2F17%2F%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E4%B9%8B%E7%A0%B4%E5%9D%8F%E5%8F%8C%E4%BA%B2%E5%A7%94%E6%B4%BE%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6%2F</url>
<content type="text"><![CDATA[双亲委派模型并不是一个强制性的约束模型,而是 Java 设计者推荐给开发者的类加载器实现方式。在 Java 的世界中大部分类加载器都遵循这个原则,但是显然也有例外。 在《深入理解 JVM 虚拟机》一书中,作者提出双亲委派模型目前出现过 3 次较大规模的“被破坏”情况。 1.第一次被破坏第一次被破坏其实发生在双亲委派模型出现之前,也就是 JDK 1.2 发布之前,由于双亲委派模型在 JDK 1.2 之后才引入,而类加载器和抽象类 java.lang.ClassLoader 在 JDK 1.0 时代就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java 设计者在引入双亲委派模型时不得不做出一些妥协。为了向前兼容,JDK 1.2 之后的 java.lang.ClassLoader 添加了一个新的 protected 方法 findClass() 方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法 loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的 loadClass()。双亲委派的具体逻辑就在这个方法之中,JDK 1.2 之后已经不再提倡用户再去覆盖 loadClass() 方法,而应当把自己的类加载器逻辑写到 findClass() 方法来完成加载,这样就可以保证写出来的类加载器都是符合双亲委派规则的。 2.第二次被破坏第二次被破坏是由于这个模型自身的问题导致的,双亲委派很好地解决了各个类加载器的基础类统一的问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的 API,但世事往往没有绝对的完美,如果基础类又要调回用户的代码,那该怎么办? 这不是没有可能的事情,比如 JNDI 服务,JNDI 目前已经是 Java 的标准服务,它的代码由启动类加载器去加载(在 JDK 1.3 时放进去的 rt.jar),但 JNDI 的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的 ClassPath 下的 JNDI 接口提供者(SPI,Service Provider Interface)的代码,但启动类加载器不可能“认识”这些代码啊,那可怎么办? 为了解决这个问题,Java 设计团队只好引入另外一个不太优雅的设计:线程上下文加载器(Thread Context ClassLoader)。这个类加载器可以通过 java.lang.Thread 类的 setContextClassLoader()(原文在这里写成 setContextLoaser) 方法进行设置,如果创建线程时还未设置,它将从父线程中继承一个,如果在应用程序的全局范围内没有设置过的话,那么这个类加载器默认就是应用程序类加载器。 通过这个线程上下文类加载器,就可以做一些“舞弊”的事情了,JNDI 服务使用这个线程上下文类加载去加载所需要的 SPI 代码,也就是通过父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打破了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情,Java 中所有涉及 SPI 的加载动作都是采用的这种方式,例如 JNDI、JDBC、JCE、JAXB 和 JBI 等。 3.第三次被破坏第三次被破坏是由于用户对程序动态性追求而导致的,这里所说的“动态性”是指当前一些非常“热门”的名词:代码热替换(HotSwap)、模块热部署(Hot Deployment)等,说白了就是希望应用程序能像我们的计算机外设一样,插上鼠标和 U 盘不用重启机器就能立即使用。鼠标有问题就升级或者换个鼠标,不用停机也不用重启。对于实际生产系统来说,关机重启一次可能就要被列为生产事故。因此这种情况下热部署就非常有吸引力。 在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构。当接收到类加载请求时,OSGI 将按照下面的顺序进行类搜索: 1.将以 java.* 开头的类委派给父类加载器加载2.否则,将委派列表名单内的类委派给父类加载器进行加载3.否则,将 Import 列表中的类委派给 Export 这个类的 Bundle 的类加载器加载4.否则,查找当前 Bundle 的 ClassPath,使用自己的类加载器加载5.否则,查找类是否在自己的 Fragment Bundle 中,如果在,则委派给 Fragment Bundle 的类加载器加载6.否则,查找 Dynamic Import 列表的 Bundle,委派给对应的 Bundle 的类加载器加载7.否则,类查找失败 上述的查找顺序只有开头两点仍然符合双亲委派模型,其余的类查找都是在平级的类加载器中进行的。 4.破坏双亲委派的原理针对书中介绍的三种情况,往往使用最多的是线程上下文类加载器(TCCL,ThreadContextClassLoader)来破坏双亲委派机制。 Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI 接口中的代码经常需要加载具体的实现类。那么问题来了,SPI 的接口是 Java 核心库的一部分,是由启动类加载器(Bootstrap Classloader)来加载的;SPI 的实现类是由系统类加载器(System ClassLoader)来加载的。引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader 无法委派 AppClassLoader 来加载类。而线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。 那么我们来以 JDBC 源码来看看是如何破坏双亲委派模型的。 JDBC 也属于 SPI,其 Driver 接口是 Java 核心类库的一部分,但是 Driver 的实现类却是由第三方实现,是需要使用系统类加载器进行加载的,符合上述说的情况。 JDBC 案例分析我们先来看平时是如何使用 MySQL 获取数据库连接的: 12345// 加载Class到AppClassLoader(系统类加载器),然后注册驱动类// Class.forName("com.mysql.jdbc.Driver").newInstance(); String url = "jdbc:mysql://localhost:3306/testdb"; // 通过java库获取数据库连接Connection conn = java.sql.DriverManager.getConnection(url, "name", "password"); 以上就是 MySQL 注册驱动及获取 Connection 的过程,各位可以发现经常写的 Class.forName 被注释掉了,但依然可以正常运行,这是为什么呢?这是因为从 JDK 1.6 开始自带的 JDBC 4.0 版本已支持 SPI 服务加载机制,只要 MySQL 的 jar 包在类路径中,就可以注册 MySQL 驱动。 那到底是在哪一步自动注册了 MySQL Driver 的呢?重点就在 DriverManager.getConnection() 方法中。我们都知道调用一个类的静态方法会自动初始化该类(前提是该类还没有被初始化过),进而执行其静态代码块,DriverManager 中的静态代码块如下: 1234static { loadInitialDrivers(); println("JDBC DriverManager initialized");} 初始化方法 loadInitialDrivers() 的代码如下: private static void loadInitialDrivers() { String drivers; try { // 先读取系统属性 drivers = AccessController.doPrivileged(new PrivilegedAction() { public String run() { return System.getProperty(“jdbc.drivers”); } }); } catch (Exception ex) { drivers = null; } // 通过SPI加载驱动类 AccessController.doPrivileged(new PrivilegedAction() { public Void run() { ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class); Iterator driversIterator = loadedDrivers.iterator(); try{ while(driversIterator.hasNext()) { driversIterator.next(); } } catch(Throwable t) { // Do nothing } return null; } }); // 继续加载系统属性中的驱动类 if (drivers == null || drivers.equals(“”)) { return; } String[] driversList = drivers.split(“:”); println(“number of Drivers:” + driversList.length); for (String aDriver : driversList) { try { println(“DriverManager.Initialize: loading “ + aDriver); // 使用AppClassloader加载 Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); } catch (Exception ex) { println(“DriverManager.Initialize: load failed: “ + ex); } }}从上面可以看出 JDBC 中的 DriverManager 的加载 Driver 的步骤顺序依次是: 通过 SPI 方式,读取 META-INF/services 下文件中的类名,使用 TCCL(线程上下文类加载器) 加载;通过 System.getProperty(“jdbc.drivers”) 获取设置,然后通过系统类加载器加载。 下面详细分析 SPI 加载的那段代码。 JDBC 中的 SPI上面说了那么多 SPI,可能你还没弄懂什么是 SPI,所以先来看看什么是 SPI 机制,引用一段博文中的介绍: SPI 机制简介SPI 的全名为 Service Provider Interface,主要是应用于厂商自定义组件或插件中。在java.util.ServiceLoader 的文档里有比较详细的介绍。简单的总结下 java SPI 机制的思想:我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml 解析模块、JDBC 模块等方案。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。 Java SPI 就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似 IOC 的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。SPI 具体约定Java SPI 的具体约定为:当服务的提供者提供了服务接口的一种实现之后,在 jar 包的 META-INF/services/ 目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该 jar 包 META-INF/services/ 里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。JDK 提供服务实现查找的一个工具类:java.util.ServiceLoader。 知道 SPI 的机制后,我们来看刚才的代码: 12345678910ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);Iterator<Driver> driversIterator = loadedDrivers.iterator();try{ while(driversIterator.hasNext()) { driversIterator.next(); }} catch(Throwable t) { // Do nothing} 注意 driversIterator.next() 最终就是调用 Class.forName(DriverName, false, loader) 方法,也就是最开始我们注释掉的 Class.forName 加载驱动的方法。好,那句因 SPI 而省略的代码现在解释清楚了,那我们继续看给这个方法传的 loader 是怎么来的。 因为这句 Class.forName(DriverName, false, loader) 代码所在的类在 java.util.ServiceLoader 类中,而ServiceLoader.class 又加载在 BootrapLoader 中,因此传给 forName 的 loader 必然不能是 BootrapLoader。这时候只能使用 TCCL 了,也就是说把自己加载不了的类加载到 TCCL 中(通过 Thread.currentThread() 获取,简直作弊啊!)。 可以再看下 ServiceLoader.load(Class) 的代码,发现的确如此: 1234public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl);} ContextClassLoader 默认存放了 AppClassLoader 的引用,由于它是在运行时被放在了线程中,所以不管当前程序处于何处(BootstrapClassLoader 或是 ExtClassLoader 等),在任何需要的时候都可以用 Thread.currentThread().getContextClassLoader() 取出应用程序类加载器来完成需要的操作。 到这儿差不多把 SPI 机制解释清楚了。直白一点说就是,我(JDK)提供了一种帮你(第三方实现者)加载服务(如数据库驱动、日志库)的便捷方式,只要你遵循约定(把类名写在 /META-INF 里),那当我启动时我会去扫描所有 jar 包里符合约定的类名,再调用 forName 加载,但我的 ClassLoader 是没法加载的,那就把它加载到当前执行线程的 TCCL 里,后续你想怎么操作(驱动实现类的 static 代码块)就是你的事了。 好,刚才说的驱动实现类就是 com.mysql.jdbc.Driver,它的静态代码块里头又写了什么呢?是否又用到了 TCCL 呢?我们继续看下一个例子。 校验实例的归属com.mysql.jdbc.Driver 加载后运行的静态代码块: 12345678static { try { // Driver已经加载到TCCL中了,此时可以直接实例化 java.sql.DriverManager.registerDriver(new com.mysql.jdbc.Driver()); } catch (SQLException E) { throw new RuntimeException("Can't register driver!"); }} registerDriver 方法将 driver 实例注册到系统的 java.sql.DriverManager 类中,其实就是 add 到它的一个名为 registeredDrivers 的静态成员 CopyOnWriteArrayList 中 。 到此驱动注册基本完成,接下来我们回到最开始的那段样例代码:java.sql.DriverManager.getConnection()。它最终调用了以下方法: private static Connection getConnection( String url, java.util.Properties info, Class<?> caller) throws SQLException { /* 传入的caller由Reflection.getCallerClass()得到,该方法 * 可获取到调用本方法的Class类,这儿获取到的是当前应用的类加载器 */ ClassLoader callerCL = caller != null ? caller.getClassLoader() : null; synchronized(DriverManager.class) { if (callerCL == null) { callerCL = Thread.currentThread().getContextClassLoader(); } } if(url == null) { throw new SQLException(“The url cannot be null”, “08001”); } SQLException reason = null; // 遍历注册到registeredDrivers里的Driver类 for(DriverInfo aDriver : registeredDrivers) { // 检查Driver类有效性 if(isDriverAllowed(aDriver.driver, callerCL)) { try { println(" trying " + aDriver.driver.getClass().getName()); // 调用com.mysql.jdbc.Driver.connect方法获取连接 Connection con = aDriver.driver.connect(url, info); if (con != null) { // Success! return (con); } } catch (SQLException ex) { if (reason == null) { reason = ex; } } } else { println(" skipping: " + aDriver.getClass().getName()); } } throw new SQLException("No suitable driver found for "+ url, "08001"); } private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) { boolean result = false; if(driver != null) { Class<?> aClass = null; try { // 传入的classLoader为调用getConnetction的当前类加载器,从中寻找driver的class对象 aClass = Class.forName(driver.getClass().getName(), true, classLoader); } catch (Exception ex) { result = false; } // 注意,只有同一个类加载器中的Class使用==比较时才会相等,此处就是校验用户注册Driver时该Driver所属的类加载器与调用时的是否同一个 // driver.getClass()拿到就是当初执行Class.forName("com.mysql.jdbc.Driver")时的应用AppClassLoader result = ( aClass == driver.getClass() ) ? true : false; } return result; } 由于 TCCL 本质就是当前应用类加载器,所以之前的初始化就是加载在当前的类加载器中,这一步就是校验存放的 driver 是否属于调用者的 Classloader。例如在下文中的 Tomcat 里,多个 webapp 都有自己的 Classloader,如果它们都自带 mysql-connect.jar 包,那底层 Classloader 的 DriverManager 里将注册多个不同类加载器的 Driver 实例,想要区分只能靠 TCCL 了。 Tomcat与Spring的类加载案例Tomcat 中的类加载器在 Tomcat 目录结构中,有三组目录(/common/,/server/和shared/)可以存放公用 Java 类库,此外还有第四组 Web 应用程序自身的目录 /WEB-INF/ ,把 Java 类库放置在这些目录中的含义分别是: 放置在 common 目录中:类库可被 Tomcat 和所有的 Web 应用程序共同使用。放置在 server 目录中:类库可被 Tomcat 使用,但对所有的 Web 应用程序都不可见。放置在 shared 目录中:类库可被所有的 Web 应用程序共同使用,但对 Tomcat 自己不可见。放置在 /WebApp/WEB-INF 目录中:类库仅仅可以被此 Web 应用程序使用,对 Tomcat 和其他 Web 应用程序都不可见。为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat 自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,如下图所示: 灰色背景的 3 个类加载器是 JDK 默认提供的类加载器,这 3 个加载器的作用前面已经介绍过了。而 CommonClassLoader、CatalinaClassLoader、SharedClassLoader 和 WebAppClassLoader 则是 Tomcat 自己定义的类加载器,它们分别加载 /common/、/server/、/shared/ 和 /WebApp/WEB-INF/ 中的 Java 类库。其中 WebApp 类加载器和 JSP 类加载器通常会存在多个实例,每一个 Web 应用程序对应一个 WebApp 类加载器,每一个 JSP 文件对应一个 JSP 类加载器。 从图中的委派关系中可以看出,CommonClassLoader 能加载的类都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 自己能加载的类则与对方相互隔离。WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 实例之间相互隔离。而 JasperLoader 的加载范围仅仅是这个 JSP 文件所编译出来的那一个 Class,它出现的目的就是为了被丢弃:当服务器检测到 JSP 文件被修改时,会替换掉目前的 JasperLoader 的实例,并通过再建立一个新的 JSP 类加载器来实现 JSP 文件的 HotSwap 功能。 Spring 加载问题Tomcat 加载器的实现清晰易懂,并且采用了官方推荐的“正统”的使用类加载器的方式。这时作者提一个问题:如果有 10 个 Web 应用程序都用到了 Spring 的话,可以把 Spring 的 jar 包放到 common 或 shared 目录下让这些程序共享。Spring 的作用是管理每个 web 应用程序的 bean, getBean 时自然要能访问到应用程序的类,而用户的程序显然是放在/WebApp/WEB-INF 目录中的(由 WebAppClassLoader 加载),那么在 CommonClassLoader 或 SharedClassLoader 中的 Spring 容器如何去加载并不在其加载范围的用户程序(/WebApp/WEB-INF/)中的 Class 呢? 实际上,Spring 根本不会去管自己被放在哪里,它统统使用 TCCL 来加载类,而 TCCL 默认设置为了 WebAppClassLoader,也就是说哪个 WebApp 应用调用了 Spring,Spring 就去取该应用自己的 WebAppClassLoader 来加载 bean,简直完美~ 源码分析有兴趣的可以接着看看具体实现: 在 web.xml 中定义的 listener 为 org.springframework.web.context.ContextLoaderListener,它最终调用了 org.springframework.web.context.ContextLoader 类来装载 bean,具体方法如下(删去了部分不相关内容): 1234567891011121314151617181920212223242526272829303132public WebApplicationContext initWebApplicationContext(ServletContext servletContext) { try { // 创建WebApplicationContext if (this.context == null) { this.context = createWebApplicationContext(servletContext); } // 将其保存到该webapp的servletContext中 servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context); // 获取线程上下文类加载器,默认为WebAppClassLoader ClassLoader ccl = Thread.currentThread().getContextClassLoader(); // 如果spring的jar包放在每个webapp自己的目录中 // 此时线程上下文类加载器会与本类的类加载器(加载spring的)相同,都是WebAppClassLoader if (ccl == ContextLoader.class.getClassLoader()) { currentContext = this.context; } else if (ccl != null) { // 如果不同,也就是上面说的那个问题的情况,那么用一个map把刚才创建的WebApplicationContext及对应的WebAppClassLoader存下来 // 一个webapp对应一个记录,后续调用时直接根据WebAppClassLoader来取出 currentContextPerThread.put(ccl, this.context); } return this.context; } catch (RuntimeException ex) { logger.error("Context initialization failed", ex); throw ex; } catch (Error err) { logger.error("Context initialization failed", err); throw err; }} 具体说明都在注释中,Spring 考虑到了自己可能被放到其他位置,所以直接用 TCCL 来解决所有可能面临的情况。 总结通过上面的两个案例分析,我们可以总结出线程上下文类加载器的适用场景: 当高层提供了统一接口让低层去实现,同时又要是在高层加载(或实例化)低层的类时,必须通过线程上下文类加载器来帮助高层的 ClassLoader 找到并加载该类。当使用本类托管类加载,然而加载本类的 ClassLoader 未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管。 参考文章真正理解线程上下文类加载器(多案例分析)JVM双亲委派机制与Tomcat]]></content>
<categories>
<category>JVM</category>
</categories>
<tags>
<tag>JVM</tag>
<tag>深入理解Java虚拟机</tag>
<tag>Java虚拟机</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Spring Cloud快速开发入门第02篇]]></title>
<url>%2F2019%2F12%2F12%2FSpring%20Cloud%E5%BF%AB%E9%80%9F%E5%BC%80%E5%8F%91%E5%85%A5%E9%97%A8%E7%AC%AC02%E7%AF%87%2F</url>
<content type="text"><![CDATA[0.学习目标 会配置Hystix熔断 会使用Feign进行远程调用 能独立搭建Zuul网关 能编写Zuul的过滤器 1.Hystrix1.1.简介Hystrix,英文意思是豪猪,全身是刺,看起来就不好惹,是一种保护机制。 Hystrix也是Netflix公司的一款组件。 主页:https://github.com/Netflix/Hystrix/ 那么Hystix的作用是什么呢?具体要保护什么呢? Hystix是Netflix开源的一个延迟和容错库,用于隔离访问远程服务、第三方库,防止出现级联失败。 1.2.雪崩问题微服务中,服务间调用关系错综复杂,一个请求,可能需要调用多个微服务接口才能实现,会形成非常复杂的调用链路: 如图,一次业务请求,需要调用A、P、H、I四个服务,这四个服务又可能调用其它服务。 如果此时,某个服务出现异常: 例如微服务I发生异常,请求阻塞,用户不会得到响应,则tomcat的这个线程不会释放,于是越来越多的用户请求到来,越来越多的线程会阻塞: 服务器支持的线程和并发数有限,请求一直阻塞,会导致服务器资源耗尽,从而导致所有其它服务都不可用,形成雪崩效应。 这就好比,一个汽车生产线,生产不同的汽车,需要使用不同的零件,如果某个零件因为种种原因无法使用,那么就会造成整台车无法装配,陷入等待零件的状态,直到零件到位,才能继续组装。 此时如果有很多个车型都需要这个零件,那么整个工厂都将陷入等待的状态,导致所有生产都陷入瘫痪。一个零件的波及范围不断扩大。 Hystix解决雪崩问题的手段有两个: 线程隔离 服务熔断 1.3.线程隔离,服务降级1.3.1.原理线程隔离示意图: 解读: Hystrix为每个依赖服务调用分配一个小的线程池,如果线程池已满调用将被立即拒绝,默认不采用排队.加速失败判定时间。 用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,或者请求超时,则会进行降级处理,什么是服务降级? 服务降级:优先保证核心服务,而非核心服务不可用或弱可用。 用户的请求故障时,不会被阻塞,更不会无休止的等待或者看到系统崩溃,至少可以看到一个执行结果(例如返回友好的提示信息) 。 服务降级虽然会导致请求失败,但是不会导致阻塞,而且最多会影响这个依赖服务对应的线程池中的资源,对其它服务没有响应。 触发Hystix服务降级的情况: 线程池已满 请求超时 1.3.2.动手实践1.3.2.1.引入依赖首先在itcast-service-consumer的pom.xml中引入Hystrix依赖: 1234<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId></dependency> 1.3.2.2.开启熔断 可以看到,我们类上的注解越来越多,在微服务中,经常会引入上面的三个注解,于是Spring就提供了一个组合注解:@SpringCloudApplication 因此,我们可以使用这个组合注解来代替之前的3个注解。 12345678910111213@SpringCloudApplicationpublic class ItcastServiceConsumerApplication { @Bean @LoadBalanced public RestTemplate restTemplate(){ return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(ItcastServiceConsumerApplication.class, args); }} 1.3.2.3.编写降级逻辑我们改造itcast-service-consumer,当目标服务的调用出现故障,我们希望快速失败,给用户一个友好提示。因此需要提前编写好失败时的降级处理逻辑,要使用HystixCommond来完成: 12345678910111213141516171819@Controller@RequestMapping("consumer/user")public class UserController { @Autowired private RestTemplate restTemplate; @GetMapping @ResponseBody @HystrixCommand(fallbackMethod = "queryUserByIdFallBack") public String queryUserById(@RequestParam("id") Long id) { String user = this.restTemplate.getForObject("http://service-provider/user/" + id, String.class); return user; } public String queryUserByIdFallBack(Long id){ return "请求繁忙,请稍后再试!"; }} 要注意,因为熔断的降级逻辑方法必须跟正常逻辑方法保证:相同的参数列表和返回值声明。失败逻辑中返回User对象没有太大意义,一般会返回友好提示。所以我们把queryById的方法改造为返回String,反正也是Json数据。这样失败逻辑中返回一个错误说明,会比较方便。 说明: @HystrixCommand(fallbackMethod = “queryByIdFallBack”):用来声明一个降级逻辑的方法 测试: 当itcast-service-provder正常提供服务时,访问与以前一致。但是当我们将itcast-service-provider停机时,会发现页面返回了降级处理信息: 1.3.2.4.默认FallBack我们刚才把fallback写在了某个业务方法上,如果这样的方法很多,那岂不是要写很多。所以我们可以把Fallback配置加在类上,实现默认fallback: 1234567891011121314151617181920212223242526@Controller@RequestMapping("consumer/user")@DefaultProperties(defaultFallback = "fallBackMethod") // 指定一个类的全局熔断方法public class UserController { @Autowired private RestTemplate restTemplate; @GetMapping @ResponseBody @HystrixCommand // 标记该方法需要熔断 public String queryUserById(@RequestParam("id") Long id) { String user = this.restTemplate.getForObject("http://service-provider/user/" + id, String.class); return user; } /** * 熔断方法 * 返回值要和被熔断的方法的返回值一致 * 熔断方法不需要参数 * @return */ public String fallBackMethod(){ return "请求繁忙,请稍后再试!"; }} @DefaultProperties(defaultFallback = “defaultFallBack”):在类上指明统一的失败降级方法 @HystrixCommand:在方法上直接使用该注解,使用默认的剪辑方法。 defaultFallback:默认降级方法,不用任何参数,以匹配更多方法,但是返回值一定一致 1.3.2.5.设置超时在之前的案例中,请求在超过1秒后都会返回错误信息,这是因为Hystix的默认超时时长为1,我们可以通过配置修改这个值: 我们可以通过hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds来设置Hystrix超时时间。该配置没有提示。 1234567hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 6000 # 设置hystrix的超时时间为6000ms 改造服务提供者 改造服务提供者的UserController接口,随机休眠一段时间,以触发熔断: 123456789@GetMapping("{id}")public User queryUserById(@PathVariable("id") Long id) { try { Thread.sleep(6000); } catch (InterruptedException e) { e.printStackTrace(); } return this.userService.queryUserById(id);} 1.4.服务熔断1.4.1.熔断原理熔断器,也叫断路器,其英文单词为:Circuit Breaker 熔断状态机3个状态: Closed:关闭状态,所有请求都正常访问。 Open:打开状态,所有请求都会被降级。Hystix会对请求情况计数,当一定时间内失败请求百分比达到阈值,则触发熔断,断路器会完全打开。默认失败比例的阈值是50%,请求次数最少不低于20次。 Half Open:半开状态,open状态不是永久的,打开后会进入休眠时间(默认是5S)。随后断路器会自动进入半开状态。此时会释放部分请求通过,若这些请求都是健康的,则会完全关闭断路器,否则继续保持打开,再次进行休眠计时 1.4.2.动手实践为了能够精确控制请求的成功或失败,我们在consumer的调用业务中加入一段逻辑: 123456789@GetMapping("{id}")@HystrixCommandpublic String queryUserById(@PathVariable("id") Long id){ if(id == 1){ throw new RuntimeException("太忙了"); } String user = this.restTemplate.getForObject("http://service-provider/user/" + id, String.class); return user;} 这样如果参数是id为1,一定失败,其它情况都成功。(不要忘了清空service-provider中的休眠逻辑) 我们准备两个请求窗口: 一个请求:http://localhost/consumer/user/1,注定失败 一个请求:http://localhost/consumer/user/2,肯定成功 当我们疯狂访问id为1的请求时(超过20次),就会触发熔断。断路器会断开,一切请求都会被降级处理。 此时你访问id为2的请求,会发现返回的也是失败,而且失败时间很短,只有几毫秒左右: 不过,默认的熔断触发要求较高,休眠时间窗较短,为了测试方便,我们可以通过配置修改熔断策略: 123circuitBreaker.requestVolumeThreshold=10circuitBreaker.sleepWindowInMilliseconds=10000circuitBreaker.errorThresholdPercentage=50 解读: requestVolumeThreshold:触发熔断的最小请求次数,默认20 errorThresholdPercentage:触发熔断的失败请求最小占比,默认50% sleepWindowInMilliseconds:休眠时长,默认是5000毫秒 2.Feign在前面的学习中,我们使用了Ribbon的负载均衡功能,大大简化了远程调用时的代码: 1String user = this.restTemplate.getForObject("http://service-provider/user/" + id, String.class); 如果就学到这里,你可能以后需要编写类似的大量重复代码,格式基本相同,无非参数不一样。有没有更优雅的方式,来对这些代码再次优化呢? 这就是我们接下来要学的Feign的功能了。 2.1.简介有道词典的英文解释: 为什么叫伪装? Feign可以把Rest的请求进行隐藏,伪装成类似SpringMVC的Controller一样。你不用再自己拼接url,拼接参数等等操作,一切都交给Feign去做。 项目主页:https://github.com/OpenFeign/feign 2.2.快速入门改造itcast-service-consumer工程 2.2.1.导入依赖1234<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId></dependency> 2.2.2.开启Feign功能我们在启动类上,添加注解,开启Feign功能 12345678@SpringCloudApplication@EnableFeignClients // 开启feign客户端public class ItcastServiceConsumerApplication { public static void main(String[] args) { SpringApplication.run(ItcastServiceConsumerApplication.class, args); }} 删除RestTemplate:feign已经自动集成了Ribbon负载均衡的RestTemplate。所以,此处不需要再注册RestTemplate。 2.2.3.Feign的客户端在itcast-service-consumer工程中,添加UserClient接口: 内容: 123456@FeignClient(value = "service-provider") // 标注该类是一个feign接口public interface UserClient { @GetMapping("user/{id}") User queryById(@PathVariable("id") Long id);} 首先这是一个接口,Feign会通过动态代理,帮我们生成实现类。这点跟mybatis的mapper很像 @FeignClient,声明这是一个Feign客户端,类似@Mapper注解。同时通过value属性指定服务名称 接口中的定义方法,完全采用SpringMVC的注解,Feign会根据注解帮我们生成URL,并访问获取结果 改造原来的调用逻辑,调用UserClient接口: 123456789101112131415@Controller@RequestMapping("consumer/user")public class UserController { @Autowired private UserClient userClient; @GetMapping @ResponseBody public User queryUserById(@RequestParam("id") Long id){ User user = this.userClient.queryUserById(id); return user; }} 2.2.4.启动测试访问接口: 正常获取到了结果。 2.3.负载均衡Feign中本身已经集成了Ribbon依赖和自动配置: 因此我们不需要额外引入依赖,也不需要再注册RestTemplate对象。 2.4.Hystrix支持Feign默认也有对Hystrix的集成: 只不过,默认情况下是关闭的。我们需要通过下面的参数来开启:(在itcast-service-consumer工程添加配置内容) 123feign: hystrix: enabled: true # 开启Feign的熔断功能 但是,Feign中的Fallback配置不像hystrix中那样简单了。 1)首先,我们要定义一个类UserClientFallback,实现刚才编写的UserClient,作为fallback的处理类 12345678910@Componentpublic class UserClientFallback implements UserClient { @Override public User queryById(Long id) { User user = new User(); user.setUserName("服务器繁忙,请稍后再试!"); return user; }} 2)然后在UserFeignClient中,指定刚才编写的实现类 123456@FeignClient(value = "service-provider", fallback = UserClientFallback.class) // 标注该类是一个feign接口public interface UserClient { @GetMapping("user/{id}") User queryUserById(@PathVariable("id") Long id);} 3)重启测试: 2.5.请求压缩(了解)Spring Cloud Feign 支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。通过下面的参数即可开启请求与响应的压缩功能: 123456feign: compression: request: enabled: true # 开启请求压缩 response: enabled: true # 开启响应压缩 同时,我们也可以对请求的数据类型,以及触发压缩的大小下限进行设置: 123456feign: compression: request: enabled: true # 开启请求压缩 mime-types: text/html,application/xml,application/json # 设置压缩的数据类型 min-request-size: 2048 # 设置触发压缩的大小下限 注:上面的数据类型、压缩大小下限均为默认值。 2.6.日志级别(了解)前面讲过,通过logging.level.xx=debug来设置日志级别。然而这个对Fegin客户端而言不会产生效果。因为@FeignClient注解修改的客户端在被代理时,都会创建一个新的Fegin.Logger实例。我们需要额外指定这个日志的级别才可以。 1)设置com.leyou包下的日志级别都为debug 123logging: level: cn.itcast: debug 2)编写配置类,定义日志级别 内容: 12345678@Configurationpublic class FeignLogConfiguration { @Bean Logger.Level feignLoggerLevel(){ return Logger.Level.FULL; }} 这里指定的Level级别是FULL,Feign支持4种级别: NONE:不记录任何日志信息,这是默认值。 BASIC:仅记录请求的方法,URL以及响应状态码和执行时间 HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息 FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。 3)在FeignClient中指定配置类: 12345@FeignClient(value = "service-privider", fallback = UserFeignClientFallback.class, configuration = FeignConfig.class)public interface UserFeignClient { @GetMapping("/user/{id}") User queryUserById(@PathVariable("id") Long id);} 4)重启项目,即可看到每次访问的日志: 3.Zuul网关通过前面的学习,使用Spring Cloud实现微服务的架构基本成型,大致是这样的: 我们使用Spring Cloud Netflix中的Eureka实现了服务注册中心以及服务注册与发现;而服务间通过Ribbon或Feign实现服务的消费以及均衡负载。为了使得服务集群更为健壮,使用Hystrix的融断机制来避免在微服务架构中个别服务出现异常时引起的故障蔓延。 在该架构中,我们的服务集群包含:内部服务Service A和Service B,他们都会注册与订阅服务至Eureka Server,而Open Service是一个对外的服务,通过均衡负载公开至服务调用方。我们把焦点聚集在对外服务这块,直接暴露我们的服务地址,这样的实现是否合理,或者是否有更好的实现方式呢? 先来说说这样架构需要做的一些事儿以及存在的不足: 破坏了服务无状态特点。 为了保证对外服务的安全性,我们需要实现对服务访问的权限控制,而开放服务的权限控制机制将会贯穿并污染整个开放服务的业务逻辑,这会带来的最直接问题是,破坏了服务集群中REST API无状态的特点。 从具体开发和测试的角度来说,在工作中除了要考虑实际的业务逻辑之外,还需要额外考虑对接口访问的控制处理。 无法直接复用既有接口。 当我们需要对一个即有的集群内访问接口,实现外部服务访问时,我们不得不通过在原有接口上增加校验逻辑,或增加一个代理调用来实现权限控制,无法直接复用原有的接口。 面对类似上面的问题,我们要如何解决呢?答案是:服务网关! 为了解决上面这些问题,我们需要将权限控制这样的东西从我们的服务单元中抽离出去,而最适合这些逻辑的地方就是处于对外访问最前端的地方,我们需要一个更强大一些的均衡负载器的 服务网关。 服务网关是微服务架构中一个不可或缺的部分。通过服务网关统一向外系统提供REST API的过程中,除了具备服务路由、均衡负载功能之外,它还具备了权限控制等功能。Spring Cloud Netflix中的Zuul就担任了这样的一个角色,为微服务架构提供了前门保护的作用,同时将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主体能够具备更高的可复用性和可测试性。 3.1.简介官网:https://github.com/Netflix/zuul Zuul:维基百科 电影《捉鬼敢死队》中的怪兽,Zuul,在纽约引发了巨大骚乱。 事实上,在微服务架构中,Zuul就是守门的大Boss!一夫当关,万夫莫开! 3.2.Zuul加入后的架构 不管是来自于客户端(PC或移动端)的请求,还是服务内部调用。一切对服务的请求都会经过Zuul这个网关,然后再由网关来实现 鉴权、动态路由等等操作。Zuul就是我们服务的统一入口。 3.3.快速入门3.3.1.新建工程填写基本信息: 添加Zuul依赖: 3.3.2.编写配置12345server: port: 10010 #服务端口spring: application: name: api-gateway #指定服务名 3.3.3.编写引导类通过@EnableZuulProxy注解开启Zuul的功能: 12345678@SpringBootApplication@EnableZuulProxy // 开启网关功能public class ItcastZuulApplication { public static void main(String[] args) { SpringApplication.run(ItcastZuulApplication.class, args); }} 3.3.4.编写路由规则我们需要用Zuul来代理service-provider服务,先看一下控制面板中的服务状态: ip为:127.0.0.1 端口为:8081 映射规则: 12345678910server: port: 10010 #服务端口spring: application: name: api-gateway #指定服务名zuul: routes: service-provider: # 这里是路由id,随意写 path: /service-provider/** # 这里是映射路径 url: http://127.0.0.1:8081 # 映射路径对应的实际url地址 我们将符合path 规则的一切请求,都代理到 url参数指定的地址 本例中,我们将 /service-provider/**开头的请求,代理到http://127.0.0.1:8081 3.3.5.启动测试访问的路径中需要加上配置规则的映射路径,我们访问:http://127.0.0.1:10010/service-provider/user/1 3.4.面向服务的路由在刚才的路由规则中,我们把路径对应的服务地址写死了!如果同一服务有多个实例的话,这样做显然就不合理了。我们应该根据服务的名称,去Eureka注册中心查找 服务对应的所有实例列表,然后进行动态路由才对! 对itcast-zuul工程修改优化: 3.4.1.添加Eureka客户端依赖1234<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency> 3.4.2.添加Eureka配置,获取服务信息12345eureka: client: registry-fetch-interval-seconds: 5 # 获取服务列表的周期:5s service-url: defaultZone: http://127.0.0.1:10086/eureka 3.4.3.开启Eureka客户端发现功能123456789@SpringBootApplication@EnableZuulProxy // 开启Zuul的网关功能@EnableDiscoveryClientpublic class ZuulDemoApplication { public static void main(String[] args) { SpringApplication.run(ZuulDemoApplication.class, args); }} 3.4.4.修改映射配置,通过服务名称获取因为已经有了Eureka客户端,我们可以从Eureka获取服务的地址信息,因此映射时无需指定IP地址,而是通过服务名称来访问,而且Zuul已经集成了Ribbon的负载均衡功能。 12345zuul: routes: service-provider: # 这里是路由id,随意写 path: /service-provider/** # 这里是映射路径 serviceId: service-provider # 指定服务名称 3.4.5.启动测试再次启动,这次Zuul进行代理时,会利用Ribbon进行负载均衡访问: 3.5.简化的路由配置在刚才的配置中,我们的规则是这样的: zuul.routes.<route>.path=/xxx/**: 来指定映射路径。<route>是自定义的路由名 zuul.routes.<route>.serviceId=service-provider:来指定服务名。 而大多数情况下,我们的<route>路由名称往往和服务名会写成一样的。因此Zuul就提供了一种简化的配置语法:zuul.routes.<serviceId>=<path> 比方说上面我们关于service-provider的配置可以简化为一条: 123zuul: routes: service-provider: /service-provider/** # 这里是映射路径 省去了对服务名称的配置。 3.6.默认的路由规则在使用Zuul的过程中,上面讲述的规则已经大大的简化了配置项。但是当服务较多时,配置也是比较繁琐的。因此Zuul就指定了默认的路由规则: 默认情况下,一切服务的映射路径就是服务名本身。例如服务名为:service-provider,则默认的映射路径就 是:/service-provider/** 也就是说,刚才的映射规则我们完全不配置也是OK的,不信就试试看。 3.7.路由前缀配置示例: 12345zuul: routes: service-provider: /service-provider/** service-consumer: /service-consumer/** prefix: /api # 添加路由前缀 我们通过zuul.prefix=/api来指定了路由的前缀,这样在发起请求时,路径就要以/api开头。 3.8.过滤器Zuul作为网关的其中一个重要功能,就是实现请求的鉴权。而这个动作我们往往是通过Zuul提供的过滤器来实现的。 3.8.1.ZuulFilterZuulFilter是过滤器的顶级父类。在这里我们看一下其中定义的4个最重要的方法: 12345678910public abstract ZuulFilter implements IZuulFilter{ abstract public String filterType(); abstract public int filterOrder(); boolean shouldFilter();// 来自IZuulFilter Object run() throws ZuulException;// IZuulFilter} shouldFilter:返回一个Boolean值,判断该过滤器是否需要执行。返回true执行,返回false不执行。 run:过滤器的具体业务逻辑。 filterType:返回字符串,代表过滤器的类型。包含以下4种: pre:请求在被路由之前执行 route:在路由请求时调用 post:在route和errror过滤器之后调用 error:处理请求时发生错误调用 filterOrder:通过返回的int值来定义过滤器的执行顺序,数字越小优先级越高。 3.8.2.过滤器执行生命周期这张是Zuul官网提供的请求生命周期图,清晰的表现了一个请求在各个过滤器的执行顺序。 正常流程: 请求到达首先会经过pre类型过滤器,而后到达route类型,进行路由,请求就到达真正的服务提供者,执行请求,返回结果后,会到达post过滤器。而后返回响应。 异常流程: 整个过程中,pre或者route过滤器出现异常,都会直接进入error过滤器,在error处理完毕后,会将请求交给POST过滤器,最后返回给用户。 如果是error过滤器自己出现异常,最终也会进入POST过滤器,将最终结果返回给请求客户端。 如果是POST过滤器出现异常,会跳转到error过滤器,但是与pre和route不同的是,请求不会再到达POST过滤器了。 所有内置过滤器列表: 3.8.3.使用场景场景非常多: 请求鉴权:一般放在pre类型,如果发现没有访问权限,直接就拦截了 异常处理:一般会在error类型和post类型过滤器中结合来处理。 服务调用时长统计:pre和post结合使用。 3.9.自定义过滤器接下来我们来自定义一个过滤器,模拟一个登录的校验。基本逻辑:如果请求中有access-token参数,则认为请求有效,放行。 3.9.1.定义过滤器类 内容: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556@Componentpublic class LoginFilter extends ZuulFilter { /** * 过滤器类型,前置过滤器 * @return */ @Override public String filterType() { return "pre"; } /** * 过滤器的执行顺序 * @return */ @Override public int filterOrder() { return 1; } /** * 该过滤器是否生效 * @return */ @Override public boolean shouldFilter() { return true; } /** * 登陆校验逻辑 * @return * @throws ZuulException */ @Override public Object run() throws ZuulException { // 获取zuul提供的上下文对象 RequestContext context = RequestContext.getCurrentContext(); // 从上下文对象中获取请求对象 HttpServletRequest request = context.getRequest(); // 获取token信息 String token = request.getParameter("access-token"); // 判断 if (StringUtils.isBlank(token)) { // 过滤该请求,不对其进行路由 context.setSendZuulResponse(false); // 设置响应状态码,401 context.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED); // 设置响应信息 context.setResponseBody("{\"status\":\"401\", \"text\":\"request error!\"}"); } // 校验通过,把登陆信息放入上下文信息,继续向后执行 context.set("token", token); return null; }} 3.9.2.测试没有token参数时,访问失败: 添加token参数后: 3.10.负载均衡和熔断Zuul中默认就已经集成了Ribbon负载均衡和Hystix熔断机制。但是所有的超时策略都是走的默认值,比如熔断超时时间只有1S,很容易就触发了。因此建议我们手动进行配置: 1234567hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 2000 # 设置hystrix的超时时间为6000ms]]></content>
<categories>
<category>Spring Cloud</category>
<category>微服务</category>
</categories>
<tags>
<tag>Spring Cloud</tag>
<tag>微服务</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Spring Cloud快速开发入门第01篇]]></title>
<url>%2F2019%2F12%2F11%2FSpring%20Cloud%E5%BF%AB%E9%80%9F%E5%BC%80%E5%8F%91%E5%85%A5%E9%97%A8%E7%AC%AC01%E7%AF%87%2F</url>
<content type="text"><![CDATA[0.学习目标 了解系统架构的演变 了解RPC与Http的区别 知道什么是SpringCloud 独立搭建Eureka注册中心 独立配置Robbin负载均衡 1.系统架构演变随着互联网的发展,网站应用的规模不断扩大。需求的激增,带来的是技术上的压力。系统架构也因此不断的演进、升级、迭代。从单一应用,到垂直拆分,到分布式服务,到SOA,以及现在火热的微服务架构,还有在Google带领下来势汹涌的Service Mesh。我们到底是该乘坐微服务的船只驶向远方,还是偏安一隅得过且过? 其实生活不止眼前的苟且,还有诗和远方。所以我们今天就回顾历史,看一看系统架构演变的历程;把握现在,学习现在最火的技术架构;展望未来,争取成为一名优秀的Java工程师。 1.1.集中式架构当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是影响项目开发的关键。 存在的问题: 代码耦合,开发维护困难 无法针对不同模块进行针对性优化 无法水平扩展 单点容错率低,并发能力差 1.2.垂直拆分当访问量逐渐增大,单一应用无法满足需求,此时为了应对更高的并发和业务需求,我们根据业务功能对系统进行拆分: 优点: 系统拆分实现了流量分担,解决了并发问题 可以针对不同模块进行优化 方便水平扩展,负载均衡,容错率提高 缺点: 系统间相互独立,会有很多重复开发工作,影响开发效率 1.3.分布式服务当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式调用是关键。 优点: 将基础服务进行了抽取,系统间相互调用,提高了代码复用和开发效率 缺点: 系统间耦合度变高,调用关系错综复杂,难以维护 1.4.流动计算架构(SOA)SOA :面向服务的架构 当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键 以前出现了什么问题? 服务越来越多,需要管理每个服务的地址 调用关系错综复杂,难以理清依赖关系 服务过多,服务状态难以管理,无法根据服务情况动态管理 服务治理要做什么? 服务注册中心,实现服务自动注册和发现,无需人为记录服务地址 服务自动订阅,服务列表自动推送,服务调用透明化,无需关心依赖关系 动态监控服务状态监控报告,人为控制服务状态 缺点: 服务间会有依赖关系,一旦某个环节出错会影响较大 服务关系复杂,运维、测试部署困难,不符合DevOps思想 1.5.微服务前面说的SOA,英文翻译过来是面向服务。微服务,似乎也是服务,都是对系统进行拆分。因此两者非常容易混淆,但其实却有一些差别: 微服务的特点: 单一职责:微服务中每一个服务都对应唯一的业务能力,做到单一职责 微:微服务的服务拆分粒度很小,例如一个用户管理就可以作为一个服务。每个服务虽小,但“五脏俱全”。 面向服务:面向服务是说每个服务都要对外暴露Rest风格服务接口API。并不关心服务的技术实现,做到与平台和语言无关,也不限定用什么技术实现,只要提供Rest的接口即可。 自治:自治是说服务间互相独立,互不干扰 团队独立:每个服务都是一个独立的开发团队,人数不能过多。 技术独立:因为是面向服务,提供Rest接口,使用什么技术没有别人干涉 前后端分离:采用前后端分离开发,提供统一Rest接口,后端不用再为PC、移动段开发不同接口 数据库分离:每个服务都使用自己的数据源 部署独立,服务间虽然有调用,但要做到服务重启不影响其它服务。有利于持续集成和持续交付。每个服务都是独立的组件,可复用,可替换,降低耦合,易维护 微服务结构图: 2.服务调用方式2.1.RPC和HTTP无论是微服务还是SOA,都面临着服务间的远程调用。那么服务间的远程调用方式有哪些呢? 常见的远程调用方式有以下2种: RPC:Remote Produce Call远程过程调用,类似的还有RMI。自定义数据格式,基于原生TCP通信,速度快,效率高。早期的webservice,现在热门的dubbo,都是RPC的典型代表 Http:http其实是一种网络传输协议,基于TCP,规定了数据传输的格式。现在客户端浏览器与服务端通信基本都是采用Http协议,也可以用来进行远程服务调用。缺点是消息封装臃肿,优势是对服务的提供和调用方没有任何技术限定,自由灵活,更符合微服务理念。 现在热门的Rest风格,就可以通过http协议来实现。 如果你们公司全部采用Java技术栈,那么使用Dubbo作为微服务架构是一个不错的选择。 相反,如果公司的技术栈多样化,而且你更青睐Spring家族,那么SpringCloud搭建微服务是不二之选。在我们的项目中,我们会选择SpringCloud套件,因此我们会使用Http方式来实现服务间调用。 2.2.Http客户端工具既然微服务选择了Http,那么我们就需要考虑自己来实现对请求和响应的处理。不过开源世界已经有很多的http客户端工具,能够帮助我们做这些事情,例如: HttpClient OKHttp URLConnection 接下来,不过这些不同的客户端,API各不相同 2.3.Spring的RestTemplateSpring提供了一个RestTemplate模板工具类,对基于Http的客户端进行了封装,并且实现了对象与json的序列化和反序列化,非常方便。RestTemplate并没有限定Http的客户端类型,而是进行了抽象,目前常用的3种都有支持: HttpClient OkHttp JDK原生的URLConnection(默认的) 我们导入课前资料提供的demo工程: 首先在项目中注册一个RestTemplate对象,可以在启动类位置注册: 12345678910111213@SpringBootApplicationpublic class HttpDemoApplication { public static void main(String[] args) { SpringApplication.run(HttpDemoApplication.class, args); } @Bean public RestTemplate restTemplate() { return new RestTemplate(); }} 在测试类中直接@Autowired注入: 1234567891011121314@RunWith(SpringRunner.class)@SpringBootTest(classes = HttpDemoApplication.class)public class HttpDemoApplicationTests { @Autowired private RestTemplate restTemplate; @Test public void httpGet() { // 调用springboot案例中的rest接口 User user = this.restTemplate.getForObject("http://localhost/user/1", User.class); System.out.println(user); }} 通过RestTemplate的getForObject()方法,传递url地址及实体类的字节码,RestTemplate会自动发起请求,接收响应,并且帮我们对响应结果进行反序列化。 学习完了Http客户端工具,接下来就可以正式学习微服务了。 3.初识SpringCloud微服务是一种架构方式,最终肯定需要技术架构去实施。 微服务的实现方式很多,但是最火的莫过于Spring Cloud了。为什么? 后台硬:作为Spring家族的一员,有整个Spring全家桶靠山,背景十分强大。 技术强:Spring作为Java领域的前辈,可以说是功力深厚。有强力的技术团队支撑,一般人还真比不了 群众基础好:可以说大多数程序员的成长都伴随着Spring框架,试问:现在有几家公司开发不用Spring?SpringCloud与Spring的各个框架无缝整合,对大家来说一切都是熟悉的配方,熟悉的味道。 使用方便:相信大家都体会到了SpringBoot给我们开发带来的便利,而SpringCloud完全支持SpringBoot的开发,用很少的配置就能完成微服务框架的搭建 3.1.简介SpringCloud是Spring旗下的项目之一,官网地址:http://projects.spring.io/spring-cloud/ Spring最擅长的就是集成,把世界上最好的框架拿过来,集成到自己的项目中。 SpringCloud也是一样,它将现在非常流行的一些技术整合到一起,实现了诸如:配置管理,服务发现,智能路由,负载均衡,熔断器,控制总线,集群状态等等功能。其主要涉及的组件包括: Eureka:服务治理组件,包含服务注册中心,服务注册与发现机制的实现。(服务治理,服务注册/发现) Zuul:网关组件,提供智能路由,访问过滤功能 Ribbon:客户端负载均衡的服务调用组件(客户端负载) Feign:服务调用,给予Ribbon和Hystrix的声明式服务调用组件 (声明式服务调用) Hystrix:容错管理组件,实现断路器模式,帮助服务依赖中出现的延迟和为故障提供强大的容错能力。(熔断、断路器,容错) 架构图: 以上只是其中一部分。 3.2.版本因为Spring Cloud不同其他独立项目,它拥有很多子项目的大项目。所以它的版本是版本名+版本号 (如Angel.SR6)。 版本名:是伦敦的地铁名 版本号:SR(Service Releases)是固定的 ,大概意思是稳定版本。后面会有一个递增的数字。 所以 Edgware.SR3就是Edgware的第3个Release版本。 我们在项目中,会是以Finchley的版本。 其中包含的组件,也都有各自的版本,如下表: Component Edgware.SR3 Finchley.RC1 Finchley.BUILD-SNAPSHOT spring-cloud-aws 1.2.2.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT spring-cloud-bus 1.3.2.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT spring-cloud-cli 1.4.1.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT spring-cloud-commons 1.3.3.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT spring-cloud-contract 1.2.4.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT spring-cloud-config 1.4.3.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT spring-cloud-netflix 1.4.4.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT spring-cloud-security 1.2.2.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT spring-cloud-cloudfoundry 1.1.1.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT spring-cloud-consul 1.3.3.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT spring-cloud-sleuth 1.3.3.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT spring-cloud-stream Ditmars.SR3 Elmhurst.RELEASE Elmhurst.BUILD-SNAPSHOT spring-cloud-zookeeper 1.2.1.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT spring-boot 1.5.10.RELEASE 2.0.1.RELEASE 2.0.0.BUILD-SNAPSHOT spring-cloud-task 1.2.2.RELEASE 2.0.0.RC1 2.0.0.RELEASE spring-cloud-vault 1.1.0.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT spring-cloud-gateway 1.0.1.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT spring-cloud-openfeign 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT 接下来,我们就一一学习SpringCloud中的重要组件。 4.微服务场景模拟首先,我们需要模拟一个服务调用的场景,搭建两个工程:itcast-service-provider(服务提供方)和itcast-service-consumer(服务调用方)。方便后面学习微服务架构 服务提供方:使用mybatis操作数据库,实现对数据的增删改查;并对外提供rest接口服务。 服务消费方:使用restTemplate远程调用服务提供方的rest接口服务,获取数据。 4.1.服务提供者我们新建一个项目:itcast-service-provider,对外提供根据id查询用户的服务。 4.1.1.Spring脚手架创建工程借助于Spring提供的快速搭建工具: next—>填写项目信息: next —> 添加web依赖: 添加mybatis依赖: Next —> 填写项目位置: 生成的项目结构,已经包含了引导类(itcastServiceProviderApplication): 依赖也已经全部自动引入: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>cn.itcast.service</groupId> <artifactId>itcast-service-provider</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>itcast-service-provider</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.6.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- 需要手动引入通用mapper的启动器,spring没有收录该依赖 --> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> <version>2.0.4</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project> 当然,因为要使用通用mapper,所以我们需要手动加一条依赖: 12345<dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> <version>2.0.4</version></dependency> 非常快捷啊! 4.1.2.编写代码 4.1.2.1.配置属性文件,这里我们采用了yaml语法,而不是properties: 123456789server: port: 8081spring: datasource: url: jdbc:mysql://localhost:3306/mybatis #你学习mybatis时,使用的数据库地址 username: root password: rootmybatis: type-aliases-package: cn.itcast.service.pojo 4.1.2.2.实体类123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105@Table(name = "tb_user")public class User implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; // 用户名 private String userName; // 密码 private String password; // 姓名 private String name; // 年龄 private Integer age; // 性别,1男性,2女性 private Integer sex; // 出生日期 private Date birthday; // 创建时间 private Date created; // 更新时间 private Date updated; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public Integer getSex() { return sex; } public void setSex(Integer sex) { this.sex = sex; } public Date getBirthday() { return birthday; } public void setBirthday(Date birthday) { this.birthday = birthday; } public Date getCreated() { return created; } public void setCreated(Date created) { this.created = created; } public Date getUpdated() { return updated; } public void setUpdated(Date updated) { this.updated = updated; }} 4.1.2.3.UserMapper123@Mapperpublic interface UserMapper extends tk.mybatis.mapper.common.Mapper<User>{} 4.1.2.4.UserService12345678910@Servicepublic class UserService { @Autowired private UserMapper userMapper; public User queryById(Long id) { return this.userMapper.selectByPrimaryKey(id); }} 4.1.2.5.UserController添加一个对外查询的接口: 123456789101112@RestController@RequestMapping("user")public class UserController { @Autowired private UserService userService; @GetMapping("{id}") public User queryById(@PathVariable("id") Long id) { return this.userService.queryById(id); }} 4.1.3.启动并测试启动项目,访问接口:http://localhost:8081/user/1 4.2.服务调用者搭建itcast-service-consumer服务消费方工程。 4.2.1.创建工程与上面类似,这里不再赘述,需要注意的是,我们调用itcast-service-provider的解耦获取数据,因此不需要mybatis相关依赖了。 pom: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>cn.itcast.service</groupId> <artifactId>itcast-service-consumer</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>itcast-service-consumer</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.4.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project> 4.2.2.编写代码 首先在引导类中注册RestTemplate: 123456789101112@SpringBootApplicationpublic class ItcastServiceConsumerApplication { @Bean public RestTemplate restTemplate() { return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(ItcastServiceConsumerApplication.class, args); }} 编写配置(application.yml): 12server: port: 80 编写UserController: 123456789101112131415@Controller@RequestMapping("consumer/user")public class UserController { @Autowired private RestTemplate restTemplate; @GetMapping @ResponseBody public User queryUserById(@RequestParam("id") Long id){ User user = this.restTemplate.getForObject("http://localhost:8081/user/" + id, User.class); return user; }} pojo对象(User): 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102public class User implements Serializable { private static final long serialVersionUID = 1L; private Long id; // 用户名 private String userName; // 密码 private String password; // 姓名 private String name; // 年龄 private Integer age; // 性别,1男性,2女性 private Integer sex; // 出生日期 private Date birthday; // 创建时间 private Date created; // 更新时间 private Date updated; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public Integer getSex() { return sex; } public void setSex(Integer sex) { this.sex = sex; } public Date getBirthday() { return birthday; } public void setBirthday(Date birthday) { this.birthday = birthday; } public Date getCreated() { return created; } public void setCreated(Date created) { this.created = created; } public Date getUpdated() { return updated; } public void setUpdated(Date updated) { this.updated = updated; }} 4.2.3.启动测试因为我们没有配置端口,那么默认就是8080,我们访问:http://localhost/consumer/user?id=1 一个简单的远程服务调用案例就实现了。 4.3.有没有问题?简单回顾一下,刚才我们写了什么: itcast-service-provider:一个提供根据id查询用户的微服务。 itcast-service-consumer:一个服务调用者,通过RestTemplate远程调用itcast-service-provider。 存在什么问题? 在consumer中,我们把url地址硬编码到了代码中,不方便后期维护 consumer需要记忆provider的地址,如果出现变更,可能得不到通知,地址将失效 consumer不清楚provider的状态,服务宕机也不知道 provider只有1台服务,不具备高可用性 即便provider形成集群,consumer还需自己实现负载均衡 其实上面说的问题,概括一下就是分布式服务必然要面临的问题: 服务管理 如何自动注册和发现 如何实现状态监管 如何实现动态路由 服务如何实现负载均衡 服务如何解决容灾问题 服务如何实现统一配置 以上的问题,我们都将在SpringCloud中得到答案。 5.Eureka注册中心5.1.认识Eureka首先我们来解决第一问题,服务的管理。 问题分析 在刚才的案例中,itcast-service-provider对外提供服务,需要对外暴露自己的地址。而consumer(调用者)需要记录服务提供者的地址。将来地址出现变更,还需要及时更新。这在服务较少的时候并不觉得有什么,但是在现在日益复杂的互联网环境,一个项目肯定会拆分出十几,甚至数十个微服务。此时如果还人为管理地址,不仅开发困难,将来测试、发布上线都会非常麻烦,这与DevOps的思想是背道而驰的。 网约车 这就好比是 网约车出现以前,人们出门叫车只能叫出租车。一些私家车想做出租却没有资格,被称为黑车。而很多人想要约车,但是无奈出租车太少,不方便。私家车很多却不敢拦,而且满大街的车,谁知道哪个才是愿意载人的。一个想要,一个愿意给,就是缺少引子,缺乏管理啊。 此时滴滴这样的网约车平台出现了,所有想载客的私家车全部到滴滴注册,记录你的车型(服务类型),身份信息(联系方式)。这样提供服务的私家车,在滴滴那里都能找到,一目了然。 此时要叫车的人,只需要打开APP,输入你的目的地,选择车型(服务类型),滴滴自动安排一个符合需求的车到你面前,为你服务,完美! Eureka做什么? Eureka就好比是滴滴,负责管理、记录服务提供者的信息。服务调用者无需自己寻找服务,而是把自己的需求告诉Eureka,然后Eureka会把符合你需求的服务告诉你。 同时,服务提供方与Eureka之间通过“心跳”机制进行监控,当某个服务提供方出现问题,Eureka自然会把它从服务列表中剔除。 这就实现了服务的自动注册、发现、状态监控。 5.2.原理图 基本架构: Eureka:就是服务注册中心(可以是一个集群),对外暴露自己的地址 提供者:启动后向Eureka注册自己信息(地址,提供什么服务) 消费者:向Eureka订阅服务,Eureka会将对应服务的所有提供者地址列表发送给消费者,并且定期更新 心跳(续约):提供者定期通过http方式向Eureka刷新自己的状态 5.3.入门案例5.3.1.搭建EurekaServer接下来我们创建一个项目,启动一个EurekaServer: 依然使用spring提供的快速搭建工具: 选择依赖:EurekaServer-服务注册中心依赖,Eureka Discovery-服务提供方和服务消费方。因为,对于eureka来说:服务提供方和服务消费方都属于客户端 完整的Pom文件: 123456789101112131415161718192021222324252627282930313233343536373839404142434445<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>cn.itcast.eureka</groupId> <artifactId>itcast-eureka</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>itcast-eureka</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.6.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <spring-cloud.version>Finchley.RC2</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project> 编写application.yml配置: 123456789server: port: 10086 # 端口spring: application: name: eureka-server # 应用名称,会在Eureka中显示eureka: client: service-url: # EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的地址。 defaultZone: http://127.0.0.1:${server.port}/eureka 修改引导类,在类上添加@EnableEurekaServer注解: 12345678@SpringBootApplication@EnableEurekaServer // 声明当前springboot应用是一个eureka服务中心public class ItcastEurekaApplication { public static void main(String[] args) { SpringApplication.run(ItcastEurekaApplication.class, args); }} 启动服务,并访问:http://127.0.0.1:10086 5.3.2.注册到Eureka注册服务,就是在服务上添加Eureka的客户端依赖,客户端代码会自动把服务注册到EurekaServer中。 修改itcast-service-provider工程 在pom.xml中,添加springcloud的相关依赖。 在application.yml中,添加springcloud的相关依赖。 在引导类上添加注解,把服务注入到eureka注册中心。 具体操作 5.3.2.1.pom.xml参照itcast-eureka,先添加SpringCloud依赖: 123456789101112<!-- SpringCloud的依赖 --><dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Finchley.SR2</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies></dependencyManagement> 然后是Eureka客户端: 12345<!-- Eureka客户端 --><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency> 完整pom.xml: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>cn.itcast.service</groupId> <artifactId>itcast-service-provider</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>itcast-service-provider</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.6.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> <version>2.0.4</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Finchley.SR1</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement></project> 5.3.2.2.application.yml12345678910111213141516server: port: 8081spring: datasource: url: jdbc:mysql://localhost:3306/heima username: root password: root driverClassName: com.mysql.jdbc.Driver application: name: service-provider # 应用名称,注册到eureka后的服务名称mybatis: type-aliases-package: cn.itcast.service.pojoeureka: client: service-url: # EurekaServer地址 defaultZone: http://127.0.0.1:10086/eureka 注意: 这里我们添加了spring.application.name属性来指定应用名称,将来会作为应用的id使用。 5.3.2.3.引导类在引导类上开启Eureka客户端功能 通过添加@EnableDiscoveryClient来开启Eureka客户端功能 12345678@SpringBootApplication@EnableDiscoveryClientpublic class ItcastServiceProviderApplication { public static void main(String[] args) { SpringApplication.run(ItcastServiceApplication.class, args); }} 重启项目,访问Eureka监控页面查看 我们发现service-provider服务已经注册成功了 5.3.3.从Eureka获取服务接下来我们修改itcast-service-consumer,尝试从EurekaServer获取服务。 方法与消费者类似,只需要在项目中添加EurekaClient依赖,就可以通过服务名称来获取信息了! pom.xml 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>cn.itcast.service</groupId> <artifactId>itcast-service-consumer</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>itcast-service-consumer</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.6.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- Eureka客户端 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> <!-- SpringCloud的依赖 --> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Finchley.SR2</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement></project> 修改配置 123456789server: port: 80spring: application: name: service-consumereureka: client: service-url: defaultZone: http://localhost:10086/eureka 在启动类开启Eureka客户端 12345678910111213@SpringBootApplication@EnableDiscoveryClient // 开启Eureka客户端public class ItcastServiceConsumerApplication { @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(ItcastServiceConsumerApplication.class, args); }} 修改UserController代码,用DiscoveryClient类的方法,根据服务名称,获取服务实例: 123456789101112131415161718192021222324@Controller@RequestMapping("consumer/user")public class UserController { @Autowired private RestTemplate restTemplate; @Autowired private DiscoveryClient discoveryClient; // eureka客户端,可以获取到eureka中服务的信息 @GetMapping @ResponseBody public User queryUserById(@RequestParam("id") Long id){ // 根据服务名称,获取服务实例。有可能是集群,所以是service实例集合 List<ServiceInstance> instances = discoveryClient.getInstances("service-provider"); // 因为只有一个Service-provider。所以获取第一个实例 ServiceInstance instance = instances.get(0); // 获取ip和端口信息,拼接成服务地址 String baseUrl = "http://" + instance.getHost() + ":" + instance.getPort() + "/user/" + id; User user = this.restTemplate.getForObject(baseUrl, User.class); return user; }} 5)Debug跟踪运行: 生成的URL: 访问结果: 5.4.Eureka详解接下来我们详细讲解Eureka的原理及配置。 5.4.1.基础架构Eureka架构中的三个核心角色: 服务注册中心 Eureka的服务端应用,提供服务注册和发现功能,就是刚刚我们建立的itcast-eureka。 服务提供者 提供服务的应用,可以是SpringBoot应用,也可以是其它任意技术实现,只要对外提供的是Rest风格服务即可。本例中就是我们实现的itcast-service-provider。 服务消费者 消费应用从注册中心获取服务列表,从而得知每个服务方的信息,知道去哪里调用服务方。本例中就是我们实现的itcast-service-consumer。 5.4.2.高可用的Eureka ServerEureka Server即服务的注册中心,在刚才的案例中,我们只有一个EurekaServer,事实上EurekaServer也可以是一个集群,形成高可用的Eureka中心。 服务同步 多个Eureka Server之间也会互相注册为服务,当服务提供者注册到Eureka Server集群中的某个节点时,该节点会把服务的信息同步给集群中的每个节点,从而实现数据同步。因此,无论客户端访问到Eureka Server集群中的任意一个节点,都可以获取到完整的服务列表信息。 动手搭建高可用的EurekaServer 我们假设要运行两个EurekaServer的集群,端口分别为:10086和10087。只需要把itcast-eureka启动两次即可。 1)启动第一个eurekaServer,我们修改原来的EurekaServer配置: 123456789server: port: 10086 # 端口spring: application: name: eureka-server # 应用名称,会在Eureka中显示eureka: client: service-url: # 配置其他Eureka服务的地址,而不是自己,比如10087 defaultZone: http://127.0.0.1:10087/eureka 所谓的高可用注册中心,其实就是把EurekaServer自己也作为一个服务进行注册,这样多个EurekaServer之间就能互相发现对方,从而形成集群。因此我们做了以下修改: 把service-url的值改成了另外一台EurekaServer的地址,而不是自己 启动报错,很正常。因为10087服务没有启动: 2)启动第二个eurekaServer,再次修改itcast-eureka的配置: 123456789server: port: 10087 # 端口spring: application: name: eureka-server # 应用名称,会在Eureka中显示eureka: client: service-url: # 配置其他Eureka服务的地址,而不是自己,比如10087 defaultZone: http://127.0.0.1:10086/eureka 注意:idea中一个应用不能启动两次,我们需要重新配置一个启动器: 然后启动即可。 3)访问集群,测试: 4)客户端注册服务到集群 因为EurekaServer不止一个,因此注册服务的时候,service-url参数需要变化: 1234eureka: client: service-url: # EurekaServer地址,多个地址以','隔开 defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka 10086: 10087: 5.4.3.服务提供者服务提供者要向EurekaServer注册服务,并且完成服务续约等工作。 服务注册 服务提供者在启动时,会检测配置属性中的:eureka.client.register-with-eureka=true参数是否正确,事实上默认就是true。如果值确实为true,则会向EurekaServer发起一个Rest请求,并携带自己的元数据信息,Eureka Server会把这些信息保存到一个双层Map结构中。 第一层Map的Key就是服务id,一般是配置中的spring.application.name属性 第二层Map的key是服务的实例id。一般host+ serviceId + port,例如:locahost:service-provider:8081 值则是服务的实例对象,也就是说一个服务,可以同时启动多个不同实例,形成集群。 服务续约 在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告诉EurekaServer:“我还活着”。这个我们称为服务的续约(renew); 有两个重要参数可以修改服务续约的行为: 1234eureka: instance: lease-expiration-duration-in-seconds: 90 lease-renewal-interval-in-seconds: 30 lease-renewal-interval-in-seconds:服务续约(renew)的间隔,默认为30秒 lease-expiration-duration-in-seconds:服务失效时间,默认值90秒 也就是说,默认情况下每个30秒服务会向注册中心发送一次心跳,证明自己还活着。如果超过90秒没有发送心跳,EurekaServer就会认为该服务宕机,会从服务列表中移除,这两个值在生产环境不要修改,默认即可。 但是在开发时,这个值有点太长了,经常我们关掉一个服务,会发现Eureka依然认为服务在活着。所以我们在开发阶段可以适当调小。 1234eureka: instance: lease-expiration-duration-in-seconds: 10 # 10秒即过期 lease-renewal-interval-in-seconds: 5 # 5秒一次心跳 5.4.4.服务消费者 获取服务列表 当服务消费者启动时,会检测eureka.client.fetch-registry=true参数的值,如果为true,则会拉取Eureka Server服务的列表只读备份,然后缓存在本地。并且每隔30秒会重新获取并更新数据。我们可以通过下面的参数来修改: 123eureka: client: registry-fetch-interval-seconds: 5 生产环境中,我们不需要修改这个值。 但是为了开发环境下,能够快速得到服务的最新状态,我们可以将其设置小一点。 5.4.5.失效剔除和自我保护 服务下线 当服务进行正常关闭操作时,它会触发一个服务下线的REST请求给Eureka Server,告诉服务注册中心:“我要下线了”。服务中心接受到请求之后,将该服务置为下线状态。 失效剔除 有些时候,我们的服务提供方并不一定会正常下线,可能因为内存溢出、网络故障等原因导致服务无法正常工作。Eureka Server需要将这样的服务剔除出服务列表。因此它会开启一个定时任务,每隔60秒对所有失效的服务(超过90秒未响应)进行剔除。 可以通过eureka.server.eviction-interval-timer-in-ms参数对其进行修改,单位是毫秒,生产环境不要修改。 这个会对我们开发带来极大的不变,你对服务重启,隔了60秒Eureka才反应过来。开发阶段可以适当调整,比如:10秒 自我保护 我们关停一个服务,就会在Eureka面板看到一条警告: 这是触发了Eureka的自我保护机制。当一个服务未按时进行心跳续约时,Eureka会统计最近15分钟心跳失败的服务实例的比例是否超过了85%。在生产环境下,因为网络延迟等原因,心跳失败实例的比例很有可能超标,但是此时就把服务剔除列表并不妥当,因为服务可能没有宕机。Eureka就会把当前实例的注册信息保护起来,不予剔除。生产环境下这很有效,保证了大多数服务依然可用。 但是这给我们的开发带来了麻烦, 因此开发阶段我们都会关闭自我保护模式:(itcast-eureka) 1234eureka: server: enable-self-preservation: false # 关闭自我保护模式(缺省为打开) eviction-interval-timer-in-ms: 1000 # 扫描失效服务的间隔时间(缺省为60*1000ms) 6.负载均衡Ribbon在刚才的案例中,我们启动了一个itcast-service-provider,然后通过DiscoveryClient来获取服务实例信息,然后获取ip和端口来访问。 但是实际环境中,我们往往会开启很多个itcast-service-provider的集群。此时我们获取的服务列表中就会有多个,到底该访问哪一个呢? 一般这种情况下我们就需要编写负载均衡算法,在多个实例列表中进行选择。 不过Eureka中已经帮我们集成了负载均衡组件:Ribbon,简单修改代码即可使用。 什么是Ribbon: 接下来,我们就来使用Ribbon实现负载均衡。 6.1.启动两个服务实例首先参照itcast-eureka启动两个ItcastServiceProviderApplication实例,一个8081,一个8082。 Eureka监控面板: 6.2.开启负载均衡因为Eureka中已经集成了Ribbon,所以我们无需引入新的依赖,直接修改代码。 修改itcast-service-consumer的引导类,在RestTemplate的配置方法上添加@LoadBalanced注解: 12345@Bean@LoadBalancedpublic RestTemplate restTemplate() { return new RestTemplate();} 修改调用方式,不再手动获取ip和端口,而是直接通过服务名称调用: 123456789101112131415161718192021@Controller@RequestMapping("consumer/user")public class UserController { @Autowired private RestTemplate restTemplate; //@Autowired //private DiscoveryClient discoveryClient; // 注入discoveryClient,通过该客户端获取服务列表 @GetMapping @ResponseBody public User queryUserById(@RequestParam("id") Long id){ // 通过client获取服务提供方的服务列表,这里我们只有一个 // ServiceInstance instance = discoveryClient.getInstances("service-provider").get(0); String baseUrl = "http://service-provider/user/" + id; User user = this.restTemplate.getForObject(baseUrl, User.class); return user; }} 访问页面,查看结果: 完美! 6.3.源码跟踪为什么我们只输入了service名称就可以访问了呢?之前还要获取ip和端口。 显然有人帮我们根据service名称,获取到了服务实例的ip和端口。它就是LoadBalancerInterceptor 在如下代码打断点: 一路源码跟踪:RestTemplate.getForObject —> RestTemplate.execute —> RestTemplate.doExecute: 点击进入AbstractClientHttpRequest.execute —> AbstractBufferingClientHttpRequest.executeInternal —> InterceptingClientHttpRequest.executeInternal —> InterceptingClientHttpRequest.execute: 继续跟入:LoadBalancerInterceptor.intercept方法 继续跟入execute方法:发现获取了8082端口的服务 再跟下一次,发现获取的是8081: 6.4.负载均衡策略Ribbon默认的负载均衡策略是简单的轮询,我们可以测试一下: 编写测试类,在刚才的源码中我们看到拦截中是使用RibbonLoadBalanceClient来进行负载均衡的,其中有一个choose方法,找到choose方法的接口方法,是这样介绍的: 现在这个就是负载均衡获取实例的方法。 我们注入这个类的对象,然后对其测试: 测试内容: 123456789101112131415@RunWith(SpringRunner.class)@SpringBootTest(classes = ItcastServiceConsumerApplication.class)public class LoadBalanceTest { @Autowired private RibbonLoadBalancerClient client; @Test public void testLoadBalance(){ for (int i = 0; i < 100; i++) { ServiceInstance instance = this.client.choose("service-provider"); System.out.println(instance.getHost() + ":" +instance.getPort()); } }} 结果: 符合了我们的预期推测,确实是轮询方式。 我们是否可以修改负载均衡的策略呢? 继续跟踪源码,发现这么一段代码: 我们看看这个rule是谁: 这里的rule默认值是一个RoundRobinRule,看类的介绍: 这不就是轮询的意思嘛。 我们注意到,这个类其实是实现了接口IRule的,查看一下: 定义负载均衡的规则接口。 它有以下实现: SpringBoot也帮我们提供了修改负载均衡规则的配置入口,在itcast-service-consumer的application.yml中添加如下配置: 123456789101112server: port: 80spring: application: name: service-consumereureka: client: service-url: defaultZone: http://127.0.0.1:10086/eurekaservice-provider: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule 格式是:{服务名称}.ribbon.NFLoadBalancerRuleClassName,值就是IRule的实现类。 再次测试,发现结果变成了随机:]]></content>
<categories>
<category>Spring Cloud</category>
<category>微服务</category>
</categories>
<tags>
<tag>Spring Cloud</tag>
<tag>微服务</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Spring Boot快速开发入门]]></title>
<url>%2F2019%2F12%2F11%2FSpring%20Boot%E5%BF%AB%E9%80%9F%E5%BC%80%E5%8F%91%E5%85%A5%E9%97%A8%2F</url>
<content type="text"><![CDATA[0.学习目标 了解SpringBoot的作用 掌握java配置的方式 了解SpringBoot自动配置原理 掌握SpringBoot的基本使用 了解Thymeleaf的基本使用 1. 了解SpringBoot在这一部分,我们主要了解以下3个问题: 什么是SpringBoot 为什么要学习SpringBoot SpringBoot的特点 1.1.什么是SpringBootSpringBoot是Spring项目中的一个子工程,与我们所熟知的Spring-framework 同属于spring的产品: 我们可以看到下面的一段介绍: Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can “just run”. We take an opinionated view of the Spring platform and third-party libraries so you can get started with minimum fuss. Most Spring Boot applications need very little Spring configuration. 翻译一下: Spring Boot你只需要“run”就可以非常轻易的构建独立的、生产级别的spring应用。 我们为spring平台和第三方依赖库提供了一种固定化的使用方式,使你能非常轻松的开始开发你的应用程序。大部分Spring Boot应用只需要很少的配置。 其实人们把Spring Boot称为搭建程序的脚手架。其最主要作用就是帮我们快速的构建庞大的spring项目,并且尽可能的减少一切xml配置,做到开箱即用,迅速上手,让我们关注于业务而非配置。 我们可以使用SpringBoot创建java应用,并使用java –jar 启动它,就能得到一个生产级别的web工程。 1.2.为什么要学习SpringBootjava一直被人诟病的一点就是臃肿、麻烦。当我们还在辛苦的搭建项目时,可能Python程序员已经把功能写好了,究其原因主要是两点: 复杂的配置 项目各种配置其实是开发时的损耗, 因为在思考 Spring 特性配置和解决业务问题之间需要进行思维切换,所以写配置挤占了写应用程序逻辑的时间。 混乱的依赖管理 项目的依赖管理也是件吃力不讨好的事情。决定项目里要用哪些库就已经够让人头痛的了,你还要知道这些库的哪个版本和其他库不会有冲突,这也是件棘手的问题。并且,依赖管理也是一种损耗,添加依赖不是写应用程序代码。一旦选错了依赖的版本,随之而来的不兼容问题毫无疑问会是生产力杀手。 而SpringBoot让这一切成为过去! 1.3.SpringBoot的特点Spring Boot 主要特征是: 创建独立的spring应用程序 直接内嵌tomcat、jetty和undertow(不需要打包成war包部署) 提供了固定化的“starter”配置,以简化构建配置 尽可能的自动配置spring和第三方库 提供产品级的功能,如:安全指标、运行状况监测和外部化配置等 绝对不会生成代码,并且不需要XML配置 总之,Spring Boot为所有 Spring 的开发者提供一个开箱即用的、非常快速的、广泛接受的入门体验 更多细节,大家可以到官网查看。 2.快速入门接下来,我们就来利用SpringBoot搭建一个web工程,体会一下SpringBoot的魅力所在! 环境要求: 2.1.创建工程我们先新建一个空的demo工程,如下: 创建以moduel: 填写坐标信息: 目录结构: 创建完成后的目录结构: 2.2.引入依赖看到这里很多同学会有疑惑,前面说传统开发的问题之一就是依赖管理混乱,怎么这里我们还需要管理依赖呢?难道SpringBoot不帮我们管理吗? 别着急,现在我们的项目与SpringBoot还没有什么关联。SpringBoot提供了一个名为spring-boot-starter-parent的工程,里面已经对各种常用依赖(并非全部)的版本进行了管理,我们的项目需要以这个项目为父工程,这样我们就不用操心依赖的版本问题了,需要什么依赖,直接引入坐标即可! 123456789101112131415161718192021222324<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>cn.itcast.springboot</groupId> <artifactId>itcast-springboot</artifactId> <version>1.0-SNAPSHOT</version> <!-- 所有的springboot的工程都以spring父工程为父工程 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.6.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies></project> 2.3.编写HelloController 代码: 12345678910111213@RestController@EnableAutoConfigurationpublic class HelloController { @GetMapping("show") public String test(){ return "hello Spring Boot!"; } public static void main(String[] args) { SpringApplication.run(HelloController.class, args); }} 2.4.启动测试 bingo!访问成功! 2.5.详解入门工程中:pom.xml里引入了启动器的概念以@EnableAutoConfiguration注解。 2.5.1.启动器为了让SpringBoot帮我们完成各种自动配置,我们必须引入SpringBoot提供的自动配置依赖,我们称为启动器。spring-boot-starter-parent工程将依赖关系声明为一个或者多个启动器,我们可以根据项目需求引入相应的启动器,因为我们是web项目,这里我们引入web启动器: 123456<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency></dependencies> 需要注意的是,我们并没有在这里指定版本信息。因为SpringBoot的父工程已经对版本进行了管理了。 这个时候,我们会发现项目中多出了大量的依赖: 这些都是SpringBoot根据spring-boot-starter-web这个依赖自动引入的,而且所有的版本都已经管理好,不会出现冲突。 2.5.2.@EnableAutoConfiguration关于这个注解,官网上有一段说明: Enable auto-configuration of the Spring Application Context, attempting to guess and configure beans that you are likely to need. Auto-configuration classes are usually applied based on your classpath and what beans you have defined. 简单翻译以下: 开启spring应用程序的自动配置,SpringBoot基于你所添加的依赖和你自己定义的bean,试图去猜测并配置你想要的配置。比如我们引入了spring-boot-starter-web,而这个启动器中帮我们添加了tomcat、SpringMVC的依赖。此时自动配置就知道你是要开发一个web应用,所以就帮你完成了web及SpringMVC的默认配置了! 总结,SpringBoot内部对大量的第三方库或Spring内部库进行了默认配置,这些配置是否生效,取决于我们是否引入了对应库所需的依赖,如果有那么默认配置就会生效。 所以,我们使用SpringBoot构建一个项目,只需要引入所需依赖,配置就可以交给SpringBoot处理了。 2.6.优化入门程序现在工程中只有一个Controller,可以这么玩;那么如果有多个Controller,怎么办呢? 添加Hello2Controller: 代码: 123456789@RestControllerpublic class Hello2Controller { @GetMapping("show2") public String test(){ return "hello Spring Boot2!"; }} 启动重新启动,访问show2测试,失败: 难道要在每一个Controller中都添加一个main方法和@EnableAutoConfiguration注解,这样启动一个springboot程序也太麻烦了。也无法同时启动多个Controller,因为每个main方法都监听8080端口。所以,一个springboot程序应该只有一个springboot的main方法。 所以,springboot程序引入了一个全局的引导类。 2.5.1.添加引导类通常请求下,我们在一个springboot工程中都会在基包下创建一个引导类,一些springboot的全局注解(@EnableAutoConfiguration注解)以及springboot程序的入口main方法都放在该类中。 在springboot的程序的基包下(引导类和Controller包在同级目录下),创建TestApplication.class: 内容如下: 1234567@EnableAutoConfigurationpublic class TestApplication { public static void main(String[] args) { SpringApplication.run(TestApplication.class, args); }} 并修改HelloController,去掉main方法及@EnableAutoConfiguration: 12345678@RestControllerpublic class HelloController { @GetMapping("show") public String test(){ return "hello Spring Boot!"; }} 启动引导类,访问show测试: 发现所有的Controller都不能访问,为什么? 回想以前程序,我们在配置文件中添加了注解扫描,它能扫描指定包下的所有Controller,而现在并没有。怎么解决——@ComponentScan注解 2.5.2.@ComponentScanspring框架除了提供配置方式的注解扫描<context:component-scan />,还提供了注解方式的注解扫描@ComponentScan。 在TestApplication.class中,使用@ComponentScan注解: 123456789@EnableAutoConfiguration@ComponentScanpublic class TestApplication { public static void main(String[] args) { SpringApplication.run(TestApplication.class, args); }} 重新启动,访问show或者show2: 我们跟进该注解的源码,并没有看到什么特殊的地方。我们查看注释: 大概的意思: 配置组件扫描的指令。提供了类似与<context:component-scan>标签的作用 通过basePackageClasses或者basePackages属性来指定要扫描的包。如果没有指定这些属性,那么将从声明这个注解的类所在的包开始,扫描包及子包 而我们的@ComponentScan注解声明的类就是main函数所在的启动类,因此扫描的包是该类所在包及其子包。一般启动类会放在一个比较浅的包目录中。 2.5.3.@SpringBootApplication我们现在的引导类中使用了@EnableAutoConfiguration和@ComponentScan注解,有点麻烦。springboot提供了一种简便的玩法:@SpringBootApplication注解 使用@SpringBootApplication改造TestApplication: 12345678@SpringBootApplicationpublic class TestApplication { public static void main(String[] args) { SpringApplication.run(TestApplication.class, args); }} 点击进入,查看源码: 发现@SpringBootApplication其实是一个组合注解,这里重点的注解有3个: @SpringBootConfiguration @EnableAutoConfiguration:开启自动配置 @ComponentScan:开启注解扫描 2.5.4.@SpringBootConfiguration@SpringBootConfiguration注解的源码: 我们继续点击查看源码: 通过这段我们可以看出,在这个注解上面,又有一个@Configuration注解。通过上面的注释阅读我们知道:这个注解的作用就是声明当前类是一个配置类,然后Spring会自动扫描到添加了@Configuration的类,并且读取其中的配置信息。而@SpringBootConfiguration是来声明当前类是SpringBoot应用的配置类,项目中只能有一个。所以一般我们无需自己添加。 3.默认配置原理springboot的默认配置方式和我们之前玩的配置方式不太一样,没有任何的xml。那么如果自己要新增配置该怎么办?比如我们要配置一个数据库连接池,以前会这么玩: 1234567<!-- 配置连接池 --><bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /></bean> 现在该怎么做呢? 3.1.回顾历史事实上,在Spring3.0开始,Spring官方就已经开始推荐使用java配置来代替传统的xml配置了,我们不妨来回顾一下Spring的历史: Spring1.0时代 在此时因为jdk1.5刚刚出来,注解开发并未盛行,因此一切Spring配置都是xml格式,想象一下所有的bean都用xml配置,细思极恐啊,心疼那个时候的程序员2秒 Spring2.0时代 Spring引入了注解开发,但是因为并不完善,因此并未完全替代xml,此时的程序员往往是把xml与注解进行结合,貌似我们之前都是这种方式。 Spring3.0及以后 3.0以后Spring的注解已经非常完善了,因此Spring推荐大家使用完全的java配置来代替以前的xml,不过似乎在国内并未推广盛行。然后当SpringBoot来临,人们才慢慢认识到java配置的优雅。 有句古话说的好:拥抱变化,拥抱未来。所以我们也应该顺应时代潮流,做时尚的弄潮儿,一起来学习下java配置的玩法。 3.2.尝试java配置java配置主要靠java类和一些注解来达到和xml配置一样的效果,比较常用的注解有: @Configuration:声明一个类作为配置类,代替xml文件 @Bean:声明在方法上,将方法的返回值加入Bean容器,代替<bean>标签 @Value:属性注入 @PropertySource:指定外部属性文件。 我们接下来用java配置来尝试实现连接池配置 3.2.1.引入依赖首先在pom.xml中,引入Druid连接池依赖: 12345<dependency> <groupId>com.github.drtrang</groupId> <artifactId>druid-spring-boot2-starter</artifactId> <version>1.1.10</version></dependency> 3.2.2.添加jdbc.properties1234jdbc.driverClassName=com.mysql.jdbc.Driverjdbc.url=jdbc:mysql://127.0.0.1:3306/leyoujdbc.username=rootjdbc.password=123 3.2.3.配置数据源创建JdbcConfiguration类: 1234567891011121314151617181920212223@Configuration@PropertySource("classpath:jdbc.properties")public class JdbcConfiguration { @Value("${jdbc.url}") String url; @Value("${jdbc.driverClassName}") String driverClassName; @Value("${jdbc.username}") String username; @Value("${jdbc.password}") String password; @Bean public DataSource dataSource() { DruidDataSource dataSource = new DruidDataSource(); dataSource.setUrl(url); dataSource.setDriverClassName(driverClassName); dataSource.setUsername(username); dataSource.setPassword(password); return dataSource; }} 解读: @Configuration:声明JdbcConfiguration是一个配置类。 @PropertySource:指定属性文件的路径是:classpath:jdbc.properties 通过@Value为属性注入值。 通过@Bean将 dataSource()方法声明为一个注册Bean的方法,Spring会自动调用该方法,将方法的返回值加入Spring容器中。相当于以前的bean标签 然后就可以在任意位置通过@Autowired注入DataSource了! 3.2.4.测试我们在HelloController中测试: 123456789101112@RestControllerpublic class HelloController { @Autowired private DataSource dataSource; @GetMapping("show") public String test(){ return "hello Spring Boot!"; }} 在test方法中打一个断点,然后Debug运行并查看: 属性注入成功了! 3.3.SpringBoot的属性注入在上面的案例中,我们实验了java配置方式。不过属性注入使用的是@Value注解。这种方式虽然可行,但是不够强大,因为它只能注入基本类型值。 在SpringBoot中,提供了一种新的属性注入方式,支持各种java基本数据类型及复杂类型的注入。 1)新建JdbcProperties,用来进行属性注入: 代码: 123456789@ConfigurationProperties(prefix = "jdbc")public class JdbcProperties { private String url; private String driverClassName; private String username; private String password; // ... 略 // getters 和 setters} 在类上通过@ConfigurationProperties注解声明当前类为属性读取类 prefix="jdbc"读取属性文件中,前缀为jdbc的值。 在类上定义各个属性,名称必须与属性文件中jdbc.后面部分一致,并且必须具有getter和setter方法 需要注意的是,这里我们并没有指定属性文件的地址,SpringBoot默认会读取文件名为application.properties的资源文件,所以我们把jdbc.properties名称改为application.properties 2)在JdbcConfiguration中使用这个属性: 通过@EnableConfigurationProperties(JdbcProperties.class)来声明要使用JdbcProperties这个类的对象 然后你可以通过以下方式在JdbcConfiguration类中注入JdbcProperties: @Autowired注入 123456789101112131415161718@Configuration@EnableConfigurationProperties(JdbcProperties.class)public class JdbcConfiguration { @Autowired private JdbcProperties jdbcProperties; @Bean public DataSource dataSource() { DruidDataSource dataSource = new DruidDataSource(); dataSource.setUrl(jdbcProperties.getUrl()); dataSource.setDriverClassName(jdbcProperties.getDriverClassName()); dataSource.setUsername(jdbcProperties.getUsername()); dataSource.setPassword(jdbcProperties.getPassword()); return dataSource; }} 构造函数注入 12345678910111213141516@Configuration@EnableConfigurationProperties(JdbcProperties.class)public class JdbcConfiguration { private JdbcProperties jdbcProperties; public JdbcConfiguration(JdbcProperties jdbcProperties){ this.jdbcProperties = jdbcProperties; } @Bean public DataSource dataSource() { // 略 }} @Bean方法的参数注入 123456789@Configuration@EnableConfigurationProperties(JdbcProperties.class)public class JdbcConfiguration { @Bean public DataSource dataSource(JdbcProperties jdbcProperties) { // ... }} 本例中,我们采用第三种方式。 3)测试结果: 大家会觉得这种方式似乎更麻烦了,事实上这种方式有更强大的功能,也是SpringBoot推荐的注入方式。两者对比关系: 优势: Relaxed binding:松散绑定 不严格要求属性文件中的属性名与成员变量名一致。支持驼峰,中划线,下划线等等转换,甚至支持对象引导。比如:user.friend.name:代表的是user对象中的friend属性中的name属性,显然friend也是对象。@value注解就难以完成这样的注入方式。 meta-data support:元数据支持,帮助IDE生成属性提示(写开源框架会用到)。 3.4.更优雅的注入事实上,如果一段属性只有一个Bean需要使用,我们无需将其注入到一个类(JdbcProperties)中。而是直接在需要的地方声明即可: 1234567891011@Configurationpublic class JdbcConfiguration { @Bean // 声明要注入的属性前缀,SpringBoot会自动把相关属性通过set方法注入到DataSource中 @ConfigurationProperties(prefix = "jdbc") public DataSource dataSource() { DruidDataSource dataSource = new DruidDataSource(); return dataSource; }} 我们直接把@ConfigurationProperties(prefix = "jdbc")声明在需要使用的@Bean的方法上,然后SpringBoot就会自动调用这个Bean(此处是DataSource)的set方法,然后完成注入。使用的前提是:该类必须有对应属性的set方法! 我们将jdbc的url改成:/heima,再次测试: 3.5.SpringBoot中的默认配置通过刚才的学习,我们知道@EnableAutoConfiguration会开启SpringBoot的自动配置,并且根据你引入的依赖来生效对应的默认配置。那么问题来了: 这些默认配置是怎么配置的,在哪里配置的呢? 为何依赖引入就会触发配置呢? 这些默认配置的属性来自哪里呢? 其实在我们的项目中,已经引入了一个依赖:spring-boot-autoconfigure,其中定义了大量自动配置类: 还有: 非常多,几乎涵盖了现在主流的开源框架,例如: redis jms amqp jdbc jackson mongodb jpa solr elasticsearch … 等等 我们来看一个我们熟悉的,例如SpringMVC,查看mvc 的自动配置类: 打开WebMvcAutoConfiguration: 我们看到这个类上的4个注解: @Configuration:声明这个类是一个配置类 @ConditionalOnWebApplication(type = Type.SERVLET) ConditionalOn,翻译就是在某个条件下,此处就是满足项目的类是是Type.SERVLET类型,也就是一个普通web工程,显然我们就是 @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class }) 这里的条件是OnClass,也就是满足以下类存在:Servlet、DispatcherServlet、WebMvcConfigurer,其中Servlet只要引入了tomcat依赖自然会有,后两个需要引入SpringMVC才会有。这里就是判断你是否引入了相关依赖,引入依赖后该条件成立,当前类的配置才会生效! @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) 这个条件与上面不同,OnMissingBean,是说环境中没有指定的Bean这个才生效。其实这就是自定义配置的入口,也就是说,如果我们自己配置了一个WebMVCConfigurationSupport的类,那么这个默认配置就会失效! 接着,我们查看该类中定义了什么: 视图解析器: 处理器适配器(HandlerAdapter): 还有很多,这里就不一一截图了。 另外,这些默认配置的属性来自哪里呢? 我们看到,这里通过@EnableAutoConfiguration注解引入了两个属性:WebMvcProperties和ResourceProperties。 我们查看这两个属性类: 找到了内部资源视图解析器的prefix和suffix属性。 ResourceProperties中主要定义了静态资源(.js,.html,.css等)的路径: 如果我们要覆盖这些默认属性,只需要在application.properties中定义与其前缀prefix和字段名一致的属性即可。 3.6.总结SpringBoot为我们提供了默认配置,而默认配置生效的条件一般有两个: 你引入了相关依赖 你自己没有配置 1)启动器 之所以,我们如果不想配置,只需要引入依赖即可,而依赖版本我们也不用操心,因为只要引入了SpringBoot提供的stater(启动器),就会自动管理依赖及版本了。 因此,玩SpringBoot的第一件事情,就是找启动器,SpringBoot提供了大量的默认启动器,参考课前资料中提供的《SpringBoot启动器.txt》 2)全局配置 另外,SpringBoot的默认配置,都会读取默认属性,而这些属性可以通过自定义application.properties文件来进行覆盖。这样虽然使用的还是默认配置,但是配置中的值改成了我们自定义的。 因此,玩SpringBoot的第二件事情,就是通过application.properties来覆盖默认属性值,形成自定义配置。我们需要知道SpringBoot的默认属性key,非常多,参考课前资料提供的:《SpringBoot全局属性.md》 4.SpringBoot实战接下来,我们来看看如何用SpringBoot来玩转以前的SSM,我们沿用之前讲解SSM用到的数据库tb_user和实体类User 4.1.创建工程 4.2.编写基本代码 pom.xml: 12345678910111213141516171819202122232425<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>cn.itcast.user</groupId> <artifactId>itcast-user</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.6.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies></project> 参照上边的项目,编写引导类: 1234567@SpringBootApplicationpublic class UserApplication { public static void main(String[] args) { SpringApplication.run(UserApplication.class); }} 编写UserController: 123456789@RestController@RequestMapping("user")public class UserController { @GetMapping("hello") public String test(){ return "hello ssm"; }} 4.3.整合SpringMVC虽然默认配置已经可以使用SpringMVC了,不过我们有时候需要进行自定义配置。 4.3.1.修改端口添加全局配置文件:application.properties 端口通过以下方式配置 12# 映射端口server.port=80 重启服务后测试: 4.3.2.访问静态资源现在,我们的项目是一个jar工程,那么就没有webapp,我们的静态资源该放哪里呢? 回顾我们上面看的源码,有一个叫做ResourceProperties的类,里面就定义了静态资源的默认查找路径: 默认的静态资源路径为: classpath:/META-INF/resources/ classpath:/resources/ classpath:/static/ classpath:/public/ 只要静态资源放在这些目录中任何一个,SpringMVC都会帮我们处理。 我们习惯会把静态资源放在classpath:/static/目录下。我们创建目录,并且添加一些静态资源: 重启项目后测试: 4.3.3.添加拦截器拦截器也是我们经常需要使用的,在SpringBoot中该如何配置呢? 拦截器不是一个普通属性,而是一个类,所以就要用到java配置方式了。在SpringBoot官方文档中有这么一段说明: If you want to keep Spring Boot MVC features and you want to add additional MVC configuration (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc. If you wish to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, you can declare a WebMvcRegistrationsAdapter instance to provide such components. If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc. 翻译: 如果你想要保持Spring Boot 的一些默认MVC特征,同时又想自定义一些MVC配置(包括:拦截器,格式化器, 视图控制器、消息转换器 等等),你应该让一个类实现WebMvcConfigurer,并且添加@Configuration注解,但是千万不要加@EnableWebMvc注解。如果你想要自定义HandlerMapping、HandlerAdapter、ExceptionResolver等组件,你可以创建一个WebMvcRegistrationsAdapter实例 来提供以上组件。 如果你想要完全自定义SpringMVC,不保留SpringBoot提供的一切特征,你可以自己定义类并且添加@Configuration注解和@EnableWebMvc注解 总结:通过实现WebMvcConfigurer并添加@Configuration注解来实现自定义部分SpringMvc配置。 实现如下: 首先我们定义一个拦截器: 123456789101112131415161718@Componentpublic class MyInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("preHandle method is running!"); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("postHandle method is running!"); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("afterCompletion method is running!"); }} 然后定义配置类,注册拦截器: 123456789101112131415@Configurationpublic class MvcConfiguration implements WebMvcConfigurer { @Autowired private HandlerInterceptor myInterceptor; /** * 重写接口中的addInterceptors方法,添加自定义拦截器 * @param registry */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(myInterceptor).addPathPatterns("/**"); }} 接下来运行并查看日志: 123preHandle method is running!postHandle method is running!afterCompletion method is running! 你会发现日志中只有这些打印信息,springMVC的日志信息都没有,因为springMVC记录的log级别是debug,springboot默认是显示info以上,我们需要进行配置。 SpringBoot通过logging.level.*=debug来配置日志级别,*填写包名 12# 设置org.springframework包的日志级别为debuglogging.level.org.springframework=debug 再次运行查看: 4.4.整合连接池jdbc连接池是spring配置中的重要一环,在SpringBoot中该如何处理呢? 答案是不需要处理,我们只要找到SpringBoot提供的启动器即可: 在pom.xml中引入jdbc的启动器: 12345678910<!--jdbc的启动器,默认使用HikariCP连接池--><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId></dependency><!--不要忘记数据库驱动,因为springboot不知道我们使用的什么数据库,这里选择mysql--><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId></dependency> SpringBoot已经自动帮我们引入了一个连接池: HikariCP应该是目前速度最快的连接池了,我们看看它与c3p0的对比: 因此,我们只需要指定连接池参数即可: 12345678910# 连接四大参数spring.datasource.url=jdbc:mysql://localhost:3306/heimaspring.datasource.username=rootspring.datasource.password=root# 可省略,SpringBoot自动推断spring.datasource.driverClassName=com.mysql.jdbc.Driverspring.datasource.hikari.idle-timeout=60000spring.datasource.hikari.maximum-pool-size=30spring.datasource.hikari.minimum-idle=10 当然,如果你更喜欢Druid连接池,也可以使用Druid官方提供的启动器: 123456<!-- Druid连接池 --><dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.6</version></dependency> 而连接信息的配置与上面是类似的,只不过在连接池特有属性上,方式略有不同: 12345678910#初始化连接数spring.datasource.druid.initial-size=1#最小空闲连接spring.datasource.druid.min-idle=1#最大活动连接spring.datasource.druid.max-active=20#获取连接时测试是否可用spring.datasource.druid.test-on-borrow=true#监控页面启动spring.datasource.druid.stat-view-servlet.allow=true 4.5.整合mybatis4.5.1.mybatisSpringBoot官方并没有提供Mybatis的启动器,不过Mybatis官方自己实现了: 123456<!--mybatis --><dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version></dependency> 配置,基本没有需要配置的: 1234# mybatis 别名扫描mybatis.type-aliases-package=cn.itcast.pojo# mapper.xml文件位置,如果没有映射文件,请注释掉mybatis.mapper-locations=classpath:mappers/*.xml 需要注意,这里没有配置mapper接口扫描包,因此我们需要给每一个Mapper接口添加@Mapper注解,才能被识别。 123@Mapperpublic interface UserMapper {} user对象参照课前资料,需要通用mapper的注解: 接下来,就去集成通用mapper。 4.5.2.通用mapper通用Mapper的作者也为自己的插件编写了启动器,我们直接引入即可: 123456<!-- 通用mapper --><dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> <version>2.0.2</version></dependency> 不需要做任何配置就可以使用了。 123@Mapperpublic interface UserMapper extends tk.mybatis.mapper.common.Mapper<User>{} 4.6.整合事务其实,我们引入jdbc或者web的启动器,就已经引入事务相关的依赖及默认配置了 至于事务,SpringBoot中通过注解来控制。就是我们熟知的@Transactional 123456789101112131415@Servicepublic class UserService { @Autowired private UserMapper userMapper; public User queryById(Long id){ return this.userMapper.selectByPrimaryKey(id); } @Transactional public void deleteById(Long id){ this.userMapper.deleteByPrimaryKey(id); }} 4.7.启动测试在UserController中添加测试方法,内容: 1234567891011121314151617@RestController@RequestMapping("user")public class UserController { @Autowired private UserService userService; @GetMapping("{id}") public User queryUserById(@PathVariable("id")Long id){ return this.userService.queryById(id); } @GetMapping("hello") public String test(){ return "hello ssm"; }} 我们启动项目,查看: 4.8.完整项目结构 完整的pom.xml: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>cn.itcast.user</groupId> <artifactId>itcast-user</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.6.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--jdbc的启动器,默认使用HikariCP连接池--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!--不要忘记数据库驱动,因为springboot不知道我们使用的什么数据库,这里选择mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--mybatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <!-- 通用mapper --> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> <version>2.0.2</version> </dependency> </dependencies></project> 完整的application.properties: 123456789101112server.port=80logging.level.org.springframework=debugspring.datasource.url=jdbc:mysql://localhost:3306/heimaspring.datasource.username=rootspring.datasource.password=root# mybatis 别名扫描mybatis.type-aliases-package=cn.itcast.pojo# mapper.xml文件位置,如果没有映射文件,请注释掉# mybatis.mapper-locations=classpath:mappers/*.xml 5.Thymeleaf快速入门SpringBoot并不推荐使用jsp,但是支持一些模板引擎技术: 以前大家用的比较多的是Freemarker,但是我们今天的主角是Thymeleaf! 5.1.为什么是Thymeleaf?简单说, Thymeleaf 是一个跟 Velocity、FreeMarker 类似的模板引擎,它可以完全替代 JSP 。相较于其他的模板引擎,它有如下四个极吸引人的特点: 动静结合:Thymeleaf 在有网络和无网络的环境下皆可运行,即它可以让美工在浏览器查看页面的静态效果,也可以让程序员在服务器查看带数据的动态页面效果。这是由于它支持 html 原型,然后在 html 标签里增加额外的属性来达到模板+数据的展示方式。浏览器解释 html 时会忽略未定义的标签属性,所以 thymeleaf 的模板可以静态地运行;当有数据返回到页面时,Thymeleaf 标签会动态地替换掉静态内容,使页面动态显示。 开箱即用:它提供标准和spring标准两种方言,可以直接套用模板实现JSTL、 OGNL表达式效果,避免每天套模板、改jstl、改标签的困扰。同时开发人员也可以扩展和创建自定义的方言。 多方言支持:Thymeleaf 提供spring标准方言和一个与 SpringMVC 完美集成的可选模块,可以快速的实现表单绑定、属性编辑器、国际化等功能。 与SpringBoot完美整合,SpringBoot提供了Thymeleaf的默认配置,并且为Thymeleaf设置了视图解析器,我们可以像以前操作jsp一样来操作Thymeleaf。代码几乎没有任何区别,就是在模板语法上有区别。 接下来,我们就通过入门案例来体会Thymeleaf的魅力: 5.2.提供数据编写一个controller方法,返回一些用户数据,放入模型中,将来在页面渲染 123456789@GetMapping("/all")public String all(ModelMap model) { // 查询用户 List<User> users = this.userService.queryAll(); // 放入模型 model.addAttribute("users", users); // 返回模板名称(就是classpath:/templates/目录下的html文件名) return "users";} 5.3.引入启动器直接引入启动器: 1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId></dependency> SpringBoot会自动为Thymeleaf注册一个视图解析器: 与解析JSP的InternalViewResolver类似,Thymeleaf也会根据前缀和后缀来确定模板文件的位置: 默认前缀:classpath:/templates/ 默认后缀:.html 所以如果我们返回视图:users,会指向到 classpath:/templates/users.html 一般我们无需进行修改,默认即可。 5.4.静态页面根据上面的文档介绍,模板默认放在classpath下的templates文件夹,我们新建一个html文件放入其中: 编写html模板,渲染模型中的数据: 注意,把html 的名称空间,改成:xmlns:th="http://www.thymeleaf.org" 会有语法提示 1234567891011121314151617181920212223242526272829303132333435<!DOCTYPE html><html xmlns:th="http://www.thymeleaf.org"><head> <meta charset="UTF-8"> <title>首页</title> <style type="text/css"> table {border-collapse: collapse; font-size: 14px; width: 80%; margin: auto} table, th, td {border: 1px solid darkslategray;padding: 10px} </style></head><body><div style="text-align: center"> <span style="color: darkslategray; font-size: 30px">欢迎光临!</span> <hr/> <table class="list"> <tr> <th>id</th> <th>姓名</th> <th>用户名</th> <th>年龄</th> <th>性别</th> <th>生日</th> </tr> <tr th:each="user : ${users}"> <td th:text="${user.id}">1</td> <td th:text="${user.name}">张三</td> <td th:text="${user.userName}">zhangsan</td> <td th:text="${user.age}">20</td> <td th:text="${user.sex}">男</td> <td th:text="${user.birthday}">1980-02-30</td> </tr> </table></div></body></html> 我们看到这里使用了以下语法: ${} :这个类似与el表达式,但其实是ognl的语法,比el表达式更加强大 th-指令:th-是利用了Html5中的自定义属性来实现的。如果不支持H5,可以用data-th-来代替 th:each:类似于c:foreach 遍历集合,但是语法更加简洁 th:text:声明标签中的文本 例如<td th-text='${user.id}'>1</td>,如果user.id有值,会覆盖默认的1 如果没有值,则会显示td中默认的1。这正是thymeleaf能够动静结合的原因,模板解析失败不影响页面的显示效果,因为会显示默认值! 5.5.测试接下来,我们打开页面测试一下: 5.6.模板缓存Thymeleaf会在第一次对模板解析之后进行缓存,极大的提高了并发处理能力。但是这给我们开发带来了不便,修改页面后并不会立刻看到效果,我们开发阶段可以关掉缓存使用: 12# 开发阶段关闭thymeleaf的模板缓存spring.thymeleaf.cache=false 注意: 在Idea中,我们需要在修改页面后按快捷键:`Ctrl + Shift + F9` 对项目进行rebuild才可以。 eclipse中没有测试过。 我们可以修改页面,测试一下。]]></content>
<categories>
<category>Spring Boot</category>
<category>微服务</category>
</categories>
<tags>
<tag>微服务</tag>
<tag>Spring Boot</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Hexo个人博客一些写作标签]]></title>
<url>%2F2019%2F12%2F08%2FHexo%E4%B8%AA%E4%BA%BA%E5%8D%9A%E5%AE%A2%E4%B8%80%E4%BA%9B%E5%86%99%E4%BD%9C%E6%A0%87%E7%AD%BE%2F</url>
<content type="text"><![CDATA[[TOC] 一、代码标签代码:123{% codeblock lang:command %}这是一行代码{% endcodeblock %} 效果: 1这是一行代码 1这是一行代码 二、文本居中标签 代码: 123{% cq %}四郎,那年杏花微雨,你说你是果郡王。原来自那时起,一切便都是错的。{% endcq %} 效果: 四郎,那年杏花微雨,你说你是果郡王。原来自那时起,一切便都是错的。 三、note标签 _config 文件配置关键字:note, 我的配置如下: style: flaticons: trueborder_radius: 3 代码: 1234567891011121314151617181920212223{% note default %}default{% endnote %}{% note primary %}primary{% endnote %}{% note success %}success{% endnote %}{% note info %}info{% endnote %}{% note warning %}warning{% endnote %}{% note danger %}danger{% endnote %} 效果: default primary success info warning danger 四、label标签 _config 文件配置关键字:Label , 需要用的话把值设为true即可。 代码: 1234567891011{% label default@默认 %}{% label primary@主要 %}{% label success@成功 %}{% label info@信息 %}{% label warning@警告 %}{% label danger@危险 %} 效果: 默认 主要 成功 信息 警告 危险 Lruihao博客 Sanarous的个人博客]]></content>
<categories>
<category>博客</category>
</categories>
<tags>
<tag>博客</tag>
<tag>写作标签</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Hexo个人博客自定义友链页面]]></title>
<url>%2F2019%2F12%2F08%2FHexo%E4%B8%AA%E4%BA%BA%E5%8D%9A%E5%AE%A2%E8%87%AA%E5%AE%9A%E4%B9%89%E5%8F%8B%E9%93%BE%E9%A1%B5%E9%9D%A2%2F</url>
<content type="text"><![CDATA[[TOC] 一、常规操作:在主题配置文件中添加在next主题的配置文件\themes\next_config.yml 中添加友链12345678910# Blog rollslinks_icon: linklinks_title: Links# links_layout: blocklinks_layout: inlinelinks: 博采众长: https://lruihao.cn/ 乐余地: https://www.leridy.pw/ 同盟源: https://tmy123.com/ Yremp: https://yremp.live next主题 的友链,默认是在主题配置文件中 links 下添加,当链接变多以后,侧栏页面的排版很不美观。当友链达到10+以上,那么侧边栏就会很不雅观。 二、骚操作:自定义友链页面这时候就需要给友链新增一个单独的页面了,下面说一下具体步骤。 2.1 新增 links 页面 在控制台使用命令创建: 1hexo new page links 也可在博客根目录 /source 下手动创建 links 文件夹和里边的 index.md 文件 然后在博客根目录 /source 下会生成一个 links 文件夹,打开其中的 index.md 文件,在头部写入 type = “links”,这个一定要写,如下: 12345---title: 友情链接date: 2019-12-08 03:21:39type: "links"--- 2.2 配置 menu 主题配置文件中menu下添加: 1links: /links/ || link 在 /themes/next/languages/zh-Hans.yml 文件中 menu 下增加中文描述 1links: 友链 做完这些工作,接下来就是要增加友链页面的样式了 2.3 友链页面样式效果图: 2.3.1新增 links.swig 页 在 /themes/next/layout/ 新建 links.swig,内容如下: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104 {% block content %} {######################} {### LINKS BLOCK ###} {######################} <div id="links"> <style> #links{ margin-top: 5rem; } .links-content{ margin-top:1rem; } .link-navigation::after { content: " "; display: block; clear: both; } .card { width: 300px; font-size: 1rem; padding: 10px 20px; border-radius: 4px; transition-duration: 0.15s; margin-bottom: 1rem; display:flex; } .card:nth-child(odd) { float: left; } .card:nth-child(even) { float: right; } .card:hover { transform: scale(1.1); box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.12), 0 0 6px 0 rgba(0, 0, 0, 0.04); } .card a { border:none; } .card .ava { width: 3rem!important; height: 3rem!important; margin:0!important; margin-right: 1em!important; border-radius:4px; } .card .card-header { font-style: italic; overflow: hidden; width: 236px; } .card .card-header a { font-style: normal; color: #2bbc8a; font-weight: bold; text-decoration: none; } .card .card-header a:hover { color: #d480aa; text-decoration: none; } .card .card-header .info { font-style:normal; color:#a3a3a3; font-size:14px; min-width: 0; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; } </style> <div class="links-content"> <div class="link-navigation"> {% for link in theme.mylinks %} <div class="card"> <img class="ava" src="{{ link.avatar }}"/> <div class="card-header"> <div> <a href="{{ link.site }}" target="_blank"> {{ link.nickname }}</a> <a href="{{ link.site }}" target="_blank"><span class="focus-links">关注</span></a> </div> <div class="info">{{ link.info }}</div> </div> </div> {% endfor %} </div> {{ page.content }} </div> </div> {##########################} {### END LINKS BLOCK ###} {##########################}{% endblock %} 2.3.2 修改 page.swig 修改 /themes/next/layout/page.swig 文件,在开头的 block title 内部 12#}{% elif page.type === "tags" and not page.title %}{# #}{{ __('title.tag') + page_title_suffix }}{# 这个位置下添加代码: 123<!-- 友情链接-->#}{% elif page.type === 'links' and not page.title %}{# #}{{ __('title.links') + page_title_suffix }}{# 效果如下: 2.3.3引入 links.swig 接着在 /themes/next/layout/page.swig 中 PAGE BODY 内部,引入刚才新建的 page.swig : 123<!-- 友情链接-->{% elif page.type === 'links' %} {% include 'links.swig' %} 比如我是在 1{% elif page.type === 'categories' %} 这个if下追加的: 到这里就完成页面样式的配置了。 2.4 配置友链 接下来,在 /themes/next/_config.yml 文件中配置友链,末尾处新增 mylinks ,如下 12345678910111213141516171819202122232425262728mylinks:# 友链交换 已经添加贵站# 名称:AnFrank# 地址:https://enfangzhong.github.io/# 描述:既可以早九晚五又可以浪迹天涯。# 头像:https://enfangzhong.github.io/images/avatar.jpg- nickname: AnFrank #友链名称 site: https://enfangzhong.github.io/ #友链地址 info: 既可以早九晚五又可以浪迹天涯。 #友链说明 avatar: https://enfangzhong.github.io/images/avatar.jpg #友链头像- nickname: 博采众长 #友链名称 site: https://lruihao.cn/ #友链地址 info: 分享一些有趣程序、干货、技巧、开发教程、心情和学习记录等等。 #友链说明 avatar: https://lruihao.cn/images/avatar.png #友链头像- nickname: Yremp site: https://yremp.live info: 流年,谁给过的倾城 avatar: https://cdn.jsdelivr.net/gh/yremp/[email protected]/img/custom/head.jpg - nickname: Leaf's Blog avatar: https://leafjame.github.io/images/beichen.png site: https://leafjame.github.io info: Java狮 北漂男 摄影 旅行 赚钱 这里是配置了四个友链,多个的配置方式相同。 hexo 部署命令三连,看看效果吧~ 1hexo c && hexo g -d]]></content>
<categories>
<category>博客</category>
</categories>
<tags>
<tag>博客</tag>
<tag>自定义友链页面</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Hexo个人博客添加标签云及效果展示]]></title>
<url>%2F2019%2F12%2F08%2FHexo%E4%B8%AA%E4%BA%BA%E5%8D%9A%E5%AE%A2%E6%B7%BB%E5%8A%A0%E6%A0%87%E7%AD%BE%E4%BA%91%E5%8F%8A%E6%95%88%E6%9E%9C%E5%B1%95%E7%A4%BA%2F</url>
<content type="text"><![CDATA[[TOC] Hexo个人博客添加标签云及效果展示一、hexo-tag-cloud插件介绍hexo-tag-cloud插件是作者写的一个Hexo博客的标签云插件,旨在直观的展示标签的种类,美观大方且非常优雅。 1.1 插件下载地址:插件的GitHub地址: https://github.com/MikeCoder/hexo-tag-cloud 1.2 插件说明:说明地址: https://github.com/MikeCoder/hexo-tag-cloud/blob/master/README.ZH.md 1.3 标签云效果展示: 我的博客主页: https://enfangzhong.github.io/ 二、安装插件进入到 hexo 的根目录,在 package.json 中添加依赖: "hexo-tag-cloud": "2.0.*" 操作如下: 2.1 使用命令行进行安装复制 1npm install hexo-tag-cloud@^2.0.* --save 2.2 Git clone 下载使用命令行安装插件包的过程中可能会出现问题,安装失败,安装不完全。可以直接克隆插件到博客的插件文件夹blog/node_modules里。或者克隆到桌面,复制到博客的插件文件夹blog/node_modules里。 三、配置插件插件的配置需要对应的环境,可以在主题文件夹里找一下,有没有对应的渲染文件,然后根据渲染文件的类型,选择对应的插件配置方法。 1、swig 用户 (Next主题在列)在主题文件夹找到文件 theme/next/layout/_macro/sidebar.swig, 然后添加如下代码: 123456789101112{% if site.tags.length > 1 %}<script type="text/javascript" charset="utf-8" src="/js/tagcloud.js"></script><script type="text/javascript" charset="utf-8" src="/js/tagcanvas.js"></script><div class="widget-wrap"> <h3 class="widget-title">标签云</h3> <div id="myCanvasContainer" class="widget tagcloud"> <canvas width="250" height="250" id="resCanvas" style="width=100%"> {{ list_tags() }} </canvas> </div></div>{% endif %} 代码添加到后面即可,添加示意图如下: 2、对于ejs的用户 (默认主题landscape在列)在主题文件夹找到文件hexo/themes/landscape/layout/_widget/tagcloud.ejs,将这个文件修改如下: 复制 12345678910111213<% if (site.tags.length) { %> <script type="text/javascript" charset="utf-8" src="/js/tagcloud.js"></script> <script type="text/javascript" charset="utf-8" src="/js/tagcanvas.js"></script> <div class="widget-wrap"> <h3 class="widget-title">Tag Cloud</h3> <div id="myCanvasContainer" class="widget tagcloud"> <canvas width="250" height="250" id="resCanvas" style="width=100%"> <%- tagcloud() %> </canvas> </div> </div><% } %> 3、对于jade用户 (Apollo主题在列)找到 apollo/layout/archive.jade 文件,并且把 container 代码块修改为如下内容: 复制 123456789101112block container include mixins/post .archive h2(class='archive-year')= 'Tag Cloud' script(type='text/javascript', charset='utf-8', src='/oj-code/js/tagcloud.js') script(type='text/javascript', charset='utf-8', src='/oj-code/js/tagcanvas.js') #myCanvasContainer.widget.tagcloud(align='center') canvas#resCanvas(width='500', height='500', style='width=100%') !=tagcloud() !=tagcloud() +postList() 四、主题配置 在next主题根目录,找到 _config.yml配置文件然后在最后添加如下的配置项,可以自定义标签云的字体和颜色,还有突出高亮: 1234567# hexo-tag-cloudtag_cloud: textFont: Trebuchet MS, Helvetica textColor: '#333' textHeight: 25 outlineColor: '#E2E1D1' maxSpeed: 0.1 textColor: ‘#333’ 字体颜色textHeight: 25 字体高度,根据部署的效果调整maxSpeed: 0.1 文字滚动速度,根据自己喜好调整 五、部署5.1 本地部署预览效果1hexo clean && hexo g && hexo s 5.2 博客部署到github或者码云1hexo clean && hexo g && hexo d 推荐使用 && 作为组合命令的串联符号 注:一定要严格清理缓存,这样不容易出现问题,即需要执行hexo clean]]></content>
<categories>
<category>博客</category>
</categories>
<tags>
<tag>博客</tag>
<tag>标签云</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Hexo个人博客添加APlayer音乐播放器功能]]></title>
<url>%2F2019%2F12%2F08%2FHexo%E4%B8%AA%E4%BA%BA%E5%8D%9A%E5%AE%A2%E6%B7%BB%E5%8A%A0APlayer%E9%9F%B3%E4%B9%90%E6%92%AD%E6%94%BE%E5%99%A8%E5%8A%9F%E8%83%BD%2F</url>
<content type="text"><![CDATA[[TOC] 为什么会写hexo添加音乐播放器呢?初衷是有粉丝私信总是问我的博客中音乐播放器是怎么添加的。所以还是写一下,毕竟不是一两句话可以描述清楚。 Hexo个人博客添加APlayer音乐播放器功能一、效果图音乐播放器效果地址: https://enfangzhong.github.io/ 二、引入APlayer音乐播放器2.1 下载APlayer源码APlayer源码下载地址:https://github.com/MoePlayer/APlayer 2.2 将源码放到next主题的source文件夹中 下载到APlayer源码压缩包,解压后把dist文件夹复制到\themes\next\source目录中。 2.3 放入自己喜欢的音乐 在dist目录里,新建music.js文件,并把如下代码粘贴进去。 12345678910111213141516171819202122232425262728293031323334353637const ap = new APlayer({ container: document.getElementById('aplayer'), fixed: true, autoplay: false, audio: [ { name: "平凡之路", artist: '朴树', url: 'http://www.ytmp3.cn/down/59211.mp3', cover: 'http://p1.music.126.net/W_5XiCv3rGS1-J7EXpHSCQ==/18885211718782327.jpg?param=130y130', }, { name: '这些民谣 - 一次听个够', artist: '翁大涵', url: 'http://www.ytmp3.cn/down/60222.mp3', cover: 'http://p2.music.126.net/Wx5GNJEpay2JbfVUJc4Aew==/109951163094853876.jpg?param=130y130', }, { name: '你的酒馆对我打了烊', artist: '陈雪凝', url: 'http://www.ytmp3.cn/down/59770.mp3', cover: 'http://p1.music.126.net/LiRR__0pJHSivqBHZzbMUw==/109951163816225567.jpg?param=130y130', }, { name: 'Something Just Like This', artist: 'The Chainsmokers', url: 'http://www.ytmp3.cn/down/50463.mp3', cover: 'http://p2.music.126.net/ggnyubDdMxrhpqYvpZbhEQ==/3302932937412681.jpg?param=130y130', }, { name: 'Good Time', artist: 'Owl City&Carly Rae Jepsen', url: 'http://www.ytmp3.cn/down/34148.mp3', cover: 'http://p1.music.126.net/c5NVKUIAUcyN4BQUDbGnEg==/109951163221157827.jpg?param=130y130', } ]}); 2.4在next主题下的layout中引入APlayer音乐播放器源码在\themes\next\layout_layout.swig文件中,里新增如下代码: 12345<!-- 加入APlayer音乐播放器 --><link rel="stylesheet" href="/dist/APlayer.min.css"><div id="aplayer"></div><script type="text/javascript" src="/dist/APlayer.min.js"></script><script type="text/javascript" src="/dist/music.js"></script> 2.5 重新部署在blog目录下开启重新部署命令: 1hexo c && hexo g -d]]></content>
<categories>
<category>博客</category>
</categories>
<tags>
<tag>博客</tag>
<tag>音乐播放器</tag>
</tags>
</entry>
<entry>
<title><![CDATA[SQL语句面试经典50题]]></title>
<url>%2F2019%2F12%2F03%2FSQL%E8%AF%AD%E5%8F%A5%E9%9D%A2%E8%AF%95%E7%BB%8F%E5%85%B850%E9%A2%98%2F</url>
<content type="text"><![CDATA[SQL语句面试经典50题 2019/12/03 19:36 今天更新到这 2019/12/04 22:05 项目中完成了表单中的上传下载接口与前端对接。今天加班到八点,话说加班可以调休。希望过年调几天。 2019/12/05 01:43 今天研究了一下Flowable 工作流框架,明天继续。夜深了,休息。 2019/12/06 13:23 刚才在b站看了一个程序员老哥被裁的纪念品上面写着:感谢有你,一路同行,怀念我们一起奋斗的时光。 0.学习目标 了解SQL的作用 掌握SQL语句的编写 1. 为什么要练习sql语句?做为一个后端开发人员,sql语句的编写是至关重要的。在实习的第一天到公司,我的leader戴哥就是让我练习sql语句。尽管用mybatis plus,但是sql语句可以处理复杂的逻辑。 会写sql的程序员,才是真的的crud boy。 练习sql语句,主要看了猴子和小番茄的知乎博文分析。 这里有sql面试的50题,帮助大家更进一步的熟悉SQL. SQL是数据分析师的必备基础技能,希望大家跟我一起来打怪升级,最后成为某一领域的数据科学家。 常见的SQL面试题: 经典50题 - 知乎 https://zhuanlan.zhihu.com/p/38354000 SQL面试必会50题 - 知乎 https://zhuanlan.zhihu.com/p/43289968 2.练习题分析阶段 已知有如下4张表: 学生表:student(学号,学生姓名,出生年月,性别) 成绩表:score(学号,课程号,成绩) 课程表:course(课程号,课程名称,教师号) 教师表:teacher(教师号,教师姓名) 1.学生表Student(s_id,s_name,s_birth,s_sex) —学生编号,学生姓名, 出生年月,学生性别2.课程表Course(c_id,c_name,t_id) – —课程编号, 课程名称, 教师编号3.教师表Teacher(t_id,t_name) —教师编号,教师姓名4.成绩表Score(s_id,c_id,s_score) —学生编号,课程编号,分数 根据以上信息按照下面要求写出对应的SQL语句。 ps:这些题考察SQL的编写能力,对于这类型的题目,需要你先把4张表之间的关联关系搞清楚了,最好的办法是自己在草稿纸上画出关联图,然后再编写对应的SQL语句就比较容易了。下图是我画的这4张表的关系图,可以看出它们之间是通过哪些外键关联起来的: 3.准备阶段3.1.创建数据库和表为了演示题目的运行过程,我们先按下面语句在客户端navicat中创建数据库和表。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667-- 建表-- 学生表CREATE TABLE `Student`( `s_id` VARCHAR(20), `s_name` VARCHAR(20) NOT NULL DEFAULT '', `s_birth` VARCHAR(20) NOT NULL DEFAULT '', `s_sex` VARCHAR(10) NOT NULL DEFAULT '', PRIMARY KEY(`s_id`));-- 课程表CREATE TABLE `Course`( `c_id` VARCHAR(20), `c_name` VARCHAR(20) NOT NULL DEFAULT '', `t_id` VARCHAR(20) NOT NULL, PRIMARY KEY(`c_id`));-- 教师表CREATE TABLE `Teacher`( `t_id` VARCHAR(20), `t_name` VARCHAR(20) NOT NULL DEFAULT '', PRIMARY KEY(`t_id`));-- 成绩表CREATE TABLE `Score`( `s_id` VARCHAR(20), `c_id` VARCHAR(20), `s_score` INT(3), PRIMARY KEY(`s_id`,`c_id`));-- 插入学生表测试数据insert into Student values('01' , '赵雷' , '1990-01-01' , '男');insert into Student values('02' , '钱电' , '1990-12-21' , '男');insert into Student values('03' , '孙风' , '1990-05-20' , '男');insert into Student values('04' , '李云' , '1990-08-06' , '男');insert into Student values('05' , '周梅' , '1991-12-01' , '女');insert into Student values('06' , '吴兰' , '1992-03-01' , '女');insert into Student values('07' , '郑竹' , '1989-07-01' , '女');insert into Student values('08' , '王菊' , '1990-01-20' , '女');-- 课程表测试数据insert into Course values('01' , '语文' , '02');insert into Course values('02' , '数学' , '01');insert into Course values('03' , '英语' , '03');-- 教师表测试数据insert into Teacher values('01' , '张三');insert into Teacher values('02' , '李四');insert into Teacher values('03' , '王五');-- 成绩表测试数据insert into Score values('01' , '01' , 80);insert into Score values('01' , '02' , 90);insert into Score values('01' , '03' , 99);insert into Score values('02' , '01' , 70);insert into Score values('02' , '02' , 60);insert into Score values('02' , '03' , 80);insert into Score values('03' , '01' , 80);insert into Score values('03' , '02' , 80);insert into Score values('03' , '03' , 80);insert into Score values('04' , '01' , 50);insert into Score values('04' , '02' , 30);insert into Score values('04' , '03' , 20);insert into Score values('05' , '01' , 76);insert into Score values('05' , '02' , 87);insert into Score values('06' , '01' , 31);insert into Score values('06' , '03' , 34);insert into Score values('07' , '02' , 89);insert into Score values('07' , '03' , 98); Student表: Course表: Teacher表: Score表: 4.面试题为了方便学习,我将50道面试题进行了分类练习 4.1.练习:简单查询 1.查询姓“猴”的学生名单 2.查询姓“孟”老师的个数123SELECT COUNT(*) AS 姓“孟”老师的个数FROM teacherWHERE t_name LIKE '孟%' 4.2.练习:汇总、分组、分组条件查询4.2.1.练习:汇总 3. 面试题:查询课程编号为“0002”的总成绩1234567891011/*分析思路select 查询结果 [总成绩:汇总函数sum]from 从哪张表中查找数据[成绩表score]where 查询条件 [课程号是0002]*/SELECT sum(s_score)FROM scoreWHERE c_id ='0002' 4. 查询选了课程的学生人数12345678/*这个题目翻译成大白话就是:查询有多少人选了课程select 学号,成绩表里学号有重复值需要去掉from 从课程表查找score;*/SELECT COUNT(DISTINCT c_id) AS 学生人数FROM score 4.2.2 练习:分组 5.查询各科成绩最高和最低的分以如下的形式显示:课程号,最高分,最低分 12345678910111213141516171819/*分析思路select 查询结果 [课程ID:是课程号的别名,最高分:max(成绩) ,最低分:min(成绩)]from 从哪张表中查找数据 [成绩表score]where 查询条件 [没有]group by 分组 [各科成绩:也就是每门课程的成绩,需要按课程号分组];*//*上述题的拆分:课程id为0002的最高分与最低分SELECT MAX(s_score) AS 最高分, MIN(s_score) AS 最低分FROM scoreWHERE c_id = '0002'*/SELECT c_id,MAX(s_score) AS 最高分,MIN(s_score) AS 最低分FROM scoreGROUP BY c_id 6.查询每门课程被选修的学生数— 通过对成绩表的课程id进行分组,然后对该学号进行计数 1234567891011/*分析思路select 查询结果 [课程号,选修该课程的学生数:汇总函数count]from 从哪张表中查找数据 [成绩表score]where 查询条件 [没有]group by 分组 [每门课程:按课程号分组];*/SELECT c_id,COUNT(c_id)FROMscoreGROUP BY c_id 7.查询男生、女生人数 在学生表中对性别进行分组 计数 1234567891011121314/*分析思路select 查询结果 [性别,对应性别的人数:汇总函数count]from 从哪张表中查找数据 [性别在学生表中,所以查找的是学生表student]where 查询条件 [没有]group by 分组 [男生、女生人数:按性别分组]having 对分组结果指定条件 [没有]order by 对查询结果排序[没有];*/SELECT s_sex,COUNT(s_sex) AS 个数FROM studentGROUP BY s_sex 4.2.3 分组结果的条件 8.查询平均成绩大于60分学生的学号和平均成绩1234567891011121314151617181920/* 题目翻译成大白话:平均成绩:展开来说就是计算每个学生的平均成绩这里涉及到“每个”就是要分组了平均成绩大于60分,就是对分组结果指定条件分析思路select 查询结果 [学号,平均成绩:汇总函数avg(成绩)]from 从哪张表中查找数据 [成绩在成绩表中,所以查找的是成绩表score]where 查询条件 [没有]group by 分组 [平均成绩:先按学号分组,再计算平均成绩]having 对分组结果指定条件 [平均成绩大于60分]*/-- 在成绩表中对学号进行分组求平均成绩 having条件是平均成绩>60分select s_id,AVG(s_score)FROM scoreGROUP BY s_idHAVING AVG(s_score)>60ORDER BY AVG(s_score) DESC 9.查询至少选修两门课程的学生学号1234567891011121314151617/* 翻译成大白话:第1步,需要先计算出每个学生选修的课程数据,需要按学号分组第2步,至少选修两门课程:也就是每个学生选修课程数目>=2,对分组结果指定条件分析思路select 查询结果 [学号,每个学生选修课程数目:汇总函数count]from 从哪张表中查找数据 [课程的学生学号:课程表score]where 查询条件 [至少选修两门课程:需要先计算出每个学生选修了多少门课,需要用分组,所以这里没有where子句]group by 分组 [每个学生选修课程数目:按课程号分组,然后用汇总函数count计算出选修了多少门课]having 对分组结果指定条件 [至少选修两门课程:每个学生选修课程数目>=2]*/SELECT s_id,COUNT(c_id) as 选修课程数目FROM scoreGROUP BY c_idHAVING COUNT(c_id)>=2 10.查询同名同性学生名单并统计同名人数123456789101112131415161718/* 翻译成大白话,问题解析:1)查找出姓名相同的学生有谁,每个姓名相同学生的人数查询结果:姓名,人数条件:怎么算姓名相同?按姓名分组后人数大于等于2,因为同名的人数大于等于2分析思路select 查询结果 [姓名,人数:汇总函数count(*)]from 从哪张表中查找数据 [学生表student]where 查询条件 [没有]group by 分组 [姓名相同:按姓名分组]having 对分组结果指定条件 [姓名相同:count(*)>=2]order by 对查询结果排序[没有];*/SELECT s_name,COUNT(s_name)FROM studentGROUP BY s_nameHAVING COUNT(s_name)>=2 11.查询不及格的课程并按课程号从大到小排列123456789101112131415/* 分析思路select 查询结果 [课程号]from 从哪张表中查找数据 [成绩表score]where 查询条件 [不及格:成绩 <60]group by 分组 [没有]having 对分组结果指定条件 [没有]order by 对查询结果排序[课程号从大到小排列:降序desc];*/SELECT c_idFROM scoreWHERE s_score<60GROUP BY c_idORDER BY c_id 12.查询每门课程的平均成绩,结果按平均成绩升序排序,平均成绩相同时,按课程号降序排列1234567891011121314/* 分析思路select 查询结果 [课程号,平均成绩:汇总函数avg(成绩)]from 从哪张表中查找数据 [成绩表score]where 查询条件 [没有]group by 分组 [每门课程:按课程号分组]having 对分组结果指定条件 [没有]order by 对查询结果排序[按平均成绩升序排序:asc,平均成绩相同时,按课程号降序排列:desc];*/SELECT c_id,AVG(s_score) as 平均成绩FROM scoreGROUP BY c_idORDER BY AVG(s_score) ASC,c_id DESC 13.检索课程编号为“0004”且分数小于60的学生学号,结果按按分数降序排列12345678910111213/* 分析思路select 查询结果 []from 从哪张表中查找数据 [成绩表score]where 查询条件 [课程编号为“04”且分数小于60]group by 分组 [没有]having 对分组结果指定条件 []order by 对查询结果排序[查询结果按按分数降序排列];*/select s_id,s_scoreFROM scoreWHERE c_id = "0004" AND s_score >=60ORDER BY s_score DESC 14.统计每门课程的学生选修人数(超过2人的课程才统计)要求输出课程号和选修人数,查询结果按人数降序排序,若人数相同,按课程号升序排序 123456789101112131415/* 分析思路select 查询结果 [要求输出课程号和选修人数]from 从哪张表中查找数据 []where 查询条件 []group by 分组 [每门课程:按课程号分组]having 对分组结果指定条件 [学生选修人数(超过2人的课程才统计):每门课程学生人数>2]order by 对查询结果排序[查询结果按人数降序排序,若人数相同,按课程号升序排序];*/SELECT c_id,COUNT(score.s_id) as '选修人数'FROM scoreGROUP BY c_idHAVING COUNT(score.s_id) > 2ORDER BY COUNT(score.s_id) DESC,c_id ASC 15.查询两门以上不及格课程的同学的学号及其平均成绩1234567891011121314151617181920212223242526272829303132333435363738394041424344/*分析思路先分解题目:1)[两门以上][不及格课程]限制条件2)[同学的学号及其平均成绩],也就是每个学生的平均成绩,显示学号,平均成绩分析过程:第1步:得到每个学生的平均成绩,显示学号,平均成绩第2步:再加上限制条件:1)不及格课程2)两门以上[不及格课程]:课程数目>2/* 第1步:得到每个学生的平均成绩,显示学号,平均成绩select 查询结果 [学号,平均成绩:汇总函数avg(成绩)]from 从哪张表中查找数据 [涉及到成绩:成绩表score]where 查询条件 [没有]group by 分组 [每个学生的平均:按学号分组]having 对分组结果指定条件 [没有]order by 对查询结果排序[没有];*/select 学号, avg(成绩) as 平均成绩from scoregroup by 学号;/* 第2步:再加上限制条件:1)不及格课程2)两门以上[不及格课程]select 查询结果 [学号,平均成绩:汇总函数avg(成绩)]from 从哪张表中查找数据 [涉及到成绩:成绩表score]where 查询条件 [限制条件:不及格课程,平均成绩<60]group by 分组 [每个学生的平均:按学号分组]having 对分组结果指定条件 [限制条件:课程数目>2,汇总函数count(课程号)>2]order by 对查询结果排序[没有];*/SELECT s_id,COUNT(s_score),avg(s_score) AS 平均成绩FROM scoreWHERE s_score<60GROUP BY s_idHAVING COUNT(s_score)>=2 4.3.复杂查询16.查询所有课程成绩小于60分学生的学号、姓名这道题目知乎猴子解法是存在问题的。我这边提供我的理解吧。 第一种做法:将学号分组 在每个学号组 找成绩最高值<60的学号 1234567SELECT score.s_id,student.s_nameFROM scoreINNER JOIN studentON score.s_id = student.s_idGROUP BY s_idHAVING MAX(s_score) <60 第二种解法: step1: 查询出学生课程数的统计量 step2: 查询出学生课程成绩<60分对应课程数的统计量 1234567891011121314151617181920SELECT A.s_id,S.s_nameFROM(SELECT s_id,COUNT(c_id) AS cntFROM scoreGROUP BY score.s_id) AS AINNER JOIN(SELECT s_id,COUNT(c_id) AS cntFROM scoreWHERE s_score<60GROUP BY s_id) AS BON A.s_id = B.s_idINNER JOIN student AS SON A.s_id = S.s_idWHERE A.cnt = B.cnt 17、查询没有学全所有课的学生的学号、姓名12345678910111213141516171819-- 第一种解法:-- 首先学号分组 做having条件:每个学号统计出不同课程数量 小于 课程表中不同课程数量-- 上面结果为没有学全所有课程的学号-- 然后在学生表中 WHERE s_id IN ()条件 查找出学号、姓名。(这一步也可以用 INNER JOIN ON。)SELECT s_id,s_name FROM studentWHERE s_id IN(SELECT s_idFROM scoreGROUP BY s_idHAVING COUNT(DISTINCT c_id) < (SELECT COUNT(DISTINCT c_id) FROM course))-- 这种解法后来发现score表会存在一些学生没有成绩。比如说王菊同学一门课程成绩都不存在。-- 但是上述做法没有把王菊同学查找出来。-- 第二种解法: 18、查询出只选修了两门课程的全部学生的学号和姓名1990年出生的学生名单查询各科成绩前两名的记录分组取每组最大值、最小值,每组最大的N条(top N)记录。4.4 sql面试题:topN问题4.5 多表查询 查询所有学生的学号、姓名、选课数、总成绩查询平均成绩大于85的所有学生的学号、姓名和平均成绩查询学生的选课情况:学号,姓名,课程号,课程名称查询出每门课程的及格人数和不及格人数使用分段[100-85],[85-70],[70-60],[<60]来统计各科成绩,分别统计:各分数段人数,课程号和课程名称查询课程编号为0003且课程成绩在80分以上的学生的学号和姓名|4.6 sql面试题:行列如何互换?下面是学生的成绩表(表名score,列名:学号、课程号、成绩) 使用sql实现将该表行转列为下面的表结构 【面试题类型总结】这类题目属于行列如何互换,解题思路如下:]]></content>
<categories>
<category>SQL</category>
</categories>
<tags>
<tag>SQL</tag>
</tags>
</entry>
<entry>
<title><![CDATA[秋招的那些事]]></title>
<url>%2F2019%2F11%2F11%2F%E7%A7%8B%E6%8B%9B%E7%9A%84%E9%82%A3%E4%BA%9B%E4%BA%8B%2F</url>
<content type="text"><![CDATA[[TOC] 秋招的那些事一、秋招1.1 提前批博主在秋招之前参与了提前批的神仙打架。根据记忆提前批就面了字节跳动、京东、浦发银行,当时投递的是推荐算法,没有准备好就上战场了。 1.2 秋招每年秋招是9-11月这个阶段,一般互联网公司都集中在9月。10月还存在一些公司鞭尸。毕竟offer收割机都是手握多个offer。 二、实习 2.1 博主研二下期间(3-5月)还在写小paper。花了时间准备一篇专利,只是~~~~~。错过了实习的金三银四。 2.2 搞完论文大概是5月份了吧,当时全身心的投入到了算法与大数据的知识海洋中了,没有参与实习。 2.3 我实习在武汉最湿冷的时候。出不出太阳,全看心情。偶尔昨天气温20度,今天就5度。平时下下小雨。 2.4 南湖大道与雄庄路的十字路口总是那么的拥挤。每天早上循环播放着下一站茶山刘by房东的猫度过。 北京朋友的朋友圈全是雪雪雪。雪花儿,武汉期待你的到来。 听说今天北京下雪了。雪花儿,我在武汉等你的到来。 ——————————————————2019/11/30 夜深了,今天暂且写到这咯。]]></content>
<categories>
<category>秋招</category>
<category>面试</category>
</categories>
<tags>
<tag>秋招</tag>
<tag>面试</tag>
</tags>
</entry>
<entry>
<title><![CDATA[个性化推荐算法实践第12章推荐算法回顾与总结]]></title>
<url>%2F2019%2F06%2F01%2F%E4%B8%AA%E6%80%A7%E5%8C%96%E6%8E%A8%E8%8D%90%E7%AE%97%E6%B3%95%E5%AE%9E%E8%B7%B5%E7%AC%AC12%E7%AB%A0%E6%8E%A8%E8%8D%90%E7%AE%97%E6%B3%95%E5%9B%9E%E9%A1%BE%E4%B8%8E%E6%80%BB%E7%BB%93%2F</url>
<content type="text"><![CDATA[[TOC] 个性化推荐算法实践第12章推荐算法回顾与总结 推荐算法实战课程,课程是有问答专区,如果你有问题可以在问答专区提问,我会在每天固定时间解答课程,结合问答专区能够让你更快的掌握知识。 开始本章节的内容之前我们首先来回顾一下上一章节的内容,上一章节我们对之前所讲述过的排序部分的内容进行了总结与回顾。 本章节我们对课程所讲述过的全部内容进行总结与回顾,下面一起来看一下本章节的内容大纲。 一、个性化推荐算法离线架构 个性化推荐算法离线架构,无论是在个性化召回部分还是在个性化排序部分,我们都有一套离线处理,得到我们模型的流程,我们将这套流程的抽象一下给大家解析一下。 二、个性化推荐算法在线架构 无论是在个性化召回部分还是在个性化排序不分,我们都曾经将在线部分的架构呢,给大家讲解过,这里我们一起回顾一下。 三、本课程所讲述过的算法模型的内容回顾 一、个性化推荐算法离线架构下面一起来看一下离线架构。 我们原始的日志呢,包含以下四个部分。 ①用户的点击与用户的展示日志。 ②我们记录用户信息的日志,这里包含用户的静态信息和统计的一些动态的信息,包括用户喜欢什么样的类型的内容等等。 ③item的信息 ④实时流式信息(Streaming)。这里的流式是指包含用户的一些实时行为。比如说用户订阅某个频道、用户将某些物品加入到购物车等等等等。 基于以上的日志,我们首先进行样本的筛选,我们将测试样本有噪声的样本的剔除掉得到我们的样本,得到样本之后呢,我们再进行特征选择。 在个性化召回部分,可能我们只是更多的需要我们的点击与展示数据。但是在我们的个性化排序部分,可能我们还需要用户的信息(user info),项目的信息(item info)以及上下文信息等等的一些信息。 基于我们的特征选择与样本选择之后呢,我们得到了我们的训练数据集以及测试数据,接下来无论是在召回部分还是在排序部分,我们都使用的训练数据集去训练模型,使用我们的测试数据去评估我们模型的表现。如果我们模型的表现达到了我们的要求,我们就将我们得到的模型文件的导出。 这里在个性化召回部分呢,可能是导出的直接用户的推荐结果,也可能是item相似度的列表亦或是我们的深度学习的模型文件。 在排序部分我们得到的模型文件的基本都是模型的实例化本身。像逻辑回归部分呢,其实我们只需要得到不同特征对应的参数即可。 二、个性化推荐算法在线架构1、Recall在线架构 说完了离线架构,下面我们来一起回顾一下在线架构的召回部分,大部分情况下我们召回部分得到的结果呢,是直接写入到KV存储当中了。用户访问我们的Server的时候呢,直接召回到自己对应的推荐结果,我们拿到推荐结果对应的id呢,从我们的Detail Server当中的获取Detail 传给我们的排序部分,但是呢在一些复杂场景情况下,我们比如训练了一些深度学习的模型,那么我们在用户访问我们的Server的时候呢,我们首先得需要拼接一下用户侧(user info)的特征,然后呢访问我们的Server得到用户的向量表示然后再进行召回。 2、Rank在线架构说完了个性化召回的在线架构的,下面我们来一起看一下排序部分在线架构。排序部分在线架构根据我们模型的不同的分为以下几种。 1、如果是像逻辑回归或者是GBDT这种浅层模型。我们这里的Rank Server可以直接将模型加载到内存当中,与我们的推荐引擎的进行服务的交互。用户访问我们的Server的时候,我们首先召回得到我们的候选集列表,对于每一个候选集列表,那我们去KV当中的拼一下我们的特征,就是获得我们用户侧(user info)的特征以及项目侧(item info)的特征包括一些上下文特征等等。我们将拼接的特征的传递给让给Rank Server。Rank Server用模型来进行下预测,将预测的结果呢再传递给我们的推荐引擎。推荐引擎的依据我们每一个item预测的得分的进行一下排序,这个顺序呢就是展示给用户的顺序。 2、如果我们这里采用像WD这样的深度学习模型的话,我们这里的Rank Server需要与我们的TF Server进行交互,这里的Rank Server相当于我们这里的请求的透传,并且返回的结果呢,也透传给我们的推荐引擎。 三、本课程所讲述过的算法模型的内容回顾回顾完了在线架构,我们来一起看一下本课程具体讲解了哪些算法与模型? 首先来看个性化召回部分,这里我们介绍了基于领域的CF、LFM、Person Rank 、Item2Vec、ContentBased。我们CF是在个性化推荐算法实战入门必修课里介绍的,入门必修课是一门免费的课程大家都可以看到。还有我们基于内容的推荐以及呢,我们这里基于深度学习的推荐Item2Vec。 我们在排序部分的介绍了浅层模型Rank LR(逻辑回归),浅层模型组合GBDT。介绍了浅层模型不同模型的组合:LR+GBDT的混合模型。最后我们介绍了深度学习模型WD。以上的每一个算法或者模型呢,我们都是从物理意义数学公式推导,代码实战等几个方面来给大家介绍的,那么好了,本章节的内容到这里就全部结束了。本章节重点回顾了我们本次课程所讲述的全部内容。到这里本次个性化推荐算法实战课程的全部内容就结束了,非常感谢大家对于本课程的认真学习。祝大家在本次课程中学习一切顺利。]]></content>
<categories>
<category>推荐算法</category>
</categories>
<tags>
<tag>推荐算法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[个性化推荐算法实践第11章排序模型总结与回顾]]></title>
<url>%2F2019%2F06%2F01%2F%E4%B8%AA%E6%80%A7%E5%8C%96%E6%8E%A8%E8%8D%90%E7%AE%97%E6%B3%95%E5%AE%9E%E8%B7%B5%E7%AC%AC11%E7%AB%A0%E6%8E%92%E5%BA%8F%E6%A8%A1%E5%9E%8B%E6%80%BB%E7%BB%93%E4%B8%8E%E5%9B%9E%E9%A1%BE%2F</url>
<content type="text"><![CDATA[[TOC] 个性化推荐算法实践第11章排序模型总结与回顾 model在测试数据集效果回顾1、逻辑回归模型、gbdt模型、gbdt模型与逻辑回归混合模型以及wd模型在测试数据集上的效果进行一下简单的回顾 LTR中特征维度浅析 2、我们会对工业界实际项目中建立排序模型所使用的特征进行一下简单的浅析。 工业界Rank技术展望3、对工业界的排序技术进行一下展望 一、model在测试数据集效果回顾1、效果回顾 下面首先来回顾一下各模型在测试数据集上的表现。由于我们各Rank模型在线上实际使用时呢是对item进行打分,然后呢不同的item按照这个得分的展现给用户,所以呢我们这里更加关心的是模型对于我们item预测得分序的关系,所以呢也就是AUC我们这里从我们模型交叉验证得到的AUC以及模型在测试数据集表现的AUC两点的进行一下回顾。 我们在排序部分的重点介绍了四种模型分别是逻辑回归、GBDT、GBDT与逻辑回归的混合模型以及我们基于深度学习的WD模型。 首先呢从我们的模型交叉验证的效果来看一下GBDT与逻辑回归的混合模型的表现要好于GBDT。GBDT要好于我们的逻辑回归模型,由于我们的WD并没有采用我行交叉验证的方式呢,去评估,所以这里没有数据。好了,我们再来看一下训练好的模型呢,在测试数据集上的表现也就是看一下模型的泛化能力。这里同样的GBDT与逻辑回归的混合模型的是要好GBDT,GBDT是要比逻辑回归好,这里要注意一下,在实际的我们的项目中WD模型实际上是表现的最好,但是在这里限于我们的样本的数量了只有3万等等的一些限制。WD表现的要略低于GBDT。不过没有关系,如果大家有机会在实际项目海量的数据集当中了去实践一下,不同的Rank模型的话,大家一定能得到下面的结论,WD模型的表现是要好于GBDT与逻辑回归的混合模型。混合模型是要好于GBDT模型。GBDT是要好于我们的逻辑回归模型。但我实际工作中零一搭建个性化排序系统时得到的结论也是这样的。 2、离线评估模型交叉验证(model cv)在我们训练不同的排序模型,将不同的模型放到线上时,我们如何来评价离线的准入以及在线的收益呢?下面来看一下排序模型的评估,首先来看一下离线评估,第一点的,我们需要看一下模型交叉验证得到的指标,这些指标的,包括我们课上重点提到的AUC,以及准确召回等等的一些指标,这些指标我们需要明确它的物理意义,这样才能够帮助我们清晰地判断模型的效果。 model test data performance最终的我们还需要判断一下模型的泛化能力,也就是模型在测试数据集上的表现,比如模型在测试数据集上得到的AUC,得到的准确率等等,我们结合着不同的业务场景的也会有一些独自的评判标准,比如我们在信息流场景当中呢,我们可能更关注的是session的平均点击位置,这里简单的解释一下。我们每一个session展示了,比如说三条数据。 在我们的测试数据上的原有的情况下,比如我们点击了这3条,经过我们训练模型对于这三个数据得重新打分之后呢,我们能否将已经击的这个第二条呢?学习到第一的位置,这样我们的平均点击位置的就更靠前了,这样的效果也就说明了是更好的。当然了,不同的业务场景还有一些其他的指标。 3、在线评估业务指标下面我们来看一下在线指标,首先呢是业务指标。比如说点击率,购买率,平均阅读时长,总的交易额度等等,我们根据不同的业务场景的制订了评价的指标,最终的结果呢也已在线AB测试得到的业务指标的生效为准。 平均点击位置离线的评价指标的只是我们能否准入的一个衡量的标准,并不能决定我们在线实际效果的好坏,当然了这里还有一些辅助的评价指标,比如像之前我们介绍的平均点击位置。当然呢在线评估时,我们首先来看一下业务指标,其次呢是我们的辅助评价指标。好啦,在线离线的评估呢,我们已经说完了。 二、LTR中特征维度浅析1、特征维度 特征维度 下面来看一下特征了有哪一些?我们的构建排序模型是常用的特征的有以下几个方面,我们来简单的介绍一下,用户侧的特征包含了用户的静态的属性,比如说年龄,性别,地域,还有一些简单的统计特征,比如该用户在我们平台上浏览过多少个商品?点击过多少个商品的购买过多少个商品的,购买过多少个商品啊?近30天浏览了多少商品的这种长短时的统计,最后还有一些用户侧的高维的特征,我们基于它的浏览点击购买历史的给他打上一些标签,比如说呢,她就喜欢某某品牌的香水某某品牌的鞋子等等。刚刚介绍用户特征侧时是以电商场景举例。对于其他的产品道理也是一样的,比如说信息流。我们只需要统计一下用户发现喜欢财经呢,还是喜欢体育,是喜欢娱乐呢?还是喜欢科技,甚至呢我们还可以给他打上标签儿,是科比的标签的还是鹿晗的标签等等?道理都是一样的。 商品侧的特征基础的特征包含商品的名称,商品的上线日期啊等等统计的特征的包含商品被购买的次数。商品的点击率呀,商品的购买率啊等等。一些高维的标签,那比如说这个商品的,他是深受90后欢迎啊,深受年轻女性的欢迎啊,我们的上下文的特征的有,当前是星期几呀?现在是几点呀?用户请求我们服务时所处的地理位置信息等等。用户和item的关系。比如说这个商品呢,是该用户两个月之前加入到购物车里头,一个月之前点击过的呀,半年之前购买过呀,等等的一些信息,还有我们的统计登录信息,比如说呢商品的上架的时间呢与购买率间的关系,比如说近一个月之内上架的商品打开的购买率是多少?近两个月等等我们统计出来显然的又增加了一维特征。好了我们曾经无数次说过特征与样本是决定我们最终这个整体表现的天花板,而我们采用不同的模型的只能去逼近这个天花板。像我刚才介绍这些不同维度特征时呢,我们是用电商场景举例的,但是呢实际的项目中呢,假如没有做过电商的场景,可以根据特征的大体由这五个维度来想到一些电商场景下应该有哪些特征?大家在自己的项目当中呢,或者自己解决实际问题过程中的,也一定要结合的实际去构造我们需要的特征。 2、特征的数目 特征的数目 我们说过为了防止过拟合,那我们尽量要将特征与样本的数目来维持在1:100。举例,比如说我们这里有1000个训练样本,那么这里我选择了十个特征,这是没有问题的,但是呢,有的同学说我没有找到十个特征,我只找到了八个那也是没有问题的。 可能最终我们学习出来的效果不会很好。但是有的同学说我找到了50个特征,那么显然呢,这个模型呢就会过拟合。他在测试数据集上的表现的就会比较差,就是说它的泛化能力就不会很强。 三、工业界Rank技术展望1、多目标学习我们知道了在信息流场景中的我们既想用户拿多点击,也就是点击率预估模型也想用户停留的阅读时长的要长一点。这样呢就是两个目标。之前的可能有很多方式呢,比如说训练两个模型,一个呢是点击率预估模型,一个是平均阅读时长预估模型,然后乘起来,比如说那像电商场景中的我们既想用户呢,他得购买率也就是说最终的转化率呢要高又想拿我们懂得交易额度也能高。有人呢对这种多目标问题的提出了一种将不同的目标的融合到一个网络里进行学习的方法。 2、强化学习我们知道强化学习的是成功保证历史最大回报率的一种办法,现在呢这种算法呢,在游戏里应用的比较广泛也比较成熟,但排序的领域也有一些落地与尝试。希望大家的能够对这些较新的技术进行不断的追求,不断的学习。不断的探究,不断的尝试。那么本章节的内容到这里就全部结束了,本章节的重点是对之前多讲述过的排序部分的内容进行了总结回顾,下一章节我们将会对个性化推荐算法课程的内容来进行一下总结。]]></content>
<categories>
<category>推荐算法</category>
</categories>
<tags>
<tag>推荐算法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[个性化推荐算法实践第10章基于深度学习的排序模型WideAndDeep]]></title>
<url>%2F2019%2F06%2F01%2F%E4%B8%AA%E6%80%A7%E5%8C%96%E6%8E%A8%E8%8D%90%E7%AE%97%E6%B3%95%E5%AE%9E%E8%B7%B5%E7%AC%AC10%E7%AB%A0%E5%9F%BA%E4%BA%8E%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%E7%9A%84%E6%8E%92%E5%BA%8F%E6%A8%A1%E5%9E%8BWideAndDeep%2F</url>
<content type="text"><![CDATA[[TOC] 个性化推荐算法实践第10章基于深度学习的排序模型WideAndDeep欢迎来到本次个性化推荐算法实战课程,本课程是有问答专区,如果你有问题可以在问答专区提问,我会在每天固定时间解答,课程结合问答专区能够让您更快的掌握知识。 开始本章节的课程之前,我们首先来回顾一下上一章节的内容,上一章节我们重点讲述了树模型GBDT的数学原理,以及在测试数据集上代码实战了GBDT模型的训练,并且介绍了GBDT与逻辑回归的混合模型,那么本章节我们将重点介绍深度学习,在点击率预估方面的实战,分别介绍WD模型的数学原理,以及在测试数据集上代码实战WD模型的训练。 背景介绍之深度学习 DNN网络结构与数学原理 WD(wide and deep)网络结构与数学原理 下面我们首先来看一下本章节的内容大纲。1、背景知识介绍之深度学习,由于本章节所介绍的内容与之前我们所学习的浅层模型有较大的不同,所以我们首先介绍一下背景知识,什么是深度学习?2、DNN网络结构与数学原理,介绍完什么是深度学习,我们便选取一种具有代表性的网络DNN,来从它的数学原理如何进行参数学习来详解一下。3、WD网络结构与数学原理,WD模型便是我们所说的,使用深度学习来完成点击率预估实战所采用的模型,wd模型实际上是DNN与逻辑回归的混合模型,但是与之前我们所介绍过的GBDT与逻辑回归的混合模型不同,WD呢是联合训练的,所以我们要学习一下它的网络结构,并且详细的了解一下它的数学原理是如何做到联合训练的。 一、背景介绍之什么是深度学习1.1什么是神经元?下面开始本小节的内容,本小节将重点介绍背景知识,什么是深度学习?深度学习,实际上是利用神经网络学习出一种非线性函数,该函数的输入是我们从训练数据中提取的特征,该函数的输出是训练数据所对应的label,那么说到这里,问题的核心就变成了什么是神经网络,在介绍神经网络之前,我们首先介绍一下它的组成成分,神经元,什么是神经元呢?下面来看一下,这里所说的神经元呢,实际上是指的是人工神经元,与我们生物上神经元的概念呢有所不同。但是呢,我们通过这个网络结构呢,实际上能够联想一下我们生物上的神经元,与该结构有极大的相似性。 下面我们来看一下该结构,这里有几路输入,每一路输入了对应的输入的参数。也就是说我们最终的加权和是什么呢?我这里简单的写一下,加权和=$w_1x_1+w_2x_2+w_3x_3+…+w_nx_n$。加权和之后呢,还要做一下激活函数,这里的激活函数呢,主要是去线性化。常用的激活函数呢,我们在介绍逻辑回归模型的时候,也曾介绍过一种阶跃函数,大家应该还记得。这里的整体的网络结构呢与我们的逻辑回归模型的有一定的相似性。 1.2激活函数那么下面我们来看一下常用的激活函数有哪一些呢? 1.2.1阶跃函数 sigmod第1种呢是我们比较熟悉的阶跃函数,该函数我们曾在逻辑回归里介绍过,该函数的取值范围呢是[0~1],在x等于0时的函数值是0.5,他有一个特点那便是在x大于0的时候,很快函数值就趋向于1。x小于0的时候,函数值非常快的就趋向于0。 1.2.2双曲正切第2种呢,是双曲正切,该函数的图像呢与阶跃函数几乎是一样的,只不过呢,它的取值范围呢是[-1,+1],而且呢,该函数在取值为中间值变为最大值,以及取值为中间值变为最小值的速度呢,要比阶跃函数要快一些。 1.2.3修正线性单元第3种,修正线性单元,当输入大于零时,该函数的输出的是输入的本身,当输入小于零时,该函数的输出呢是0。在神经网络的参数学习中呢,如果我们采用之前讲述过的随机梯度下降的方法,修正线性单元函数能够更快的达到收敛。 1.3 什么是神经网络? 好了介绍完了激活函数,下面我们来学习一下什么是神经网络。神经网络呢是由许多神经元组成的,我们先来简单的看一下网络结构,网络结构呢分为输入层,隐层以及输出层。 输出层呢,可以是一个也可以是多个。像我们做点击率预估呢,那便是一个。如果我们做分类呢是多分类的话,显然就是多个。有几个分类呢,也就有几个输出。输入指的是什么呢?输入指的是我们提取的特征,输入层呢,与隐层之间的采用的是全连接,也就是说我这里的隐层1与我输入层的每一个输入,都有参数相连。这里的隐层2呢,对于输入层的每一个输入呢,也都是有参数相连的。这里参数相连去加权求和之后呢,同样需要有激活函数来去线性化。 我们发现如果输入层与隐层不是全连接的话,而是一一连接的话,那么该网络也便回退到了我们之前讲述过的逻辑回归模型。为什么呢?很好理解,如果是一一连接的话,很像我们之前讲述过的w1与x1相乘,w2与x2相乘。 这样全连接呢,实际上相当于我们在逻辑回归模型里所做的特征交叉,但是呢,这里交叉的力度呢,会更强一点。 举个例子,如果我们这里只有三个输入特征,且并非全连接。对该隐层的第1个节点,我们让三个输入特征的前两个与它连接,实际上这就相当于完成了我们之前所做的两维特征的特征交叉。 而这里呢,由于每一个节点与之输入层都是全连接的,所以参数的规模呢要比之前大了非常多。我们来看一下一共有多少个参数呢?比如说我们的输入层一共有三个节点,隐层也是有三个节点,那么隐层的每一个节点,显然啦,与输入层之间的都是全连接,也就是都是三个参数,以及每一个隐层的节点呢,都需要一个偏执,所以这里总的参数呢,是3×3+3=12。而相同情景下逻辑回归的参数呢,只有三个,所以从参数量级上呢,神经网络还是要大很多的。 如果按当时我们训练逻辑回归模型所举的例子,输入特征100多维这里就按100维来算的话,隐层的节点如果有n个。那么这里的总参数便是(100n+n)也就是101n,而我们知道逻辑回归当中的这种情形下只有100个参数,所以呢,总体来讲参数的量级上呢,差了n倍,这个n呢是隐层的节点数目,也就是说隐藏的节点数目越多的话,参数的量级差距越大,神经网络能够学习到的隐含特征的也就越丰富。 1.3深度学习与传统的机器学习有哪些流程的异同呢?DL DIFFS ML 介绍完了神经网络,下面我们来看一下深度学习与传统的机器学习有哪些流程的异同呢?首先呢,我们来看一下传统的机器学习。这里以我们所学习过的逻辑回归模型的训练为例,我们拿到训练数据之后呢,需要做特征工程,这里的特征工程呢,是指我们要挑选相应的特征,我们要做特征的离散化。归一化甚至呢,我们要区分连续特征以及离散特征不同的处理,最终呢,我们还要做特征的交叉等等,我们把特征工程处理好了之后呢,我们便得到了训练样本,训练样本呢,在传入的模型当中呢,去进行权重的学习,也就是参数的学习,最终呢,我们得到模型来完成结果的预测。 但是在深入学习中呢,我们只需要对输入的训练样本呢进行基础特征的抽取,而不再需要做繁琐的特征工程,繁琐的特征工程呢,相当于交由我们模型当中的多维参数来帮我们学习,这里呢,神经网络呢,就学习了这些复杂特征,进而呢在隐层之间呢,我们学习模型的参数,得到模型之后呢,我们便用来预测结果。好了本小节就全部结束了,本小节重点介绍了背景知识之什么是深度学习,下一小节我们将选取一种经典的深度神经网络DNN来介绍它的网络结构与数学原理。 二、DNN网络结构与反向传播算法2.1DNN网络结构开始本小节的课程之前,我们首先来回顾一下上一小节的内容,上一小节我们重点介绍的背景知识,什么是深度学习,那么本小节我们叫重点介绍,DNN网络结构以及DNN网络结构参数学习的数学原理,下面开始本小节的内容,下面我们来看一下DNN网络结构,DNN呢,实际上是深度神经网络与我们之前介绍过的,神经网络的结构呢,有相似性,也有不同的地方相似的地方呢。 相似的地方就是这里也分为三个大部分,第1大部分呢便是输入层,这是我们特征。多了的地方,第2大部分呢是隐层是我们基础特征的抽象到高阶特征,然后高阶特征之间参数不停的学习的过程。第3部分呢便是输出层,这里可以是一个节点,像我们在点击率预估问题中呢,便是一个输出,也可以是多个输出,像我们在多分类问题当中呢,便是多个输出。 但是这里与我们之前讲过的神经网络有不同的地方呢,便是我们的隐层呢,这里可以是多层,每一层的节点呢可以变得不同,好了这便是DNN网络结构。当然这里输入层与隐层,隐层与隐层,隐层与输出层之间的也都是全连接。 2.2 DNN模型参数2.2.1 隐层的层数,每个隐层神经元的个数,以及激活函数好了,下面让我们来看一下DNN模型当中有哪些重要的参数是值得我们注意的。首先呢,便是隐层的层数,每个隐层神经元的个数,以及激活函数。隐层的层数以及每个隐层神经元的个数呢,决定了网络的参数的量级,这里上一小节呢,我们曾经简单的举例过,三个维度特征的输入以及单隐层,三个隐层节点的话,它的参数呢是3×3+3,那么很明显我们这里可以以此类推,如果隐层与隐层之间的计算方式呢,也是这么计算,模型的参数的量级呢,是由隐层的层数以及每个隐层神经元的个数来决定的。当然了,还与我们输入特征的维度有直接的关系。激活函数的上一小节我们曾经介绍过三种,这里我们说过激活函数呢是决定我们参数收敛的快慢的。 2.2.2 输入输出层的向量维度输入输出层的向量维度,如果是单维度的输出的话,像我们点击率预估这种问题就需要单维度的输出。如果是多维度的输出,像多分类问题呢,就需要多维度的输出。输入层的向量呢,是我们选定好基础特征之后呢,在完成,像字符串的哈希,然后做一个简单的Embedding或者说是我们这里将连续值呢进行分段离散等等的操作之后呢,我们得到的一个输入层的向量。 2.2.3 不同层之间神经元的连接权重与偏移值B我们需要学习的参数是什么呢?就是不同层之间神经元的连接权重,这里可能是输入层与隐层,隐层与隐层,隐层与输出层之间的连接权重,以及呢每一个节点上的偏移值,这是我们模型需要学习的参数。 2.3 前向传播 下面我们来了解一下DNN模型的函数表达式,我们只要了解了网络的任何一个节点的输出值也便得到了模型的输出,因为模型的输出实际上就是输出层节点的输出值。 $a_{j}^{t}=f\left(\sum_{k} w_{j k}^{t} a_{k}^{t-1}+b_{j}^{t}\right)$$z_{j}^{t}=\sum_{k} w_{j k}^{t} a_{k}^{t-1}+b_{j}^{t}$ 下面我们来一起看一下公式,下面来解释一下公式,借助于一个简单的网络。t是指的这里的网络中的第t层,t-1呢是t层前面的一层。比如这一层,我们定义为第t层,那么这一层的前一层显然就是t-1层。 如果大家不好理解的话,可以想象一下,当t为0时也便是输入层。当t为1时,便是第一个隐层,当t为大T,便是输出层,这样也许就会好理解一点。a是指的每一个节点的激活值,这里的j表示的是第t个层上我们的这个第j个节点的激活值,这里我们可以把j想象成为1 2…..的节点。如果是1的话,那么便表示第t层上第1个节点的激活值,如果是2的话,并表示第t层上第2个节点的激活值。这里的w是指的第(t-1)层上第k个的节点指向第t层上j这个节点。这里的a同样是激活值,它表示的是第(t-1)层上第k个节点的激活值,b表示第t层上第j个节点的偏移值。如果这里我们求的是第1个节点的激活值的话,那么显然这里也便是第1个节点的偏移值好了。 下面我们以第t层上第1个节点的激活值求值来举例说明一下这个公式。这里呢,如果我们要求第t层上第1个节点的激活值,那么显然我们要依赖于第(t-1)层上,每一个节点的激活值。那么公式应该如下$w_{11}^{t}a_{1}^{t-1}+w_{12}^{t}a_{2}^{t-1}+w_{13}^{t}*a_{3}^{t-1}+b_{1}^t$,最终呢,我们还要加一个$b_{1}^t$偏执,这个偏执得到的加权求和呢。我们再过一下激活函数f,也便得到了我们的激活值。我们把加权求和没有经过激活函数的部分呢定义为$z_{j}^t$。$a_{j}^t$实际上也就是f(z)。 好了,经过我们的讲述呢,我们发现当模型的w与b,也就是所有的参数固定之后呢,我们输入层的特征输入之后,我们第1层的激活值是由我们的输入与w、d参数得到的。第2层呢是由我们第一隐层的激活值呢,与w、b参数达到的,我们这个过程呢是逐步向前去传播。我们把模型根据输入得到输出的过程呢,叫做前向传播。 2.4 反向传播下面我们来学习一下DNN模型是如何学习我们的参数w与b的。 2.4.1 Our Target$\frac{\partial L}{\partial w_{j k}^{t}} \quad \frac{\partial L}{\partial b_{j}^{t}}$ 我们的目标是什么呢?我们的目标很简单,目标是求得损失函数,对模型中任意两层上两个节点连接的偏导,以及求得损失函数,对任意层上节点的偏置的偏导,如果我们得到了这两个偏导的话,我们发现我们就能将模型中的任意一个参数呢进行梯度下降,这样呢,经过数次迭代,我们最终就能将模型完成收敛,也便学习到了我们需要学的w与b。 2.4.2 What We Have$\frac{\partial L}{\partial a_{j}^{T}} \quad \frac{\partial L}{\partial z_{j}^{T}}$ 我们现在已知的是什么呢?我们现在已经知道了,是损失函数对于输出层节点激活值的偏导,这里的T表示的是输出层,为什么说我们已知道了,我们来详细的写一下公式,假使我们这里的loss函数呢是平方损失函数$(y-a_{j}^{T})^2$。对于每一个样本,我们的损失函数呢是这样的。我们发现以loss函数对于我们这里的输出层的输出激活值,去取偏导的话,很明显的,我们是能够得到答案的,也便是$-2(y-a_{j}^{T})$。同样的这里我们知道了,loss函数对于最后一层节点输出激活值的偏导,也便知道了,我们这里的loss函数对于$z_{j}^T$的偏导,我们来推导一下。用一个链导法则。 $\frac{\partial L}{\partial a_{j}^{T}} \quad \frac{\partial a}{\partial z_{j}^{T}}$ a = f(z) 前一部分呢,是我们已经得到答案的,而后一部分呢,我们又曾经说过a呢,实际上等于我们的f(z), 这里的f是激活函数,所以这一部分呢也很容易求的,所以我们说了我们知道了loss函数,对于最后一层节点激活值的偏导也便知道了loss函数对于z的偏导。 2.4.3反向传播的推导 \frac{\partial L}{\partial b_{j}^{t} } = \frac{\partial L}{\partial z_{j}^{t} } * \frac{\partial z_{j}^{t} }{\partial b_{j}^{t} }=\frac{\partial L}{\partial z_{j}^{t} }$z_{j}^{t}=\sum_{k} \mathcal{W}_{j k}^{t} a_{k}^{t-1}+b_{j}^{t}$ $\frac{\partial L}{\partial w_{j k}^{t}}=\frac{\partial L}{\partial z_{j}^{t}} \frac{\partial z_{j}^{t}}{\partial w_{j k}^{t}}=\frac{\partial L}{\partial z_{j}^{t}} a_{k}^{t-1}$ 下面我们来看一下是如何一步一步,通过我们已知的这些东西去进行推导的来看推导,这里我们的目标之一呢是求的loss函数,对于任意节点的偏移,它的偏导,好了这里我们应用一下链导法则,首先呢,我们对loss函数呢,求z值的偏导,既然呢对z值呢,求我们偏移的偏导。 大家应该对这个公式有印象,这个公式是前面我们说的前项传播的公式,所以这里我们看到z值对于b值的偏导呢,实际上是1,因为呢,在z对b偏转的过程中呢,前面这一部分呢相当于是常数。 $\frac{\partial L}{\partial b_{j}^{t}}=\frac{\partial L}{\partial z_{j}^{t}} * \frac{\partial z_{j}^{t}}{\partial b_{j}^{t}}=\frac{\partial L}{\partial z_{j}^{t}} $ $\frac{\partial L}{\partial w_{j k}^{t}}=\frac{\partial L}{\partial z_{j}^{t}} \frac{\partial z_{j}^{t}}{\partial w_{j k}^{t}}=\frac{\partial L}{\partial z_{j}^{t}} a_{k}^{t-1}$好了我们再来看一下,loss函数对于任意的网络中的w值,求偏导的整体过程,同样这里我们也采用链导法则,首先呢是loss函数对于z的偏导。z对于w的偏导。 $z_{j}^{t}=\sum_{k} \mathcal{W}_{j k}^{t} a_{k}^{t-1}+b_{j}^{t}$ 我们看到z对于任意的w的偏导呢,显然呢,是这里的上一层k节点的激活值$a_{k}^{t-1}$,所以我们看到经过我们的推导呢,我们这里只需要知道loss函数对于任意节点的z值,它的的偏导我们也便得到了最终想要的答案,但是我们这里已知的是我们的loss函数对于我们输出节点的z值,所以说如果我们可以利用倒数第2层节点z值与输出层节点z值之间的关系,逐渐的将loss函数对于输入层节点的z值的偏导,传递的倒数第2层,进而呢由倒数第2层传递到倒数第3层,这样逐一的反向传播,我们便可以得到loss函数对于任意节点z值的偏导,继而我们便得到了loss函数,对于模型参数的偏导,也便完成了我们这里的学习。 2.4.4 反向传播的核心部分$\frac{\partial L}{\partial z_{j}^{t-1}}=\sum_{k} \frac{\partial L}{\partial z_{k}^{t}} \frac{\partial z_{k}^{t}}{\partial z_{j}^{t-1}}$$z_{k}^{t}=\sum_{j} w_{k j}^{t} a_{j}^{t-1}+b_{k}^{t}$ $\frac{\partial z_{k}^{t}}{\partial z_{j}^{t-1}}=\frac{\partial z_{k}^{t}}{\partial a_{j}^{t-1}} \frac{\partial a_{j}^{t-1}}{\partial z_{j}^{t-1}}=w_{k j}^{t} \frac{\partial a_{j}^{t-1}}{\partial z_{j}^{t-1}}$ 好了下面我们来看推导的最核心的部分,我们说过现在问题的核心呢,变成了如何由损失函数对于输出层节点的。向前传播到倒数第2层,进而传播到倒数第3层,那么这里我们用一个普遍的公式,便是我们知道了第t层的z值的偏导,怎么由第t层z值的偏导呢去推导出第(t-1)层z值的偏导,如果我们知道了这个式子,问题也便解决了,普遍情况下的链导法的是损失函数对$z_k^t$求导,$z_k^t$对$z_j^{t-1}$求导即可。 为什么这里还有一个累加呢?我们借助于简单的网络来说明一下,我们看到这里上一层的任意节点,对下一层的每一个节点都有贡献,都有函数关系,所以我们在$z_k^t$对$z_j^{t-1}$,求偏导时,我们需要每一个节点都算一下偏导,所以这里出现了累加,好了我们将前向传播的式子呢,jk对调一下,我们之前讲前向传播时是求的第t层上第j个节点的a值或者z值。这里呢,我们是求第t层上第k个节点的z值,实际上我们只是把jk对调了一下即可。好了,我们这里呢,损失函数对第t层的z的偏导我们是知道的,因为呢,我们是从最后的T也就是输出层。往前传播的这里只需要求后一部分即可,我们采用链导法的,借助于激活值,我们看到呢后一部分是直接能够得到答案的,因为我们说过a等于f(z),这里的f是激活函数。 同样呢,我们根据这个式子$\frac{\partial z_{k}^{t}}{\partial z_{j}^{t-1}}=\frac{\partial z_{k}^{t}}{\partial a_{j}^{t-1}} \frac{\partial a_{j}^{t-1}}{\partial z_{j}^{t-1}}=w_{k j}^{t} \frac{\partial a_{j}^{t-1}}{\partial z_{j}^{t-1}}$。前一部分呢,也是能够得到答案的,是这里的$w_{kj}^{t}$,既然这样的话,我们的整体流程也便窜了起来,由于我们这里损失函数对于z值偏等的过程中呢,是从后向前求的,所以我们这里的整体流程呢被称为反向传播。 2.4.5方向传播的流程 对应输入x,设置合理的输入向量 前向传播逐层逐个神经元求解加权和与激活值 对于输出层求解输出层损失函数对于z值的偏导 反向传播逐层求解损失函数对z值的偏导 得到损失函数对于任意节点z值的偏导,也变得到了w与b的梯度 好了,下面我们来看一下整体的反向传播流程是怎么样的,第1步对于从样本中获取的输入呢,我们经过哈希或Embedding等过程呢,设置好合理的输入向量维度,我们根据初始化的模型的参数w与b,逐层的前向传播,这样我们就得到了任意节点的加权和、z值与激活值a值。 然后我们对于输出层求解输出层损失函数对于z值的偏导,这我们是能够得到的,继而反向传播逐层求解损失函数对z值的偏导,这里我们是根据损失函数对$z^t$的偏导与损失函数对于$z^{t-1}$偏导之间的关系反向传播逐层得到的。最终呢,我们得到了损失函数,对于任意节点z值的偏导,也变得到了w与b的梯度,这里我们在推导的时候曾经详细的讲述过。 好了,我们得到了梯度之后呢,便完成了第1轮的迭代,之后呢,我们再逐次将反向传播的过程呢,不停的去进行,直到我们的参数收敛,模型也变训练好了。好了,本小节就到这里,本小节主要讲述了dnn的网络结构以及DNN模型求解的数学原理,下一小节我们将介绍wd模型的网络结构与数学原理。 三、wide and deep的网络结构以及数学原理介绍 开始本小节的课程之前,我们首先来回顾一下上一小节的内容,上一小节我们重点介绍了dnn的网络结构以及数学原理,那么本小节我们将重点介绍wd的网络结构以及数学原理下面开始本小节的内容,下面来看一下本小节的内容会从哪几个方面展开。 1、wd的物理意义,首先会给大家介绍一下wd为什么会优于我们之前单独介绍过的逻辑回归模型,以及上一小节介绍过的深度神经网络。 2、wd的网络结构,会向大家展示一下wd模型是如果构造的。 3、wd的数学原理,我们会一起看一下反向传播算法在wd上是如何进行的。 3.1 wd的物理意义论文:wide & deep learning for recommender systems好了下面我们来看一下wd的物理意义。wd出自于谷歌的论文,这篇论文我会在附件中提供给大家,如果大家需要可以读一下这篇论文呢,详述了wd的优点以及wd的网络结构,包括5个在wd上做的一些实验的结果好了下面我们来介绍一下wd为什么会优于我们之前单独介绍过的逻辑回归模型以及深度神经网络模型。 Generalization and memorization 泛化与记忆首先呢,我们推荐系统当中呢有两个概念,泛化以及记忆,这里的泛化是指的我们推荐的多样性。记忆就比如说某人啊来到我们的推荐系统,一直点击宫斗类型的电视剧,那么这是我们的推荐系统也会一直给他推荐宫斗类型的电视剧,此过程我们称之为记忆,但是呢,这个人也有可能会喜欢历史类型的电视剧的多样性为泛化。我们如何将历史类型的电视剧的特征与该用户的特征在排序时呢,给他学到一个较高的参数呢,如果我们单纯的用逻辑回归的话,我们就需要组合特征,但是组合特征也有一个问题,如果我们的训练数据中没有该用户行为过历史题材电视剧的样本的话,我们的模型也是学不到组合特征对应的参数的,但是呢,深度神经网络DNN是可以的,我们曾经说过,深度神经网络呢可以将我们传入的基础特征进行高维的组合,在隐层当中呢,学出一些高维的特征,但是单独的深度神经网络也有一个问题,如果某用户的行为数据呢并不是十分的充分,那么我们学习的深度神经网络呢,可能会对该用户进行过于的泛化。推荐的结果呢,大多数是他不喜欢的好了,wd的便是能够结合逻辑回归的记忆能力,以及我们深度神经网络的泛化能力,很好的平衡了这两点。 3.2 wd网络结构 下面我们来一起看一下wd的网络结构,wd的网络结构呢分为两大部分,一部分的是wide部分,这一部分和我们之前介绍过的逻辑回归模型呢,有极大的相似性,比如我们这里输入三个特征,那么它同样呢是将这三个特征呢与对应的参数组合$w1x1+w2x2+w3*x3$,第2部分呢是deep部分,deep部分与我们曾经介绍过的深度神经网络呢,几乎是一样的,这里也分为输入层以及隐藏,当然了,我们这里也是全连接的。 这里唯一与之前所介绍过的有所不同的是,wd模型的输出呢是将(wide侧的参数与特征的加权和)与(deep侧最后一个隐层的输出),做相加在一起经过激活函数,最终得到我们模型的输出。 这样的结构呢,能够保证我们在每一次反向传播的过程中呢,不仅更新地deep侧的参数,同时呢也会更新wide侧的参数,这样也就保证了我们所说的联合训练。通常情况下,我们将离散特征以及离散特征的组合特征放入到wide侧,而我们将连续特征的放入到我们的深度神经网络侧,对于一些字符型的特征,我们通常是先做一下哈希,再做一下Embedding,然后传入到deep侧。 3.3 模型的输出好了,下面我们来一起看一下模型的输出。模型的输出,包含两部分。 第1部分的是我们的wide侧的输出,这里的x便是我们wide输入的特征,这里的cross便是特征的组合。 第2部分同样的我们这里的deep侧。倒数第2层节点的激活值也就是最后一个隐层的激活值,与我们输出节点与deep侧相连的w的乘积,再加上偏执。 最终2部分的加和呢,要过一下我们的激活函数,这里的激活函数呢是阶跃函数,但是在deep侧的隐层之间的参数学习是我们采用的激活函数是修正线性单元。 3.4 WD model的反向传播大家一定要注意一下,好了下面我们来看一下反向传播是如何在wd上进行的。 wide参数的学习过程$\frac{\partial L}{\partial w_{w i d e j}}=\frac{\partial L}{\partial a^{T}} \frac{\partial a^{T}}{\partial z^{T}} \frac{\partial z^{T}}{\partial w_{w i d e j}}=\frac{\partial L}{\partial a^{T}} \sigma^{\prime}\left(z^{T}\right) x_{w i d e j}$ 首先呢,我们先来看一下wide参数的学习过程,我们这里采用梯度下降的学习方法,所以呢,我们这里只需要求得,损失函数对于w和任意参数的偏导,也便能够进行参数学习,我们来看一下公式推导,这里采用链导法则。损失函数对w偏导,实际上也就是损失函数对输出层激活值的偏导在乘以激活值,对z值的偏导,在乘z值对参数w的偏大。 损失函数对输出层激活值的偏导,无论我们是采用平方分式函数,还是我们采用对数损失函数,我们曾经都详细的讲过,这一部分的值应该是多少,这里不太赘述。 激活函数的导数计算:我们知道a等于f(z),这里的f就是激活函数,所以呢,这一部分也便是激活函数的导数。我们把$\sigma^{\prime}\left(z^{T}\right) $放进去即可, 最后一部分的,由于我们输出层的输出。实际上我们说过是有两部分的由wide侧以及deep侧,显然呢,我们这里对于wide侧的参数的偏导与deep侧没有关系,也便是我们这里deep侧参数对应的特征。好了,该部分的推导那就讲到这里。 WD model的反向传播$\frac{\partial L}{\partial z_{j}^{t-1}}=\sum_{k} \frac{\partial L}{\partial z_{k}^{t}} \frac{\partial z_{k}^{t}}{\partial a_{j}^{t-1}} \frac{\partial a_{j}^{t-1}}{\partial z_{j}^{t-1}}=\sum_{k} \frac{\partial L}{\partial z_{k}^{t}} w_{d e e p kj}^{t} \frac{\partial a_{j}^{t-1}}{\partial z_{j}^{t-1}}$$z_{k}^{t}=\sum_{j} w_{d e e p k j}^{t} a_{j}^{t-1}+b_{k}^{t} \rightarrow t \neq T \quad z_{k}^{t}=\left(\sum_{j} w_{d e e p kj}^{t} a_{j}^{t-1}+b_{k}^{t}\right)+w_{\text {wide}} * X \rightarrow t=T$ 下面我们来看一下deep侧参数的学习过程,与我们之前讲述过的dnn网络的反向传播是一样的,我们这里的核心呢都是损失函数,对于输出层z值的偏导,逐渐的前向传播,我们逐渐的得到,损失函数对倒数第2层z值的偏导逐次向前,这里唯一不同的是什么呢? 不同的是我们这里的前向传播的过程呢,如果当我们是最后一层时,我们这里会多出了一些wide侧的特征。 下面来看一下公式推导,这里我们在求上一层损失函数,对z值的偏导时,为什么这里会有一个累加呢?我们在上一小节曾经介绍过,这是因为(t+1)层上任意节点的激活值,都曾经被t层上第j节点贡献过,所以呢,我们需要累加。 好了,我们看到这3部分。 第1部分的,损失函数对于上一层节点的z值的偏导,这里我们是知道的,因为在最开始的时候呢,我们是知道,对输出层z值的偏导逐渐的向前传播,也便知道了t层。 第2部分,中间这一部分呢,根据下面我们两个式子,这两个式子我们分别发现的,虽然带我们的输出层的,我们有wide侧的特征,但是呢,并不影响我们这里求偏导,因为对于我们这里的偏导,wide侧的部分相当于常数,所以我们还是得到了相同的答案。 第3部分,最后一部分呢也是我们刚才说过的,a=f(z)。相当于呢,我们是激活函数的导数,也没有问题。我们得到了损失函数,对于任意层结点的z值的偏导。 $\frac{\partial L}{\partial b_{j}^{t}}=\frac{\partial L}{\partial z_{j}^{t}} * \frac{\partial z_{j}^{t}}{\partial b_{j}^{t}}=\frac{\partial L}{\partial z_{j}^{t}} $ $ \frac{\partial L}{\partial w_{j k}^{t}}=\frac{\partial L}{\partial z_{j}^{t}} \frac{\partial z_{j}^{t}}{\partial w_{j k}^{t}}=\frac{\partial L}{\partial z_{j}^{t}} a_{k}^{t-1}$ 根据上一小节我们讲过的,此时我们便得到了损失函数,对于deep侧对于每一个偏执以及每一个w的偏导,这样我们便能够进行我们的梯度下降,进行参数学习了。好了,这就是wd模型的反向传播。 3.5server架构 下面我们来看一下,我们得到了最终的模型之后呢,我们是如何与推荐引擎呢进行交互的,这里由于我们采用的是TensorFlow呢,实现我们的wd模型,这里我们必须搭一个TensorFlow serving来提供我们的深度学习的rank服务。 此时呢,推荐引擎呢,需要发动请求到rank server,rank server与TensorFlow serving呢,进行一次交互,将请求呢透传的TensorFlow serving,TensorFlow serving得到的结果呢,返回给我们这里的rank server。rank server再将结果呢透回给我们这里的推荐引擎,来完成每一个item对该user的得分,进而完成排序,我们之前讲述过的浅层模型是rank server可以直接将我们得到的模型文件的load的内存当中,然后利用API呢,去提供打分。 好了,本小节的内容就到这里,本小节重点介绍了wd的网络结构以及数学原理,那么下一小节我们将代码实在wd模型。]]></content>
<categories>
<category>推荐算法</category>
</categories>
<tags>
<tag>推荐算法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[个性化推荐算法实践第09章浅层排序模型gbdt]]></title>
<url>%2F2019%2F06%2F01%2F%E4%B8%AA%E6%80%A7%E5%8C%96%E6%8E%A8%E8%8D%90%E7%AE%97%E6%B3%95%E5%AE%9E%E8%B7%B5%E7%AC%AC09%E7%AB%A0%E6%B5%85%E5%B1%82%E6%8E%92%E5%BA%8F%E6%A8%A1%E5%9E%8Bgbdt%2F</url>
<content type="text"><![CDATA[[TOC] 个性化推荐算法实践第09章浅层排序模型gbdt本章节重点介绍排序模型gbdt。分别介绍梯度提升树以及xgboost的数学原理。并介绍gbdt与LR模型的混合模型网络。最合结合公开数据集,代码实战训练gbdt模型以及gbdt与LR混合模型。 gbdt通过多轮迭代,每轮迭代产生一个弱分类器,每个分类器在上一轮分类器的残差基础上进行训练。对弱分类器的要求一般是足够简单,并且是低方差和高偏差的。因为训练的过程是通过降低偏差来不断提高最终分类器的精度,(此处是可以证明的)。 GBDT(Gradient Boosting Tree)背景知识介绍: 由于GBDT(Gradient Boosting Tree)是由Boosting Tree,也就是很多个树模型的组合,首先了解什么是决策树,决策树是如果构建的。 GBDT数学原理与构建方法: 我们需要这个GBDT模型是如何将很多个树模型组合在一起,并且发挥了比一颗树更大威力的。 XGBoost数学原理与构建方法: XGBoost是陈天奇提出的GBDT一种改进与落地方法,由于其高性能以及优良的表现,在工业界广泛使用。 从头了解Gradient Boosting算法 :https://blog.csdn.net/qq_36510261/article/details/78875278 GBDT(Gradient Boosting Decision Tree)入门(一):https://blog.csdn.net/qq_38150441/article/details/80343626 一、GBDT(Gradient Boosting Tree)背景知识介绍介绍什么是决策树,并且向大家展示分类与回归树是如何构建的(决策树是如果构建的)。 1、什么是决策树树的数据结构大家都应该了解,树有根节点,左右子树,叶子节点等等组成。 什么是决策树呢?在决策树中我们可以这样认为,叶子节点对应的是模型的输出,其余节点每一个节点对应特征。而决策树呢,就是样本在该节点对应的特征由于取值的不同划分到左右两颗子树里面。同时呢,在左右两颗子树里面再利用对应的节点,将数据划分,最终呢会划分到某一叶子节点,便完成了模型的输出。 大体的流程就是这个样子,如下图。我们以上一小节的曾经举过的实例来简单讲解一下。 比如说这里根节点对应的特征呢,是给没给女朋友买礼物,如果给了的话,那么该样本数据呢就会落到右子树当中,如果没有没有给啊,那么显然呢,要么会落到左子树当中。那么再依据新的节点,这个节点对应的特征可能是说陪没陪女朋友是没有吃饭,如果是陪了的话那么继续往下,没有陪的话那么继续往下到左子树,这样呢,我们一直到叶的节点,叶子节点呢表示模型的输出,由于该实力呢是2分类问题,所以拿叶子结点的输出呢,便是0或1表示呢女朋友是否开心,当然了,树模型也可以是回归树,比如呢,我们在预测某人的身高这个问题上,那么最终的叶子节点可能会输出180、175cm, 相信经过上面的介绍,大家已经对决策处有了一个初步的了解。 下面我们以一组具体的样本来给大家详述一下决策树的构建过程。我们这里有一组训练样本,这个样本呢,有5条数据,然后呢有两个特征,是否在水下生活以及是否有脚蹼,当然了它的label呢是是否鱼类,是一个二分类问题,我们看到这里5组数据呢特征的取值都是0、1,一表示带水下生活,一表示有脚趾,好了,那么我们构建出的决策树应该是什么样呢?是这样的,根节点对应的特征是是否在水下生活?如果不是的话,我们发现没在水下生活的那么很明显对应的label,全都不是鱼类,那么这里也就直接到了叶子节点,也就不是。好了,如果是在水下生活的话,我们看到在水下生活,但是呢,有是鱼类也有不是鱼类的。所以我们这里需要再借助一个特征,也就是有没有脚蹼。我们发现在水下生活,并且有脚蹼的话,图片很明显都是鱼类。如果在水下生活,并且没有脚蹼的话便不是鱼类。这就是我们基于这五条训练数据得到的模型,那么这个模型是如何工作的呢?在新的样本输入进来的时候,比如说不在水下生活也没有脚蹼,那么我们的模型是如何给出预测的呢?首先呢,不在水下生活,直接就会预测不是鱼类,这便是决策书的工作流程,我们已经了解了什么是决策处,但是哪一个特征应该是优先特征帮我们把样本到左右两颗子树,以及整体的决策树构建的流程,我们还不是很清晰,下面来一起了解一下。 2、决策树构造原理 1、CART生成 2、回归树:平方误差最小化原则(CART生成算法构造回归树) 3、分类树:基尼指数(CART生成算法构造分类树) 决策树生成 生成算法 划分标准 ID3 信息增益 C4.5 信息增益率 CART 基尼指数 2.1、CART生成CART算法原理及实现: https://blog.csdn.net/hewei0241/article/details/8280490https://blog.csdn.net/gzj_1101/article/details/78355234 这里介绍的决策树生成树的算法是CART算法,分类回归树算法:CART(Classification And Regression Tree)算法。CART中的“C”表示分类,CART中的“R”呢表示回归,当然了,我们决策树生成的算法呢,还有很多,比如说ID3、C45等等等等,这里为了与后面的知识介绍,GBDT其实相连,所以这里我们选用了CART生成算法进行介绍。 CART生成算法,既可以构造回归树。当然了构造回归树我们采用的原则是平方误差最小化。CART生成算法,也可以去构建分类树。构造分类树时,我们采用基尼指数作为选取最优划分特征的依据。 下面我们分别来看一下回归数与分类数的构造。 2.2、回归树:平方误差最小化原则(CART生成算法构造回归树)2.2.1 回归树的函数表示首先呢,我们来一起看一下回归树的函数表示: f(x)=\sum_{m=1}^{M} c_{m} I\left(x \in R_{m}\right)这里的M表示一共有M个叶子节点,$R_{m}$表示m叶子节点对应的区域,如果我们输入的样本属于这个区域的话,这里公式中的I便为1。如果不属于,公式中的I便为0。也就是说我们的样本是属于这个区域的话,这里公式中的I便为1。如果不属于,公式中的I便为0。也就是说如果我们的样本是m区域,那么对应的输出为$c_{m}$。 {\sum_{x_{i} \in \mathcal{R}_{m}}\left(y_{i}-f\left(x_{i}\right)\right)^{2}} {c_{m}=\operatorname{ave}\left(y_{i} | x_{i} \in R_{m}\right)}$c_{m}$的定义需要满足在m区域的所有样本对应的模型的输出,与自己label差的平方和是最小的。显然$c_{m}$应该等于在m区域所有样本的label的平均值。 下面介绍回归树如何选取最优划分特征。也就是说哪一个特征应该放到根节点,哪一个特征应该放在第二层的节点。 2.2.2 最优划分特征j表示的是特征,s表示的是特征取值的一个临界分隔值。 如果这个特征取值$x^j$比这个临界分隔值s小的话,我们便划分到第一部分 如果这个特征取值$x^j$比这个临界分隔值s大的话,我们便划分到第二部分 对应第一部分和第二部分,有一个常数值,就是常数c,这个c是怎么定义的呢?c是属于区域所有样本的平均值。 倾向性得分。 区域的lable做差。 可以以18做划分 也可以 最优化的划分。 2.2.3 树的构建流程 遍历所有的特征,特征的最佳划分对应的得分,选取最小的得分的特征 遍历所有的特征,特征的最佳划分对应的得分,选取最小的得分的特征为最优特征,会将其放在根节点。 将数据依据此选取的特征划分分成两部分 基于此特征我们将数据分为两部分,也就是树的左右子树。 继续在左右两部分遍历变量找到划分特征直到满足停止条件 在左右子树里,我们再继续进行上面的操作,继续寻找最优划分特征。当然呢,在左右子树里,根节点特征被拿掉了,我们继续这一个流程,直到满足停止条件。在回归树当中呢,停止条件有两个。一是子树中只剩下一个特征。二是达到了我们之前设定的树的深度。 对应分类树来说呢,同样,如果说子树中label只剩下一种分类或者说特征只剩下一个,也是要进行停止的。 分类树与回归树的构建流程是一致的。但是分类树的最佳划分特征选取的是基尼指数。 2.3、分类树:基尼指数(CART生成算法构造分类树)基尼指数公式 {\operatorname{Gini}(D)=1-\sum_{k=1}^{K}\left(\frac{\left|C_{k}\right|}{|D|}\right)^{2}} {D_{1}=\{(x, y) \in D | A(x) \geq a\}, D_{2}=D-D_{1}} {\operatorname{Gini}(D, A)=\frac{\left|D_{1}\right|}{|D|} \operatorname{Gini}\left(D_{1}\right)+\frac{\left|D_{2}\right|}{|D|} \operatorname{Gini}\left(D_{2}\right)}对于某样本D的基尼系数是由上述公式求解的。公式中的$C_k$表示样本中的第k个分类它的样本数。分母表示总的样本数。如果这个样本只有一个分类,我们发现这个样本的基尼系数为0。这也是我们希望看到的。 如果我们将样本中的某特征以a为临界值进行划分,我们便得到了$D_1$,$D_2$两个部分。对于特征A以a为邻近值划分下的基尼指数了,也便是我们的第一的基尼系数加上第二的基尼系数,当然了这里都是带权的,这个权重呢,也便是我们划分的第一的样本除以总的样本的数目第二。第2个样本除以总的样本的数目。 下面我们以一个具体的例子来解释一下这个公式,这个实例呢,是我们之前介绍什么是二叉树的时候曾经用。 好了下面我们来看一下水下生活这个特征,它的基尼指数是多少呢?这里显然呢,水下生活这个特征只能以01为划分。这样呢,我们划分水下生活这个特征成的第一呢是占了3/5,然后这里面呢有2/3的呢,是1是鱼类这个分类,也就是1减去2/3的平方,再减去哪1/3的平方也就得到了4/9【1-(2/3)^2 - (1/3)^2 = 4/9】。我们看到零它占了2/5,但是呢,零对应的所有的内部呢都是一样的,也就是说呢,1减去1的平方也就成了0【1-(2/2)^2=0】,所以呢,对于水下生活这一个特征,它的基尼指数呢是12/45,≈0.26667。 我们再来。看一下是否有脚趾这样一个特征,它的基尼指数,我们看到呢,它将样本划分成了两部分,一部分呢占据4/5,然后呢,这里面呢它应该是呢,1-1/2的平方再减去1/2的平方也就得到了2/4【1-(1/2)^2 - (1/2)^2 = 1/2】,也就是1/2,然后呢,另一部分呢是1/5,然后呢乘以呢,是什么呢?1-1的平方也就是0【1-(1/1)^2】,最终呢他得到的是4/10。 显然呢水下生活它的基尼指数更低一点,它是零点二几,是否有脚趾的基尼指数是0.4。所以说呢,我们的根节点呢,应该用是否在水下生活。本小节的内容到这里就全部结束了,本小节重点讲述了GDP的背景知识决策数,讲述的决策处是什么以及决策处如何构建。 防止回归树过拟合:回归树剪枝(预剪枝和后剪枝),控制树的生长(如控制树深),在树每次生长的时候对一些特征进行采样,甚至可以加一些限制条件比如说在叶子节点上最少要多少个样本。叶子节点少于10个样本是不可以的。必须要在阈值样本数量才可以进行学习。 二、梯度提升树的数学原理与构建流程集成学习Ensemble Learning :Bagging 和 Boosting相关的博文 Bagging和Boosting 概念及区别:https://www.cnblogs.com/liuwu265/p/4690486.html Bagging与随机森林算法原理小结 :https://www.cnblogs.com/pinard/p/6156009.html Bagging(Bootstrap aggregating)、随机森林(random forests)、AdaBoost :https://blog.csdn.net/xlinsist/article/details/51475345 bagging和boosting 总结,较全:https://blog.csdn.net/u014114990/article/details/50948079 集成学习Ensemble Learning与树模型、Bagging 和 Boosting、模型融合:https://blog.csdn.net/sinat_26917383/article/details/54667077 Boosting与Bagging主要的不同是:Boosting的base分类器是按顺序训练的(in sequence),训练每个base分类器时所使用的训练集是加权重的,而训练集中的每个样本的权重系数取决于前一个base分类器的性能。如果前一个base分类器错误分类地样本点,那么这个样本点在下一个base分类器训练时会有一个更大的权重。一旦训练完所有的base分类器,我们组合所有的分类器给出最终的预测结果。 本次的个性化推荐算法实战课程主要从boosting来进行梯度提示树的学习。 2.1 boosting 什么是boosting 大家好,欢迎来到本次的个性化推荐算法实战课程,开始本小节的课程之前,我们首先来回顾一下上一小节的内容,上一个小节我们重点讲述了决策处的构建过程,那么本小节我们将重点介绍梯度提升数的构建过程,下面开始本小节的内容,我们首先来看一下什么是提升方法,其实我反而有这么一种基本的思想,对于一些复杂的任务,无论是回归问题还是分类问题,如果许多专家得出的结论会比其中某一专家得出来的结论更加靠谱,这就是我们俗语说的三个臭皮匠顶一个诸葛亮的道理,那么问题来了,我们在实际的任务中呢学习一个非常非常良好的模型是比较费力的,而学习很多弱的模型呢是比较简单的,如果我们能将这学习到的许多热的模型组合起来,也就完成了我们提升方法的思想,也就通过群体决策来战胜了一个强大的模型,那么无论是对于分类问题还是回归问题,实际上呢提升方法呢,就是从弱学习模型开始出发,反复的学习,得到一系列的弱分类器或回归器将它们组合的,里面构成了强大的模型,大多数提升方法都是通过改变训练数据的权重或者说改变训练数据的值,来完成对于弱模型的学习,这样对于提升方法来说呢,有两个问题需要考虑。 1、第一个问题是每一个弱模型的学习过程中呢,我们怎么改变训练样本的值?或者说是训练样本权重的分布?2、第二个问题呢是如何将这些弱模型的组合成一个强模型? 如何改变训练数据的权重 我们来分别看一下第一个问题如何改变训练数据的权重和值。我们以分类问题来举例,对于刚开始学习到的弱分类器,我们在下一轮学习做分类器的过程中呢,我们会减弱那些被上一轮分类器正确分类的样本,而去增加那些上一轮弱分类器分类错误的样本的权重,这样一来便可以对那些没有被正确分类的样本重点照顾,只有这样才能再将多个弱分类器组合的过程中呢,有希望战胜一个强大的分类器, 如何组合多个基础model第2个问题如何组合多个基础的模型,我们在训练得到了多个弱模型之后呢,不同的提升方法的处理是有所不同的,比如像业界比较著名的ada提升方法的便采用了加权表决的方法,具体的在分类问题呢,它会加大分类误差率较小的弱分类器的权重,使其在表决中呢起到更大的作用,那么对于梯度提升术呢,这里显然呢,它采用的是等权的加和也就是每一棵树呢,它起到的作用都是相同的,对于回归问题呢也是如此,下面我们来看一下提升树的具体的数学原理。 2.2 Boosting Tree 提升树模型函数 {f_{M}(x)=\sum_{m=1}^{M} T\left(x ; \boldsymbol{\theta}_{m}\right)}首先呢,我们看一下提升数的函数表示,这里呢,我来解释一下公式,这是每一棵树它的函数表示,那么这里的提升树呢,由M棵树来构成,每一棵树呢,大家都可以把它想象成上一节我们讲过的决策树,$\theta$是这一棵树的参数,好了,我们看到呢,每一棵树呢都是等权相加的,那么这样的一个提升树模型,我们是怎么样来进行学习的呢? {f_{m}(x)=f_{m-1}(x)+T\left(x ; \theta_{m}\right)}这里呢,我们采用前项分布的算法,这是一种启发式的学习方法,他能够保证了在每一轮的学习当中呢,只学习一棵树的参数,这样有什么好处呢?我们以一个简单的例子来说明一下,如果提升树一共有十棵树,那么我们在学习第一棵树的时候也就是$f_1$,那么嘛,他显然就等于$t_1$,但是$f_2$他等于$f_1$加上$t_2$,这里呢,由于$f_1$已经变成了已知了,那么此时我们只需要学习$t_2$,它保证了我们每一轮的迭代都是可学习的。 {\boldsymbol{\theta}_{m}=\arg \min _{\theta_{m}} \sum_{i}^{N} L\left(y_{i}, f_{m-1}\left(x_{i}\right)+T\left(x_{i} ; \boldsymbol{\theta}_{m}\right)\right)}这样我们如何去学习$t_2$呢?只需要按照我们的损失函数。在损失函数上的我们只需要最小化这个公式就可以,$y_{i}$是样本对应的label,$f_{m-1}\left(x_{i}\right)$这是我们上一轮已经学习到的函数,这是$T\left(x ; \theta_{m}\right)$我们这一轮待学习的函数 这样启发式的方法保证了我们的可学习性,如果我们不采用启发式的方法,大家看一下这里我们的LOSS函数${f_{M}(x)=\sum_{m=1}^{M} T\left(x ; \boldsymbol{\theta}_{m}\right)}$应该是$f_{M}$,$f_{M}$的话这里相当于每一轮不是有一棵树的参数需要学习,而是有M颗树需要学习。那是根本没法学习的。 2.3 迭代损失函数$L(y, f(x))=(y-f(x))^{2}$$L\left(y, f_{m}(x)\right)=\left[y-f_{m-1}(x)-T\left(x ; \theta_{m}\right)\right]^{2}$ 下面我们来一起看一下迭代过程中的损失函数,如果我们的损失函数呢,采用我们之前经常讲的平方损失函数的话,那么第m颗树的学习就变成了下面这个公式,我来解释一下,这里的y是label,这里的$f_{m-1}(x)$是上一轮迭代迭代完成之后,我们的函数变成的样子,也就是说我们第m颗树的学习,只需要去拟合上一轮我们迭代完成$f_{m-1}(x)$之后这个模型对于最终样本的偏差度我们把这个称为残差。 举一个例子比如说我们要预测某人身高,那么在上一轮迭代的过程中呢,我们已经预测出了一米75,而这个label是1米80,那下一次我们第m个树的学习呢,只需要去拟合这5cm的差距,也就是说样本的label变成了5。而不是一米80。上面介绍了一个回归的例子,实际上对于我们的应用场景点击率过来也可以看成是一个回归问题。因为我们这里可以预估出他的点击倾向性,我们设定一个阈值,比如说点击倾向性大于等于了我们这个阈值,那么我们就认为是会点击,如果小于了我们设定的阈值我们就认为是不会点击。所以呢,同样可以让它去拟合这个label与这个点击倾向性之间的差,也就是残差。 2.4 提示树的算法流程初始化$f_0(x)=0$ 对m=1,2…M计算残差$r_m$,拟合$r_m$,得到$T_m$ 更新$f_m = f_{m-1} + T_m$ 下面我们来看一下提示树的算法流程。第一步初始化$f_0(x)=0$,对于第一颗树第二颗树直到第M个颗树,我们依次计算残差,并且用这颗树去拟合这个残差,并且得到相应这颗树。 比如说,我们第1轮的时候$f_0(x)=0$,所以说呢,残差还是label本身我们便得到了$T_1$,那么对于第2个树呢,我们首先计算残差,也就是label的减去$f_1$的值,我们去拟合这一个值得到$T_2$,以此类推,我们今天能够得到每一棵树,直到得到了M。那么我们每一轮训练完一棵树之后呢我们便更新这个公式。也就是说呢,$f_2$等于$f_1$加上$T_2$,这个时候呢,我们得到了f2,对于我们训练$T_3$的时候呢,首先呢,计算残差也就是label减去$f_2$,那么$T_3$的也便去拟合这个label得到了$T_3$,以此类推呢,我们这里f3也就得到了,最终呢,我们得到$f_M$,也便完成了整个提升树的训练,下面我们以一组具体的训练样本为例来详述一下提升树的构建流程。 首先来看一下训练样本,这里的训练样本呢,只有一个特征x,只有一个输出的y,x的分布呢是1~10,label是一些浮点数,我们首先来回想一下决策处的构建过程,我们首先要找到最佳划分的特征,由于这里只有一个特征的,我们只需要找到x的最佳划分,也便是我们第1棵树的根节点特征,这样的训练数据就会被分为左右两颗子树,我们只需要分别计算左子树的label的平均值和右子树label的平均值。也便得到了$T_1$。 我们首先来看一下,怎么来寻找最佳特征的划分?这里我们首先呢以1为划分点,我们看到了小于等于1的是一区域,大于1的是二区域,这里呢,将两个区域呢,分别变成了一区域[1],二区域[2-10],c1呢就是一区域所对应的label的平均值,也就是5.56,同样的c2是什么呢?C2是二区域[2-10]对应label的平均值,这里的是7.5,我们会得到一个倾向性的得分,这个倾向性的得分呢便是,一区域里所有的数据label与c1的差的平方和我们看到的只有一个数据,差的平方和是0。 然后呢2区里所有的数据label与我们c2的差的平方的和也就得到了15.72。这样的,我们遍历了所有的分割点我们看到的123一直到9,我们都是可以分割的,这样呢,我们得到了对优的分割的应该是6,也就是说小于等于6的一组,大于6的是另外一组,那么可是我们得到了$T_1$,$T_1$是什么呢?在特征小于等于6的时候呢,它的值呢是6.24,也就是说,所有label的平均值,在特征大于6的时候呢,它的输出呢是8.91,也是这个区域所有label的平均值,这里呢,我们的呢,f1就等于f0+T1,由于我们初始化f0=0。所以呢,我们得到了f1,这个时候呢,我们只需要计算一下残差,也就是说,此时呢变成了label减去f1,我们看到呢上面讲过的,label是5.56,减去6.24等于-0.68。, 以此类推这里我们得到的残差。 我们继续再找到队友划分,再完成这一棵树的学习也就达到了72 t2呢,这里它是以单为最佳划分的,它的输出分别是哪?负的0.52以及呢0.22,这时呢,我们便得到了f2,f2=什么呢?等于f1+t2,f1呢是这里的t1,所以呢,我们的f2呢也就变成了一个单段的分段函数,分别是特征小于等于3,此时呢输出了便是6.24与负的0.52的和以及特征大于3到特征小于等于6,此时的输出是6.24与0.22的和以及第3部分也面试,特征的大于6,此时输出了也是八年级。9一与0.22的和,那么我们继续整个流程,直到我们训练完大m棵树,页面结束了,我们提升速度的学习好了,了解了提升术的学习方法,我们来一起了解一下梯度提升术的学习方法,当我们的损失函数是平方损失或者指数损失的时候,每一步的优化呢是不那么困难的,但对于一般的损失函数而言呢,每一步的优化并不是特别容易,所以呢,梯度提升数呢,只是将我们的盘差变为了什么呢?变为了损失函数的负7度,在当前模型的曲子,那么我们下一棵树呢,只要去拟合这个值便得到了t小m好了,本小节的内容到这里就全部结束了,本小姐。人员予以构建流程,那么下一小节我们将给大家介绍插队故事的数学原理与构建流程。 2.5 梯度提示树残差的数值改变 r_{m}=-\left[\frac{\partial L\left(y, f\left(x_{i}\right)\right)}{\partial f\left(x_{i}\right)}\right]_{f(x)-f_{m-1}(x)}三、xgboost数学原理介绍开始本小节的课堂之前,我们首先来回顾一下,上一小节的内容,上一小节我们重点讲述了梯度提升数的数学原理与构建过程,那么本小节我们将带大家一起学习一下XGBOST的数学原理与构建流程,XGBOST的数学原理,陈天琪博士曾经公开过一份PPT,我也会在附录里提供给大家方便大家的学习。 下面开始本小节的内容,我们首先来看一下XGBOST的函数表示。 3.1 XGBoost模型函数$f_{M}(x)=\sum_{m=1}^{M} T\left(x ; \boldsymbol{\theta}_{m}\right)$$f_{m}(x)=f_{m-1}(x)+T\left(x ; \theta_{m}\right)$$\arg \min _{\theta_{m}} \sum_{i=1}^{N} L\left(y_{i}, f_{m-1}\left(x_{i}\right)+T\left(x_{i} ; \theta_{m}\right)\right)+\Omega\left(T_{m}\right)$ 同样呢,这里呢,也是有多棵树构成,我们知道多棵树构成的这种学习方法呢,也是使用我们前面讲述过的前项分布,同样呢,既然是前项分布算法,我们就需要优化出这个目标函数,这样呢,我们在每一次学习的过程中呢,就能够学习到一棵树,将这一棵树的参数学习好之后呢,我们再累加起来逐次迭代,这样呢,就能够学习到最终的M棵树。 这里与我们前面讲述的不同的是呢,对于每一棵树都有一个正则化项,也就是说当我们学习第小m棵树的时候呢,会设定一个小m这棵树的正则化,它表示小m这棵树,它的参数的复杂程度,具体的公式后面我们会详细介绍。 如果大家还记得上一节课我们在讲提升树时,对于小m这棵树,我们只需要你和label与$f_{m-1}$的残差即可构建出小m这个树,如果是梯度提升树的话,我们也只需要去拟合损失函数的负梯度,在当前的函数值,当前呢,指的是$f_{m-1}$。 3.2 优化目标的泰勒展开目标函数:$\arg \min _{\theta_{m}} \sum_{i=1}^{N} L\left(y_{i}, f_{m-1}\left(x_{i}\right)+T\left(x_{i} ; \theta_{m}\right)\right)+\Omega\left(T_{m}\right)$ 泰勒展开公式:$f(x+\Delta x) \approx f(x)+f^{\prime}(x) \Delta x+1 / 2 f^{\prime \prime}(x) \Delta x^{2}$ 根据泰勒展开公式优化的目标函数:$\min _{\theta_{m}} \sum_{i=1}^{N}\left[g_{i} T_{m}+0.5 * h_{i} T_{m}^{2}\right]+\Omega\left(T_{m}\right)$ $g_{i}=\frac{\partial L\left(y_{i}, f_{m-1}\right)}{\partial f_{m-1}}, h_{i}=\frac{\partial^{2} L\left(y_{i}, f_{m-1}\right)}{\partial f_{m-1}}$ 但是呢XGBoost采用了另一种思路,我们来看一下,首先呢,我们先介绍一下泰勒公式,相信呢,大家对这个公式呢,都不是很陌生,泰勒公式的二阶段展开是这样的$f(x+\Delta x) \approx f(x)+f^{\prime}(x) \Delta x+1 / 2 f^{\prime \prime}(x) \Delta x^{2}$。我们的优化目标,可以把这一部分($y_{i}, f_{m-1}\left(x_{i}\right)$)看成x,加号后面的部分$T\left(x_{i} ; \theta_{m}\right)$看成$\Delta x$,那么我们便可以按照泰勒公式进行展开。展开之后呢,我们的目标函数得到的是什么呢? $\min _{\theta_{m}} \sum_{i=1}^{N}\left[g_{i} T_{m}+0.5 * h_{i} T_{m}^{2}\right]+\Omega\left(T_{m}\right)$ 第1部分显然呢是loss,在$f_{m-1}$, 这个函数下的loss,对于我们的优化目标来说呢,我们此时构建小m这棵树,那么log在$f_{m-1}$这个地方,它就变成了常数,常数呢对于我们的优化目标是不需要的,我们需要的是损失函数的导数在当前模型的值,以及损失函数的二阶导在当前模型的字,分别对应上面的函数的导数与函数的二阶导数,我们前面说过$\Delta x$呢,看成了这里的$T_{m}$。所以呢,我们便得到了优化目标。 3.3 定义模型复杂度$f(x)=\sum_{j=1}^{Q} c_{j} I\left(x \in R_{j}\right)$$\Omega\left(T_{m}\right)=\partial Q+0.5 \beta \sum_{j=1}^{Q} c_{j}^{2}$ 下面我们来看一下正则化项部分是如何定义模型参数的复杂度。XGBoost里,所有的树呢,都是回归树,我们在讲决策树的时候曾经讲过回归树的模型呢,可以这样$f(x)=\sum_{j=1}^{Q} c_{j} I\left(x \in R_{j}\right)$进行表示,这里呢一共有Q个区域,每一个区域的输出值呢是c,我们讲过c呢,实际上是在该区域所有的样本对应的label的平均值。 好了,我们把复杂度$\Omega\left(T_{m}\right)=\partial Q+0.5 \beta \sum_{j=1}^{Q} c_{j}^{2}$定义为所有的叶子结点的数目,以及呢,每一个叶子结点对应的输出的平方,当然了这里有两个正则化的系数。 这种定义方式的让我们联想到了曾经讲述过的逻辑回归的正则化,非常像我们曾经讲过的,l1正则化与l2正则化,这里的0.5呢与我们前面的泰勒展开呢,相对应我们来看一下,泰勒展开里呢,也有一个0.5,所以呢,我们后面呢,把0.5呢从我们的智能化系数当中的提取出来,我们看到这里的优化目标呢,是从每一个训练样本的角度去进行考虑的,为了接下来我们在构建树的过程中呢,好展开我们的思路,这里呢,我们将优化目标进行一下转化。看一下是如何转化的。 3.4 目标转化 {\min _{\theta_{m}} \sum_{i=1}^{N}\left[g_{i} T_{m}+0.5 * h_{i} T_{m}^{2}\right]+\Omega\left(T_{m}\right)} {\min _{\theta_{m}} \sum_{i=1}^{N}\left[g_{i} T_{m}+0.5 * h_{i} T_{m}^{2}\right]+\partial Q+0.5 \beta \sum_{j=1}^{Q} c_{j}^{2}} {\min _{\theta_{m}} \sum_{j=1}^{Q}\left[\left(\sum_{i \in R_{j}} g_{i}\right) c_{j}+0.5\left(\sum_{i \in R_{j}} h_{i}+\beta\right) c_{j}^{2}\right]+\alpha Q}1、本来优化目标函数: ${\min _{\theta_{m}} \sum_{i=1}^{N}\left[g_{i} T_{m}+0.5 * h_{i} T_{m}^{2}\right]+\Omega\left(T_{m}\right)}$ 这是我们本来的优化目标,它是按照样本的维度N 。我们首先呢把正则化项$\Omega\left(T_{m}\right)$写入进来。 2、目标函数: ${\min _{\theta_{m}} \sum_{i=1}^{N}\left[g_{i} T_{m}+0.5 * h_{i} T_{m}^{2}\right]+\partial Q+0.5 \beta \sum_{j=1}^{Q} c_{j}^{2}}$ 3、被转化后的目标函数:${\min _{\theta_{m}} \sum_{j=1}^{Q}\left[\left(\sum_{i \in R_{j}} g_{i}\right) c_{j}+0.5\left(\sum_{i \in R_{j}} h_{i}+\beta\right) c_{j}^{2}\right]+\alpha Q}$。 我来解释一下公式,之前的我们说过$T_m$每一个样本的输出呢,实际上也就是对应的区域$c_j$的输出,也就是这里的$c_j$。 每一个样本都会对应到每一个叶子节点,这里假使我们小m这棵树呢,一共有10个叶子结点,那么很明显,我们所有样本的输出都会归结到这10个叶子结点。 这里需要重点考虑一下损失函数的一阶导与二阶导。 只需要将样本是属于这一个叶子节点的一阶导,放到这一个区域$c_j$的前面,比如说c1在一个区域,我们的一阶导呢是说我这个样本如果它属于c1这个区域。那么,我的这个样本呢,它对应的一阶导便是其中的一个系数,我们将这个系数累加起来也就是这一部分。 同样的如果呢,如果这个样本是属于c1这个区域,我们将它的二阶导也累加起来当做它的区域。这里呢,合并了一下同类项,把我们的智能化系数呢也合并进来了。就变成了下面这个式子。 就是说遍历所有的样本,变成了遍历所有的叶子节点,相信大家也应该能够理解了。 可能这里大家对本次函数的一阶导二阶导的概念不是很清晰,我们以一个简单的例子来说一下,我们就以这个损失函数呢,是我们长距离的平方根式函数。那么它对于这个FM减1的一阶导应该是什么呢?应该就等于呢,也很简单,就等于那二位的WiFi,减去哪,FM点c,那前面还有一个符号,二跌倒是什么呢?二跌倒就是在这个的基础上呢,再对FM减1求一次等,很明显的就得到2,下面我们来一起看一下目标函数的最优解,我们这里定义了一下大j与大h,那么目标函数呢,就变成了如下这个样子,显然呢,这是一种ax的平方加bx的行驶。我们知道,但是这种形势下呢,如果a是大于0的话,他便是开口向上的抛物线,他的最小值的取代x=负的分母是二倍的a分子呢,是b的情况下,那么好了,这里同样是当我们的c呢取代,负两倍的这一部分,然后呢分子呢是大j的情况下,显然也就是当c等于这一部分的时候,整体的,我们的优化目标函数呢,将会取得最小值,最小值也变是这个式子,我们来一起看一下我们是如何使用目标函数的最小值,去选取我们的最优划分特征的。这是我们获得的目标函数的最小值。我们在cc布什里是依据如下这个公式来选取我们的最优化分特征的,这里呢,我简单解释一下,比如呢,我们这里以年龄这个特征为例,依据年龄呢以18岁痕迹,大于18岁的呢,挖到了左边,小于等于18岁的呢,挖掉了右边,那么这时呢,我们只需要将整体的目标函数的最小值分别减去左半部分目标函数的最小值,再减去右半部分目标函数的最小值,这里的鱿鱼多了一次化身的,会使燕子节点的数目增加1,所以呢,还需要减去一下郑德华的系数,我们遍历完所有的划分,比如说18岁为划分25岁为划分等等等等,得到的最大的收益便是该特征下最佳划分,那么我们遍历所有的特征。每个课堂对应的收益的最大值,也便是我们首先要放到根节点上的特征,进而呢不停的去构建我们的树,直到满足我们前面讲述过的停滞条件,我们来看一下caccabc,第1步呢,也是初始化f0,进而呢对于第1棵树以及之后的每一棵树呢,都应用我们刚才讲述过的,选择最优划分特征的方法呢,去构造树,对于构造完成的数量,我们将它并不是100%的放入到模型当中的,这里会有一个不长,这个不长的,在程序中的默认值是0.3,主要是为了防止过拟合,好了,这就是xz boss的模型的总体流程,那么本小节的内容到这里就全部结束了,本小节重点介绍了xx模型。与总体流程,那么下一小节我们将给大家介绍gbdt模型与逻辑回归模型的混合模型。 四、gbdt模型与逻辑回归LR模型的混合模型个性化推荐算法实战课程开始本小节的课程之前,我们首先来回顾一下上一小节的内容,三位小姐我们重点介绍了差距boss的模型,数学原理与构建流程,那么本小节,我们将重点介绍GB dt与lr的混合模型,下面开始本小节的课程,下面来看一下背景知识,本小节所讲述的GDP与lr模型的混合模型,是出自于当年Facebook所发表的一篇论文,他的题目呢?我贴在你这里,他的paper呢我也会付给大家,如果大家感兴趣的话可以读一下关于GDP与l2模型混合模型的构建思路,主要的原因是什么呢?是因为我们知道我们在构建逻辑,回归模型是需要繁琐的特征处理,如果大家还记得我们在讲逻辑回归模型的构建过程中的时候,我们。与特征的处理,要对特征进行分类,将特征呢分为连续特征与离散特征,对于连续特征,我们首先要统计它的分布,然后呢,按照区间段进行特征的离散化对于离散特征,我们需要统计它的值域,然后呢,进行我们的特征离散化,当然呢,这些特征处理完之后呢,我们还要进行组合特征的筛选,这个过程呢将非常的繁琐,而且呢组合特征需要手动来执行,那么我们并不能完全的发挥出组合特征的威力,所以呢,逻辑回归模型的训练呢是非常非常的繁复,而恰巧我们的树模型呢在连续特征处理时,是不需要进行这么繁琐的处理,我们只需要把连续特征输给数,然后呢数倍基于我们前面讲述的一系列的最优划分特征。去将最优的划分点给选取出来,也就相当于呢,进行了一系列的规则转化,最终呢,将所有的输出的落到某一叶子节点上,那么对于一旦特征呢,我们也是需要刚开始呢,将它进行零一编码,也就是弯号的编码编程,我们可以识别的零一特征,然后呢,在进行我们刚才说过的这一系列的操作,最终呢也是将某一样本的收入呢落到具体的某个燕子节点上,而我们的GDP模型呢,有很多棵树,每一棵树呢都是一样特征,转化到了某一个业主节点上进行输出,那么我们看到实际上在不同的数的输出当中的我们相当于把特征的变换成了一种新的特征,这一新的特征呢是经过我们数内部的结构去进行不断的筛选转化的。高危特征呢,我们再把它放到逻辑回归模型里去训练,得到我们的参数,这时呢,两者模型便能有效的结合,下面我们来一起看一下模型的结构,我们首先来看一下GB dt模型的第1棵树的结构,这里的树的深度呢是2,我们看到呢,他有四个燕子结顶,大家来看一下第2棵树的结构,同样呢,它的深度呢也是2也有4个叶子结点,但是呢,要么对于在数一当中的输出呢是落到了第2个一的节点,要么对于数2的输出呢是落到了第4个电子结点,此时呢,我们便可以进行高为特征的编码,第1个数的编码也就是0 100, 第2个书的编码也就是0001,大家这里对电的节点有概念吗?可以联想一下我们前面讲。绝色素的模型的输出输出的函数是你的小c,如果大家不进的话,我简单的写一下。这里的c也便是我们这里所说的燕子姐姐,由于这里的树的深度是2了,所以一共有4个区域,如果我们样本最终呢属于哪个区域,也就是我们这里我说的,你也没别的,所以呢,我们便将它对应的,落到第几个月的节点,这一位的编码程序,其余的编码成0,这样呢,我们便得到了高为特征,由于这里的我们一共有两棵树,所以呢,这里它都能围住,是吧,如果我们GB dt模型里数的数占多一点数的深度呢,也算深一点,那么懂得特征的维度呢,也会变得更高,这里我们得到了高危的转化特征之后呢,便可以用来训练我们的逻辑回归模型,这里需要训练的是w1到w8这8个参数好了,这边是我们的GDP。LI混合模型的网络结构,但是大家这里一定要注意一下这里的训练呢,不是联合训练,是我们的样本,首先训练得到数之后再将样本呢,通过数呢进行编码,得到我们的高为特征,用高为特征呢再去训练我们的lr模型,最终呢,我们在线上预测的时候呢,也是需要我们首先呢将数模型保存,然后呢,将lr模型的参数呢也保存样本过来之后呢,先通过数模型进行特征的转换,然后呢,再将转化完成的特征呢,与我们训练得到的参数呢,去得到我们最终的结果,下面我们来看一下gpdt模型与l2模型,混合模型的优缺点分析,首先来看一下优点,优点呢便是利用数模型的做特征转化,这样呢既可以节省了我们。为逻辑回归模型去手动构造特征的繁琐工程量,同样呢,又利用了数模型能够将特征的较好的处理,而不需要大量的手动工作量的特点,可谓是强强联合,但是呢,它同样也有缺点,它的缺点呢是两个模型的是分别训练的,并不是联合训练,并不是联合训练的,就没有统一的目标函数,没有统一的目标函数呢,理论的可解释性就不强,但是无论如何呢,这种尝试呢也是有创新性的,在我实际的工作经验中呢,发现l2与GDP的混合模型的效果是要比GB dt单独的模型以及l2单独的模型效果要更加好的,本小节的内容到这里就全部结束了,本小节重点讲述了GB dt与lr混合模型的相关制度。那么下一小节我们将带大家一起代码实战GDP模型的训练。 九、boosted Tree学习笔记Introduction to Boosted Trees:https://homes.cs.washington.edu/~tqchen/pdf/BoostedTree.pdf boosted Tree中文学习笔记:https://zhuanlan.zhihu.com/p/26214650 面试重点: 支持向量机通俗导论(理解SVM的三层境界)SVM:https://blog.csdn.net/v_JULY_v/article/details/7624837 XGBoost 手推(重点,逢场必问) 通俗理解kaggle比赛大杀器xgboost https://blog.csdn.net/v_JULY_v/article/details/81410574 xgboost原理:https://www.cnblogs.com/harvey888/p/7203256.html 手推记录-XGboost:https://blog.csdn.net/u014472643/article/details/80658009 30分钟看懂xgboost的基本原理:https://zhuanlan.zhihu.com/p/73725993 机器学习竞赛大杀器XGBoost—原理篇:https://zhuanlan.zhihu.com/p/31654000 面试记录-蚂蚁金服-算法工程师(共四面)通过:https://blog.csdn.net/u014472643/article/details/81979749 机器学习算法中 GBDT 和 XGBOOST 的区别有哪些?https://www.zhihu.com/question/41354392/answer/98658997 线性回归正则化 —— 岭回归与Lasso回归: https://www.cnblogs.com/Belter/p/8536939.html 逻辑回归(logistics regression): https://blog.csdn.net/jk123vip/article/details/80591619 https://blog.csdn.net/weixin_39445556/article/details/83930186 树集成优点: 1、不需要做数据归一化featureNormalize、幅度缩放scaling 2、同一条路径下是不同条件的组合,可以完成非线性的切分(同一条路径下可以组合不同的feature) 3、在工程上来说,树模型是可扩展的。假如加计算资源,可以加速他的计算。 树模型的最优划分属性是可以并行去计算的,那么可以加速他的计算。 FM模型 XdeepFM模型]]></content>
<categories>
<category>推荐算法</category>
</categories>
<tags>
<tag>推荐算法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[个性化推荐算法实践第08章浅层排序模型逻辑回归]]></title>
<url>%2F2019%2F06%2F01%2F%E4%B8%AA%E6%80%A7%E5%8C%96%E6%8E%A8%E8%8D%90%E7%AE%97%E6%B3%95%E5%AE%9E%E8%B7%B5%E7%AC%AC08%E7%AB%A0%E6%B5%85%E5%B1%82%E6%8E%92%E5%BA%8F%E6%A8%A1%E5%9E%8B%E9%80%BB%E8%BE%91%E5%9B%9E%E5%BD%92%2F</url>
<content type="text"><![CDATA[[TOC] 个性化推荐算法实践第08章浅层排序模型逻辑回归本章节重点介绍点击率预估模型,逻辑回归模型,以及选取实例数据集,从特征选择到模型训练、模型评估等几个方面来代码实战逻辑回归模型。 本章节重点介绍一种排序模型,逻辑回归模型。从逻辑回归模型的背景知识与数学原理进行介绍。并介绍样本选择与特征选择相关知识。最后结合公开数据集。代码实战训练可用的逻辑回归模型。 逻辑回归模型的背景介绍一、LR(logistic regression逻辑回归)背景知识介绍将会介绍什么是点击率预估、什么是分类模型以及LR模型的基本使用流程、LR模型的基本训练流程,从这几个方面介绍LR的背景知识。 1. 点击率预估与分类模型什么是点击率预估呢? 相信点击率的概念大家都知道,在系统中,点击率 = 点击的数目 / 总展现的数目,而点击率预估就是针对特定的用户在当前上下文结合用户当前的特征给出的item可能被点击的概率,预估方法可以是一些简单的规则也可以是使用模型,目的就是得到不同的item在此时应该被展现的顺序关系。 点击率预估在推荐、搜索、广告领域都被广泛使用。 那么什么是分类模型呢? 用一个简单的例子进行讲解:假如去菜市场买牛肉,这个牛肉可以称之为新鲜或者不新鲜,我们之前的判断就是根据这个肉的时间。假如有一个模型可以再牛肉到来的时候就给出标签,是新鲜或者不新鲜,那么这样的一个模型就是一个分类模型,而且是一个二分类,因为这里的label只有新鲜或者不新鲜。 当然,这个分类模型也可以是多分类,如果说这个label有很多个,那么便是多分类。我们这里讲的点击率预估实际也是一个二分类问题,因为,点了样本就是1,没点就是0。就在这两类之间,我们会给出每一个样本预估出它倾向1的概率,基于次概率的大小决定了此item的展现顺序。 2. 什么是LR?假设二维空间中有一些数据点,我们想用一条直线来拟合这些数据点经过的路径,这便是回归。但是,LR是一个分类模型,需要在样本进入模型之后给出分类标签。 所以LR对于回归得到的数值会进行一个处理,使之变成0,1这样的标签。这个梳理就是将得到的数值输入到一个函数中,这个函数就是单位阶跃函数,后面会详细讲解这个函数。 这里说的二维空间是只有一个特征的,用在之前讲解的牛肉是不是新鲜这个案例里,这个特征就是时间。那么如果扩展到三维空间中,可以再加入一个特征,比如说这个牛肉的含水量。那么这个时候,可能就不是用一条直线,而是用一个面来拟合整体样本经过的路径。 3. sigmoid函数:单位阶跃函数这个函数有一个特点,那就是输入为0的时候,值为0.5,但是输入>0 的部分,很快便逼近于1,输入 < 0 的部分,很快便逼近于0,所以称之为阶跃函数。 这样的函数,很少有在中间的部分,所以可以很容易的分辨出是1还是0,正好符合0、1分类模型的要求。 4. LR模型的工作流程前面介绍过LR模型的主体流程是:首先对数据进行一个拟合,不管是二维空间里 y = k x + b 这样的直线,还是三维空间里y = a1 x1 + b2 x2 + c 这样的平面,亦或是更高维度我们需要学习更多的参数。像y = k x + b 中的k、b,像三维空间里我们用平面 y = a1 x1 + b2 x2 + c 这里面的a1、b2、c这样的参数都是我们需要学习的。学习完这些参数之后,也就得到了模型。得到模型之后,用参数与输入的特征进行相乘,同时将得到的数值放入前面介绍过的单位阶跃函数中得到类别。 下面用具体的例子,详细介绍一下具体的工作流程: 这里是三个训练数据,训练数据的label就是女朋友是不是开心。我们看到这是一个二分类问题,女朋友开心,label就是1;女朋友不开心,那么label就是0。同时这里每一个训练样本都有三个特征,分别是买礼物、说早安、陪吃饭。 样本1 是买了礼物,说了早安,陪了吃饭,那么女朋友是开心的。 样本2 是没有买礼物,没有说早安,陪了吃饭,女朋友是不开心的。 样本3 是买了礼物,说了早安,,没有陪吃饭,女朋友是开心的。 这里的目标是通过这三个样本,学习出一个LR的模型,然后能够通过这个模型,帮助我们预测女朋友是否开心。我们需要学习的参数是什么呢?很明显就是 a x1 + b x2 + c * x3,这里的a、b、c这三个参数,在得到了这三个参数之后,在接下来的某一天,我们将买没买礼物,说没说早安,有没有陪吃饭带入到这个公式中,也便能够自己知道女朋友是否开心。 以上就是LR模型的工作流程 5. LR模型的整体训练流程(1)从log中获取训练样本与特征 在工业界中,样本与特征都不会是处理好的,我们需要自己从日志中,根据需要去提取。比如在推荐系统中,我们可能有展示日志,有点击日志,有基于点击统计User Profile,也有入库时就获得的item info信息。首先,我们需要判断,哪些展现,哪些点击是需要的,也就是样本选择。因为,有很多样本含有脏数据,我们需要去除掉。 其次,我们需要判断我们需要的是用户的哪些特征。比如说用户的年龄、性别,或者是item的哪些特征,比如item的种类、item的title、item的时长等等。这就是特征选择。 (2)模型的参数学习 获取了样本与特征之后,需要决定模型学习的学习率,是否正则化,以及选用哪一种正则化的方式,同样还有学习的方法等等参数设定。 得到了模型之后,同样需要进行离线评估。 这里面包含了模型本身的指标,还有模型应用到测试集中的指标,来看一下我们的模型是否可用。如果模型可用,需要将模型实例化。 (3)模型预测 这里的预测包含可能是将已经实例化的模型录入到内存中提供服务,也可能是基于已经实例化好的模型再搭建一个模型的服务。 预测部分需要将待预测的数据集带入到训练格式去抽取特征,抽取完特征的待预测的样本放入到模型中便能得到预测的结果。 6. LR模型的优缺点优点:易于理解,计算代价小 这个易于理解是指,我们在建模过程中我们把则认为对结果又影响的特征罗列出来就可以,比如说我们认为哪些因素会影响女朋友开心或不开心呢,那就是买没买礼物,说没说早安,有没有陪吃饭,有没有送回寝室…然后学习这些因素的权重就可以。 在推荐系统中,我们预估这个item能不能被user点击。同样也可以选取一些关键的特征,比如说item的历史点击率,item所属的类型,然后用户喜欢看的类型,这个item的长度等等。 由于LR模型是一个浅层模型,而且需要学习的参数的数目与特征的维度是一一对应的,比如说有100维的特征,那么就需要学习100个参数,所以计算代价是比较小的。 缺点:容易欠拟合,需要特征工程来补足模型容易欠拟合的缺点 欠拟合是一个专业术语,解释一下: 欠拟合表示模型没有从训练数据中学习到所有的规律。我们以女朋友是否开心为例,比如说陪吃饭,以及买礼物都不能单独的影响女朋友是否开心,也许这里需要一个交叉特征,比如陪吃饭和买礼物放在一起的一个特征。当这两样同时做了,女朋友才会开心,如果没做或者是只做了一样,女朋友都是不会开心的。 那么在这种情况下,我们发现,由于LR不能主动的去想学习这些交叉特征,所以需要我们大量的构造特征。这也就是说,特征工程来补足模型容易欠拟合的缺点。 二、LR算法数学原理解析将会介绍LR模型的函数表达式、损失函数以及梯度,并且介绍什么是正则化。(LR模型参数迭代的数学原理) 逻辑回归模型的数学原理 1、单位阶跃函数(sigmoid)单位阶跃函数及其导数 单位阶跃函数的函数表达式: f(x)=\frac{1}{1+\exp (-x)}当 x = 0 时,f(x) = 0.5 ; 当 x = 10 时,f(x) 接近于1 ,这也就是之前说过的,当x > 0 的时候,会非常快速的接近于1 ;当x < 0 的时候,会非常快速的接近于0 。这完全符合LR模型,对0-1分类时的要求。 下面再来看一下单位阶跃函数的导数: f^{\prime}(x)=\frac{\exp (-x)}{(1+\exp (-x))^{2}}经过简单转换,上述式子转换为: f^{\prime}(x)=\frac{1}{1+\exp (-x)} * \frac{1+\exp (-x)-1}{1+\exp (-x)}也就是 f(x) 的导数 = f(x) * (1 - f(x)) 2、LR模型的函数表达式LR模型分为两个步骤: 拟合数据点; 公式: w=w_{1} \times x_{1}+w_{2} \times x_{2}+\ldots+w_{n} \times x_{n}这里的w1,w2…是需要学习的参数,这里的x1,x2…是选取的特征。 用上一部分中的例子来分析,x1可能是陪吃饭,x2可能是送礼物等。 将第一步回归得到的数值带入到阶跃函数中,进而得到分类。 y=\operatorname{sigmoid}(w)也就是女朋友是否开心的倾向性,或者说item是否被用户点击的倾向性。 LR模型的函数表达式就介绍到这里,之前介绍个性化召回算法LFM的时候,曾经介绍过一种最优化的方法来学习参数—梯度下降。 梯度下降需要首先设定损失函数,进而得到梯度,完成参数的迭代。 下面看一下LR模型的损失函数。 3、LR模型的损失函数 \operatorname{loss}=\log \prod_{i=1}^{n} p\left(y_{i} | x_{i}\right)这个损失函数采用的是log损失函数,与之前介绍word2vec算法的损失函数是一致的。 这里没有用平方损失函数的原因是:这里LR模型需要两个步骤,第二步是将第一步拟合的值带入到单位阶跃函数中,如果此时使用平方损失函数的话,损失函数并不是下凸的,而且有很多个波谷,我们在梯度下降的过程中,很容易学习到并不是最低点的波谷,也就是不能学习到最小化的loss function。所以这里采用log 损失函数。 下面解释一下公式: i :样本的数目。也就是说这里有n个样本。那么对于第1个样本呢,该模型下希望预测的概率最准。 p:概率,也就是本来是1的label,希望也是1;如果是0,预测成0 为了统一得到最大化的概率,当label是0的时候,我们就预测(1-这个条件概率),那么整体对于这个损失函数,我们直接最大化这个损失函数,便能够将参数学习到。 下面看一下条件概率: p\left(y_{i} | x_{i}\right)=h_{w}\left(x_{i}\right)^{y_{i}}\left(1-h_{w}\left(x_{i}\right)\right)^{1-y_{i}}这个条件概率就是刚才解释过的,如果这里y = 0,我们看到是后面这一部分起作用,那么也就是我们说的来预测(1-这个概率)。这里的w就是上一篇文章介绍的LR函数表达式的第一部分,也就是所有的参数与特征相乘得到的结果。 下面看一下,将条件概率带入到损失函数中,得到的: \operatorname{loss}=-\left(y_{i} \log h_{w}\left(x_{i}\right)+\left(1-y_{i}\right) \log \left(1-h_{w}\left(x_{i}\right)\right)\right)来看单一样本,这里不再关心 n 个样本。 我们来看单一样本,单一样本这里我们知道$h_{w}\left(x_{i}\right)^{y_{i}}$被log一下,$y^i$是可以提到前面去的;同时,相乘被log一下就变成了相加,就得到了上面式子。 之前说过,loss函数需要最大化,那么这里加了一个负号,所以上面式子中的 loss 需要最小化,也就是使用梯度下降法就可以。 这里再次重申,这里$x_i$表示,第 i 个样本对应的所有特征;这里$y_i$ 是第 i 个样本对应的label。如果想表示第 i 个样本的第一个特征,会在$x_i$的右上角标明$x_{i}^1$,以示区分。 4、梯度下面看一下loss损失函数对于参数w的梯度: 首先,这里选取参数的某一个,这里选择特征$x_j$对应的参数$w_j$来进行演示求偏导,这里应用链导法则(也便等于loss函数对于LR模型输出的偏导): \frac{\partial \operatorname{loss}}{\partial w_{j}}=\frac{\partial \operatorname{loss}}{\partial h_{w}\left(x_{i}\right)} \frac{\partial h_{w}\left(x_{i}\right)}{\partial w} \frac{\partial w}{\partial w_{j}}这里$h_{w}\left(x_{i}\right)$表示:第 i 个样本的输入到 LR 模型中,我们给出的输出。它的公式是$f(x)=\frac{1}{1+\exp (-w)}$。这里的W表示为$w=w_{1} \times x_{1}+w_{2} \times x_{2}+\ldots+w_{n} \times x_{n}$,那么这里$\frac{\partial w}{\partial w_{j}}$就是$x_j$。也就是第i个样本的第j维度的特征。 损失函数 \operatorname{loss}=-\left(y_{i} \log h_{w}\left(x_{i}\right)+\left(1-y_{i}\right) \log \left(1-h_{w}\left(x_{i}\right)\right)\right)损失函数求导 看一下第一部分: \frac{\partial \operatorname{loss} }{\partial h_{w}\left(x_{i}\right)}=-\left(\frac{y_{i}}{h_{w}\left(x_{i}\right)}+\frac{y_{i}-1}{1-h_{w}\left(x_{i}\right)}\right)看一下剩余部分: 根据sigmod阶跃函数f(x) 的导数 = f(x) * (1 - f(x))。$\frac{\partial w}{\partial w_{j}}$就是$x_{i}^j$。 \frac{\partial h_{w}\left(x_{i}\right)}{\partial w} \frac{\partial w}{\partial w_{j}}=h_{w}\left(x_{i}\right)\left(1-h_{w}\left(x_{i}\right)\right) x_{i}^{j}带入梯度公式,得到: \frac{\partial l o s s}{\partial w_{j}}=\left(h_{w}\left(x_{i}\right)-y_{i}\right) x_{i}^{j}那么 i 样本对应的梯度已经得到了,如果这里有n个样本的话。同理,对每一个样本求得梯度,然后去 1/n ,就得到了平均梯度。 得到平均梯度之后,用梯度下降对这一维度的特征进行更新: w_{j}=w_{j}-\alpha \frac{\partial loss}{\partial w_{j}}当然,要对所有维度的特征都进行更新,即w1, w2…同样也是按照这种方式来进行更新迭代的,这里$\alpha$是指学习率。 四、正则化 什么是过拟合? 过拟合就是模型对于训练数据过分的学习,对训练数据完美的适配。有时候训练数据并不能反应事情的本质。 eg.也许女朋友不开心是因为考试挂科了,假如给与我们的训练数据中,全都没有挂科,那么我们后面如何买礼物,如何陪吃饭,我们都会发现,女朋友都会不开心。我们便无法学习到真正事物的本质。也就是我们所说的泛化能力减弱。 完全为防止过拟合,就提出了正则化的概念。 常用的正则化方法有两种:L1、L2 首先看一下L1正则化的公式: \operatorname{loss}_{-} n e w=\operatorname{loss} +\alpha \sum_{i=1}^{n}\left|w_{i}\right|L1正则化的公式,是在原来的损失函数的基础上,将所有权值的绝对值求和,这种方式下,使模型的参数变得稀疏,会产生一部分为0的参数。物理意义上说,就是起作用的特征变少了,模型变得简单了。这样也就不容易过拟合了。 下面看一下L2正则化的公式: \operatorname{loss}_{-} n e w=\operatorname{loss}+\alpha|w|^{2}是在原来损失函数的基础上,加上每一个权重的平方和。因为要最小化损失函数,所以这里倾向于将每一个权重学的比较小。试想一下,如果某一个权重比较大的话,面对数据分布变化的特质数据会产生结果上的非常大的扰动。这也不利于模型的泛化能力。$\alpha$是指正则化参数。 三、样本选择与特征构建将会介绍模型训练中非常重要的一步,那就是样本的选择、特征的选择与处理。我们知道样本与特征决定了模型表现的天花板,而选择什么样的模型只是来逼近这个天花板。 回忆一下,8-1中给出的实例,当时用了3个样本,3个特征来演示LR模型的工作原理。但是,可能会有疑问,为什么只有3个样本?在实际的项目中,可能会有非常多的样本,其中有些样本是可以用的,有些样本是不可以用的,到底哪些可以用,哪些不可以用。包括我们有很多的特征,依据什么规则来判断是否对最终的结果有效都是下面要介绍的内容。 3.1、样本选择下面首先看一下样本方面的知识。 在点击率预估过程中,需要的样本是带有label的,也就是点击或者未点击,这是大前提。也就是说,每个用户的每次刷新,我们都能对应上item是否被点击。这么多的样本都是我们训练时候的有效样本么? 当然不是! 下面首先看一下样本的选择规则。 1、样本选择规则这里面主要包含两个因素,1. 采样比例; 2. 采样率。 ①采样比例 正负样本需要维持一个正常的比例,正常的比例需要符合产品的实际形式。比如说某个产品,用户三次到来就会产生一次购买,那么我们的正负样本就是1:2的比例。 当然,模型训练还有很多的采样规则,比如说在某些模型训练的时候,我们需要确保userid的样本达到平均水平,比如说最少要20个。这个时候,就需要做样本增强。对于该userid下的样本,我们需要给他一个特定的权重,来确保它虽然样本少,但是也能达到最低要求。 ②采样率 当模型没有办法用所有的训练数据的时候,必须设定一定的采样率。常用的随机采样的方法就是其中的一种。 2、样本过滤规则样本过滤规则有两个大方面: 结合业务情况 比如在样本选取时,需要去除爬虫带来的虚假请求,测试人员构造的测试 id 数据,作弊数据等等。还需要根据特定场景下模型的目标来保证样本选取的有效性。 异常点的过滤 常用的方法有基于统计的方法。举个例子,比如说,某个特征,就以某item被评论的数目这个特征为例,99%的评论数目都均匀的分布在0-5000之间,而top 1% 有几十万、几百万这种数量级的评分。对于这种数据,我们选取一个阈值,大于这个阈值的直接去掉,为什么呢?因为这会给我们在特征归一化的过程中,造成极大的样本分布不均。 还有就是基于距离的方法。比如说,某一样本数据与其余样本点的之间的距离,有80%都超过了我们所设定的阈值,这个样本就需要被过滤掉。 3.例子 下面以具体的实例来说明如何选取有效的数据来保证模型训练的目标。 这是某个推荐系统展现给用户的推荐列表,用户依次看到的是itemid1 、itemid2 ….itemid5,同时用户对着5个item分别做了不同的处理,用户点击了itemid1和itemid4,并没有点击itemid2、itemid3、itemid5。这时,我们在构建训练样本的时候,是不是这5个样本都需要呢? 答案是否定的。 这里我们只需要前四个。为什么不要第5个呢?下面解释一下原因呢: 我们模型的最终学习目的是希望用户能够在最开始的位置发生点击,而不用下拉。所以,目标是将最终的推荐列表学习成1,1,0,0,0的形式。在这里我们发现,逆序对是0,0,然后1。对于最后的这个0,我们有两方面的原因不选择它作为训练样本,第一方面,我们不能确定用户是否真的看到了这个数据,对于以上的4条,我们可以确定用户真的看到了,因为最后的点击发生在第四条,用户想要点击到第4条,就需要下拉看到第4条数据;第二个原因,是因为这个数据对我们学习的目标是没有帮助的,如果选取还会增加负样本所占的比例。 结合刚才的分析,最终在这一次展现当中,我们得到了4条样本。 他们分别是位于位置1、位置2、位置3、位置4的样本,我们对每一条样本选取了一些特征,打上label。同时并没有选取位置5的数据。 3.2、特征方面首先对特征进行一个概述,特征如果按照数值类型可以分为连续值类型和离散值类型。举个例子,连续值类型,像item的平均观看时长可能是3.75分钟,4.28分钟等等。所以说,是不可穷举的。而离散值类型是可以穷举的,比如说某人的学历,就是小学、初中、高中、大学、研究生、博士等。 同样,按照统计的力度同样可以分为低纬度和高纬度,低纬度的特征包含像人的年龄、性别,高纬度的特征像这个人他过去30天喜欢什么样的电影,这个人历史上喜欢什么类型的电影等等。 根据数值变化的幅度,可以将特征分为稳定特征和动态特征,稳定特征就想item的历史点击率,而动态特征就想item的天级别的点击率。 特征的概述就概述到这里。 下面看一下如何做特征选择。 1. 特征的统计和分析首先需要知道特征的获取难度,比如说,我们想使用用户的年龄和性别这两个特征,我们发现用户画像中并没有这两个维度。如果需要的话,我们需要根据用户的行为建立一个模型,预估这两个特征。显然成本是较大的,就需要放弃。 第二个是需要看一下覆盖率,同样是用户的年龄和性别这两个特征进行举例。如果说发现能够获取到,但是在整体的覆盖率上不足1%,我们也是不能够用的。 下一个就是特征的准确率,我们发现视频的平均播放时长都只有几毫秒,显然这是违背常识的,那么这个特征也是不能够使用的。 在我们初步分析了哪些特征可以使用之后,我们到底选取什么样的特征来完成训练呢? 2. 特征的选择主要分为两个大方面: (1)根据自己的建模常识,也就是说我们想预估一个目标的话,我们知道哪些特征与这个目标是紧密相连的。比如我们想预估这个item的点击率,那么很明显这个item的类别与这个用户喜欢观看的类别是强相关的特征。还有一些强相关的特征,比如这个item的历史点击率,在我们初步根据自己的常识选取了这么多的特征之后,我们训练出了第一版的模型。 (2)那么剩下的特征选择第二步就是基于模型的表现。我们训练完了基线版的模型之后,如果不能够满足我们对于目标的需要的话,我们应该不停的增减特征来发现增减特征对模型指标的影响。如果说减掉某个特征,反而指标变好的话,那么很明显这个特征就不应该是我们需要的。 实际上,在我们选定了特征之后,想要让模型能够识别这些特征,我们需要将特征变成数字。这也就是特征的预处理。 3. 特征的预处理特征的预处理往往包含三大步骤: (1)缺省值的填充 缺省值是指某些样本里的某些特征是缺失的,我们应该用什么样的规则来填充呢? 业界常用的规则有:使用这个特征的众数,或者是平均数来填充。 (2)归一化 归一化是指将不同维度的数值特征都转化到0,1之间,这样有利于减少由于不同特征绝对数值的影响,对模型权重的影响。举个例子:比如说有一个特征是收入,这个收入可能是几千几万的大数据;还有一个特征是工作的时长,这个工作的时长可能每周都在40-60小时之间,这样可以发现数量值是不一样的。我们需要将他们归一化,这个归一化可以使用排序归一化,以及最大值归一化等等。 排序归一化是指:这一维度的特征按数字进行排序,排序最大的数字将变成1,排序最小的数字变成0。那么举一个例子:如果说一共有10个样本,那么这10个样本之间进行了排序,按照数字的大小,显然最小的对应0.1,第二小的对应0.2,依次类推,最大的变成了1,这样就归一化了0-1之间。如果样本的数目更大,就依此类推。 最大值归一化是指:我们统计出这一维度特征的最大值,然后让所有数字都除以这个最大值,显然这一维度的特征就会被归一化到0-1之间。 (3)离散化 特征的离散化并不是所有模型都需要的,但是逻辑回归LR模型是需要的。 下面以具体的例子讲解什么是离散化: 首先以一个连续值举例,这个值表示人平均每周工作的时长,这个人每周工作23小时,我们怎么离散化呢? 首先需要统计一下,我们样本当中这一维度特征的分布,比如我们想四段离散化,这里我们就需要统计一下四分之一分位点。这里就是0-18小时是一个区间,18-25小时是一个区间,25-40小时是一个区间,40-无穷大是另一个区间。那么我们总共有4个区间,这4个区间我们发现23是位于18-25这个区间,那么就离散化成了 [0,1,0,0]。如果这个值不是23,而是60,显然特征就会被离散化成[0,0,0,1]。当然,这里也可以不被4段化,而是5段,我们只需要按相应的去统计就可以了。 下面再以一个离散值来举例,如果说一个人的国家是中国,系统中只有三个国家,那中国对应的实例化特征就是[1,0,0],美国便是[0,1,0]。 四、代码实战LR之样本选择12]]></content>
<categories>
<category>推荐算法</category>
</categories>
<tags>
<tag>推荐算法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[个性化推荐算法实践第07章综述学习排序]]></title>
<url>%2F2019%2F06%2F01%2F%E4%B8%AA%E6%80%A7%E5%8C%96%E6%8E%A8%E8%8D%90%E7%AE%97%E6%B3%95%E5%AE%9E%E8%B7%B5%E7%AC%AC07%E7%AB%A0%E7%BB%BC%E8%BF%B0%E5%AD%A6%E4%B9%A0%E6%8E%92%E5%BA%8F%2F</url>
<content type="text"><![CDATA[[TOC] 个性化推荐算法实践第07章综述学习排序综述学习排序的思路,并介绍工业界排序架构以及本课程重点讲解的学习排序模型。 一、什么是学习排序(Learn To Rank)?说起学习排序,首先介绍一下排序,排序是在搜索场景以及推荐场景中应用的最为广泛的。 传统的排序方法是基于构造相关度函数,使相关度函数对于每一个文档进行打分,得分较高的文档,排的位置就靠前。但是,随着相关度函数中特征的增多,使调参变得极其的困难。所以后来便将排序这一过程引入机器学习的概念,也就变成这里介绍的学习排序。 那么这里介绍的排序都是学习排序中的Pointwise,指对于单独的文档进行预估点击率,将预估点击率最大的文档排到前面。 所以特征的选择与模型的训练是至关重要的。 那么什么是学习排序呢? 学习排序:将个性化召回的物品候选集根据物品本身的属性结合用户的属性,上下文等信息给出展现优先级的过程便是学习排序。 下面用一个例子进行展示: 假设这里有一个用户A,基于他的历史行为给出了召回,可能是很多种召回算法,经过合并之后得到的6个item[a,b,c,d,e,f]。经过排序,最终将这6个item的优先级固定为c、a、f、d、b、e。 得到优先级的过程就是由排序得到的。分别根据item本身的属性,以及user当前的一些上下文和user固定的一些属性,得到此时最佳的顺序应该是将c给展示,这样以保证最后的点击率最高。 二、排序在个性化推荐系统中的重要作用之前介绍过,在个性化的算法中后端的主要流程是:召回—>排序—>策略调整。 我们说过,召回决定了推荐效果的天花板,那么排序就决定了逼近天花板的程度。 1、排序决定了最终的推荐效果 用户看到的顺序基本就是由排序这一步骤所决定的,如果用户在前面的位置就能够看到自己感兴趣的物品,那么用户就会在推荐系统总停留较长的时间;反之,如果需要用户几次刷新之后,才能得到自己想要的物品,那么用户下一次将不会在信任推荐效果,导致在推荐系统中停留的时间较短。 在工业界中,排序这一部分分为三个步骤: (1)prerank(预排序) 也就是排序之前的部分,由于排序的模型由浅层模型切换到深层模型的时候,耗时在不停的增加。比如之前召回可以允许有5000个物品去做浅层模型,比如说逻辑回归,就是训练出一组参数,那么整体的打分过程耗时很短。但是,如果当排序模型切换到深层模型,比如说DNN,那么整体需要请求一次新的深度学习的服务,那么这5000个item去请求的时间显然是不能承受的。所以要先有一个粗排。这个粗排会将这5000个召回的物品进行第一次排序,将候选集缩小到一定范围之内。这样使排序模型的总处理时间满足系统的性能要求。粗排往往以一些简单的规则为主,比如说使用后验CTR或者说对于新的物品使用入库时的预估CTR等等。 (2)Rank(主排序) 主排序部分就是重点部分,现在业界比较流行的还有一次重排(ReRank)。 主排序模型的分类: a. 单一的浅层模型:浅层模型是相较于深度模型而言的,浅层模型的代表有LR(逻辑回归)、FM。 这一类模型在学习排序初期是非常受欢迎的,因为模型线上处理时间较短,所以它支持特征的维度就会非常的高。但是也存在很多问题:比如像LR模型,需要研发者具有很强的样本筛选以及特征处理能力,这个包含像特征的归一化、离散化、特征的组合等等。 所以,后期发展了浅层模型的组合。 b. 浅层模型的组合 这里比较著名的树模型的组合:GBDT组合,LR+GBDT等等。这一类模型不需要特征的归一化、离散化,能够较强的发现特征之间的规律,所以相较于单一的浅层模型具有一定的优势。 c. 深度学习模型 随着深度学习在工业界应用的不断成熟,以及像tensorflow等深度学习框架的开源,现在工业界大部分的主排序模型都已经切换到了深度学习模型。 (3)Rerank(重排序) 这个重排是将主排序的结果再放入一个类似于session model或者说是强化学习的一个模型里面去进行一个重排序,这种主要是突出了用户最近几次行为的session特征,将与最近几次session内用户行为相近的item给优先的展示,以便获取用户行为的连续性。 KDD2018 | 电商搜索场景中的强化排序学习:形式化、理论分析以及应用http://www.sohu.com/a/244970525_129720 由于单一item在重排模型的耗时要比主模型长很多,所以重排部分只是会影响主排序头部的一些结果,比如说top 50 的结果去进行一个重排。那么既然是这样的话,可以看到,最能影响结果的还是主排序模型。 三、工业界推荐系统中排序架构解析工业界中排序是如何落地的。 算法的后端主流程是:召回之后排序。 召回完item之后,我们将item集合传给排序部分,排序部分会调用打分框架,得到每一个item在当前上下文下,对当前user的一个得分,进而根据得分决定展现顺序。 下面看一下打分框架内部的构成: 首先会将每一个item以及user去提取特征,注意这里提取的特征要与离线训练模型的特征保持一致。提取完特征之后,我们向排序服务发出请求,排序服务会返回给我们一个得分,推荐引擎会基于此得分完成排序。经过简单的策略调整之后,展现给用户。 这里需要特别注意的是,排序服务与离线训练好的排序模型之间的通信。 如果是单一的浅层模型,像LR,那么可以直接将训练好的模型参数存入内存。当排序服务需要对外提供服务的时候,直接加载内存中模型的参数即可。像FM以及GBDT等等,我们只需要离线训练好模型,将模型实例化到硬盘当中。在在线服务当中,由于这些模型都有相应的库函数,他们提供了模型的加载以及模型对外预测等一系列接口,所以便可以完成打分。 但是,对于像深度学习的话,我们在训练完成之后,我们还需要提供一个深度学习的服务供排序服务调用。]]></content>
<categories>
<category>推荐算法</category>
</categories>
<tags>
<tag>推荐算法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[个性化推荐算法实践第06章个性化召回算法总结与评估方法的介绍]]></title>
<url>%2F2019%2F06%2F01%2F%E4%B8%AA%E6%80%A7%E5%8C%96%E6%8E%A8%E8%8D%90%E7%AE%97%E6%B3%95%E5%AE%9E%E8%B7%B5%E7%AC%AC06%E7%AB%A0%E4%B8%AA%E6%80%A7%E5%8C%96%E5%8F%AC%E5%9B%9E%E7%AE%97%E6%B3%95%E6%80%BB%E7%BB%93%E4%B8%8E%E5%9B%9E%E9%A1%BE%2F</url>
<content type="text"><![CDATA[[TOC] 个性化推荐算法实践第06章个性化召回算法总结与评估方法的介绍本章节重点总结前面几章节介绍过的个性化召回算法。并介绍如何从离线与在线两个大方面评估新增一种个性化召回算法时的收益。 一、个性化召回算法的总结这里会将之前介绍过的几种算法进行归类,并简短介绍每一种个性化召回算法的核心原理;同时演示工业界中多种召回算法共存的架构。 下面看一下之前讲过的个性化召回算法的分类: 基于邻域的:CF、LFM、基于图的推荐personal rank item-CF是item根据user的贡献,得到item的相似度矩阵,用户根据用户点击过的item的相似item来完成推荐。 user-CF是user根据item的贡献,得到user的相似度矩阵,用户将根据相似用户点击过的物品完成自己的推荐。 LFM是根据user-item矩阵,将矩阵分解,从而得到user与item的隐向量,将两个向量点乘得到的数值取top k 便完成了推荐。 psersonal rank是根据user与item的二分图,在这个二分图之间随机游走,便得到物品对固定用户的倾向度,把这个倾向度得分叫做PR值。那么取PR值的top k也就完成了用户的推荐。 基于内容的:content-based算法 content-base算法的主要流程是: 首先将item进行刻画,然后将user进行刻画,然后将user的刻画与item的刻画在线上推荐的时候串联起来。 基于神经网络(Neural network)的:item2vec item2vec首先根据用户的行为得到由item得到的句柄,根据训练语料得到item embedding的向量,得到这个向量之后就能得到item的相似度矩阵。从而根据用户的历史点击推荐相似的item给用户,也就完成了推荐。 下面看一下工业界中推荐系统中多种召回并存的架构: 后端算法的核心逻辑: 首先是召回,召回之后是排序,排序之后是策略调整,然后就将结果返回给web层。 接下来看一下,具体在召回阶段是如何多种算法并存的。 比如这里的算法A,召回了两个item,分别是a、b;算法B召回了3个item,分别是a、c、d;同理算法C召回了4个item,分别是e、f、d、c。 那么每一种算法召回的数目是如何确定的呢? 这里有两种形式: 形式一:为了满足rank阶段的性能要求,这里指定召回阶段召回的数目,比如说50个,那么各种算法根据以往的表现来平分这50个,每一个算法有一个比例,比如说算法A是0.2,算法B是0.3,算法C是0.5。这样每一个算法也就有了自己召回的上限。 形式二:rank阶段毫无性能压力,我们给算法A写了多少个推荐都能全部召回,其余算法也是相同的处理。 在召回完成之后,我们需要进行一个合并。合并完成之后,我们得到item a~f,将重复召回的进行去重,但是也会给item a标记上它同时是属于算法A和算法B召回的。召回完成之后,这些item进入排序阶段。 二、个性化召回算法的评价在现有的个性化召回体系下,如果要新增一种个性化召回算法,需要知道这种个性化召回算法会对系统造成怎样的影响,是正向收益还是负向收益。所以需要从离线和在线两个方面对个性化召回算法进行评价。 离线评价准入: 也就是说,在我们新增一种个性化召回算法的时候,我们离线选取了一部分训练文件来训练个性化召回算法的模型。我们根据这个模型得到了一些推荐结果,同时有必要保留一些测试集。在测试集上评价推荐结果的可靠程度。这个可靠程度首先是要有一个预期,这个算法会给线上带来正向还是负向的收益。 当然,最终的结果仍然需要在线上生产环境中去评价真实的受益,也就是做A/B test。 如何在离线进行评价的? 评价方法:评测新增算法推荐结果在测试集上的表现。 这里用一个例子来具体说明: 如果新增了某种个性化召回算法,对于user A我们给出了推荐结果a、b、c。恰巧这里我们获得了user A在测试集上的展现数据,就是a、b、c、m,那么在这里我们发现有3个是重合的,也就是a、b、c,那么这3个就是分母,如果我们在得到了用户A在测试集上的点击数据,这个点击数据恰好是a、c。我们发现这里的推荐结果是a、b、c是分母,然后有两个被点击了,那么a、c就是分子,最后的点击率就是 2/3。 如果这个数据是高于基线的点击率的话,那么就可以将这种推荐算法放到线上做A/B test(A/B测试)。 当然了,我们知道线下的评价结果与线上真实环境中的结果是有差异的,但是这种方式是能够给我们一个最基础的、直观的评判,是否可以准入到线上。 这里简单解释一下什么是测试集? 举例:如果我们要使用itemCF这种个性化召回算法,那么我们首先需要计算item的相似度矩阵,我们这里以过去一周的用户的真实的展现与点击数据为依据来训练这个相似度矩阵。我们只使用周一到周五的数据来训练,周六、周日的数据便是这里的测试集。 在线评价收益:A/B test 线上的评价分为两步: (1)定义指标: 这里需要根据不同的情况,比如说在信息流场景下,我们最关心的就是点击率,平均阅读时长等等指标;但是在电商系统中,我们可能更加关注的是转化率及总的交易额度。 总之要根据自己的产品,来找到最能够评价产品的核心指标。 (2)生产环境A/B test: 往往采用以划分user id尾号的形式,比如说分出1%的流量在原来的个性化召回体系框架上增加要实验的个性化召回算法。实验几天之后,与基线去比较核心指标的优劣。如果收益是正向的,我们就保留。]]></content>
<categories>
<category>推荐算法</category>
</categories>
<tags>
<tag>推荐算法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[个性化推荐算法实践第05章基于内容的推荐方法ContentBased]]></title>
<url>%2F2019%2F06%2F01%2F%E4%B8%AA%E6%80%A7%E5%8C%96%E6%8E%A8%E8%8D%90%E7%AE%97%E6%B3%95%E5%AE%9E%E8%B7%B5%E7%AC%AC05%E7%AB%A0%E5%9F%BA%E4%BA%8E%E5%86%85%E5%AE%B9%E7%9A%84%E6%8E%A8%E8%8D%90%E6%96%B9%E6%B3%95ContentBased%2F</url>
<content type="text"><![CDATA[[TOC] 个性化推荐算法实践第05章基于内容的推荐方法Content Based本章节重点介绍一种基于内容的推荐方法content based。从content based算法的背景与主体流程进行介绍。并代码实战content based算法。 第一部分:基于内容的推荐的理论知识部分一、个性化召回算法Content based背景介绍之前博文讲到的CF、LFM、Personal Rank都同属于基于领域的推荐。item2vec属于基于深度学习的推荐。 基于内容推荐不同于之前讲过的任何一种个性化召回算法,属于独立的分支。我们有必要去了解该算法出现的背景。 思路简单,可解释性强 任何一个推荐系统的初衷,都是推荐出用户喜欢的item。 基于内容的推荐,恰恰是根据用户的喜好之后,给予用户喜欢的物品。 eg:某一个用户经常点击体育类的新闻,那么在这个用户下一次访问这个网站系统的时候,自然而然的给用户推荐体育类型的新闻。那么对于推荐结果可解释性非常的强。 用户推荐的独立性 基于内容的推荐,推荐的结果只与该用户本身的行为有关系,其余用户的行为是影响不到该用户的推荐结果的。但是,联想一下,之前提到的无论是CF还是LFM、personal rank以及item2vec,其余用户的行为都会一定程度上,或多或少的干预到最后的推荐结果。 问世较早,流行度高 由于基于内容推荐思路的极简性,可解释性,所以它出现的非常早;并且,无论是在工业界,还是研究界,都最为一种基础的算法,流行度非常的高。 但是,任何事物都是有两面性的,基于内容的推荐不是说是完美的,它同样有一些非常明显的缺点。 (1)对于推荐的扩展性较差:也就是说,如果一个用户之前经常访问体育类型的新闻,那么在之后的推荐之中,倾向于在体育范围内不断地挖掘;很难完成跨领域的物品推荐。 (2)需要积累一定量的用户的行为,才能够完成基于内容的推荐。 二、Content-based算法的主体流程介绍实际上该算法的主体流程大部分不属于个性化推荐的范畴,应该从属于NLP或者用户画像的范畴。只有极小部分属于个性化推荐算法实战的范畴。 item profile:对item的刻画 针对于基于内容的推荐下,对item的刻画大体可以分为两大类:(1)关键词刻画;(2)类别的刻画。 比如,在信息流场景下,我们需要刻画出这篇新闻属于财经还是娱乐;那么在电商场景下也是一样的,我们需要刻画出这个物品它属于图书还是说属于母婴,具体的关键词上也会有这个图书是数据机器学习的还是人文情感的,这个物品是参与满减的,还是参与包邮的等等。 第一步完成内容的物品刻画之后,第二步需要对用户进行刻画。 user profile 传统范畴的用户画像是比较宽泛的,它不仅包含了用户的动态特征,还包含了它的一些静态特征。 而我们用在基于内容推荐里的更多的是聚焦在用户的长期、短期行为,进而通过行为的分析将用户感兴趣的topic、或者用户感兴趣的类别给予刻画。 那么,有了item的刻画,有了user的刻画,第三步就是在线上完成个性化推荐的过程。 online recommendation 给用户推荐他最感兴趣的一些topic,或者说一些类别。 假设某个用户经常点击明星新闻,当用户访问系统的时候,我们应该明星最新的新闻最及时的推荐给用户,这样点击率自然很高。 那么经过这三步流程的分析,可以发现:实际上,前两步更多的同属于NLP或者说是用户画像的范畴,第三步更多的是我们个性化推荐的内容实战范畴。 下面将每一部分的技术要点进行解析: (1)item profile a. Topic finding(Topic 发现):首先要选定特征,这里的特征是title和内容主体的分词,那么得到词语的分词之后,针对于topic的发掘采用命名实体识别的方式。这个命名实体识别的方式可以去匹配关键词词表,那么得到了关键词之后,我们需要对这些关键词进行一定的排名,那么将排名最高的top 3 或者top 5给 item 完成label。 至于这里的排名,会使用一些算法和规则,算法诸如:TF-IDF,规则是基于自己的场景总结出来的修正错误case的一些规则。 b. Genre Classify(类别的划分):首先选定好特征,这里同样是利用一些文本信息,比如说title,分词(正文中所有的去过标点,去过停用词)得到的词向量,这里词向量在浅层模型中可以直接one-hot编码,在深层模型中,首先可以先进行一个embedding,这里使用的分类模型主要是像LR、GBDT、CNN等等。 分类器的使用,是使用多种分类器,分别占不同的权重,然后对结果进行一个线性的加权,从而得到正确的分类。 以上是针对于文档的topic 发掘或者说是类别的分类进行的叙述。那么对于短视频,实际上现在引入了一些更多的特征,比如关键帧所对应图像的分类识别,以及音频所对应的语音识别后,文字的处理等一些有意义的尝试。 (2)user profile a. Genre/Topic(类别的划分)(Topic 发现): 一个层面是用户对哪些种类的新闻或者是物品感兴趣;另一个层面是对哪些关键词感兴趣。 现在多是基于统计的方式,业界也在做一些尝试,比如引入分类器等等。 b. Time Decay: 注意时间衰减,不同时期的行为所占权重是不同的。 最终,针对于某个用户最想想刻画得到的结果是,用户对于不同种类item的倾向性,eg,比如这个用户对于娱乐倾向性是0.7,对于财经的倾向性是0.3。 (3)线上推荐部分 a. find top k Genre/Topic ; b. get the best n item for fix genre/topic 第一步,基于用户的刻画,找到用户最感兴趣的top k个分类,由于这top k个分类都是带有权重的,那么第二步,相应给每个分类得到n个最好的分类下的item. 这里有两点说明, a. 由于权重的不同,从种类下召回的数目是不同的。比如某人对财经感兴趣,对娱乐也感兴趣,但是对娱乐感兴趣的程度更高。那么对娱乐召回的数目就要多于财经召回的数目。 b. best的理解:这里的best对于不是新item来讲,就是它的后验CTR;如果是新的item,在入库的时候,都会给出一个预估的CTR,那么就用这个预估的CTR来作为衡量的标准。 第二部分:基于内容的推荐的代码实战部分]]></content>
<categories>
<category>推荐算法</category>
</categories>
<tags>
<tag>推荐算法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[个性化推荐算法实践第04章基于深度学习的个性化召回算法item2vec]]></title>
<url>%2F2019%2F06%2F01%2F%E4%B8%AA%E6%80%A7%E5%8C%96%E6%8E%A8%E8%8D%90%E7%AE%97%E6%B3%95%E5%AE%9E%E8%B7%B5%E7%AC%AC04%E7%AB%A0%E5%9F%BA%E4%BA%8E%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%E7%9A%84%E4%B8%AA%E6%80%A7%E5%8C%96%E5%8F%AC%E5%9B%9E%E7%AE%97%E6%B3%95item2vec%2F</url>
<content type="text"><![CDATA[[TOC] 个性化推荐算法实践第04章基于深度学习的个性化召回算法item2vec本章节重点介绍一种基于深度学习的个性化召回算法item2vec。从item2vec的背景与物理意义以及算法的主流程进行介绍。并对该算法依赖的模型word2vec数学原理进行浅析。最后结合公开数据集代码实战item2vec算法。 本章节重点介绍基于深度学习的个性化召回算法item2vec。分为两部分:第一部分,item2vec背景和物理意义,论文依据,算法流程,以及item2vec的依据的模型word2vec的数学原理等等理论部分解析。第二部分,根据示例数据,item2vec的算法流程,编程实战,训练模型得到item对应的向量完成推荐并分析推荐结果。 一、基于深度学习的个性化召回算法item2vec1、个性化召回算法item2vec背景与物理意义个性化召回算法item2vec背景 Item2item的推荐方式效果显著: 很多场景下item2item的推荐方式要优于user2item; item2item的推荐方式:在获取item相似度矩阵之后,根据用户的最近的行为,根据行为过的item找到相似的item,完成推荐,如itemCF。 user2item:根据用户的基本属性和历史行为等基于一定的模型,算出最可能喜欢的item列表写在KV存储中;当用户访问系统的时候,将这些item列表推荐给用户,像userCF、LFM、personal rank算法等都是这种方式。 NN model(神经网络)模型的特征抽象能力 神经网络的特征抽象能力是要比浅层的模型特征抽象能力更强,主要有两方面原因: (1)输入层与隐含层,隐层与输入层之间,所有的网络都是全连接的网络; (2)激活函数的去线性化; 基于上述,基于神经网络的item2item的个性化召回算法item2vec也就在这个大背景下产生了。 3、依据的算法论文:Item2Vec: Neural Item Embedding for Collaborative Filtering 核心内容1:论文首先介绍了item2vec落地场景是类似于相关推荐的场景。也就是说用户点击了某item a APP,那么推荐一款类似的APP给用户。 核心内容2:介绍了item2vec所选的model,也就是word2vec算法原理,文中采用了负采样的训练方法进行介绍,因为我们知道word2vec还有一种也就所说的哈夫曼树的方式进行训练。那么我们后面也用负采样的方式进行介绍word2vec的算法原理。 核心内容3:论文抽象了算法的整体流程。 核心内容4:论文将给出了与之前流行的item2item算法进行结果对比。 个性化召回算法item2vec物理意义在介绍item2item之前,先介绍一下原型word2vec。 word2vec 根据所提供的语料,语料可以想象成一段一段的文字,将语料中的词embedding成词向量,embedding成词向量之间的距离远近可以表示成词与词之间的远近。(word2vec原理中详细介绍怎么样做到可以表征词与词距离的远近) item2item (1)将用户行为序列转换成item组成的句子。 解释:在系统中,无论是用户的评分系统,还是信息流场景下用户的浏览行为,或者是电商场景下用户的购买行为。在某一天内,用户会进行一系列的行为,那么将这一系列的行为抽象出来。每一个用户组成的item与item之间的这种序列的连接关系就变成了之前所说的文字组成的一段一段的句子,那么这里的每一个word相当于这里的item。 (2)模仿word2vec训练word embedding 过程,将item embedding。 word embedding的过程只需要提供语料,也就是一段一段的文字,那么训练得到的word embedding可以表征词语义的远近,那么同样希望表征item之间内涵的远近。 所以,可以将第一步构成的item语料放到word2vec中,也能够完成item embedding。embedding完成的向量同样可以表示item之间的隐语义的远近,也就是说,可以表示item之间的相似性。 以上就是item2vec的物理意义。 个性化召回算法item2vec缺陷(1)用户的行为序列时序性缺失: 在介绍物理意义的时候,说过将用户的行为转化成由item组成的句子,这里句子之间词与词之间的顺序与按照用户行为顺序进行排列和不按照用户行为顺序进行排列的结果是几乎一致的。 也就是用户的行为顺序性,模型是丢失的。 (2)用户行为序列中的item强度是无区分性的: 这里比如说,在信息流场景中,观看短视频的50%或者80%或者100%,在用item组成的句子当中,同样都是出现一次的,而不是说观看100%就会出现2次。 再如,在电商场景中,可能你购买一件商品,或者说你加了购物车,都会出现一次;而不会说,你购买了就会出现两次。加了购物车,在句子当中出现一次。在item cf算法中,item 相似度矩阵计算 ,当时引入了用户行为item的总数,用户行为item的时间这两个特征,来分别将公式进行升级。所以在item2vec算法中,是没有这个消息的。所以是item2vec的缺陷之一。 2、item2vec算法应用的主流程(1)从log中抽取用户行为序列 按照每个用户“天”级别,行为过的item,构成一个完整的句子,这里的行为根据不同的推荐系统所指的不同。比如:在信息流场景下,用户点击就可以认为是行为;那么在评分系统中,我们可能需要评分大于几分;在电商系统中,可能希望用户购买,得到用户行为序列所构成的item句子。 (2)将用户序列当做语料训练word2vec得到item embedding 在训练过程中,word2vec代码不需要书写,但是有很多参数是需要我们具体设定的。 (3)得到item sim关系用于推荐 根据第二个步骤中得到每一个item所embedding的向量,可以计算每一个item最相似的top k个,然后将相似度矩阵离线写入到KV当中,当用户访问我们的推荐系统的时候,用户点击了哪些item,推出这些item所最相似的top k个给用户,就完成了推荐。 eg: 1、首先第一步:这是我们从推荐系统log中获得的,也就是说User A行为过item a、item b、item d,User B行为过item a、item c,User C行为过item b、item e。 2、继而我们需要将这些转化成句子。句子1就是a、b、d。句子2就是a、c。句子3就是b、e。这里我们已经没有了user之间的关系,只留下item组成的句子。 3、将句子放入到放入word2vec模型,这里就是一个输入(word2vec模型是一个三层的神经网络:输入层、隐含层、输出层,具体每一层的作用,公式原理后面详细介绍。) 经过word2vec模型的训练,会得到每一个item对应的embedding层向量。这里只写了item a [0.1,0.2,0.14……..0.3] ,item b [0.4,0.6,0.14……..0.3]。item c,item d,item e也是可以得到的。 4、得到item向量之后,就得到item的sim关系。基于item sim的关系,我们便完成了用户的推荐。 3、item2vec依赖模型word2vec介绍(连续词袋模型和跳字模型(skip-gram))item2vec依赖模型word2vec的数学原理详细介绍 word2vec model 是围绕负采样的训练方法进行介绍,因为我们知道word2vec还有一种也就所说的哈夫曼树的方式进行训练(这里就不做介绍了)。 word2vec有两种形式:连续词袋模型和跳字模型(skip-gram) CBOW(continuous bag of words)连续词袋模型 skip gram()跳字模型 二、item2vec依赖模型word2vec之CBOW数学原理介绍1、CBOW网络结构 网络分为三层:输入层、投影层、输出层。 输入层:上下文;比如说这里有五个词W(t+2)、W(t+1)、W(t)、W(t-1)、W(t-2)。这里我们需要输入的训练数据是W(t)的上下文,即是W(t+2)、W(t+1)、W(t-1)、W(t-2)。 投影层:将上下文的词输入的向量加起来;因为给每个词都初始化了一个向量。比如说我们需要让它的长度是16。那么刚开始的时候,这些词W(t+2)、W(t+1)、W(t-1)、W(t-2),再包括了W(t)都有一个初始化的向量。把这些向量加到了投影层。 输出层:当前词; 投影层与输出层之间是全连接,如果输出的这个词是这里的W(t)的话,希望最大化的就是这个概率。而除了W(t),词典(词典指训练语料包含的所有的词)中所有的其他的词,其余词的概率我们都希望最小那个概率。 这就引申出一个问题,如果其余词都是负样本的话,负样本太多,训练太慢。 所以,采用负采样(后面介绍)的方法。这里我们先不要关心负采样的具体是怎么做的,后面我们会详细的介绍 也就是说,有了正样本W(t)与它的上下文,负样本是通过负采样得到的,我们希望最小化负样本所对应的模型的输出。这里实际上,最后一层的输出不仅仅是只有W(t),实际上是有字典中每一个词。因为每一个词都有一个与输出层之间的全连接,这也是模型需要训练的参数之一。 然后,模型需要得到的是每一个词,我们初始化的那个向量,让它训练,根据我们传入的训练样本,训练完成的向量,就是最后模型输出。我们就是依赖每一个词对应的向量,完成item相似度矩阵之间的运算。 与传统的监督模型有所不同,传统的模型在训练完成之后,需要将它保存,然后对外提供服务,当我们传入真实样本的时候,希望得到一个输出值;而这里不是。下面我们来看下一中形式。 2、CBOW的数学公式 问题抽象 g(w)=\prod_{u \in w \cup N E G(w)} p(u | \operatorname{Context}(w))上式是想最大化的条件概率函数。 下面我来解释一下这个公式: $\text { Context }(w)$:某一词W(t)的上下文的词,已知上下文,想预测中间词。 可能有的同学对于这个训练样本还不是很了解,下面我举一个最简单的例子,有一句话是我是中国人,那么经过分词之后呢,变成了(我 是 中国 人)这么四个字也就是w1,w2,w3,w4,如果我们选窗口是1的话,在这里第1组的训练样本变成了(我 是 中国 人),其中这个”是”就是公式这里的w,它的上下文呢,w(t+1)呢是’中国’,w(t-1)是”我”这个词。 我们知道了”中国”,”我”这个词。如果u是W的话(这个u是公式中W”是”的话),则是需要最大化条件概率;如果是W的负样本(NEG(w))(除了”是”,以外的其他词),我们想要把这个概率最小化,最小化这个条件概率$p(u | \text { Context }(w))$,也就是最大化$1-p(u | \text { Context }(w))$条件概率。那么无论是u是w,或者u是我们选择的负采样的负样本都能统一起来了,实际上这个式子由两部分组成。 这里的条件概率是指: p(u | \text { Context }(w))=\sigma\left(X_{w}^{T} \theta^{u}\right)^{L^{w}(u)}\left(1-\sigma\left(X_{w}^{T} \theta^{u}\right)\right)^{\left(1-L^{w}(u)\right)}这个式子由两部分组成: (1)当u=w时,也就是说label $L^{w}(u)$=1,起作用的是前一部分,因为后面的指数变为零次幂($1-L^{w}(u)$=0),零次幂的话,后面部分就等于1了。起作用的是前一部分,我们是想最大化的正样本的概率。解释一下这里的$X_{w}^{T}$和$\theta^{u}$: $X_{w}^{T}$:CBOW的时候,投影层是将w对应的上下文的词向量加和。也就是我们这里举例子的”我”和”中国”对应的向量加和,便是这里的$X_{w}^{T}$。 $\theta^{u}$:隐含层(投影层)和输出层对应的词为u的时候,它们之间的全连接。 所以,想通过模型训练,让这两个参数$X_{w}^{T}$和$\theta^{u}$相乘得到的结果为1。(按照我们的距离就是16维的横竖向量相乘应该是一个常数,这里我们希望通过训练,让他最终相乘得到的数字是1) (2)负采样部分,也就是后面那一部分,我们这里负采样选取的负样本,希望这一部分是0。也就是希望这一部分$1-\sigma\left(X_{w}^{T} \theta^{u}\right)$为1,也就是最大化$1-\sigma\left(X_{w}^{T} \theta^{u}\right)$这一部分。 损失函数 $\operatorname{Loss}=\log (g(w))$ 这里采用对数损失函数,之前LFM采用的是平方损失函数。 公式带入: \text {Loss}=\sum\left(L^{w}(u) * \log \left(\sigma\left(x_{w}^{T} \theta^{u}\right)\right)+\left(1-L^{w}(u)\right) * \log \left(1-\sigma\left(x_{w}^{T} \theta^{u}\right)\right)\right)取对数之后,简单讲解一下,之前的累乘,由于我们取对数,就变成了累加。而之前里面是两部分相乘,,由于我们取对数,就变成了两部分相加。而且对数里面的幂次,可以直接变成了这里的系数。 对$X_{w}^{T}$和$\theta^{u}$求偏导,求完偏导之后,使用梯度上升法不断迭代这里我们这里需要的参数$X_{w}^{T}$和$\theta^{u}$,继而便能够去迭代每一个词对应的词向量。 梯度: $\frac{\partial L o s s}{\partial \theta^{u}}=\left(L^{w}(u)-\delta\left(x_{w}^{T} \theta^{u}\right)\right) x_{w}$ $\theta^{u}=\theta^{u}+\alpha * \frac{\partial L o s s}{\partial \theta^{u}}$ $\frac{\partial L o s s}{\partial x_{w}}=\left(L^{w}(u)-\delta\left(x_{w}^{T} \theta^{u}\right)\right) \theta^{u}$ $v(w_{context}) = v(w_{context}) + \sum_{u \in w \cup NEG(w)}\alpha *\frac{\partial L o s s}{\partial x_{w}}$ 梯度公式1: 首先对$\theta^{u}$求偏导得到了上述的结果。我们先不在此处讲解推导过程。推导过程和排序部分的逻辑回归的部分完全一样。(推导过程也并不是很复杂,只需要记住链式求导法以及加上激活函数。sigmod函数的导数是等于他的本身乘以(1-他的本身),即是s(x)*(1-s(x)))。这两个小技巧比较容易得到。 公式中,$L^{w}(u)$是label,值是1或者0,如果当这里的词是中心词的时候就是1,如果这里的词是负采样中选取的负样本,那么就是0。$\delta\left(x_{w}^{T} \theta^{u}\right)$这个是模型的输出,实际上是投影层对应的向量$x_{w}^{T}$,并且乘以$\theta^{u}$向量得到的一个值,我们在用激活函数激活一下,也得到了一个零一之间的值。这里的$x_{w}$便是投影层上下文向量的加和。 由于我们之前看到的损失函数里$x_{w}$与$\theta^{u}$是对偶的,所以loss函数对$x_{w}$的偏导也便是与上面对偶的形式,只不过括号外面是乘以$\theta^{u}$。 梯度公式2: 既然分别都得到偏导之后呢,我们如果去更新呢。对于$\theta^{u}$我们根据学习率去对于$\theta^{u}$更新就可以了。但是对于$x_{w}$更新,我们看到由于这里损失函数对$x_{w}$求偏导呢,是与这里的$\theta^{u}$有关系的。这个u我们知道它有可能是中心词,也有可能是负采样所选出来的负样本,所以他是一系列的,我们将这一系列的词,或者说是正负样本对。学习完之后我们得到一个总的梯度。得到这个总的梯度之后呢,是$x_{w}^{T}$的梯度,也就是$x_{w}^{T}$可以去更新它自己。这里$x_{w}^{T}$是所有上下文词向量的加和。这里也采用了上下文的每一个词都共享这个梯度,来更新自己的向量。 这里就是$x_{w}^{T}$对于正负样本对他的梯度的加和。然后我们将上下文中的每一词对应的词向量都以这个梯度去更新。 3、训练的主流程 选取中心词w以及负采样出NEG(w) 根据训练的语料,选取中心词w与上下文的词构成的正样本以及负采样选取出的负样本。 分别获得损失函数对于$X_w$和的$\theta^u$梯度 $X_w$:隐含层(投影层)的向量,是上下层向量的一个累加和; $\theta^u$:正负样本的每一个词都有一个$\theta^u$; 更新$\theta^u$以及中心词对应的上下文context(w)中的每一个词的词向量。 这里更新的时候需要注意: 以一个实例来说明: 中心词所对应的负样本,假使我们选了5个,加上中心词与上下文组成的正样本,这里一共有6个样本。在$X_w$的梯度的过程当中,实际上是6个梯度的加和,构成了它自己的梯度。在每一词所需要更新的$\theta^u$以及$X_w$的1/6的时候,首先先更新$X_w$的1/6。因为$X_w$是依赖于$\theta^u$的。如果这一次我们将$\theta^u$更新呢,再更新$X_w$的话,就错了。 故需要先更新$X_w$,在更新$\theta^u$。 三、item2vec依赖模型word2vec之skip gram数学原理介绍1、skip gram网络结构我们首先来看一下它的网络结构,这里同样有三层构成(输入层、投影层、输出层)。 与CBOW网络结构不同的是,这里的投影层与输入层完成是一样的,也就是说,投影层是W(t)的输入向量。 这里的核心目标是说,当W(t)已知的情况下,去预测它的周围词,我们将这个条件概率最大化就是我们的目标。所以,这里也是有两组参数需要更新的: (1)W(t)与词典中的每一个词所对应的这种全连接网络,这个参数需要更新; (2)W(t)本身对应的初始化的向量。比如说我们想要把每个词映射成16维,这个向量也是需要更新的, 这里与之前有两点不同: (1)这里每一次更新,只能更新W(t)一个词语对应的向量;而CBOW模型一次可以更新4个(4:针对上图)。也就是对应的这个上下文,如果我们上下文的窗口选的更长一点的话,可能会更新的更多一次。 (2)在对W(t)的每一个词进行上下文训练的时候,都需要对输出的词进行一次负采样,来构成训练的负样本。 2、skip gram的数学公式 问题抽象 G=\prod_{u \in \text { Contert }(w)} \prod_{z \in {u} \cup NEG(u)} p(z | w)skip gram是已知中间词,去最大化它相邻词的概率。 举个栗子:”我 是 中国”为例子,这个w就是”是”这个词。这里的z就是”我”或者”中国”他们对应的正样本,以及通过负采样选取的负样本,最大化正样本的输出概率,并且最小化负样本的输出概率,也就是最大化(1-负样本)的输出概率。 与CBOW的不同:CBOW的时候,是选取一次负采样;而这里对于中间词的上下文的每一个词,也就是”我”或者”中国”,每一次都需要进行一个负采样。 下面看一下条件概率: p(z | w)=\left(\delta\left(v(w)^{T} \theta^{z}\right)\right)^{L^{u}(z)} *\left(1-\delta\left(v(w)^{T} \theta^{z}\right)\right)^{1-L^{u}(z)}这个条件概率与之前的CBOW大体形式一样,也就是说当label $L^{w}(u)$=1的时候,我们还是希望最大化这个条件概率。当label $L^{w}(u)$=0的时候(看后半部分),我们需要最大化$(1-\delta\left(v(w)^{T} \theta^{z}\right))$,即最大化1减去模型输出。 这个条件概率与之前的CBOW,不同之处: (1)隐含层(投影层)输出的是中间词对应的词向量;而CBOW是输出的所有中间词上下文词向量对应的和; (2)这里的$\theta^{z}$:上下文的词,或者是上下文的词选出来的负样本的词与输出层之间的全连接;目标是中间词$v(w)^{T}$对应的向量以及$\theta^{z}$进行参数学习。进而得到中间词词向量的最佳表示。 损失函数 \text {Loss}=\sum_{u \in \text {Context}(w)} \sum_{z \in {u} \cup N E G(u)} L^{u}(z) * \log \left(\delta\left(v(w)^{T} \theta^{z}\right)\right)+\left(1-L^{u}(z)\right) * \log \left(1-\delta\left(v(w)^{T} \theta^{z}\right)\right) 采用log损失函数。将上述的式子log一下,两个连乘,变成了两个连加(之前的乘变成了加)。幂次也可以放到log的前面。 但是,可以发现如果按照这个loss去对$v(w)^t$或者$\theta^z$求偏导,在每一轮迭代的时候,只能够对词向量$v(w)^t$进行一次迭代。这里需要进行上下文窗口次的负采样才能对一个词的词向量进行迭代。显然,效率有些低。 在真正的word2vec实现的时候,需要变换一下思路: \text {G}=\sum_{w^c \in \text {context}(w)} \sum_{u \in {w} \cup N E G(u)} p(u|w^c)同样也是基于像CBOW一样的思想,已知上下文$(w^c)$的情况下,最大化中间词u。但是这里上下文$(w^c)$的每一个词都是独立的,不像CBOW是对上下文中所有的词向量进行累加。 下面重新看一下损失函数: \text {Loss}=\sum_{w^c \in \text {Context}(w)} \sum_{u \in {w} \cup N E G(u)} L^{w}(u) * \log \left(\delta\left(v(w^c)^{T} \theta^{z}\right)\right)+\left(1-L^{w}(u)\right) * \log \left(1-\delta\left(v(w^c)^{T} \theta^{u}\right)\right)这个log损失函数,如果我们忽略掉前半部分的累加。我们只看后面这部分。如果把上下文中的单个的词,变成了$w^x$的话,这一部分的损失函数与上一节讲过的GBOW的损失函数就一样了。也就是说每一次不是用所有上下文的累加和向量来进行梯度的学习。而是对每一个词单独学习梯度,并进行单独更新。实际上梯度形式也比较容易。只是比上一节求出来的多了一次累加。 Skip Gram训练主流程 对于中心词上下文词context(w)中的每一个词$w^c$,都需要选取一次负采样,也就是选取词w的正负样本,构造出正负样本; 计算loss对于theta以及$w^c$的偏导;($w^c$指的是我们举例的”我 是 中国”的”我” 或者”中国”),计算偏导也是有顺序的,像CBOW首先更新loss对于$w^c$的偏导,因为这个偏导是最后我们需要更新词向量偏导的 1/n (n=负采样的数目 + 正样本 + 1); 更新$w^c$对应的词向量; skip gram与CBOW相比,每一次负采样skip gram只能更新一个词对应的词向量;而CBOW在一次负采样,可以更新n(n指窗口)个词。 负采样的算法 假设词典(训练样本中所有的词)中有n个词,每一个词都会计算出一个长度,这个长度是一个0-1之间的长度,有的短,有的长,所有词的长度加起来的长度=1。 1、每一词的长度计算: \operatorname{len}(\text {word})=\frac{(\text {counter }(\text {word}))^{\alpha}}{\sum_{w \in D}(\text {counter }(w))^{\alpha}}分子:该单词在所有语料中出现的次数;幂次:相当于做了一个平缓;源码中这个平缓是3/4。 分母:语料中(字典中)所有词出现的次数的累加和。 显而易见这个长度是一个0-1之间的长度。而且所有的词的长度累加便是1。这样每一个词都有自己一个值域。比如说这里的w1(词1)可能是0-0.05,w2可能是0.05-0.11。每一个词的值域都是采用前开后闭的区间。 2、然后,初始一个非常大的数,源码中采用10^8,将0-1进行等分。然后每一段都会对应一个词(w1,w2,….,wn)的值域。 eg:比如这里的m1属于了w1,m2也属于了w1,但是m4和m5属于w2。 在每次进行负采样的过程中,会随机一个0-M之间的数,随机完数字之后,也就知道了随机的哪一个词。eg:随机到了1,那么m1就对应了词1(w1),那么也就是词1。如果我们随机到了4和5,那么m4和m5对应的是w2,那么就是词2是负样本。 注意:如果随机到的词和中心词相同,那么就跳过这次,再进行一次随机。]]></content>
<categories>
<category>推荐算法</category>
</categories>
<tags>
<tag>推荐算法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[个性化推荐算法实践第03章基于图的个性化推荐召回算法PersonalRank]]></title>
<url>%2F2019%2F06%2F01%2F%E4%B8%AA%E6%80%A7%E5%8C%96%E6%8E%A8%E8%8D%90%E7%AE%97%E6%B3%95%E5%AE%9E%E8%B7%B5%E7%AC%AC03%E7%AB%A0%E5%9F%BA%E4%BA%8E%E5%9B%BE%E7%9A%84%E4%B8%AA%E6%80%A7%E5%8C%96%E6%8E%A8%E8%8D%90%E5%8F%AC%E5%9B%9E%E7%AE%97%E6%B3%95PersonalRank%2F</url>
<content type="text"><![CDATA[[TOC] 个性化推荐算法实践第03章基于图推荐的个性化推荐召回算法基于随机游走的Personal Rank本章节重点介绍一种基于图的个性化推荐召回算法personal rank。从personal rank算法的理论知识与数学原理进行介绍。并结合公开数据集,代码实战personal rank算法的基础版本与矩阵升级版本。 基于图的推荐—基于随机游走的personal rank算法实现 博客第一部分:理论部分主要介绍该算法的背景、物理意义、数学公式推导,以及结合在大数据量实际推荐系统开发工作中为了满足训练速度等方面的要求,对数学公式的矩阵化升级。 博客第二大部分:主要介绍结合第一大部分的数学公式与虚拟log数据为大家编程实战该算法。 1、个性化召回算法Personal Rank背景与物理意义1、首先介绍基于图的个性化召回算法—personal rank的背景。 (1)用户行为很容易表示为图 图这种数据结构有两个基本的概念—顶点和边。 在实际的个性化推荐系统中,无论是信息流场景、电商场景或者是O2O场景,用户无论是点击、购买、分享、评论等等的行为都是在user和item两个顶点之间搭起了一条连接边,构成了图的基本要素。 实际上这里user与item构成的图是二分图,后面会介绍二分图的概念以及结合具体的例子展示如何将用户行为转换为图。 (2)图推荐在个性化推荐领域效果显著 2、二分图二分图又称为二部图,是图论中的一种特殊模型。设G=(V,E)是一个无向图,如果顶点V可分割为两个互不相交的子集(A,B),并且图中的每条边(i,j)所关联的两个顶点 i 和 j 分别属于这两个不同的顶点集(i in A, j in B),则称图G为一个二分图。 (如果有一种无向图,它的定点可以分成两个独立的集合,并且互不相交,且所有的边关联顶点,都从属于这个集合。那么这样的图可以称为二分图。) 则,推荐系统中,user、item恰好满足两种独立的集合,并且用户行为总是从user顶点到item顶点集合,所以由推荐系统中user和item之间构成的图就是二分图。 接下来结合具体实例讲解如何将用户的行为转化为二分图。 假设某推荐系统中有4个用户:A B C D,以及从日志(log)中发现对如下item有过行为: 即:user A 对 item a、b、d有过行为,userB 对 item a、c有过行为,userC对 item b、e有过行为,userD 对 item c、d有过行为。 首先将user、item分成两组不相交的集合,如下: 然后,将所有user 对 item 有过行为的进行连线,就可以得到二分图,如下: 此时问题也就抽象出来了,对于userA 来说,item c 和item e哪个更值得推荐? 这里共有5个item,其中userA 已经对item a、b、d有过行为,这里行为是指信息流产品中的点击或者电商产品中的购买等表示user对item喜欢的这种操作。 那么personal rank恰恰是这么一种算法,它能够结合用户行为构成的二分图,对于固定用户对item集合的重要程度给出排序,也就是说将user A 没有对item c 和item e有过行为,但是personal rank算法可以给出item c 和item e对于user A来说,哪个更值得推荐。 下面从物理意义的角度来分析一下,从二分图上如何分析出来item集合对user的重要程度。 3、物理意义(1)两个顶点之间连通的路径数 如果要比较两个item顶点对固定user的重要程度,只需分别看一下user到两个item顶点的路径数,路径数越多的顶点越重要。 (2)两个顶点之间连通的路径长度 同样路径数的情况下,总路径长度越短的顶点越重要。 (3)两个顶点之间连通路径经过顶点的出度 这里解释一下出度的概念:出度是指顶点对外连接边的数目。如user A对item a、b、d有过行为,即为有条连接边,则A的出度为3。如果前两项都相同,则两个item对固定user 的重要程度则比较经过顶点所有的出度和,如果出度和越小则越重要。 结合刚才所举的具体二分图的例子,给大家介绍—对于user A来说,item c 和item e哪个更值得推荐? 2、Personal Rank算法example解析 例子分析 1.分别有几条路径连通? 首先看A-c 之间有几条路径连通:分别是A-a-B-c,A-d-D-c 两条路径连通。 再来看A-e 之间有几条路径连通:A-b-C-e一条路径 从这一角度出发,可以知道 c 比 e 重要。 2.连通路径的长度分别是多少? 首先看A-c 之间有几条路径连通:分别是A-a-B-c,A-d-D-c ,长度都为3 再来看A-e 之间有几条路径连通:A-b-C-e长度为3 3.连通路径的经过顶点出度分别是多少? 首先看A-a-B-c这条路径:A出度是3,a出度是2,B出度是2,c出度是2 再看A-d-D-c这条路径:A出度是3,d出度是2,D出度是2,c出度是2 再看A-b-C-e这条路劲:A出度是3,b出度是2,C出度是2,e出度是1 实例中这里我们物理意义得到的结果。接下来使用程序来完成person Rank算法的时候同样可以得到相同的结论。 虽然 e 的出度和更小,但是由于1中 c 有两条路径,且1的优先级更高,所以还是应该推荐 c。 3、Personal Rank算法公式解析personal rank是可以通过用户行为划分二分图为固定user得到item重要程度排序的一种算法。 1.算法的文字阐述 随机游走算法PersonalRank实现基于图的推荐对用户A进行个性化推荐,从用户A节点开始在用户-物品二分图random walk,以alpha的概率从A的出边中,等概率选择一条游走过去,到达该顶点后(举例顶点a),由alpha的概率继续从顶点a的出边中,等概率选择一条继续游走到下一个节点,或者(1-alpha)的概率回到顶点A,多次迭代。直到各顶点对于用户A的重要度收敛。 后续我们在实现person rank算法的时候用不同的alpha值来做实验,熟悉是Google的pageRank算法的童鞋们可以发现PageRank与person rank算法有极大的相似性。只不过PageRank算法没有固定的起点。 2.算法的数学公式 PR(v)=\left\{\begin{matrix} \alpha * \sum_{v^{\sim} \in i n(v)} \frac{P R\left(v^{\sim}\right)}{\left|o u t\left(v^{\sim}\right)\right|} \ldots\left(v !=v_{A}\right) & \\ (1-\alpha)+\alpha * \sum_{v^{\sim} \in i n(v)} \frac{P R\left(v^{\sim}\right)}{\left|o u t\left(v^{\sim}\right)\right|} \cdots\left(v=v_{A}\right) & \end{matrix}\right.把不同item对user的重要程度描述为PR值。 为了便于理解,同样适用A作为固定起点。user A的PR值初始化为1,其余节点的PR值初始化为0。 这里使用 a 节点和 A 节点阐述公式的上半部分和公式的下半部分: 首先看公式的上部分,根据person rank的算法描述,节点a只可能是节点A与节点B,以alpha概率从他们的出边中等概率的选择了与节点a相互连的这条边。 具体来看,从user A出发有3条边,以3条边中等概率的选择了节点a连接的这条边,以1/3的概率选择连接节点a;user B以1/2的概率选择了连接节点a。 结合阐述看一下公式的上半部分:对于不是A节点的PR值,也就是 a 的PR值,那么首先要找到连接该顶点节点,同时分别计算他们PR值得几分之几贡献到要求节点的PR值。那么A将自己PR值得1/3贡献给了 a ,B将自己PR值得1/2贡献给了 a,分别求和,乘alpha,得到 a 的PR值。 接下来看下半部分:如果要求A节点本身的PR值,首先知道任意节点都会以(1-alpha)的概率回到本身,那么对于一些本来就与A节点相连的节点,比如这里的 a 节点或者 b 节点,它们除了以(1-alpha)的概率直接回到A以外;还可以以alpha的概率从自己的出边中等概率的选择与A相邻的这条边,比如这里的 a 节点,可以以1/2的概率选择回到A节点,所以就构成了下半部分的前后两个部分。 经过分析可以发现,personal rank算法求item对固定user的PR值,需要每次迭代在全图范围内迭代,时间复杂度在工业界实际算法落地的时候是不能接受的,所以要让尽可能多的user并行迭代。结合之前许多其他算法训练的工业界实现,很容易想到矩阵化实现,下面看personal rank算法的矩阵化实现。 3.算法抽象—矩阵式 $r=(1-\alpha) r_{0}+\alpha M^{T} r$ $M_{i j}=\frac{1}{|o u t(i)|} j \in \operatorname{out}(i) e l s e 0$ 假设这里共有m个user,n个item。 R矩阵是m+n行,1列矩阵,表示其余顶点对该固定顶点的PR值。当然得到了这个,就得到了固定顶点下,其余所有顶点的重要程度排序,这里只需要排出m个user节点。只看n个item节点对该固定顶点的排序。也就得到了该固定顶点下推荐的item。 r0 是m+n行,1列的矩阵,负责选取某一节点是固定节点,它的数值只有1行唯一,其余行全为0。唯一的行,即为选取了该行对应的顶点为固定顶点。那么得到的就是该固定顶点下,其余节点对该固定节点的重要程度的排序。 M 是 m+n行 * m+n列的矩阵,也就是行包含了所有的节点,列也包含了所有的节点。 它是转移矩阵,数值定义如下:1.第一行第二列的数值距离,如果第一行对应的数值顶点由出边连接到了第二列的顶点,那么该值就为第一行顶点的出度的倒数;如果没有连接边,那么就是0。 我们很容易联想到,第一个式子包含了刚才所说的非矩阵化的personal rank的公式的上下两部分。 \left(E-\alpha M^{T}\right) * r=(1-\alpha) r_{0}上述公式是本部分中第一个公式,移项、合并同类项之后得到的。 r=\left(E-\alpha M^{T}\right)^{-1}(1-\alpha) r_{0}该公式是上一公式两个同时乘以$\left(E-\alpha M^{T}\right)$转置的之后得到的。 刚才说过,r0是m+n行,1列的矩阵,它能够选取固定的顶点,得到固定顶点的推荐结果。如果将r0变为(m+n)*(m+n)的矩阵,也就得到了所有顶点的推荐结果。 由于得到的推荐结果是考虑顶点之间的PR值的顺序关系,并非一个绝对数值,所以可以将$(1-\alpha) $舍去。所以$\left(E-\alpha M^{T}\right)^{-1}$即为所有顶点的推荐结果。每一列表示该顶点下,其余顶点对于该顶点的PR值。 但是,需要注意的是,每一个user能够行为的item毕竟是少数,所以这里的M矩阵是稀疏矩阵,$\left(E-\alpha M^{T}\right)^{-1}$同样也是稀疏矩阵。]]></content>
<categories>
<category>推荐算法</category>
</categories>
<tags>
<tag>推荐算法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[个性化推荐算法实践第02章基于邻域的个性化召回算法LFM]]></title>
<url>%2F2019%2F06%2F01%2F%E4%B8%AA%E6%80%A7%E5%8C%96%E6%8E%A8%E8%8D%90%E7%AE%97%E6%B3%95%E5%AE%9E%E8%B7%B5%E7%AC%AC02%E7%AB%A0%E5%9F%BA%E4%BA%8E%E9%82%BB%E5%9F%9F%E7%9A%84%E4%B8%AA%E6%80%A7%E5%8C%96%E5%8F%AC%E5%9B%9E%E7%AE%97%E6%B3%95LFM%2F</url>
<content type="text"><![CDATA[[TOC] 个性化推荐算法实践第02章基于邻域的个性化召回算法LFM本章节重点介绍一种基于邻域的个性化召回算法,LFM。从LFM算法的理论知识与数学原理进行介绍。并结合公开数据集,代码实战LFM算法。 个性化召回算法LFM(latent factor model)算法综述 包含了LFM背景,结合LFM的实例与求解方法。 以及LFM算法的应用场景。 LFM(latent factor model)理论知识与公式推导 LFM在建模后如何定义损失函数 如何迭代到模型收敛得到模型参数 LFM(latent factor model)算法与CF算法的优缺点比较 从理论的体系的完整度,离线训练复杂度 在线推荐的可解释性 完成LFM与CF算法的优缺点比较 个性化召回算法LFM(latent factor model)算法综述对于基于邻域的机器学习算法来说,如果要给一个用户推荐商品,那么有两种方式。 一种是基于物品的,另一种是基于用户的。 基于物品的是,从该用户之前的购买商品中,推荐给他相似的商品。 基于用户的是,找出于该用户相似的用户,然后推荐给他相似用户购买的商品。 但是,推荐系统除了这两种之外,还有其他的方式。例如如果知道该用户的兴趣分类,可以给他推荐该类别的商品。 为了实现这一功能,我们需要根据用户的行为数据得到用户对于不同分类的兴趣,以及不同商品的类别归属。 LFM—隐语义模型,属于协同领域。 LFM算法的背景提到协同领域,很多人首先想到的就是item CF与user CF,那么这里说到的LFM与这两者又有什么区别呢? 首先简单回忆一下item CF与user CF。 item CF:主体是item,首先考虑的是item层面。也就是说,可以根据目标用户喜欢的物品,寻找和这些物品相似的物品,再推荐给用户。 user CF:主体是user,首先考虑的是user层面。也就是说,可以先计算和目标用户兴趣相似的用户,之后再根据计算出来的用户喜欢的物品给目标用户推荐物品。 那么LFM呢? LFM:先对所有的物品进行分类,再根据用户的兴趣分类给用户推荐该分类中的物品。 那LFM具体的意义是什么呢? 为了方便大家理解,这里通过item CF再进一步说明。 item CF算法,是将item进行划分,这样一旦item贡献的次数越多,就会造成两个item越相近。举个例子来说,就是当你看你喜欢的电视节目的时候,为了不错过精彩的内容,你广告部分也会看;这时,后台就会统计,你看了电视节目,也看了广告。这样就可能分析出电视节目与广告比较接近。 然而,事实上两者并不一样,如果我们知道两者属于不同的tag,我们不会将他们放到一起,进行降权处理。但是建立大量的tag体系就需要消耗大量的人力标注打标签,人力标注不适用。继而需要机器学习完成分类。在0-1搭建个性化推荐系统算法中显然是不切实际的。那么LFM算法就应运而生。 LFM是根据用户对item的点击与否,来获取user与item之间的关系,item与item之间的关系。 我的理解就是,LFM不仅会考虑item,也会考虑item。 2、什么是LFM算法LFM算法输入:user对item的点击矩阵 LFM模型参数:每一个user的向量表示和每一个item的向量表示。 方式:用user矩阵和item矩阵的矩阵乘 拟合 user对item的点击矩阵 我们知道行向量乘以列向量是一个常数。完美情况下,user向量和item向量的相乘可以拟合点击矩阵的数值。我们可以看出本模型是从user对item的点击矩阵得到的user-item的向量。所以lfm也是矩阵分解的算法的一种。 LFM算法举例 输入:user对item的点击矩阵(1代表点击、0代表未点击) user对item的点击矩阵 item1 item2 item3 user1 1 0 1 user2 0 1 0 user3 1 1 0 输出: user1:[0.123, 0.325, …, 0.623], … item1:[0.214, 0.034, …, 0.241], … user和item的向量维度是自定义的,用user与item作内积即可判断user对item的偏好 图片左边是点击矩阵,user对item的行为矩阵(点击表示为1,没有点击表示为0)。图片右边是模型收敛得到的是use和item的向量。对于这个维度可以是之前设定的。维度可以理解为有哪一些特征会影响uers对item的喜好程度。特征例如:item title,是否含有图片,小清新的、吉他伴奏的、摇滚等等这些特征会影响用户的偏好。这里比如来说统计出来有7个特征。那么在模型设置的时候就可以把维度设置为7,得到的向量显然就是user1,Item1都会表示7维度的向量。那么将user1,item1的转置乘起来的话应该是一个常数。如果将每一个user和item的向量乘起来可以和点击矩阵的常数无限接近的话。那么这个模型的效果也就越好。 3、 LFM算法的应用场景举例(1)获取user的item推荐列表(计算用户toplike) 模型得到了user和item的向量,针对于用户没有被展现的item,我们可以计算出他的一个用户对item的倾向性得分。取top即toplike,后直接完成用户对item的喜爱度列表,写入离线即可完成对在线的推荐。 (2)获取item间的相似度列表(计算item的topsim) 得到item的向量可以用很多表示距离的公式包括cos等等,计算出每一个item的相似度矩阵,该矩阵可以用于线上推荐。当用户点击item之后,给其推荐与该item的topsim item。 (3)挖掘item间隐含topic挖掘(计算item的topic) 根据得到的item向量,可以用聚类的方法,如K-means,层次聚类等等,取出一些隐含的类别。也就是一些隐含的topic,能将item分成不同的簇,推荐时按簇推荐 LFM(latent factor model)理论知识与公式推导本小节主要从数学上重点介绍LFM,主要从建模方法、迭代、收敛等方面认识LFM算法 (1)LFM建模公式 p(u, i)=p_{u}^{T} q_{i}=\sum_{f=1}^{F} p_{u f} q_{i f}p(u,i)表示user-item对,如果user点击了item,那么p(u,i)=1,否则p(u,i)=0。模型的最终输出为user向量和item向量,即$p_u$和$q_i$。其中F表示维度,也就是上一小节阐述的user对item喜欢与否的影响因素的个数。 那么具体如何得到$p_u$和$q_i$呢?我们用机器学习中监督学习的思想解决。在F设定好之后,$p_{u}$和$q_{i}$可以用随机数进行初始化,初始化之后,如何进行迭代呢? 这里采用的方法是梯度下降。分别从损失函数对user向量的偏导和损失函数对item向量的偏导,以及user向量对item向量的迭代来分别介绍。 (2)损失函数 LFM loss function l o s s=\sum_{(u, i) \in D}\left(p(u, i)-p^{L F M}(u, i)\right)^{2}解释公式:p(u,i)是训练样本的label,也就是说如果user点击了item,那么p(u,i)=1,否则p(u,i)=0。后面项是模型预估的user对item喜好程度,也就是前面所说的模型产出的参数p_{u}和q_{i}转置的乘积。这里的D是所有的训练样本的集合。 可以看到如果模型预估的数值与label越接近的话,损失函数数值越小,反之则越大。这里为了防止过拟合,增加了正则化项。如下公式,前半部分是将模型对于user-item对的喜好程度,并进行展开,是前面建模公式中讲到的,得到如下: \operatorname{loss}=\sum_{(u, i) \in D}\left(p(u, i)-\sum_{f=1}^{F} p_{u f} q_{i f}\right)^{2}+\partial\left|p_{u}\right|^{2}+\partial\left|q_{i}\right|^{2}这里$\partial$是正则化系数,是用来平衡平方损失与正则化项,这里采用的是L2正则化,正则化目的是为了让模型更加简单化,防止由于$p_{u}$和$q_{i}$过度拟合训练样本中的数据使模型的参数过度复杂,造成泛化能力减弱。 (3)LFM算法迭代 损失函数对$p_{uf}$和$q_{if}$的偏导:$\frac{\partial l o s s}{\partial p_{u f}}=-2\left(p(u, i)-p^{L E M}(u, i)\right) q_{i f}+2 \partial p_{u f}$ $\frac{\partial l o s s}{\partial q_{i f}}=-2\left(p(u, i)-p^{L F M}(u, i)\right) p_{u f}+2 \partial q_{i f}$ 损失函数对$p_{uf}$和$q_{if}$的偏导之后,我们应用梯度下降的方法可以看到: $p_{u f}=p_{u f}-\beta \frac{\partial l o s s}{\partial p_{u f}}$ $q_{i f}=q_{i f}-\beta \frac{\partial l o s s}{\partial q_{i f}}$ 其中,$\beta$是learning rate,即学习率。编程时候也会按照上述思路来实现代码。 (4)影响因素 哪些参数的设定会影响最终的模型效果? 1.负样本的选取 比起正样本,负样本的数量是非常多的。因为,展现给用户的item比用户点击的item要多的多。我们要有一定的负采样规则,我们选取那些充分展现而用户没有点击的item作为负样本。 那么什么叫做充分展现? 充分展现是指,这个item在所有的用户中,已经有了比较高的展现次数。 然后,我们就可以用user没有点击过的物品中,按照该item在所有用户中的展现次数做排序(来降序),取一定数目的item作为负样本。这个一定数目只要保证对该user来说,它的正负样本均衡就可以。比如,一个用户点击了100个item,那么同样也取100个负样本来保证正负样本的均衡。 2.隐特征F、正则参数$\partial$、学习率(learning rate $\beta$) 以上是对模型比较重要的三个参数。 其中正则参数$\partial$和学习率(learning rate $\beta$)通常设置为0.01-0.05,隐特征个数F通常设置为10-32之间。 当然你可以在实验过程中,根据具体数据分布来固定一些参数,对另一些参数做差异化实验。 比如说我们固定了正则参数$\partial$和学习率(learning rate $\beta$)都为0.01的情况下,我们来变隐特征F,将它从16-32-64等等取值来看最终的效果。上述的一些参数对模型的快速收敛和模型效果都是非常重要的。 LFM(latent factor model)算法与CF算法的优缺点比较对于用户比较多的系统,用item CF比user CF更加具有可行性。 1.理论基础 LFM是比较传统的监督学习的打法,根据训练样本的label设定损失函数,利用最优化的方法使损失函数最小化。只不过这里的特征是隐特征,不像其他模型中的特征那样直观。 item CF是基于公式间求解相似度矩阵,相对来说,缺少了学习的过程。 所以,从理论基础的完备性上,LFM相对更好。 2.离线计算空间、时间复杂度 空间复杂度方面:item CF需要存储item sim表,需要的空间复杂度=物品数的平方;而LFM只需要存储user向量和item向量,所以空间复杂度=物品数目隐类数+用户数目隐类数。 相比较而言,显然LFM的空间复杂度更低。 时间复杂度方面:假设有M个用户,他们的平均点击序列长度为K,那么计算item相似度矩阵的时间复杂度=MKK;假设有D个样本,迭代N次,F是隐类的个数,那么训练LFM模型所需要的时间复杂度=DFN。由于LFM需要迭代,所以在耗时上略高于item CF,但是它们的耗时处于同一数量级。 3.在线推荐与推荐解释 item CF可以将item sim矩阵写入redis或者内存,线上基于用户实时点击去推荐,可以做到较好的响应用户的实时行为。 LFM由于得到user和item向量,在计算用户的toplike物品的时候,如果推荐系统的物品总量很大,那么就需要将每一个item做向量的点乘运算,复杂度是比较高的,离线计算相对来说比较耗时。得到用户的toplike物品表之后,也是写入redis或者内存之中,线上用户访问系统的时候,直接推荐计算好的toplike列表。但是这样就不能对用户的实时行为进行感知。现在像facebook也推出了一些向量召回的引擎,可以做到在线实时召回,但是依然也不能够在用户有了新行为之后立刻重新训练LFM模型得到新的向量。 所以,这这一方面,LFM效果略差。]]></content>
<categories>
<category>推荐算法</category>
</categories>
<tags>
<tag>推荐算法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[个性化推荐算法实践第01章个性化推荐算法综述]]></title>
<url>%2F2019%2F06%2F01%2F%E4%B8%AA%E6%80%A7%E5%8C%96%E6%8E%A8%E8%8D%90%E7%AE%97%E6%B3%95%E5%AE%9E%E8%B7%B5%E7%AC%AC01%E7%AB%A0%E4%B8%AA%E6%80%A7%E5%8C%96%E6%8E%A8%E8%8D%90%E7%AE%97%E6%B3%95%E7%BB%BC%E8%BF%B0%2F</url>
<content type="text"><![CDATA[[TOC] 个性化推荐算法实践第01章个性化推荐算法综述第01部分个性化推荐算法综述个性化推荐算法综述部分,主要介绍个性化推荐算法综述,本课程内容大纲以及本课程所需要准备的编程环境与基础知识。 个性化推荐算法综述介绍本课程的主要内容与需要准备的知识,并介绍个性化推荐在工业界的落地场景与架构 1、什么是推荐系统?在介绍推荐算法之前需要先介绍一下什么是信息过载。 信息过载就是信息的数量远超于人手工可以遍历的数量。比如,当你没有目的性的去逛超市,你不可能把所有的商品都看一遍都有什么。同样,无论是去书店看书,还是在电影网站上搜索电影,这些物品的量级对于没有目的性、需求性的用户而言都是信息过载。 那么什么是推荐系统呢? 就是当用户的目的不明确、且该服务对于用户而言构成了信息过载;但该系统基于一定的策略规则,将物品进行了排序,并将前面的物品展示给了用户,这样的系统就可以称之为推荐系统。 举例说明,在网站购物过程中,无论是天猫或者京东这样的平台,如果我们有明确的需求去搜索框里检索。如希望买啤酒,那么检索结果就是很多种类的啤酒;如果没有明确的需求,就会有猜你喜欢等等模块,这些模块就是推荐系统基于一定的规则策略计算出来的,这些规则策略就是个性化推荐算法。 2、个性化推荐算法在系统中所起到的作用 推荐系统在工业界落地较成功的三大产品:电商、信息流、地图基于位置服务的(LBS)的推荐 推荐系统如今在工业界中落地较为成功的有三类产品,分别是电商、地图、基于LBS的推荐。电商中,用户需要面对数以十万计的新闻与短视频,地图中用户需要面对数以百万计的餐馆等等;但是用户首先看到的都不会是全部的内容,只会是几个或者几十个新闻、短视频、餐馆等等,决定从物品海洋里选择哪些展现给用户的就是个性化推荐算法。 如果推荐的精确,也就是说该推荐系统推荐的恰好是用户想要的、或者是促进了用户的需求,那么就会推动用户在该电商上进行消费、停留、阅读等等。所以,在推荐系统中最为重要的就是个性化推荐算法。 3、如何衡量个性化推荐算法在产品中起到的作用分为线上和线下两个部分。其中线下部分主要依托于模型本身的评估指标,比如个性化召回算法中模型的准确率等等;在线上,基于业务本身的核心指标,比如基于信息流产品中的平均阅读时长等等。 信息流中的点击率 ctr 与停留时长 dwell time 电商中的GMV(Gross Merchandise Volume,网站成交金额) 4、推荐算法介绍包括:个性化召回算法、个性化排序算法 5、评估指标:包括:在线评估指标和离线评估指标 个性化召回1、什么是个性化召回? 在item全集中选取一部分作为候选集。 这里就存在一个问题,就是说为什么要选取一部分作为作为候选集,而不是全部?其原因在于:1.不同的用户不会喜欢所有类型的item;2.基于服务性能的考虑,如果选择了全部的item作为候选集,对于后续的排序就将耗费大量的时间,对于整体推荐的后端,服务响应时间将会是灾难性的。 根据用户的属性行为上下文等信息从物品全集中选取其感兴趣的物品作为候选集。 下面举例说明: 如果某个推荐系统中,物品全集是如下左图中9个item,这里有两个用户A和B,他们分别对不同的item感兴趣。这里拿信息流产品举例,如果user A对体育类新闻感兴趣,user B对娱乐类新闻感兴趣,那就按照简单的类别召回,得到结果如下右图所示。 在候选集{a,b,c,….,g,h,i}中为User A,User B选取一部分item作为候选集。 2、召回的重要作用1、召回决定了最终推荐结果的天花板 为什么这么说呢?这里先看一下推荐系统的整体架构,工业中的个性化推荐系统中的策略部分的架构主要由一下三部分构成:召回、排序、以及最后的策略调整部分,其中召回部分包括各路个性化召回之后将所有的item merge进入rank部分,rank只是调整召回完item的展现顺序,rank完之后还有一些策略的调整,比如信息流场景中的控制相同作者的数目等等,所以可以看到个性化召回的候选集是多么的重要,因为最终展现给用户的就是从这个候选集中选出来的。那么就可能会有疑问,为什么不能将所有的item进行排序?这是为了保证后端响应时间。 与用户离的最近的是端,在移动互联网的时代主要的流量集中在了移动app也可以是网站前端。连接接前后端的是WEB API层。WEB层主要给APP端提供API服务,解析端上发来的请求,调用后端rpc服务。得到的结果投全到端上。web api层尽量不做策略业务逻辑,但是会做一些诸如log写实时信息队列,或写分布式存储这样的事情来方便后续的数据分析和模型训练。 最后是后端的RPC服务。个性化推荐算法主要发挥作用的部分。 RPC服务的三大策略部分。 第一:个性化召回,基于用户的行为,通过算法模型来为用户精准推荐。或基于用户画像的标签推荐同类型的item。举个栗子,如果某个用户过往经常点击体育类的item,那么用户画像就给她标上体育的lable。那么有较新的体育类新闻,会优先推荐给改用户。召回决定了最终推荐结果的天花板,因为这一步决定了候选集。 第二部分:排序部分。第一部分召回了用户感兴趣的物品集合后,我们需要决策 出展现给用户的顺序。好的顺序可以让用户在列表的开始找到自己的所需,完成转化。因为用户的每一次下拉都是有成本的,如何不能在最初的几屏里,显示用户的所需,用户就很可能流失掉。结合刚才召回所举的例子,给用户召回了体育类的item,不同的item可能会有不同的浏览人数,评论人数,发布时间,不同的字数,不同的时长,不同的发布时间等等,同样该用户也有体育类的细分的倾向性。 第三部分:策略调整部分,基于业务场景的策略调整部分。由于召回和排序大多数是基于模型来做的,所以基于业务场景的策略调整部分可以增加一些规则来fit业务场景。比如在信息流场景中,我们不希望给用户一直连续推荐同一个作者的新闻,我们可以加一些打散的策略。 2、个性化召回解析 个性化召回算法分为哪几大类? 基于用户行为的(也就是用户基于推荐系统推荐给他的item点击或者没点。) CF(基于邻域的算法:user CF item CF)、矩阵分解、基于图的推荐(graph-based model)——基于图的随机游走算法:PersonalRank 这一类的个性化召回算法总体来说就是推荐结果的可解释性较强,比较通俗易懂,但是缺少一些新颖性。 基于user profile的 经过用户的自然属性,也就是说经过用户的偏好统计,那么基于这个统计的类别去召回。推荐效果不错,但是可扩展性较差。也就是说一旦用户被标上了某一个类别或者某几个类别的标签之后,很难迁移到其余的一些标签。 基于用户的偏好的统计的类别类召回。效果不错,可扩展性比较差。 隐语义模型Latent Factorization Model(LFM) 新颖性、创新性十足,但是可解释性不是那么强。 3、工业界个性化召回架构 整体的召回架构可以分为两大类: 第一大类是离线模型。根据用户行为基于离线的model file算出推荐结果,这些推荐结果可以是用户喜欢哪些item集合,也可以是item之间的相似度文件 ,计算出具有某种lable的item的排序。然后离线计算好的排序的文件写入KV存储。在用户访问服务的时候,Recall部分直接从KV中读取。因为我们直接存储的是item ID,我们读取到的item id的时候还需要去Detail Server中得到每个item id的详情,然后将详情拼接好传给rank。(在线的server recall部分直接调用这个结果,拿到ID之后访问detail server得到详情,再往rank部分传递) 第二大类是深度学习模型,如果采用深度学习的一些model,这是需要将model file算出来的item embedding的向量也需要离线存入KV中,但是用户在访问我们的KV的时候,在线访问深度学习模型服务(recall server)的User embedding。同时去将user embedding层的向量和item embedding层的向量做最近邻计算,并得到召回。 第02部分 个性化召回算法协同过滤理论部分本章节主要讲解itemcf与usercf的基础理论部分与理论公式升级部分,并详细介绍itemcf与usercf的优缺点分析 Item Collaborative Filtering(Item CF)背景 信息过载,用户需求不明确 强依赖于用户行为 工业界主流落地场景 信息流 电商 o2oLBS 含义:给用户推荐他之前喜欢的物品相似的物品 如何衡量相似 基于用户行为,如果喜欢2个物品的用户重合度越高,那么2个物品也就越相似。 如何衡量喜欢 看用户是否真实点击,在电商场景下,更看重实际转化(实际消费购买);信息流场景下,更看重真实的点击(基于一定时长下的停留) 物品之间相似度公式: $s_{i j}=\frac{|u(i) \cap u(j)|}{\sqrt{|u(i)| \cup|u(j)|}}$ u(i)表示喜欢物品i的用户数,$|u(i) \cap u(j)|$表示同时喜欢物品i和物品j的用户数 根据用户行为计算出用户相似度矩阵: 分子:u(i)表示对item (i)有过行为的用户集合 ,u(j)表示对item (j)有过行为的用户集合 ,分子表示user的重合程度,重合度越高,越相似。 分母:归一化(举例:对item(i)有过行为的用户2个,对item(j)有过行为的用户3个$\sqrt{2} \sqrt{3}=\sqrt{6}$,物理意义:惩罚的热门物品与其他物品的相似度,因为热门物品对应的user倒排会非常长造成了与很多物品都有重合,如果分母除以一个很大的数,将相似度的数值趋于0。 用户u对物品j的兴趣公式: $p_{u j}=\sum_{i \in N(u) \cap s(j, k)} s_{i j} * r_{u i}$ N(u) 表示用户喜欢的物品集合,$s(j, k) $表示和物品j最相似的k个物品的集合。$s_{ij}$表示物品i和j的相似度. 表示用户u对物品i的兴趣。 $r_{ui}$表示user对 i的行为得分,$s_{ij}$表示物品的相似度得分 $p_{uj}$对user进行item(j)的推荐,根据item(i)来完成推荐的,item(i)是user行为过的物品并且取与item(i)最相似的top k个(一般50个), Item CF在工业界落地公式升级1: 理论意义:针对于活跃用户应该被降低在相似度公式中的贡献度。 如果在某电商系统中,如果某User A是批发商,他购买了很多物品,可能有啤酒,书刊等,这都不能真实的表现他的兴趣。还有一名User B,他只买了啤酒和书刊,这能完全表现他的兴趣。如果我们在计算啤酒和书刊的相似度的时候,如果按照之前相似度公式User A和User B对啤酒和书刊的相似度贡献是一样的。这样子显然是不合理的。我们需要降低User A在相似度计算过程中的贡献度。 $s_{i j}=\frac{\sum_{u \in u(i) \cap u(j)} \frac{1}{\log (1+|N(u)|)}}{\sqrt{|u(i)||u(j)|}}$ u(i)表示喜欢物品i的用户数,$|u(i) \cap u(j)|$表示同时喜欢物品i和物品j的用户数 与之前的相似度计算公式公式相比,分母部门没有发生任何变化。那么我们重点看分子部分。之前的相似度计算公式中每个重合用户对相似度的贡献是一样的,都是1.但是升级后的公式,我们发现每个用户对相似度的贡献变成了不一样。N(u)表示用户u所行为过的item的总数。如果一个用户行为过的item的总数越多,那么他的相似度就越低。这样子是符合常理的认知的。 分子:N(u)表示用户u所行为过的item总数,如果用户行为过的总数越多,对相似度贡献越低。 Item CF在工业界落地公式升级2: 理论意义:用户在不同时间对item的操作应给予时间衰减惩罚。 因为在很多场景中,用户的兴趣随时间是有变化的。如在信息流场景中,可能30前看过的短视频,30天后就不一定喜欢了。因为在做物品相似度矩阵计算的时候,就假定了用户的行为可以反映用户的兴趣。所以需要给予时间衰减降权。 $s_{i j}=\frac{\sum_{u \in u(i) \cap u(j)} {f(\Delta t)}}{\sqrt{|u(i)||u(j)|}}$ $f(\Delta t)=\frac{1}{1+\alpha \Delta t}$的物理意义就是说如果item i与item j被行为时间的差异越小,那么就越逼近于1.如果他们item i与item j被行为时间的差异越大,那么这个贡献就越低。 与之前的相似度计算公式相比,分母部门没有发生任何变化。那么我们重点看分子部分。每个用户对相似度的贡献也发生了变化。这里的变化主要由$\Delta t$决定的。$\Delta t$是指用户item i与 item j所行为的时间的差异。 User Collaborative Filtering(User CF)意义:给用户推荐相似兴趣用户感兴趣的物品。 举个栗子:在我们读书的时候是否经常问学长学姐该读什么样的书或者下载什么样的论文?学长学姐就会给你推荐。在这个栗子中学长学姐和你就属于具有相同爱好的用户群体,因为你们具有相同的研究领域。 那么所以基于用户的协同过滤的算法有两个步骤: 1、找到相似兴趣用户的集合 那么问题来了,如何评价相似兴趣用户集合?区别于很多传统做法,这里主要采用基于用户行为重合度的方法,举例来说,如果两个用户的行为具有很高的重合度,那么他们具有很高的相似性。那么他们可以称为相似兴趣用户集合。 2、推荐相似用户行为过,而该用户并没有行为过的item。举个栗子,如果我们发现两个用户A、B都非常喜欢足球相关的视频,行为重合度极高。而用户B经常点击天下足球相关的视频,用户A并没有行为,那么我们可以推荐天下足球相关的视频给用户A。 栗子: User A 可以给User D 推荐b 基于用户的协同过滤算法的步骤 1、计算相似用户的相似度矩阵 相似度公式 $s_{u v}=\frac{|N(u) \cap N(v)|}{\sqrt{|N(u) | N(v)|}}$ N(u)表示用户u有过行为item的集合。N(v)表示用户v有过行为item的集合。 分子是item的重合程度。显然重合程度越高,user越相似。 分母做了一个归一化,物理意义上解释了惩罚了操作过多的用户与其他用户的相似程度。因为操作过多的用户对应的item的序列会非常的长,造成了与很多的用户都有相似。分母除以一个很大的数之后呢,就能把相似度得分的数值趋于0.在得到用户相似度矩阵过后,我们根据用户的行为点击,来完成相似用户的item推荐。 下面来看公式 $p_{u i}=\sum_{v \in s(u, k) \cap u(i)} S_{u v} {r}_{v i}$ ${r}_{v i}$表示用户v对item i的行为得分。在前面介绍item cf公式的时候,我们提到过对于不同的行为,我们对用户的行为得分定义不同。我们这里将行为得分归一化为1. $S_{u v} $表示user u与uer v的相似度得分。这里根据用户v来完成对用户u的推荐。所以这里要介绍用户v。用户v是用户u的前TOP k个的相似用户。并且用户v行为过的物品 item i,用户user u 没有行为。那么我们便得到了用户u对item i的推荐度得分。 工业界落地时公式升级1: 理论意义:降低那些异常活跃物品对与用户相似度的贡献 举个栗子解释: 如果某个电商系统中,User (A) 与User(B)同时购买了《新华字典》,User (A) 与User(C)同时购买了《机器学习》,并且他们都只有这一本书重合,User (A) 与User(B)、User (A) 与User(C)的重合度都是1,显然不合理,因为购买《新华字典》并不能十分准确的反映用户的兴趣,也许是给家里孩子买,换言之《新华字典》的用户倒排会非常的长,而购买《机器学习》的用户大概率可以反映他们的兴趣,因为这本书的购书群体很窄,因此我们需要降低那些很多人购买的在重合度中的贡献。 $s_{u v}=\frac{\sum_{i \in N(u) \cap N(v)} \frac{1}{\log (1+|u(i)|)}}{\sqrt{|N(u)||N(v)|}}$ 分子:基础版本的相似度计算公式当中重合的每一item对整体的贡献都是相同的。在升级版中贡献变得不同的。u(i)表示对item(i)有过行为的用户集合,如果一个item被更多的用户行为过,那么它在重合度的贡献越低。这也符合我们的认知。 公式升级2(工业界)理论意义,不同用户对同一item行为的时间段不同应该给予时间惩罚(因为很多用户不同时间段,兴趣是发生变化的,比如,2个用户都曾经点击过足球类短视频,一个用户是近期点击,另一个用户是在好几个月之前欧洲杯比赛期间,也许在目前时间节点,已经不再喜欢足球类短视频了,我们在计算用户相似度矩阵的时候,假定了用户行为可以反映出用户的兴趣,所以我们要给予时间衰减降权) $s_{u v}=\frac{\sum_{i \in N(u) \cap N(v)} f\left(\Delta t_{i}\right)}{\sqrt{|N(u) | N(v)|}}$ $f\left(\Delta t_{i}\right)=\frac{1}{1+\alpha\left|t_{u i}-t_{v i}\right|}$ 与基础版本的用户相似度矩阵计算公式,分母没有发生任何变化。 分子:每一个重合的item的贡献度得分都变得不同,它们的贡献度得分由函数$f\left(\Delta t_{i}\right)$决定。函数$f\left(\Delta t_{i}\right)$具体的得分是2个用户对同一item操作时间的差,如果$\left|t_{u i}-t_{v i}\right|$越短的话,$f\left(\Delta t_{i}\right)$的得分越高;如果$\left|t_{u i}-t_{v i}\right|$越长的话,$f\left(\Delta t_{i}\right)$的得分越低。相应的在重合度贡献中也就越低。 Itemcf VS Usercf优缺点比较: 推荐实时性 User cf用户有了新的行为,不会对结果造成很快的变化,因为User cf是基于相似度矩阵来完成推荐的,User本身的行为并不能造成自己的推荐结果发生改变。 Item cf用户一旦有了新的行为,推荐结果立刻发生改变,因为Item cf是基于相似度矩阵来完成推荐的,所以点击了物品,会立马推荐出相似的物品。 新用户/新物品的推荐 User cf新用户的到来是不能立即推荐的,需要等用户有了一定的行为并且得到了与其他用户相似度矩阵之后才可以完成推荐,新用户一旦被用户点击,User cf可以通过相似度用户矩阵将该物品推荐给相似的用户。 Item cf新用户一旦完成了Item点击,便可以推荐该Item相似的其余Item,新物品的到来,由于此时新物品 并没有与其他物品在相似度矩阵中出现,所以Item cf并不能将新物品及时地推荐出去。 推荐理由的可解释性 User cf由于是通过用户相似度矩阵来完成推荐的,结果会略显难以解释。 Item cf通过用户历史点击行为完成的推荐,所以推荐结果更加令人信服。 Item cf VS User cf适用场景性能层面考量 User cf通过计算用户相似度矩阵,所以它并不适合用户很多的场合。因为相似度矩阵计算起来代价非常大。 Item cf需要计算物品的相似度矩阵,所以Item cf适用于物品数远小于用户数场合。由于实战中用户量往往远大于物品的数量级,所以实战中更倾向于Item cf。 个性化层面考量 User cf适用于物品及时推荐下发且个性化需求不太强烈的领域。 Item cf适用于长尾物品丰富并且个性化需要强烈的领域。由于真实的推荐系统中,各种个性化召回算法组合,会有一些召回方法解决新物品及时下发问题,而我们需要个性化程度强烈,所以从个性化层面考虑,更倾向于在落地实战中采用Item cf。 ItemCF的优势: (1)计算性能高,通常用户数量远大于物品数量。 (2)可预先计算保留,物品并不善变。 ItemCF存在的问题:物品冷启动问题:当平台中物品数据较少或缺失时,无法精确计算物品相似度,解决办法: (1)文本分析,通过分析物品的介绍文本,计算相似度。(2)主题模型,通过主题模型分析物品文本主题得出主题相似度。(3)打标签,对物品打标签求得相似度。(4)推荐排行榜单。 第03部分 个性化召回算法协同过滤代码实战部分本章节结合具体的示例数据介绍itemcf与usercf基础部分的代码实战与升级部分的代码实战 3-1 数据集介绍与公共信息抽取函数代码实战本次代码实战的使用的2个数据集 rating.txt 用户对item的打分文件 userId movieId rating: userId对movieId的打分 timestamp:userId对movieId行为的时间戳 movies.txt item的info文件 movidId title:item的标题 genres:item的分类 在item CF和 User CF实战当中我们都需要得到user的点击序列,所以我们下面写一下公共的信息抽取函数。 本文使用PyCharm为代码编写平台。 1、数据集准备: 本实例使用MovieLens 数据集(下载地址:http://files.grouplens.org/datasets/movielens/ml-latest-small.zip中的ratings.csv(用户ID对电影ID的评分)以及movies.csv(电影类别明细)。如下: ratings.csv movies.csv 2、项目结构 data文件夹用于存储电影评分数据,production文件夹用于存放推荐代码,util文件夹用于存放用于读取数据的工具文件。 3、reader.py:用于读取用户的点击序列(即每个用户对那些电影进行过评分)以及电影信息(id,名称,类别)。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107# _*_ encoding: utf-8 _*_'''@File : reader.py @Contact : [email protected]@License : (C)Copyright 2019-2020@Modify Time @Author @Version @Desciption------------ ------- -------- -----------2019/7/16 13:38 anfrank 1.0 None'''# import libimport os# 获得用户的点击序列def get_user_click(rating_file): ''' get user click list[userid,itemid] :param rating_file:input file :return:输出是一个dict key:userid value:[itemid1,itemid2] ''' # 如果路径不存在,返回空数据 if not os.path.exists(rating_file): return {} # 打开文件 fp = open(rating_file) num = 0 # 用于传回的数据 user_click = {} # 循环数据 for line in fp: # 第一行是表头,需要跳过处理 if num == 0: num += 1 continue # 根据逗号提取每个项目 item = line.strip().split(',') # print(item) if len(item) < 4: continue [userid, itemid, rating, timestamp] = item if float(rating) < 3.0: # 如果评分低于3分,则视为该用户不喜欢该电影 continue # 将单一用户的点击序列添加至返回数据 if userid not in user_click: user_click[userid] = [] user_click[userid].append(itemid) fp.close() return user_click# 获取电影信息数据def get_item_info(item_file): ''' get item info[title,genres] :param item_file: input iteminfo file :return: a dict,key itemid,value:[title,genres] ''' # 若路径不存在则返回空 if not os.path.exists(item_file): return {} num = 0 item_info = {} # fp = open(item_file, 'r', encoding='UTF-8') fp = open(item_file,'r',encoding='UTF-8') for line in fp: # 第一行是表头,需要跳过处理 if num == 0: num += 1 continue # 根据逗号提取每个项目 item = line.strip().split(',') if len(item) < 3: # 若单行小于三项过滤(去除问题行) continue if len(item) == 3: [itemid, title, genres] = item # 这个elif语句是由于,有的电影名称中含有逗号,因此造成项数过多,需要另行处理 elif len(item) > 3: itemid = item[0] genres = item[-1] # 获取最后一项 title = ",".join(item[1:-1]) # 第一个到最后一个的拼接成为电影名称 # 将电影信息数据返回 if itemid not in item_info: item_info[itemid] = [title, genres] fp.close() return item_info# 测试上述方法if __name__ == '__main__': # f = open("../data/ml-1m/ratings.dat") user_click = get_user_click("../data/ml-latest-small/ratings.csv") print("输出user_click字典的用户数量:") print(len(user_click)) print("输出user_click字典的key为1点击了的item:") print(user_click['1']) item_info = get_item_info("../data/ml-latest-small/movies.csv") print("输出item_info字典的item数量:") print(len(item_info)) print("输出item_info字典的key为1的item名称") print(item_info['1']) 3-2 itemcf基础部分代码实战三、参考资料1、https://www.imooc.com/learn/1029 2、https://www.imooc.com/learn/990 3、https://study.163.com/course/introduction/1004092024.htm 4、https://blog.csdn.net/yimingsilence/article/details/54934302 5、https://blog.csdn.net/xiaokang123456kao/article/details/74735992 6、项亮. 推荐系统实践[M]. 人民邮电出版社, 2012. 第04部分 个性化推荐召回算法的离线在线评估方法从离线在线两个方面,带大家详细了解如何评估个性化推荐算法对个性化推荐系统的影响。 1、业务指标信息流更关注: 点击率:总点击次数/总展现次数 平均阅读时长(两个小指标): 推荐精准度:总阅读时长/click uv 推荐算法对整体趋势的影响:阅读总时长/show uv 电商推荐中更关注转化率: 转化率:总得成交次数/总的展现次数 还有总的成交额度( GMV) 2、 item推荐覆盖率如果我们无休止的去剥削用得的点击历史,会发现推荐的item呈现长尾,很多item无法得到充分的展现。所以我们要利用一些发现算法保证item的覆盖率。 1覆盖率:去重后推荐的所有item的id数/库里面所有的itemid 如何在个性化召回算法中从离线和在线两个方面来评价算法的好坏呢? 3、offline评价方法1评测模型推荐结果在测试集上的表现 如果在推荐系统中,某种算法给用户A推荐的物品(a、b、d)。又得到,在测试集中,用户A有过物品a,b,f,m的展现。这个时候我们发现推荐出来的真实结果和测试上的结果重合为物品(a、b)。这个集合就是我们评价的分母。 我们又发现用户a在测试集上点击了物品(a、f),那么推荐的真实物品结果(a、b、d),与用户在测试集上点击(a、f)也有重合,重合的为a,这个重合就是他的分母。点击率:总点击次数/总展现次数。综上,整体的点击率表现为1/2。 4、 online评价方法1234定义指标: 信息流中:点击率、平均阅读时长、覆盖率 生产环境中A/B test,分离出一部分流量对比 第05部分总结推荐引擎主体架构,工业界推荐系统落地 推荐引擎主体架构主要分为match召回、Rank排序、以及strategy策略调整部分。并对Match在工业界的几种落地架构进行了深入剖析。对工业界电商、信息流、地图基于位置服务的(LBS)的推荐进行了详细介绍。 CF算法的落地实战 item cf user cf item cf和user cf不同场景下的优劣对比 借助demo数据,实现itemCF与user CF 推荐系统在不同业务下的评价指标 分别从offline评价方法和online评价方法两个方面展示了,如何在新增一个召回算法来评价改算法的性能。 第06部分预习参考资料:recall:矩阵分解、graph、content based 、item2vec(用DNN来将user和item分别embedding成隐语义向量做向量召回) 推荐系统系列之隐语义模型 【机器学习】—隐语义模型DNN个性化推荐模型 DNN论文分享 - Item2vec: Neural Item Embedding for Collaborative Filtering 万物皆Embedding,从经典的word2vec到深度学习基本操作item2vec]]></content>
<categories>
<category>推荐算法</category>
</categories>
<tags>
<tag>推荐算法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[一个信用评分案例看机器学习建模基本过程]]></title>
<url>%2F2019%2F06%2F01%2F%E4%B8%80%E4%B8%AA%E4%BF%A1%E7%94%A8%E8%AF%84%E5%88%86%E6%A1%88%E4%BE%8B%E7%9C%8B%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0%E5%BB%BA%E6%A8%A1%E5%9F%BA%E6%9C%AC%E8%BF%87%E7%A8%8B%2F</url>
<content type="text"><![CDATA[[TOC] machine learning for credit scoring一个信用评分案例看机器学习建模基本过程Banks play a crucial role in market economies. They decide who can get finance and on what terms and can make or break investment decisions. For markets and society to function, individuals and companies need access to credit. Credit scoring algorithms, which make a guess at the probability of default, are the method banks use to determine whether or not a loan should be granted. This competition requires participants to improve on the state of the art in credit scoring, by predicting the probability that somebody will experience financial distress in the next two years. Dataset Attribute Information: Variable Name Description Type SeriousDlqin2yrs Person experienced 90 days past due delinquency or worse Y/N RevolvingUtilizationOfUnsecuredLines Total balance on credit divided by the sum of credit limits percentage age Age of borrower in years integer NumberOfTime30-59DaysPastDueNotWorse Number of times borrower has been 30-59 days past due integer DebtRatio Monthly debt payments percentage MonthlyIncome Monthly income real NumberOfOpenCreditLinesAndLoans Number of Open loans integer NumberOfTimes90DaysLate Number of times borrower has been 90 days or more past due. integer NumberRealEstateLoansOrLines Number of mortgage and real estate loans integer NumberOfTime60-89DaysPastDueNotWorse Number of times borrower has been 60-89 days past due integer NumberOfDependents Number of dependents in family integer Read the data into Pandas 将数据读进pandas1234567import pandas as pdpd.set_option('display.max_columns', 500)import zipfilewith zipfile.ZipFile('KaggleCredit2.csv.zip', 'r') as z: f = z.open('KaggleCredit2.csv') data = pd.read_csv(f, index_col=0)data.head() .dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } SeriousDlqin2yrs RevolvingUtilizationOfUnsecuredLines age NumberOfTime30-59DaysPastDueNotWorse DebtRatio MonthlyIncome NumberOfOpenCreditLinesAndLoans NumberOfTimes90DaysLate NumberRealEstateLoansOrLines NumberOfTime60-89DaysPastDueNotWorse NumberOfDependents 0 1 0.766127 45.0 2.0 0.802982 9120.0 13.0 0.0 6.0 0.0 2.0 1 0 0.957151 40.0 0.0 0.121876 2600.0 4.0 0.0 0.0 0.0 1.0 2 0 0.658180 38.0 1.0 0.085113 3042.0 2.0 1.0 0.0 0.0 0.0 3 0 0.233810 30.0 0.0 0.036050 3300.0 5.0 0.0 0.0 0.0 0.0 4 0 0.907239 49.0 1.0 0.024926 63588.0 7.0 0.0 1.0 0.0 0.0 1data.shape (112915, 11) 去除异常值 Drop na1data.isnull().sum(axis=0) SeriousDlqin2yrs 0 RevolvingUtilizationOfUnsecuredLines 0 age 4267 NumberOfTime30-59DaysPastDueNotWorse 0 DebtRatio 0 MonthlyIncome 0 NumberOfOpenCreditLinesAndLoans 0 NumberOfTimes90DaysLate 0 NumberRealEstateLoansOrLines 0 NumberOfTime60-89DaysPastDueNotWorse 0 NumberOfDependents 4267 dtype: int64 12data.dropna(inplace=True)data.shape (108648, 11) 创建X 和 y Create X and y12y = data['SeriousDlqin2yrs']X = data.drop('SeriousDlqin2yrs', axis=1) 1y.mean() 0.06742876076872101 123import seaborn as snsimport matplotlib.pyplot as plt%matplotlib inline 1sns.countplot(x='SeriousDlqin2yrs',data=data) <matplotlib.axes._subplots.AxesSubplot at 0x24081eb9828> 1#从样本中可以看出:label为1的样本偏少,可见样本失衡 练习1:数据集准备把数据切分成训练集和测试集 切分数据集1234567891011# Added version check for recent scikit-learn 0.18 checksfrom distutils.version import LooseVersion as Versionfrom sklearn import __version__ as sklearn_versionif Version(sklearn_version) < '0.18': from sklearn.cross_validation import train_test_splitelse: from sklearn.model_selection import train_test_splitX_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=0) 对连续值特征做幅度缩放12345from sklearn.preprocessing import StandardScalerstdsc=StandardScaler()X_train_std=stdsc.fit_transform(X_train)X_test_std=stdsc.transform(X_test) 练习2使用logistic regression/决策树/SVM/KNN…等sklearn分类算法进行分类,尝试查sklearn API了解模型参数含义,调整不同的参数。 logistic regression123456from sklearn.linear_model import LogisticRegressionlr = LogisticRegression(penalty='l1',C=1000.0, random_state=0)lr.fit(X_train_std, y_train)lr LogisticRegression(C=1000.0, class_weight=None, dual=False, fit_intercept=True, intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1, penalty='l1', random_state=0, solver='liblinear', tol=0.0001, verbose=0, warm_start=False) 1234LogisticRegression(C=1000.0, class_weight=None, dual=False, fit_intercept=True, intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1, penalty='l1', random_state=0, solver='liblinear', tol=0.0001, verbose=0, warm_start=False) LogisticRegression(C=1000.0, class_weight=None, dual=False, fit_intercept=True, intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1, penalty='l1', random_state=0, solver='liblinear', tol=0.0001, verbose=0, warm_start=False) 1print('训练集准确度:%f'%lr.score(X_train_std,y_train)) 训练集准确度:0.933126 1##### 逻辑回归模型的系数 1234567import numpy as npfeat_labels=data.columns[1:]coefs=lr.coef_indices=np.argsort(coefs[0])[::-1]for f in range(X_train.shape[1]): print('%2d) %-*s %f'%(f,30,feat_labels[indices[f]],coefs[0,indices[f]])) 0) NumberOfTime30-59DaysPastDueNotWorse 1.728754 1) NumberOfTimes90DaysLate 1.689046 2) DebtRatio 0.312098 3) NumberOfDependents 0.116383 4) RevolvingUtilizationOfUnsecuredLines -0.014289 5) NumberOfOpenCreditLinesAndLoans -0.091911 6) MonthlyIncome -0.115234 7) NumberRealEstateLoansOrLines -0.196422 8) age -0.364305 9) NumberOfTime60-89DaysPastDueNotWorse -3.247876 1#我的理解是权重绝对值大的特征标签比较重要 1#### 决策树 1234from sklearn.tree import DecisionTreeClassifiertree = DecisionTreeClassifier(criterion='entropy', max_depth=3, random_state=0)tree.fit(X_train_std, y_train) DecisionTreeClassifier(class_weight=None, criterion='entropy', max_depth=3, max_features=None, max_leaf_nodes=None, min_impurity_decrease=0.0, min_impurity_split=None, min_samples_leaf=1, min_samples_split=2, min_weight_fraction_leaf=0.0, presort=False, random_state=0, splitter='best') 12345DecisionTreeClassifier(class_weight=None, criterion='entropy', max_depth=3, max_features=None, max_leaf_nodes=None, min_impurity_split=1e-07, min_samples_leaf=1, min_samples_split=2, min_weight_fraction_leaf=0.0, presort=False, random_state=0, splitter='best') DecisionTreeClassifier(class_weight=None, criterion='entropy', max_depth=3, max_features=None, max_leaf_nodes=None, min_impurity_decrease=0.0, min_impurity_split=1e-07, min_samples_leaf=1, min_samples_split=2, min_weight_fraction_leaf=0.0, presort=False, random_state=0, splitter='best') 1print('训练集准确度:%f'%tree.score(X_train_std,y_train)) 训练集准确度:0.934217 SVM(支持向量机)太耗时间了,只取了“NumberOfTime60-89DaysPastDueNotWorse”这一项特征标签 1234X_train_std=pd.DataFrame(X_train_std,columns=feat_labels)X_test_std=pd.DataFrame(X_test_std,columns=feat_labels)X_train_std[['NumberOfTime60-89DaysPastDueNotWorse']].head() .dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } NumberOfTime60-89DaysPastDueNotWorse 0 -0.054381 1 -0.054381 2 -0.054381 3 -0.054381 4 -0.054381 1234from sklearn.svm import SVCsvm = SVC()svm.fit(X_train_std[['NumberOfTime60-89DaysPastDueNotWorse']], y_train) SVC(C=1.0, cache_size=200, class_weight=None, coef0=0.0, decision_function_shape='ovr', degree=3, gamma='auto', kernel='rbf', max_iter=-1, probability=False, random_state=None, shrinking=True, tol=0.001, verbose=False) 1234SVC(C=1.0, cache_size=200, class_weight=None, coef0=0.0, decision_function_shape=None, degree=3, gamma='auto', kernel='rbf', max_iter=-1, probability=False, random_state=None, shrinking=True, tol=0.001, verbose=False) SVC(C=1.0, cache_size=200, class_weight=None, coef0=0.0, decision_function_shape=None, degree=3, gamma='auto', kernel='rbf', max_iter=-1, probability=False, random_state=None, shrinking=True, tol=0.001, verbose=False) 1print('训练集准确度:%f'%svm.score(X_train_std[['NumberOfTime60-89DaysPastDueNotWorse']],y_train)) 训练集准确度:0.932876 KNN1234from sklearn.neighbors import KNeighborsClassifierknn = KNeighborsClassifier(n_neighbors=5, p=2, metric='minkowski')knn.fit(X_train, y_train) KNeighborsClassifier(algorithm='auto', leaf_size=30, metric='minkowski', metric_params=None, n_jobs=1, n_neighbors=5, p=2, weights='uniform') 练习3在测试集上进行预测,计算准确度 logistic regression123y_pred_lr=lr.predict(X_test)print('错误分类数: %d' % (y_test != y_pred_lr).sum())print('测试集准确度:%f'%lr.score(X_test_std,y_test)) 错误分类数: 2171 测试集准确度:0.933886 决策树123y_pred_tree=tree.predict(X_test)print('错误分类数: %d' % (y_test != y_pred_tree).sum())print('测试集准确度:%f'%tree.score(X_test_std,y_test)) 错误分类数: 2498 测试集准确度:0.935021 SVM123y_pred_svm=svm.predict(X_test[['DebtRatio']])print('错误分类数: %d' % (y_test != y_pred_svm).sum())print('测试集准确度:%f'%svm.score(X_test_std[['NumberOfTime60-89DaysPastDueNotWorse']],y_test)) 错误分类数: 4619 测试集准确度:0.934100 KNN123y_pred_knn=knn.predict(X_test)print('错误分类数: %d' % (y_test != y_pred_knn).sum())print('测试集准确度:%f'%knn.score(X_test,y_test)) 错误分类数: 2213 测试集准确度:0.932106 练习4查看sklearn的官方说明,了解分类问题的评估标准,并对此例进行评估。 y的类别12class_names=np.unique(data['SeriousDlqin2yrs'].values)class_names array([0, 1], dtype=int64) 4种方法的confusion matrix12345from sklearn.metrics import confusion_matrixcnf_matrix_lr=confusion_matrix(y_test, y_pred_lr)cnf_matrix_lr array([[30424, 0], [ 2171, 0]], dtype=int64) 12cnf_matrix_tree=confusion_matrix(y_test, y_pred_tree)cnf_matrix_tree array([[29346, 1078], [ 1420, 751]], dtype=int64) 12cnf_matrix_svm=confusion_matrix(y_test, y_pred_svm)cnf_matrix_svm array([[27661, 2763], [ 1856, 315]], dtype=int64) 12cnf_matrix_knn=confusion_matrix(y_test, y_pred_knn)cnf_matrix_knn array([[30351, 73], [ 2140, 31]], dtype=int64) 一个绘制混淆矩阵的函数1234567891011121314151617181920212223242526272829303132333435363738import itertoolsimport numpy as npimport matplotlib.pyplot as pltfrom sklearn.metrics import confusion_matrixdef plot_confusion_matrix(cm, classes, normalize=False, title='Confusion matrix', cmap=plt.cm.Blues): """ This function prints and plots the confusion matrix. Normalization can be applied by setting `normalize=True`. """ if normalize: cm = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis] print("Normalized confusion matrix") else: print('Confusion matrix, without normalization') print(cm) plt.imshow(cm, interpolation='nearest', cmap=cmap) plt.title(title) plt.colorbar() tick_marks = np.arange(len(classes)) plt.xticks(tick_marks, classes, rotation=45) plt.yticks(tick_marks, classes) fmt = '.2f' if normalize else 'd' thresh = cm.max() / 2. for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])): plt.text(j, i, format(cm[i, j], fmt), horizontalalignment="center", color="white" if cm[i, j] > thresh else "black") plt.tight_layout() plt.ylabel('True label') plt.xlabel('Predicted label') 可视化以logistic regression 为例 123456789101112np.set_printoptions(precision=2)# Plot non-normalized confusion matrixplt.figure()plot_confusion_matrix(cnf_matrix_lr, classes=class_names, title='Confusion matrix, without normalization')# Plot normalized confusion matrixplt.figure()plot_confusion_matrix(cnf_matrix_lr, classes=class_names, normalize=True, title='Normalized confusion matrix')plt.show() Confusion matrix, without normalization [[30424 0] [ 2171 0]] Normalized confusion matrix [[1. 0.] [1. 0.]] 12#可见,真实标签为“0”的分类准确率很高。 #真实标签为“1”的分类准确率很低。 练习5银行通常会有更严格的要求,因为fraud带来的后果通常比较严重,一般我们会调整模型的标准。比如在logistic regression当中,一般我们的概率判定边界为0.5,但是我们可以把阈值设定低一些,来提高模型的“敏感度”,试试看把阈值设定为0.3,再看看这时的评估指标(主要是准确率和召回率)。 tips:sklearn的很多分类模型,predict_prob可以拿到预估的概率,可以根据它和设定的阈值大小去判断最终结果(分类类别) 12345from sklearn.linear_model import LogisticRegressionlr = LogisticRegression(penalty='l2',C=1000.0, random_state=0,class_weight={1:0.3,0:0.7})lr.fit(X_train, y_train)lr LogisticRegression(C=1000.0, class_weight={1: 0.3, 0: 0.7}, dual=False, fit_intercept=True, intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1, penalty='l2', random_state=0, solver='liblinear', tol=0.0001, verbose=0, warm_start=False) 123y_pred_lr=lr.predict(X_test)print('错误分类数: %d' % (y_test != y_pred_lr).sum())print('训练集准确度:%f'%lr.score(X_test,y_test)) 错误分类数: 2169 训练集准确度:0.933456 1234from sklearn.metrics import confusion_matrixcnf_matrix_lr=confusion_matrix(y_test, y_pred_lr)cnf_matrix_lr array([[30410, 14], [ 2155, 16]], dtype=int64) 练习6:特征选择、重新建模尝试对不同特征的重要度进行排序,通过特征选择的方式,对特征进行筛选。并重新建模,观察此时的模型准确率等评估指标。 用随机森林的方法进行特征筛选12345from sklearn.ensemble import RandomForestClassifierfeat_labels = data.columns[1:]forest=RandomForestClassifier(n_estimators=10000,random_state=0,n_jobs=-1)forest.fit(X_train,y_train)importances=forest.feature_importances_ 123456import numpy as npfeat_labels=data.columns[1:]indices=np.argsort(importances)[::-1]for f in range(X_train.shape[1]): print('%2d) %-*s %f'%(f,30,feat_labels[indices[f]],importances[indices[f]])) 0) NumberOfDependents 0.188808 1) NumberOfTime60-89DaysPastDueNotWorse 0.173198 2) NumberRealEstateLoansOrLines 0.165334 3) NumberOfTimes90DaysLate 0.122311 4) NumberOfOpenCreditLinesAndLoans 0.089278 5) MonthlyIncome 0.087939 6) DebtRatio 0.051493 7) NumberOfTime30-59DaysPastDueNotWorse 0.045888 8) age 0.043824 9) RevolvingUtilizationOfUnsecuredLines 0.031928 选取4个特征,建立逻辑回归模型12X_train_4feat=X_train_std[['NumberOfDependents','NumberOfTime60-89DaysPastDueNotWorse','NumberRealEstateLoansOrLines','NumberOfTimes90DaysLate']]X_test_4feat=X_test_std[['NumberOfDependents','NumberOfTime60-89DaysPastDueNotWorse','NumberRealEstateLoansOrLines','NumberOfTimes90DaysLate']] 12345from sklearn.linear_model import LogisticRegressionlr = LogisticRegression(penalty='l1',C=1000.0, random_state=0)lr.fit(X_train_4feat, y_train)lr 1234LogisticRegression(C=1000.0, class_weight=None, dual=False, fit_intercept=True, intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1, penalty='l1', random_state=0, solver='liblinear', tol=0.0001, verbose=0, warm_start=False) 1print('训练集准确度:%f'%lr.score(X_train_4feat,y_train)) 训练集准确度:0.933086 123y_pred_lr=lr.predict(X_test_4feat)print('错误分类数: %d' % (y_test != y_pred_lr).sum())print('测试集准确度:%f'%lr.score(X_test_4feat,y_test)) 错误分类数: 2161测试集准确度:0.933701 12cnf_matrix_lr=confusion_matrix(y_test, y_pred_lr)cnf_matrix_lr array([[30381, 43], [ 2118, 53]]) 从最后的结果看,虽然经过特征选择和模型参数调整,但依然未能解决混淆矩阵指标太差的问题。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960一个完整机器学习项目流程总结1. 实际问题抽象成数学问题这里的抽象成数学问题,指的我们明确我们可以获得什么样的数据,目标是一个分类还是回归或者是聚类的问题,如果都不是的话,如果划归为其中的某类问题。2. 获取数据获取数据包括获取原始数据以及从原始数据中经过特征工程从原始数据中提取训练、测试数据。机器学习比赛中原始数据都是直接提供的,但是实际问题需要自己获得原始数据。“ 数据决定机器学习结果的上限,而算法只是尽可能的逼近这个上限”,可见数据在机器学习中的作用。总的来说数据要有具有“代表性”,对于分类问题,数据偏斜不能过于严重,不同类别的数据数量不要有数个数量级的差距。不仅如此还要对评估数据的量级,样本数量、特征数量,估算训练模型对内存的消耗。如果数据量太大可以考虑减少训练样本、降维或者使用分布式机器学习系统。3. 特征工程特征工程包括从原始数据中特征构建、特征提取、特征选择。特征工程做的好能发挥原始数据的最大效力,往往能够使得算法的效果和性能得到显著的提升,有时能使简单的模型的效果比复杂的模型效果好。数据挖掘的大部分时间就花在特征工程上面,是机器学习非常基础而又必备的步骤。数据预处理、数据清洗、筛选显著特征、摒弃非显著特征等等都非常重要。4. 训练模型、诊断、调优模型诊断中至关重要的是判断过拟合、欠拟合,常见的方法是绘制学习曲线,交叉验证。通过增加训练的数据量、降低模型复杂度来降低过拟合的风险,提高特征的数量和质量、增加模型复杂来防止欠拟合。诊断后的模型需要进行进一步调优,调优后的新模型需要重新诊断,这是一个反复迭代不断逼近的过程,需要不断的尝试,进而达到最优的状态。5. 模型验证、误差分析通过测试数据,验证模型的有效性,观察误差样本,分析误差产生的原因,往往能使得我们找到提升算法性能的突破点。误差分析主要是分析出误差来源与数据、特征、算法。6. 模型融合提升算法的准确度主要方法是模型的前端(特征工程、清洗、预处理、采样)和后端的模型融合。在机器学习比赛中模型融合非常常见,基本都能使得效果有一定的提升。7. 上线运行这一部分内容主要跟工程实现的相关性比较大。工程上是结果导向,模型在线上运行的效果直接决定模型的成败。 不单纯包括其准确程度、误差等情况,还包括其运行的速度(时间复杂度)、资源消耗程度(空间复杂度)、稳定性是否可接受。值得注意的是,以上流程只是一个指导性的机器学习流程经验,并不是每个项目都包含完整的流程。该博文主要参考资料: [1] 机器学习项目流程; [2] 一个完整机器学习项目的流程。机器学习项目流程 在微博上看到七月算法寒老师总结的完整机器的学习项目的工作流程,结合天池比赛的经历写的。现在机器学习应用非常流行,了解机器学习项目的流程,能帮助我们更好的使用机器学习工具来处理实际问题。1. 理解实际问题,抽象为机器学习能处理的数学问题 理解实际业务场景问题是机器学习的第一步,机器学习中特征工程和模型训练都是非常费时的,深入理解要处理的问题,能避免走很多弯路。理解问题,包括明确可以获得的数据,机器学习的目标是分类、回归还是聚类。如果都不是的话,考虑将它们转变为机器学习问题。参考机器学习分类能帮助从问题提炼出一个合适的机器学习方法。2. 获取数据 获取数据包括获取原始数据以及从原始数据中经过特征工程从原始数据中提取训练、测试数据。机器学习比赛中原始数据都是直接提供的,但是实际问题需要自己获得原始数据。“ 数据决定机器学习结果的上限,而算法只是尽可能的逼近这个上限”,可见数据在机器学习中的作用。总的来说数据要有具有“代表性”,对于分类问题,数据偏斜不能过于严重,不同类别的数据数量不要有数个数量级的差距。不仅如此还要对评估数据的量级,样本数量、特征数量,估算训练模型对内存的消耗。如果数据量太大可以考虑减少训练样本、降维或者使用分布式机器学习系统。3. 特征工程 特征工程是非常能体现一个机器学习者的功底的。特征工程包括从原始数据中特征构建、特征提取、特征选择,非常有讲究。深入理解实际业务场景下的问题,丰富的机器学习经验能帮助我们更好的处理特征工程。特征工程做的好能发挥原始数据的最大效力,往往能够使得算法的效果和性能得到显著的提升,有时能使简单的模型的效果比复杂的模型效果好。数据挖掘的大部分时间就花在特征工程上面,是机器学习非常基础而又必备的步骤。数据预处理、数据清洗、筛选显著特征、摒弃非显著特征等等都非常重要,建议深入学习。4. 模型训练、诊断、调优 现在有很多的机器学习算法的工具包,例如sklearn,使用非常方便,真正考验水平的根据对算法的理解调节参数,使模型达到最优。当然,能自己实现算法的是最牛的。模型诊断中至关重要的是判断过拟合、欠拟合,常见的方法是绘制学习曲线,交叉验证。通过增加训练的数据量、降低模型复杂度来降低过拟合的风险,提高特征的数量和质量、增加模型复杂来防止欠拟合。诊断后的模型需要进行进一步调优,调优后的新模型需要重新诊断,这是一个反复迭代不断逼近的过程,需要不断的尝试,进而达到最优的状态。5. 模型验证、误差分析 模型验证和误差分析也是机器学习中非常重要的一步,通过测试数据,验证模型的有效性,观察误差样本,分析误差产生的原因,往往能使得我们找到提升算法性能的突破点。误差分析主要是分析出误差来源与数据、特征、算法。6 . 模型融合 一般来说实际中,成熟的机器算法也就那么些,提升算法的准确度主要方法是模型的前端(特征工程、清洗、预处理、采样)和后端的模型融合。在机器学习比赛中模型融合非常常见,基本都能使得效果有一定的提升。这篇博客中提到了模型融合的方法,主要包括一人一票的统一融合,线性融合和堆融合。 参考:http://ask.julyedu.com/question/7013]]></content>
<categories>
<category>机器学习</category>
</categories>
<tags>
<tag>机器学习</tag>
</tags>
</entry>
<entry>
<title><![CDATA[程序员的浪漫爱心表白源码]]></title>
<url>%2F2019%2F05%2F13%2FHexo%E4%B8%AA%E4%BA%BA%E5%8D%9A%E5%AE%A2_%E7%A8%8B%E5%BA%8F%E5%91%98%E7%9A%84%E6%B5%AA%E6%BC%AB%E7%88%B1%E5%BF%83%E8%A1%A8%E7%99%BD%E6%BA%90%E7%A0%81%2F</url>
<content type="text"><![CDATA[程序员的浪漫爱心表白源码 简介我们程序员在追求爱情方面也是非常浪漫的,某位同学利用自己所学的HTML5知识自制的HTML5爱心表白动画,画面非常温馨甜蜜,这样的创意很容易打动女孩,如果你是单身的程序员,也赶紧来制作自己的爱心表白动画吧。 表白源码下载地址:https://github.com/enfangzhong/loveSource 个人博客展示:https://enfangzhong.github.io/ 在线演示 在我们rep中,我就展示了loveheart和love-ppt两个开源的表白动画。其他我就不一一全部上线演示了。 赶紧star吧。 loveheart演示地址:https://enfangzhong.github.io/love/ love-ppt演示地址:https://enfangzhong.github.io/loveshow/ 12套表白源码展示 表白网页款式01源码 表白网页款式02源码 ……….……….………. 表白网页款式06源码 表白网页款式07源码 表白网页款式08源码 ……….……….………. 表白网页款式11源码 表白网页款式12源码 祝天下有情人终成眷属!show the love line with your Mrs.Right]]></content>
<categories>
<category>博客</category>
</categories>
<tags>
<tag>博客</tag>
</tags>
</entry>
<entry>
<title><![CDATA[机器学习笔记_02决策树与随机森林]]></title>
<url>%2F2019%2F05%2F13%2F%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0_02%E5%86%B3%E7%AD%96%E6%A0%91%E4%B8%8E%E9%9A%8F%E6%9C%BA%E6%A3%AE%E6%9E%97%2F</url>
<content type="text"><![CDATA[机器学习笔记_02决策树与随机森林[TOC] 从LR到决策树1、总体流程与核心问题首先,在了解树模型之前,自然想到线性模型和树模型有什么区别呢?其中最重要的是,树形模型是一个一个特征进行处理,之前线性模型是所有特征给予权重相加得到一个新的值。决策树与逻辑回归的分类区别也在于此,逻辑回归是将所有特征通过sigmoid函数变换为概率后,通过大于某一概率阈值的划分为一类,小于某一概率阈值的为另一类;而决策树是对每一个特征做一个划分。另外逻辑回归只能找到线性分割(输入特征x与logit之间是线性的,除非对x进行多维映射),而决策树可以找到非线性分割。 而树形模型更加接近人的思维方式,可以产生可视化的分类规则,产生的模型具有可解释性(可以抽取规则)。树模型拟合出来的函数其实是分区间的阶梯函数。 决策树学习:采用自顶向下的递归的方法,基本思想是以信息熵为度量构造一棵熵值下降最快的树,到叶子节点处熵值为0(叶节点中的实例都属于一类)。 其次,需要了解几个重要的基本概念:根节点(最重要的特征);父节点与子节点是一对,先有父节点,才会有子节点;叶节点(最终标签)。 2、熵、信息增益、信息增益率信息熵(Information Entropy) 信息熵是用来评估样本集合的纯度的一个参数,就是说,给出一个样本集合,这个样本集合中的样本可能属于好多不同的类别,也可能只属于一个类别,那么如果属于好多不同的类别的话,我们就说这个样本是不纯的,如果只属于一个类别,那么,我们就说这个样本是纯洁的。 而信息熵这个东西就是来计算一个样本集合中的数据是纯洁的还是不纯洁的。下面上公式: $Ent(D)=-\sum_{k=1}^{\left|y\right|}p_{k}log_{2}p_{k}$ 下面解释一下公式的意思,其实很好理解,计算一个集合的纯度,就是把集合中每一个类别所占的比例$p_k$(k从1到 $\left | y \right |$,其中 $\left | y \right |$ 表示类别的个数)乘上它的对数,然后加到一起,然后经过计算之后,可以得到一个数据集的信息熵,然后根据信息熵,可以判断这个数据集是否纯粹。信息熵越小的话,表明这个数据集越纯粹。信息熵的最小值为0,此时数据集D中只含有一个类别。 信息增益(Information Gain) 下面来介绍信息增益,所谓的信息增益,是要针对于具体的属性来讲的,比如说,数据集D中含有两个类别,分别是好人和坏人,那么,随便选择一个属性吧,比如说性别,性别这个属性中包含两个值,男人和女人,如果用男人和女人来划分数据集D的话,会得到两个集合,分别是$D_{man}$和$D_{woman}$。划分后的两个集合中各自有 好人和坏人,所以可以分别计算划分后两个集合的纯度,计算之后,把这两个集合的信息熵求加权平均$\frac{D_{man}}{D} Ent(D_{man})+\frac{D_{woman}}{D} Ent(D_{woman})$,跟之前没有划分的时候的信息熵$Ent(D)$相比较,用后者减去前者,得到的就是属性-性别对样本集D划分所得到的信息增益。可以通俗理解为,信息增益就是纯度提升值,用属性对原数据集进行划分后,得到的信息熵的差就是纯度的提升值。信息增益的公式如下: $Gain(D,a)=Ent(D)-\sum_{v=1}^{V}\frac{\left | D^{v} \right |}{\left | D \right |}Ent(D^{v})$ 先解释一下上式中的参数,D是数据集,a是选择的属性,a中一共有V个取值,用这个V取值去划分数据集D,分别得到数据集$D_1$到$D_V$,分别求这V个数据集的信息熵,并将其求加权平均。两者的差得到的就是信息增益。 那么这个信息增益有什么用呢?有用,可以根据信息增益值的大小来判断是否要用这个属性a去划分数据集D,如果得到的信息增益比较大,那么就说明这个属性是用来划分数据集D比较好的属性,否则则认为该属性不适合用来划分数据集D。这样有助于去构建决策树。 著名的算法ID3就是采用信息增益来作为判断是否用该属性划分数据集的标准。 信息增益率(Information Gain Ratio) 为什么要提出信息增益率这种评判划分属性的方法?信息增益不是就很好吗?其实不然,用信息增益作为评判划分属性的方法其实是有一定的缺陷的,书上说,信息增益准则对那些属性的取值比较多的属性有所偏好,也就是说,采用信息增益作为判定方法,会倾向于去选择属性取值比较多的属性。那么,选择取值多的属性为什么就不好了呢?举个比较极端的例子,如果将身份证号作为一个属性,那么,其实每个人的身份证号都是不相同的,也就是说,有多少个人,就有多少种取值,它的取值很多吧,让我们继续看,如果用身份证号这个属性去划分原数据集D,那么,原数据集D中有多少个样本,就会被划分为多少个子集,每个子集只有一个人,这种极端情况下,因为一个人只可能属于一种类别,好人,或者坏人,那么此时每个子集的信息熵就是0了,就是说此时每个子集都特别纯。这样的话,会导致信息增益公式的第二项$\sum_{v=1}^{V}\frac{\left | D^{v} \right |}{\left | D \right |}Ent(D^{v})$整体为0,这样导致的结果是,信息增益计算出来的特别大,然后决策树会用身份证号这个属性来划分原数据集D,其实这种划分毫无意义。因此,为了改变这种不良偏好带来的不利影响,提出了采用信息增益率作为评判划分属性的方法。 公式如下: $Gain_ratio(D,a)=\frac{Gain(D,a)}{IV(a)}$ 其中$IV(a)$的计算方式如下: $IV(a)=-\sum_{v=1}^{V}\frac{\left | D^v \right |}{\left | D \right |}log_2\frac{\left | D^v \right |}{\left | D \right |}$ $IV(a)$被称为是的“固有值”,这个$IV(a)$的公式是不是很熟悉啊,简直和信息熵的计算公式一毛一样,就是看属性a的纯度,如果a只含有少量的取值的话,那么a的纯度就比较高,否则的话,a的取值越多,a的纯度越低,$IV(a)$的值也就越大,因此,最后得到的信息增益率就越低。 采用信息增益率可以解决ID3算法中存在的问题(ID3会对那些属性的取值比较多的属性有所偏好,如西瓜的颜色有10种),因此将采用信息增益率作为判定划分属性好坏的方法称为C4.5。 需要注意的是,增益率准则对属性取值较少的时候会有偏好,为了解决这个问题,C4.5并不是直接选择增益率最大的属性作为划分属性,而是之前先通过一遍筛选,先把信息增益低于平均水平的属性剔除掉,之后从剩下的属性中选择信息增益率最高的,这样的话,相当于两方面都得到了兼顾。 (结合信息增益与信息增益率使用) 采用信息增益率可以解决ID3算法中存在的问题,因此将采用信息增益率作为判定划分属性好坏的方法称为C4.5。需要注意的是,增益率准则对属性取值较少的时候会有偏好,为了解决这个问题,C4.5并不是直接选择增益率最大的属性作为划分属性,而是之前先通过一遍筛选,先把信息增益低于平均水平的属性剔除掉,之后从剩下的属性中选择信息增益率最高的,这样的话,相当于两方面都得到了兼顾。 基尼指数(gini index):CART中使用定义: 是一种不等性度量; 通常用来度量收入不平衡,可以用来度量任何不均匀分布; 是介于0~1之间的数,0-完全相等,1-完全不相等; 总体内包含的类别越杂乱,基尼指数就越大 基尼不纯度指标在CART算法中, 基尼不纯度表示一个随机选中的样本在子集中被分错的可能性。基尼不纯度为这个样本被选中的概率乘以它被分错的概率。当一个节点中所有样本都是一个类时,基尼不纯度为零。假设y的可能取值为{1, 2, …, m},令fifi是样本被赋予i的概率,则基尼指数可以通过如下计算: $\begin{aligned} \operatorname{Gini}(D) &=\sum_{k=1}^{|\mathcal{Y}|} \sum_{k^{\prime} \neq k} p_{k} p_{k^{\prime}} \\ &=1-\sum_{k=1}^{|\mathcal{Y}|} p_{k}^{2} \end{aligned}$ 反映了从D中随机抽取两个样例,其类别标签不一致的概率。]]></content>
<categories>
<category>机器学习</category>
</categories>
<tags>
<tag>机器学习</tag>
</tags>
</entry>
<entry>
<title><![CDATA[机器学习笔记_01线性回归和逻辑回归]]></title>
<url>%2F2019%2F05%2F10%2F%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0_01%E7%BA%BF%E6%80%A7%E5%9B%9E%E5%BD%92%E5%92%8C%E9%80%BB%E8%BE%91%E5%9B%9E%E5%BD%92%2F</url>
<content type="text"><![CDATA[机器学习笔记_01线性回归和逻辑回归[TOC] 一、什么是机器学习利用大量的数据样本,使得计算机通过不断的学习获得一个模型,用来对新的未知数据做预测。 有监督学习(分类、回归) 同时将数据样本和标签输入给模型,模型学习到数据和标签的映射关系,从而对新数据进行预测。 无监督学习(聚类)只有数据,没有标签,模型通过总结规律,从数据中挖掘出信息。 强化学习强化学习会在没有任何标签的情况下,通过先尝试做出一些行为得到一个结果,通过这个结果是对还是错的反馈,调整之前的行为,就这样不断的调整,算法能够学习到在什么样的情况下选择什么样的行为可以得到最好的结果。 就好比你有一只还没有训练好的小狗,每当它把屋子弄乱后,就减少美味食物的数量(惩罚),每次表现不错时,就加倍美味食物的数量(奖励),那么小狗最终会学到一个知识,就是把客厅弄乱是不好的行为。 【David Silve强化学习课程】: 推荐David Silver的Reinforcement Learning Course 课件链接:https://github.com/enfangzhong/DavidSilverRLPPT 机器学习基本术语与概念 二、线性回归利用大量的样本$D=\left(x_{i}, y_{i}\right)_{i=1}^{N}$,通过有监督的学习,学习到由x到y的映射f,利用该映射关系对未知的数据进行预估,因为y为连续值,所以是回归问题。 单变量情况 多变量情况 二维空间的直线,转化为高维空间的平面 2.1 线性回归的表达式机器学习是数据驱动的算法,数据驱动=数据+模型,模型就是输入到输出的映射关系。 模型=假设函数(不同的学习方式)+优化 1. 假设函数线性回归的假设函数($\theta_{0}$表示截距项,$x_0=1$,方便矩阵表达): $f(x)=\theta_{0} x_{0}+\theta_{1} x_{1}+\theta_{2} x_{2} \ldots+\theta_{n} x_{n}$向量形式(θ,x都是列向量):$f(x)=\theta^{T} x$ 2. 优化方法监督学习的优化方法=损失函数+对损失函数的优化 3. 损失函数如何衡量已有的参数$\theta$的好坏? 利用损失函数来衡量,损失函数度量预测值和标准答案的偏差,不同的参数有不同的偏差,所以要通过最小化损失函数,也就是最小化偏差来得到最好的参数。映射函数:$h_{\theta}(x)$损失函数:$J\left(\theta_{0}, \theta_{1}, \ldots, \theta_{n}\right)=\frac{1}{2 m} \sum_{i=1}^{m}\left(h_{\theta}\left(x^{(i)}\right)-y^{(i)}\right)^{2}$ 解释:因为有m个样本,所以要平均,分母的2是为了求导方便 最小化损失函数( loss function):凸函数 4. 损失函数的优化损失函数如右图所示,是一个凸函数,我们的目标是达到最低点,也就是使得损失函数最小。 多元情况下容易出现局部极值 求极值的数学思想,对公式求导=0即可得到极值,但是工业上计算量很大,公式很复杂,所以从计算机的角度来讲,求极值是利用梯度下降法。 ① 初始位置选取很重要 ② 复梯度方向更新,二维情况下,函数变换最快的方向是斜率方向,多维情况下就成为梯度,梯度表示函数值增大的最快的方向,所以要在负梯度方向上进行迭代。 ③ θ的更新公式如上图,每个参数 $\theta_1,\theta_2…$ 都是分别更新的 高维情况:梯度方向就是垂直于登高线的方向 参数更新示例: 对每个theta都进行更新: 学习率: ① 学习率太大,会跳过最低点,可能不收敛② 学习率太小收敛速度过慢 5. 过拟合和欠拟合(underfitting vs overfitting) 过拟合的原因:① 如果我们有很多的特征或模型很复杂,则假设函数曲线可以对训练样本拟合的非常好$\left(J(\theta)=\frac{1}{2 m} \sum_{i=1}^{m}\left(h_{\theta}\left(x^{(i)}\right)-y^{(i)}\right)^{2} \approx 0\right)$,学习能力太强了,但是丧失了一般性。从而导致对新给的待预测样本,预测效果差.② 眼见不一定为实,训练样本中肯定存在噪声点,如果全都学习的话肯定会将噪声也学习进去。 过拟合造成什么结果: 过拟合是给参数的自由空间太大了,可以通过简单的方式让参数变化太快,并未学习到底层的规律,模型抖动太大,很不稳定,variance变大,对新数据没有泛化能力。 所有的模型都可能存在过拟合的风险: 更多的参数,更复杂的模型,意味着有更强的能力, 但也更可能无法无天 眼见不一定为实,你看到的内容不一定是全部真实的数据分布,死记硬背不太好 6. 利用正则化解决过拟合问题正则化的作用: ① 控制参数变化幅度,对变化大的参数惩罚,不让模型“无法无天” ② 限制参数搜索空间 添加正则化的损失函数$J\left(\theta_{0}, \theta_{1}, \ldots, \theta_{n}\right)=\frac{1}{2 m} \sum_{i=1}^{m}\left(h_{\theta}\left(x^{(i)}-y^{(i)}\right)^{2}+\frac{\lambda}{2 m} \sum_{j=1}^{n} \theta_{j}^{2}\right.$ m:样本有m个n:n个参数,对n个参数进行惩罚λ:对误差的惩罚程度,λ 越大对误差的惩罚越大,容易出现过拟合,λ越小,对误差的惩罚越小,对误差的容忍度越大,泛化能力好。 7. 线性回归代码实例 三、逻辑回归监督学习,解决二分类问题。 分类的本质:在空间中找到一个决策边界来完成分类的决策 逻辑回归:线性回归可以预测连续值,但是不能解决分类问题,我们需要根据预测的结果判定其属于正类还是负类。所以逻辑回归就是将线性回归的$(-\infty,+\infty)$结果,通过sigmoid函数映射到(0,1) 之间。线性回归决策函数:$h_{\theta}(x)=\theta^{T} x$ sigmoid函数:$g(z)=\frac{1}{1+e^{-z}}$ ① 可以对$(-\infty,+\infty)$结果,映射到(0,1) 之间,作为概率。 ② $x]]></content>
<categories>
<category>机器学习</category>
</categories>
<tags>
<tag>机器学习</tag>
</tags>
</entry>
<entry>
<title><![CDATA[SQL内连接、左外连接、右外连接、交叉连接区别]]></title>
<url>%2F2018%2F10%2F17%2FSQL%E5%86%85%E8%BF%9E%E6%8E%A5%E3%80%81%E5%B7%A6%E5%A4%96%E8%BF%9E%E6%8E%A5%E3%80%81%E5%8F%B3%E5%A4%96%E8%BF%9E%E6%8E%A5%E3%80%81%E4%BA%A4%E5%8F%89%E8%BF%9E%E6%8E%A5%E5%8C%BA%E5%88%AB%2F</url>
<content type="text"><![CDATA[内连接、左外连接、右外连接、交叉连接区别在之前,我对MSSQL中的内连接和外连接所得出的数据集不是很清楚。这几天重新温习了一下SQL的书本,现在的思路应该是很清楚了,现在把自己的理解发出来给大家温习下。希望和我一样对SQL的连接语句不太理解的朋友能够有所帮助。(发这么菜的教程,各位大大们别笑话偶了,呵:D ) 有两个表A和表B。表A结构如下: Aid:int;标识种子,主键,自增ID Aname:varchar 数据情况,即用select * from A出来的记录情况如下图1所示: 图1:A表数据 表B结构如下: Bid:int;标识种子,主键,自增ID Bnameid:int 数据情况,即用select * from B出来的记录情况如下图2所示: 图2:B表数据 为了把Bid和Aid加以区分,不让大家有误解,所以把Bid的起始种子设置为100。有SQL基本知识的人都知道,两个表要做连接,就必须有个连接字段,从上表中的数据可以看出,在A表中的Aid和B表中的Bnameid就是两个连接字段。下图3说明了连接的所有记录集之间的关系: 图3:连接关系图 现在我们对内连接和外连接一一讲解。 1.内连接:利用内连接可获取两表的公共部分的记录,即图3的记录集C 语句如下:Select from A JOIN B ON A.Aid=B.Bnameid 运行结果如下图4所示:其实select from A,B where A.Aid=B.Bnameid与Select * from A JOIN B ON A.Aid=B.Bnameid的运行结果是一样的。 图4:内连接数据 2.外连接:外连接分为两种,一种是左连接(Left JOIN)和右连接(Right JOIN) (1)左连接(Left JOIN):即图3公共部分记录集C+表A记录集A1。 语句如下:select * from A Left JOIN B ON A.Aid=B.Bnameid运行结果如下图5所示: 图5:左连接数据 说明:在语句中,A在B的左边,并且是Left Join,所以其运算方式为:A左连接B的记录=图3公共部分记录集C+表A记录集A1在图3中即记录集C中的存在的Aid为:2 3 6 7 8图1中即表A所有记录集A中存在的Aid为:1 2 3 4 5 6 7 8 9表A记录集A1中存在的Aid=(图1中即A表中所有Aid)-(图3中即记录集C中存在的Aid),最终得出为:1 4 5 9由此得出图5中A左连接B的记录=图3公共部分记录集C+表A记录集A1, 最终得出的结果图5中可以看出Bnameid及Bid非NULL的记录都为图3公共部分记录集C中的记录;Bnameid及Bid为NULL的Aid为1 4 5 9的四笔记录就是表A记录集A1中存在的Aid。 (2)右连接(Right JOIN):即图3公共部分记录集C+表B记录集B1。 语句如下:select * from A Right JOIN B ON A.Aid=B.Bnameid 运行结果如下图6所示: 图6:右连接数据 说明: 在语句中,A在B的左边,并且是Right Join,所以其运算方式为:A右连接B的记录=图3公共部分记录集C+表B记录集B1在图3中即记录集C中的存在的Aid为:2 3 6 7 8图2中即表B所有记录集B中存在的Bnameid为:2 3 6 7 8 11表B记录集B1中存在的Bnameid=(图2中即B表中所有Bnameid)-(图3中即记录集C中存在的Aid),最终得出为:11由此得出图6中A右连接B的记录=图3公共部分记录集C+表B记录集B1, 最终得出的结果图6中可以看出Aid及Aname非NULL的记录都为图3公共部分记录集C中的记录;Aid及Aname为NULL的Aid为11的记录就是表B记录集B1中存在的Bnameid。 交叉连接:两张表联合没有条件情况下,条数 = 图1 * 图2 交叉连接不带WHERE子句,它返回被连接的两个表所有数据行的笛卡尔积,返回结果集合中的数据行数等于第一个表中符合查询条件的数据行数乘以第二个表中符合查询条件的数据行数。]]></content>
<categories>
<category>SQL</category>
</categories>
<tags>
<tag>SQL</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Git个人总结笔记]]></title>
<url>%2F2018%2F09%2F06%2FGit%E4%B8%AA%E4%BA%BA%E6%80%BB%E7%BB%93%E7%AC%94%E8%AE%B0%2F</url>
<content type="text"><![CDATA[Git个人总结笔记初始化一个Git仓库使用git init命令。 添加文件到Git仓库分两步: 使用命令git add ,注意,可反复多次使用,添加多个文件; 使用命令git commit -m ,完成。 查看git仓库状态要随时掌握工作区的状态,使用git status命令。 如果git status告诉你有文件被修改过,用git diff可以查看修改内容。 版本回退HEAD指向的版本就是当前版本,因此,Git允许我们在版本的历史之间穿梭,使用命令git reset —hard commit_id。 穿梭前,用git log可以查看提交历史,以便确定要回退到哪个版本。如:git reset —hard HEAD^ 要重返未来,用git reflog查看命令历史,以便确定要回到未来的哪个版本。如:git reset —hard commit_id git diff HEAD — readme.txt:查看工作区和版本库里面最新版本的区别 撤销修改git checkout — readme.txt把readme.txt文件在工作区的修改全部撤销 一种是readme.txt自修改后还没有被放到暂存区,现在,撤销修改就回到和版本库一模一样的状态; 一种是readme.txt已经添加到暂存区后,又作了修改,现在,撤销修改就回到添加到暂存区后的状态。 就是让这个文件回到最近一次git commit或git add时的状态 git reset HEAD 可以把暂存区的修改撤销掉(unstage),重新放回工作区 场景1:当你改乱了工作区某个文件的内容,想直接丢弃工作区的修改时,用命令git checkout — file。 场景2:当你不但改乱了工作区某个文件的内容,还添加到了暂存区时,想丢弃修改,分两步,第一步用命令git reset HEAD ,就回到了场景1,第二步按场景1操作。 场景3:已经提交了不合适的修改到版本库时,想要撤销本次提交,参考版本回退一节,不过前提是没有推送到远程库。 git rm用于删除一个文件。如果一个文件已经被提交到版本库,那么你永远不用担心误删,但是要小心,你只能恢复文件到最新版本,你会丢失最近一次提交后你修改的内容。git rm —cached … 远程仓库创建SSH Key:$ ssh-keygen -t rsa -C “[email protected]”github添加SHH 本地仓库进行远程同步1git remote add origin [email protected]:michaelliao/learngit.git 1git push -u origin master 第一次push加上-u参数,Git不但会把本地的master分支内容推送的远程新的master分支,还会把本地的master分支和远程的master分支关联起来 以后只要本地作了提交,就可以通过命令:1$ git push origin master 远程仓库tips: 要关联一个远程库,使用命令 1git remote add origin git@server-name:path/repo-name.git; 关联后,使用命令git push -u origin master第一次推送master分支的所有内容; 此后,每次本地提交后,只要有必要,就可以使用命令git push origin master推送最新修改; 远程仓库克隆1git clone [email protected]:michaelliao/gitskills.git 分支管理(重要)master指向最新的提交HEAD指向的就是当前分支 只有master分支的时候:每次提交,master分支都会向前移动一步,这样,随着你不断提交,master分支的线也越来越长 当我们创建新的分支,例如dev时,Git新建了一个指针叫dev,指向master相同的提交,再把HEAD指向dev,就表示当前分支在dev上: 从现在开始,对工作区的修改和提交就是针对dev分支了,比如新提交一次后,dev指针往前移动一步,而master指针不变假如我们在dev上的工作完成了,就可以把dev合并到master上。Git怎么合并呢?最简单的方法,就是直接把master指向dev的当前提交,就完成了合并:合并完分支后,甚至可以删除dev分支。删除dev分支就是把dev指针给删掉,删掉后,我们就剩下了一条master分支: 下面开始实战。首先,我们创建dev分支,然后切换到dev分支:git checkout -b dev git checkout命令加上-b参数表示创建并切换,相当于以下两条命令:12git branch devgit checkout dev git branch命令查看当前分支 把dev分支的工作成果合并到master分支上:git merge dev 合并后删除分支:git branch -d dev 总结:Git鼓励大量使用分支:1234567891011查看分支:git branch创建分支:git branch <name>切换分支:git checkout <name>创建+切换分支:git checkout -b <name>合并某分支到当前分支:git merge <name>删除分支:git branch -d <name> 本地库上使用命令git remote add把它和码云的远程库关联1git remote add origin [email protected]:liaoxuefeng/learngit.git git remote -v查看远程库信息123git remote -vorigin [email protected]:michaelliao/learngit.git (fetch)origin [email protected]:michaelliao/learngit.git (push) 删除已关联的名为origin的远程库:1git remote rm origin 关联GitHub的远程库1git remote add github [email protected]:michaelliao/learngit.git 关联码云的远程库:1git remote add gitee [email protected]:liaoxuefeng/learngit.git 12345git remote -vgitee [email protected]:liaoxuefeng/learngit.git (fetch)gitee [email protected]:liaoxuefeng/learngit.git (push)github [email protected]:michaelliao/learngit.git (fetch)github [email protected]:michaelliao/learngit.git (push) 如果要推送到GitHub,使用命令:1git push github master 如果要推送到码云,使用命令:1git push gitee master 为开源项目贡献代码1git clone [email protected]:enfangzhong/bootstrap.git 添加推特公司bootstrap项目远程仓库git remote add upstream [email protected]:twbs/bootstrap.git 查看建立连接的远程仓库 1、首先拉取推特公司最新代码git pull upstream master 2、自己创建分支git checkout -b feature/add_sth 然后去修改你自己代码 git status查看状态 git add ./ git commit -m “add sth” 3.切换到主分支,继续拉取网上最新代码 git checkout mastergit pull upstream master 4.切换到分支,进行测试,git checkout feature/add_sth 5.合并分支git rebase master git push origin feature/add_sth]]></content>
<categories>
<category>Git</category>
</categories>
<tags>
<tag>Git</tag>
</tags>
</entry>
<entry>
<title><![CDATA[JaveEE请求转发和重定向的区别]]></title>
<url>%2F2018%2F09%2F05%2FJaveEE%E8%AF%B7%E6%B1%82%E8%BD%AC%E5%8F%91%E5%92%8C%E9%87%8D%E5%AE%9A%E5%90%91%E7%9A%84%E5%8C%BA%E5%88%AB%2F</url>
<content type="text"><![CDATA[一、请求转发和重定向请求转发:request.getRequestDispatcher(URL地址).forward(request, response) 处理流程: 客户端发送请求,Servlet做出业务逻辑处理。 Servlet调用forword()方法,服务器Servlet把目标资源返回给客户端浏览器。 重定向:response.sendRedirect(URL地址) 处理流程: 客户端发送请求,Servlet做出业务逻辑处理。 Servlet调用response.sendReadirect()方法,把要访问的目标资源作为response响应头信息发给客户端浏览器。 客户端浏览器重新访问服务器资源xxx.jsp,服务器再次对客户端浏览器做出响应。重定向以上两种情况,你都需要考虑Servlet处理完后,数据如何在jsp页面上呈现。图例是请求、响应的流程,没有标明数据如何处理、展现。 二、转发和重定向的路径问题1)使用相对路径在重定向和转发中没有区别2)重定向和请求转发使用绝对路径时,根/路径代表了不同含义重定向response.sendRedirect(“xxx”)是服务器向客户端发送一个请求头信息,由客户端再请求一次服务器。/指的Tomcat的根目录,写绝对路径应该写成”/当前Web程序根名称/资源名” 。如”/WebModule/login.jsp”,”/bbs/servlet/LoginServlet”转发是在服务器内部进行的,写绝对路径/开头指的是当前的Web应用程序。绝对路径写法就是是”/login.jsp”或”/servlet/LoginServlet”。 总结:以上要注意是区分是从服务器外的请求,还在是内部转发,从服务器外的请求,从Tomcat根写起(就是要包括当前Web的根);是服务器内部的转发,很简单了,因为在当前服务器内,/写起指的就是当前Web的根目录。 三、转发和重定向的区别 request.getRequestDispatcher()是容器中控制权的转向,在客户端浏览器地址栏中不会显示出转向后的地址;服务器内部转发,整个过程处于同一个请求当中。response.sendRedirect()则是完全的跳转,浏览器将会得到跳转的地址,并重新发送请求链接。这样,从浏览器的地址栏中可以看到跳转后的链接地址。不在同一个请求。重定向,实际上客户端会向服务器端发送两个请求。所以转发中数据的存取可以用request作用域:request.setAttribute(), request.getAttribute(),重定向是取不到request中的数据的。只能用session。 forward()更加高效,在可以满足需要时,尽量使用RequestDispatcher.forward()方法。(思考一下为什么?) RequestDispatcher是通过调用HttpServletRequest对象的getRequestDispatcher()方法得到的,是属于请求对象的方法。sendRedirect()是HttpServletResponse对象的方法,即响应对象的方法,既然调用了响应对象的方法,那就表明整个请求过程已经结束了,服务器开始向客户端返回执行的结果。 重定向可以跨域访问,而转发是在web服务器内部进行的,不能跨域访问。]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Java</tag>
<tag>java编程思想</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Redis学习笔记]]></title>
<url>%2F2018%2F09%2F05%2FRedis%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%2F</url>
<content type="text"><![CDATA[Redis简介关于关系型数据库和nosql数据库 关系型数据库是基于关系表的数据库,最终会将数据持久化到磁盘上,而nosql数据 库是基于特殊的结构,并将数据存储到内存的数据库。从性能上而言,nosql数据库 要优于关系型数据库,从安全性上而言关系型数据库要优于nosql数据库,所以在实 际开发中一个项目中nosql和关系型数据库会一起使用,达到性能和安全性的双保证。 为什么要使用Redisredis在Linux上的安装 安装redis编译的c环境,yum install gcc-c++ 将redis-2.6.16.tar.gz上传到Linux系统中 解压到/usr/local下 tar -xvf redis-2.6.16.tar.gz -C /usr/local 进入redis-2.6.16目录 使用make命令编译redis 在redis-2.6.16目录中 使用make PREFIX=/usr/local/redis install命令安装redis到/usr/local/redis中 拷贝redis-2.6.16中的redis.conf到安装目录redis中 启动redis 在bin下执行命令redis-server redis.conf 如需远程连接redis,需配置redis端口6379在linux防火墙中开发 /sbin/iptables -I INPUT -p tcp —dport 6379 -j ACCEPT /etc/rc.d/init.d/iptables save 启动后看到如上欢迎页面,但此窗口不能关闭,窗口关闭就认为redis也关闭了(类似Tomcat通过bin下的startup.bat的方式) 解决方案:可以通过修改配置文件 配置redis后台启动,即服务器启动了但不会穿件控制台窗口 将redis.conf文件中的daemonize从false修改成true表示后台启动 使用命令查看6379端口是否启动ps -ef | grep redis 使用java去操作RedisRedis的常用命令 redis是一种高级的key-value的存储系统 其中的key是字符串类型,尽可能满足如下几点: key不要太长,最好不要操作1024个字节,这不仅会消耗内存还会降低查找 效率 key不要太短,如果太短会降低key的可读性 在项目中,key最好有一个统一的命名规范(根据企业的需求) 其中value 支持五种数据类型: 字符串型 string 字符串列表 lists 字符串集合 sets 有序字符串集合 sorted sets 哈希类型 hashs 我们对Redis的学习,主要是对数据的存储,下面将来学习各种Redis的数据类型的 存储操作: 存储字符串string 字符串类型是Redis中最为基础的数据存储类型,它在Redis中是二进制安全的,这 便意味着该类型可以接受任何格式的数据,如JPEG图像数据或Json对象描述信息等。 在Redis中字符串类型的Value最多可以容纳的数据长度是512M set key value:设定key持有指定的字符串value,如果该key存在则进行覆盖操作。总是返回”OK” get key:获取key的value。如果与该key关联的value不是String类型,redis将返回错误信息,因为get命令只能用于获取String value;如果该key不存在,返回null。 getset key value:先获取该key的值,然后在设置该key的值。 4)incr key:将指定的key的value原子性的递增1.如果该key不存在,其初始值 为0,在incr之后其值为1。如果value的值不能转成整型,如hello,该操作将执 行失败并返回相应的错误信息。 5)decr key:将指定的key的value原子性的递减1.如果该key不存在,其初始值 为0,在incr之后其值为-1。如果value的值不能转成整型,如hello,该操作将执 行失败并返回相应的错误信息。 6)incrby key increment:将指定的key的value原子性增加increment,如果该 key不存在,器初始值为0,在incrby之后,该值为increment。如果该值不能转成 整型,如hello则失败并返回错误信息 7)decrby key decrement:将指定的key的value原子性减少decrement,如果 该key不存在,器初始值为0,在decrby之后,该值为decrement。如果该值不能 转成整型,如hello则失败并返回错误信息 8)append key value:如果该key存在,则在原有的value后追加该值;如果该 key 不存在,则重新创建一个key/value 存储lists类型 在Redis中,List类型是按照插入顺序排序的字符串链表。和数据结构中的普通链表 一样,我们可以在其头部(left)和尾部(right)添加新的元素。在插入时,如果该键并不 存在,Redis将为该键创建一个新的链表。与此相反,如果链表中所有的元素均被移 除,那么该键也将会被从数据库中删除。List中可以包含的最大元素数量是 4294967295。 从元素插入和删除的效率视角来看,如果我们是在链表的两头插入或删除元素,这将会是非常高效的操作,即使链表中已经存储了百万条记录,该操作也可以在常量时间内完成。然而需要说明的是,如果元素插入或删除操作是作用于链表中间,那将会是非常低效的。相信对于有良好数据结构基础的开发者而言,这一点并不难理解。 lpush key value1 value2…:在指定的key所关联的list的头部插入所有的values,如果该key不存在,该命令在插入的之前创建一个与该key关联的空链表,之后再向该链表的头部插入数据。插入成功,返回元素的个数。 rpush key value1、value2…:在该list的尾部添加元素 lrange key start end:获取链表中从start到end的元素的值,start、end可为负数,若为-1则表示链表尾部的元素,-2则表示倒数第二个,依次类推… lpushx key value:仅当参数中指定的key存在时(如果与key管理的list中没有值时,则该key是不存在的)在指定的key所关联的list的头部插入value。 5)rpushx key value:在该list的尾部添加元素 6)lpop key:返回并弹出指定的key关联的链表中的第一个元素,即头部元素。 7)rpop key:从尾部弹出元素。 8)rpoplpush resource destination:将链表中的尾部元素弹出并添加到头部 9)llen key:返回指定的key关联的链表中的元素的数量。 10)lset key index value:设置链表中的index的脚标的元素值,0代表链表的头元 素,-1代表链表的尾元素。 11)lrem key count value:删除count个值为value的元素,如果count大于0,从头向尾遍历并删除count个值为value的元素,如果count小于0,则从尾向头遍历并删除。如果count等于0,则删除链表中所有等于value的元素。 12)linsert key before|after pivot value:在pivot元素前或者后插入value这个 元素。 存储sets类型 在Redis中,我们可以将Set类型看作为没有排序的字符集合,和List类型一样,我 们也可以在该类型的数据值上执行添加、删除或判断某一元素是否存在等操作。需要 说明的是,这些操作的时间是常量时间。Set可包含的最大元素数是4294967295。 和List类型不同的是,Set集合中不允许出现重复的元素。和List类型相比,Set类 型在功能上还存在着一个非常重要的特性,即在服务器端完成多个Sets之间的聚合计 算操作,如unions、intersections和differences。由于这些操作均在服务端完成, 因此效率极高,而且也节省了大量的网络IO开销 1)sadd key value1、value2…:向set中添加数据,如果该key的值已有则不会 重复添加 2)smembers key:获取set中所有的成员 3)scard key:获取set中成员的数量 4)sismember key member:判断参数中指定的成员是否在该set中,1表示存 在,0表示不存在或者该key本身就不存在 5)srem key member1、member2…:删除set中指定的成员 6)srandmember key:随机返回set中的一个成员 7)sdiff sdiff key1 key2:返回key1与key2中相差的成员,而且与key的顺序有 关。即返回差集。 8)sdiffstore destination key1 key2:将key1、key2相差的成员存储在 destination上 9)sinter key[key1,key2…]:返回交集。 10)sinterstore destination key1 key2:将返回的交集存储在destination上 11)sunion key1、key2:返回并集。 12)sunionstore destination key1 key2:将返回的并集存储在destination上 存储sortedset Sorted-Sets和Sets类型极为相似,它们都是字符串的集合,都不允许重复的成员出 现在一个Set中。它们之间的主要差别是Sorted-Sets中的每一个成员都会有一个分 数(score)与之关联,Redis正是通过分数来为集合中的成员进行从小到大的排序。然 而需要额外指出的是,尽管Sorted-Sets中的成员必须是唯一的,但是分数(score) 却是可以重复的。 在Sorted-Set中添加、删除或更新一个成员都是非常快速的操作,其时间复杂度为 集合中成员数量的对数。由于Sorted-Sets中的成员在集合中的位置是有序的,因此, 即便是访问位于集合中部的成员也仍然是非常高效的。事实上,Redis所具有的这一 特征在很多其它类型的数据库中是很难实现的,换句话说,在该点上要想达到和Redis 同样的高效,在其它数据库中进行建模是非常困难的。 例如:游戏排名、微博热点话题等使用场景。 1)zadd key score member score2 member2 … :将所有成员以及该成员的 分数存放到sorted-set中 2)zcard key:获取集合中的成员数量 3)zcount key min max:获取分数在[min,max]之间的成员 zincrby key increment member:设置指定成员的增加的分数。 zrange key start end [withscores]:获取集合中脚标为start-end的成员,[withscores]参数表明返回的成员包含其分数。 zrangebyscore key min max [withscores] [limit offset count]:返回分数在[min,max]的成员并按照分数从低到高排序。[withscores]:显示分数;[limit offset count]:offset,表明从脚标为offset的元素开始并返回count个成员。 zrank key member:返回成员在集合中的位置。 zrem key member[member…]:移除集合中指定的成员,可以指定多个成员。 zscore key member:返回指定成员的分数 存储hash Redis中的Hashes类型可以看成具有String Key和String Value的map容器。所 以该类型非常适合于存储值对象的信息。如Username、Password和Age等。如果 Hash中包含很少的字段,那么该类型的数据也将仅占用很少的磁盘空间。每一个Hash 可以存储4294967295个键值对。 1)hset key field value:为指定的key设定field/value对(键值对)。 2)hgetall key:获取key中的所有filed-vaule 3)hget key field:返回指定的key中的field的值 4)hmset key fields:设置key中的多个filed/value 5)hmget key fileds:获取key中的多个filed的值 6)hexists key field:判断指定的key中的filed是否存在 7)hlen key:获取key所包含的field的数量 8)hincrby key field increment:设置key中filed的值增加increment,如:age增加20 Redis的通用操作(见文档)Redis的特性(见文档)Redis的事务(见文档)Redis的持久化(见文档)总结: nosql redis安装——linux(重点) jedis(重点) redis的数据操作类型 5中 (了解) —- string和hash redis的其他]]></content>
<categories>
<category>Redis</category>
</categories>
<tags>
<tag>Redis</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java开发中的23种设计模式详解]]></title>
<url>%2F2018%2F09%2F03%2FJava%E5%BC%80%E5%8F%91%E4%B8%AD%E7%9A%8423%E7%A7%8D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E8%AF%A6%E8%A7%A3%2F</url>
<content type="text"><![CDATA[Java开发中的23种设计模式详解java的设计模式大体上分为三大类: 创建型模式(5种):工厂方法模式,抽象工厂模式,单例模式,建造者模式,原型模式。 结构型模式(7种):适配器模式,装饰器模式,代理模式,外观模式,桥接模式,组合模式,享元模式。 行为型模式(11种):策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。 设计模式遵循的原则有6个:1、开闭原则(Open Close Principle) 对扩展开放,对修改关闭。 2、里氏代换原则(Liskov Substitution Principle) 只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。 3、依赖倒转原则(Dependence Inversion Principle) 这个是开闭原则的基础,对接口编程,依赖于抽象而不依赖于具体。 4、接口隔离原则(Interface Segregation Principle) 使用多个隔离的借口来降低耦合度。 5、迪米特法则(最少知道原则)(Demeter Principle) 一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。 6、合成复用原则(Composite Reuse Principle) 原则是尽量使用合成/聚合的方式,而不是使用继承。继承实际上破坏了类的封装性,超类的方法可能会被子类修改。 1. 工厂模式(Factory Method) 常用的工厂模式是静态工厂,利用static方法,作为一种类似于常见的工具类Utils等辅助效果,一般情况下工厂类不需要实例化。 1234567891011121314151617181920212223242526272829interface food{}class A implements food{}class B implements food{}class C implements food{}public class StaticFactory { private StaticFactory(){} public static food getA(){ return new A(); } public static food getB(){ return new B(); } public static food getC(){ return new C(); }}class Client{ //客户端代码只需要将相应的参数传入即可得到对象 //用户不需要了解工厂类内部的逻辑。 public void get(String name){ food x = null ; if ( name.equals("A")) { x = StaticFactory.getA(); }else if ( name.equals("B")){ x = StaticFactory.getB(); }else { x = StaticFactory.getC(); } }} 2. 抽象工厂模式(Abstract Factory) 一个基础接口定义了功能,每个实现接口的子类就是产品,然后定义一个工厂接口,实现了工厂接口的就是工厂,这时候,接口编程的优点就出现了,我们可以新增产品类(只需要实现产品接口),只需要同时新增一个工厂类,客户端就可以轻松调用新产品的代码。 抽象工厂的灵活性就体现在这里,无需改动原有的代码,毕竟对于客户端来说,静态工厂模式在不改动StaticFactory类的代码时无法新增产品,如果采用了抽象工厂模式,就可以轻松的新增拓展类。 实例代码: 12345678910111213141516171819202122232425interface food{}class A implements food{}class B implements food{}interface produce{ food get();}class FactoryForA implements produce{ @Override public food get() { return new A(); }}class FactoryForB implements produce{ @Override public food get() { return new B(); }}public class AbstractFactory { public void ClientCode(String name){ food x= new FactoryForA().get(); x = new FactoryForB().get(); }} 3. 单例模式(Singleton) 在内部创建一个实例,构造器全部设置为private,所有方法均在该实例上改动,在创建上要注意类的实例化只能执行一次,可以采用许多种方法来实现,如Synchronized关键字,或者利用内部类等机制来实现。 12345678910public class Singleton { private Singleton(){} private static class SingletonBuild{ private static Singleton value = new Singleton(); } public Singleton getInstance(){ return SingletonBuild.value ;} } 4.建造者模式(Builder) 在了解之前,先假设有一个问题,我们需要创建一个学生对象,属性有name,number,class,sex,age,school等属性,如果每一个属性都可以为空,也就是说我们可以只用一个name,也可以用一个school,name,或者一个class,number,或者其他任意的赋值来创建一个学生对象,这时该怎么构造? 难道我们写6个1个输入的构造函数,15个2个输入的构造函数…….吗?这个时候就需要用到Builder模式了。给个例子,大家肯定一看就懂: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859public class Builder { static class Student{ String name = null ; int number = -1 ; String sex = null ; int age = -1 ; String school = null ; //构建器,利用构建器作为参数来构建Student对象 static class StudentBuilder{ String name = null ; int number = -1 ; String sex = null ; int age = -1 ; String school = null ; public StudentBuilder setName(String name) { this.name = name; return this ; } public StudentBuilder setNumber(int number) { this.number = number; return this ; } public StudentBuilder setSex(String sex) { this.sex = sex; return this ; } public StudentBuilder setAge(int age) { this.age = age; return this ; } public StudentBuilder setSchool(String school) { this.school = school; return this ; } public Student build() { return new Student(this); } } public Student(StudentBuilder builder){ this.age = builder.age; this.name = builder.name; this.number = builder.number; this.school = builder.school ; this.sex = builder.sex ; } } public static void main( String[] args ){ Student a = new Student.StudentBuilder().setAge(13).setName("LiHua").build(); Student b = new Student.StudentBuilder().setSchool("sc").setSex("Male").setName("ZhangSan").build(); }} 5. 原型模式(Protype)原型模式就是讲一个对象作为原型,使用clone()方法来创建新的实例。 12345678910111213141516171819202122232425262728public class Prototype implements Cloneable{ private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } @Override protected Object clone() { try { return super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); }finally { return null; } } public static void main ( String[] args){ Prototype pro = new Prototype(); Prototype pro1 = (Prototype)pro.clone(); }} 此处使用的是浅拷贝,关于深浅拷贝,大家可以另行查找相关资料。 6.适配器模式(Adapter)适配器模式的作用就是在原来的类上提供新功能。主要可分为3种: 类适配:创建新类,继承源类,并实现新接口,例如class adapter extends oldClass implements newFunc{}对象适配:创建新类持源类的实例,并实现新接口,例如class adapter implements newFunc { private oldClass oldInstance ;}接口适配:创建新的抽象类实现旧接口方法。例如abstract class adapter implements oldClassFunc { void newFunc();}7.装饰模式(Decorator) 给一类对象增加新的功能,装饰方法与具体的内部逻辑无关。例如: 12345678910111213interface Source{ void method();}public class Decorator implements Source{ private Source source ; public void decotate1(){ System.out.println("decorate"); } @Override public void method() { decotate1(); source.method(); }} 8.代理模式(Proxy)客户端通过代理类访问,代理类实现具体的实现细节,客户只需要使用代理类即可实现操作。 这种模式可以对旧功能进行代理,用一个代理类调用原有的方法,且对产生的结果进行控制。 1234567891011121314151617181920interface Source{ void method();}class OldClass implements Source{ @Override public void method() { }}class Proxy implements Source{ private Source source = new OldClass(); void doSomething(){} @Override public void method() { new Class1().Func1(); source.method(); new Class2().Func2(); doSomething(); }} 9.外观模式(Facade)为子系统中的一组接口提供一个一致的界面,定义一个高层接口,这个接口使得这一子系统更加容易使用。这句话是百度百科的解释,有点难懂,但是没事,看下面的例子,我们在启动停止所有子系统的时候,为它们设计一个外观类,这样就可以实现统一的接口,这样即使有新增的子系统subSystem4,也可以在不修改客户端代码的情况下轻松完成。 1234567891011121314151617public class Facade { private subSystem1 subSystem1 = new subSystem1(); private subSystem2 subSystem2 = new subSystem2(); private subSystem3 subSystem3 = new subSystem3(); public void startSystem(){ subSystem1.start(); subSystem2.start(); subSystem3.start(); } public void stopSystem(){ subSystem1.stop(); subSystem2.stop(); subSystem3.stop(); }} 10.桥接模式(Bridge)这里引用下http://www.runoob.com/design-pattern/bridge-pattern.html的例子。Circle类将DrwaApi与Shape类进行了桥接,代码: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546interface DrawAPI { public void drawCircle(int radius, int x, int y);}class RedCircle implements DrawAPI { @Override public void drawCircle(int radius, int x, int y) { System.out.println("Drawing Circle[ color: red, radius: " + radius +", x: " +x+", "+ y +"]"); }}class GreenCircle implements DrawAPI { @Override public void drawCircle(int radius, int x, int y) { System.out.println("Drawing Circle[ color: green, radius: " + radius +", x: " +x+", "+ y +"]"); }}abstract class Shape { protected DrawAPI drawAPI; protected Shape(DrawAPI drawAPI){ this.drawAPI = drawAPI; } public abstract void draw();}class Circle extends Shape { private int x, y, radius; public Circle(int x, int y, int radius, DrawAPI drawAPI) { super(drawAPI); this.x = x; this.y = y; this.radius = radius; } public void draw() { drawAPI.drawCircle(radius,x,y); }}//客户端使用代码Shape redCircle = new Circle(100,100, 10, new RedCircle());Shape greenCircle = new Circle(100,100, 10, new GreenCircle());redCircle.draw();greenCircle.draw(); 11.组合模式(Composite) 组合模式是为了表示那些层次结构,同时部分和整体也可能是一样的结构,常见的如文件夹或者树。举例: 12345678910111213141516171819202122abstract class component{}class File extends component{ String filename;}class Folder extends component{ component[] files ; //既可以放文件File类,也可以放文件夹Folder类。Folder类下又有子文件或子文件夹。 String foldername ; public Folder(component[] source){ files = source ;} public void scan(){ for ( component f:files){ if ( f instanceof File){ System.out.println("File "+((File) f).filename); }else if(f instanceof Folder){ Folder e = (Folder)f ; System.out.println("Folder "+e.foldername); e.scan(); } } } } 12.享元模式(Flyweight)使用共享对象的方法,用来尽可能减少内存使用量以及分享资讯。通常使用工厂类辅助,例子中使用一个HashMap类进行辅助判断,数据池中是否已经有了目标实例,如果有,则直接返回,不需要多次创建重复实例。 123456789101112131415161718192021222324abstract class flywei{ }public class Flyweight extends flywei{ Object obj ; public Flyweight(Object obj){ this.obj = obj; }}class FlyweightFactory{ private HashMap<Object,Flyweight> data; public FlyweightFactory(){ data = new HashMap<>();} public Flyweight getFlyweight(Object object){ if ( data.containsKey(object)){ return data.get(object); }else { Flyweight flyweight = new Flyweight(object); data.put(object,flyweight); return flyweight; } }}]]></content>
<categories>
<category>Java</category>
<category>设计模式</category>
<category>java编程思想</category>
</categories>
<tags>
<tag>java编程思想</tag>
<tag>设计模式</tag>
</tags>
</entry>
<entry>
<title><![CDATA[将hexo博客同时部署发布托管到github和coding]]></title>
<url>%2F2018%2F08%2F30%2FHexo%E4%B8%AA%E4%BA%BA%E5%8D%9A%E5%AE%A2_%E5%90%8C%E6%97%B6%E9%83%A8%E7%BD%B2%E5%8F%91%E5%B8%83%E6%89%98%E7%AE%A1%E5%88%B0github%E5%92%8Ccoding%2F</url>
<content type="text"><![CDATA[前言之前我们把hexo托管在github,但是毕竟github是国外的,访问速度上还是有点慢,所以想也部署一套在国内的托管平台,之前查资料听说gitcafe,但是听说gitcafe已经被coding收购了,所以就决定部署到coding。 查询了多方资料,终于鼓捣出了本地一次部署,同时更新到github以及coding。 正文_config.yml配置想要同时部署到2个平台,就要修改博客根目录下面的_config.yml文件中的deploy如下根据Hexo官方文档需要修改成下面的形式123456deploy: type: git message: [message] repo: github: <repository url>,[branch] gitcafe: <repository url>,[branch] 所以我的是这样:12345deploy: type: git repo: github: [email protected]:enfang/enfang.github.io.git,master coding: [email protected]:enfang/enfang.git,master 我这边提交采用的SSH密钥,这个方法有个好处,提交的时候不用输入用户名和密码。如果你习惯用http的方式,只要将地址改成相应的http地址即可。 coding上创建一个新项目这里只介绍coding上面如何创建项目,以及把本地hexo部署到coding上面,还不懂如何创建hexo的请看我之前的系类文章。首先我们创建一个项目,创建后进入项目的代码模块,获取到这个项目的ssh地址,我的是https://git.coding.net/enfang/enfang.git 同步本地hexo到coding上把获取到了ssh配置在上面的_config.yml文件中的deploy下,如果是第一次使用coding的话,需要设置SSH公钥,生成的方法可以参考coding帮助中心如果你看过我第一篇文章里面介绍过秘钥生成。coding上的第一篇文章github上的第一篇文章 我这里直接使用之前部署github时已经生成的公钥。 本地打开 id_rsa.pub 文件,复制其中全部内容,填写到SSH_RSA公钥key下的一栏,公钥名称可以随意起名字。完成后点击“添加”,然后输入密码或动态码即可添加完成。 添加后,测试公钥是否添加成功,在git bash命令输入:1ssh -T [email protected] 如果得到下面提示就表示公钥添加成功了:1Coding.net Tips : [Hello ! You've conected to Coding.net by SSH successfully! ] 最后使用部署命令就能把博客同步到coding上面: hexo deploy -g pages服务方式部署部署博客方式有两种,第一种就是pages服务的方式,也推荐这种方式,因为可以绑定域名,而第二种演示的方式必须升级会员才能绑定自定义域名。pages方式也很简单就是在source/需要创建一个空白文件,至于原因,是因为 coding.net需要这个文件来作为以静态文件部署的标志。就是说看到这个Staticfile就知道按照静态文件来发布。12cd source/touch Staticfile #名字必须是Staticfile 分支选择master,因为前面配置的分支是master,因此开启之后,也需要是master。然后看起之后就可访问了。 注意: 如果你的项目名称跟你coding的用户名一样,比如我的用户是叫enfang,博客项目名也叫enfang那直接访问 enfang.coding.me就能访问博客,否则就要带上项目名:enfang.coding.me/项目名 才能访问推荐项目名跟用户名一样,这样就可以省略项目名了 总结到此为止,终于可以实现一次部署,github和coding两个网站同时更新。访问速度也是唰唰唰的快,忙乎了两天终于搭好了独立博客。希望对还在搭建hexo独立博客的小伙伴有帮助。本人博客效果效果展示 欢迎访问我的博客Git托管博客效果 Coding托管博客效果 码云托管博客效果]]></content>
<categories>
<category>博客</category>
</categories>
<tags>
<tag>博客</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Hexo博客添加在线联系功能]]></title>
<url>%2F2018%2F08%2F29%2FHexo%E4%B8%AA%E4%BA%BA%E5%8D%9A%E5%AE%A2_%E6%B7%BB%E5%8A%A0%E5%9C%A8%E7%BA%BF%E8%81%94%E7%B3%BB%E5%8A%9F%E8%83%BD%2F</url>
<content type="text"><![CDATA[Hexo博客添加在线联系功能Hexo博客如何添加在线联系功能呢,发现了一个不错的网站可以提供在线联系的服务,当有用户在网页上给你留言后会通过邮件或者微信通知你,可以及时的解答用户的疑问。 最终的效果可以参考我博客的右下角,有个聊天的按钮,效果如下所示:配置方法如下:首先到DaoVoice上注册一个账号,注册完成后会得到一个app_id,获取appid的步骤如下图所示:以next主题为例,打开/themes/next/layout/_partials/head.swig文件添加如下123456789{% if theme.daovoice %} <script> (function(i,s,o,g,r,a,m){i["DaoVoiceObject"]=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;a.charset="utf-8";m.parentNode.insertBefore(a,m)})(window,document,"script",('https:' == document.location.protocol ? 'https:' : 'http:') + "//widget.daovoice.io/widget/0f81ff2f.js","daovoice") daovoice('init', { app_id: "{{theme.daovoice_app_id}}" }); daovoice('update'); </script>{% endif %} 接着打开主题配置文件_config.yml,添加如下代码:123# Online contact daovoice: truedaovoice_app_id: 这里输入前面获取的app_id 最后执行hexo clean && hexo g && hexo s就能看到效果了。 需要注意的是,next主题下聊天的按钮会和其他按钮重叠到一起,可以到聊天设置,修改下按钮的位置: 最后到右上角选择管理员,微信绑定,可以绑定你的微信号,关注公众号后打开小程序,就可以实时收发消息,有新的消息也会通过微信通知,设置页面如下:效果展示: 酱油哥博客 欢迎访问我的博客Git托管博客效果 Coding托管博客效果 码云托管博客效果]]></content>
<categories>
<category>博客</category>
</categories>
<tags>
<tag>博客</tag>
</tags>
</entry>
<entry>
<title><![CDATA[markdown的使用方法]]></title>
<url>%2F2018%2F08%2F27%2FHexo%E4%B8%AA%E4%BA%BA%E5%8D%9A%E5%AE%A2_markdown%E7%9A%84%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95%2F</url>
<content type="text"><![CDATA[markDown的使用方法第一步:下载markdown进入markdown官网,选择download,进行下载。 列表1 列表2 a 子列表1 b 子列表2 列表3 链接举例酱油哥博客斜体字体加粗 分割线 <html></html> 123456<html> <head> <title>markdown使用</tile> <head> <body><body></html> 引用 欢迎访问我的博客Git托管博客效果 Coding托管博客效果 码云托管博客效果]]></content>
<categories>
<category>博客</category>
</categories>
<tags>
<tag>博客</tag>
</tags>
</entry>
<entry>
<title><![CDATA[hexo+github搭建个人博客及美化]]></title>
<url>%2F2018%2F08%2F26%2FHexo%E4%B8%AA%E4%BA%BA%E5%8D%9A%E5%AE%A2_hexo%2Bgithub%E6%90%AD%E5%BB%BA%E4%B8%AA%E4%BA%BA%E5%8D%9A%E5%AE%A2%E5%8F%8A%E7%BE%8E%E5%8C%96%E6%9B%B4%E6%96%B0%2F</url>
<content type="text"><![CDATA[使用Hexo+Github一步步搭建属于自己的博客(基础篇) 1、准备工作安装Node.js和配置好Node.js环境,打开cmd命令行,输入:1node -v 安装Git和配置好Git环境,安装成功的象征就是在电脑上任何位置鼠标右键能够出现如下两个选择Git GUI Here和Git Bash Here。查看git是否安装成功,在cmd命令行中输入:1git --version 2、Github账户注册和新建项目,项目必须要遵守格式:账户名.github.io,不然接下来会有很多麻烦。并且需要勾选Initialize this repository with a README在建好的项目右侧有个settings按钮,点击它,向下拉到GitHub Pages,你会看到那边有个网址,访问它,你将会惊奇的发现该项目已经被部署到网络上,能够通过外网来访问它。3、安装Hexo,在自己认为合适的地方创个文件夹,我是在D盘建了一个blog文件夹。然后通过命令行进入到该文件夹里面12d:cd blog 命令行中输入npm install hexo -g,开始安装Hexo,等待安装完毕后进行下一步输入hexo init,初始化该文件夹(有点漫长的等待。。。)输入npm install,安装所需要的组件输入hexo g,首次体验Hexo输入hexo s,开启服务器,访问该网址,正式体验Hexo问题:假如页面一直无法跳转,那么可能端口被占用了。此时我们ctrl+c停止服务器,接着输入“hexo server -p 端口号”来改变端口号,如何在浏览器中输入:localhost:端口号,你就可以在本地查看部署好的个人博客项目了。4、将本地博客部署到github网站上去。将Hexo与Github page联系起来,设置Git的user name和email 设置Git的user name和email a 在博客blog目录下,右键选Git Baes Here,命令行中输入,其中的name和email替换成你自己的用户名和邮箱 12$ git config --global user.name "Name"$ git config --global user.email "[email protected]" 输入输入cd ~/.ssh,检查是否由.ssh的文件夹 输入ssh-keygen -t rsa -C “[email protected]”,连续三个回车,生成密钥,最后得到了两个文件:id_rsa和id_rsa.pub(默认存储路径是:C:\Users\Administrator.ssh)。 登录Github,点击头像下的settings,添加ssh。新建一个new ssh key,将id_rsa.pub文件里的内容复制上去输入ssh -T [email protected],测试添加ssh是否成功。如果看到Hi后面是你的用户名,就说明成功了5、配置Deployment,在其文件夹中,找到_config.yml文件,修改repo值(在末尾)1234deploy: type: git repo: [email protected]:enfangzhong/enfangzhong.github.io.git branch: master repo值是你在github项目里的ssh(右下角)6、新建一篇博客,在cmd执行命令:hexo new post “博客名”1hexo new post "你好,酱油哥" 这时候在文件夹_posts目录下将会看到已经创建的文件在生成以及部署文章之前,需要安装一个扩展:npm install hexo-deployer-git —save使用编辑器编好文章,那么就可以使用命令:hexo d -g,生成以及部署了部署成功后访问你的地址:http://用户名.github.io。那么将看到生成的文章好了,到此为止,最基本的也是最全面的hexo+github搭建博客完结。接下来进入主题优化吧主题优化展示: 酱油哥 欢迎访问我的博客Git托管博客效果 Coding托管博客效果 码云托管博客效果]]></content>
<categories>
<category>博客</category>
</categories>
<tags>
<tag>博客</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Hexo个人博客_主题优化篇]]></title>
<url>%2F2018%2F08%2F26%2FHexo%E4%B8%AA%E4%BA%BA%E5%8D%9A%E5%AE%A2_%E4%B8%BB%E9%A2%98%E4%BC%98%E5%8C%96%E7%AF%87%2F</url>
<content type="text"><![CDATA[Hexo个人博客_主题优化篇 1、Hexo访问统计功能Hexo Next 解决 Busuanzi 统计浏览失效1node -v 好了,到此为止,最基本的也是最全面的hexo+github搭建博客完结。接下来进入主题优化吧主题优化展示: 酱油哥 欢迎访问我的博客Git托管博客效果 Coding托管博客效果 码云托管博客效果]]></content>
<categories>
<category>博客</category>
</categories>
<tags>
<tag>博客</tag>
</tags>
</entry>
<entry>
<title><![CDATA[hexo博客写文章模板]]></title>
<url>%2F2017%2F02%2F01%2Fhexo%E5%8D%9A%E5%AE%A2%E6%A8%A1%E6%9D%BF%2F</url>
<content type="text"><![CDATA[[TOC] 一级标题]]></content>
<categories>
<category>博客</category>
</categories>
<tags>
<tag>博客</tag>
</tags>
</entry>
</search>