-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsearch.xml
More file actions
901 lines (901 loc) · 366 KB
/
search.xml
File metadata and controls
901 lines (901 loc) · 366 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title><![CDATA[基于WebSocket实现微信小程序的消息推送]]></title>
<url>%2F2017%2F03%2F18%2Fe5-9f-ba-e4-ba-8ewebsocket-e5-ae-9e-e7-8e-b0-e5-be-ae-e4-bf-a1-e5-b0-8f-e7-a8-8b-e5-ba-8f-e7-9a-84-e6-b6-88-e6-81-af-e6-8e-a8-e9-80-81%2F</url>
<content type="text"><![CDATA[小程序端开发微信小程序支持通过基于WebSocket进行消息推送,提供了相应的API,例如创建连接示例代码:1234567891011wx.connectSocket({ url: 'test.php', data:{ x: '', y: '' }, header:{ 'content-type': 'application/json' }, method:"GET"}) API详细说明可参见微信API文档,这里主要讲解服务端如何实现基于WebSocket的消息推送。 服务端开发证书申请服务端整体部署架构为Nginx+Tomcat,因为微信小程序要求使用WSS协议进行通信,因此,需要配置Nginx支持SSL,可以通过阿里云的证书服务申请免费证书:按照提示操作即可。 Nginx配置证书申请成功后,在Nginx中配置:12345678910111213141516171819202122upstream websocket-server { server 127.0.0.1:8080;}server { listen 443; server_name xxx.com; ssl on; ssl_certificate /usr/local/nginx/conf/server.pem; ssl_certificate_key /usr/local/nginx/conf/server.key; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; location ^~/websocket/ { proxy_pass http://websocket-server; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; proxy_connect_timeout 3600s; proxy_read_timeout 3600s; proxy_send_timeout 3600s; }} 其中需要注意的是: 使用“proxy_set_header”设置转发后HTTP报文头的Upgrade和Connection属性; 使用“proxy_connect_timeout”、“proxy_read_timeout”、“proxy_send_timeout”设置连接超时时间,因为WebSocket是长连接,而Nginx反向代理默认超时时间是60秒,这里需要将超时时间设置长一些。 服务端配置WebSocket服务是使用Spring Boot实现的。配置类代码如下:123456789101112131415161718192021222324@Configurationpublic class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(onlineWebSocketHandler(), "/websocket").setAllowedOrigins("*").addInterceptors(onlineHandshakeInterceptor()).withSockJS(); } @Bean public WebSocketHandler onlineWebSocketHandler() { return new PerConnectionWebSocketHandler(OnlineWebSocketHandler.class); } @Bean public HandshakeInterceptor onlineHandshakeInterceptor() { return new OnlineHandshakeInterceptor(); } @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); }} 其中通过WebSocketHandlerRegistry实例注册WebSocket处理器,并注册拦截器进行权限控制。需要注意的是,在创建处理器时,使用了PerConnectionWebSocketHandler进行了封装,保证每个WebSocket连接使用一个处理器进行处理,这样处理器就可以是有状态的。 服务端连接建立和消息推送WebSocket处理器代码如下:1234567891011121314151617181920212223242526272829303132333435public class OnlineWebSocketHandler extends TextWebSocketHandler { private static Logger logger = LoggerFactory.getLogger(OnlineWebSocketHandler.class); private static Map<Integer, WebSocketSession> sessionMap = Maps.newConcurrentMap(); private Integer userId; private WebSocketSession session; @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { this.userId = (Integer) session.getAttributes().get(Constant.USER_ID); this.session = session; sessionMap.put(this.userId, this.session); logger.info("add session, userId={}, total={}", this.userId, sessionMap.size()); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { sessionMap.remove(this.userId); logger.info("remove session, userId={}, total={}", this.userId, sessionMap.size()); } protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { logger.info(message.getPayload()); } public static void sendMessage(Integer userId, String message) throws IOException { if (sessionMap.containsKey(userId)) { sessionMap.get(userId).sendMessage(new TextMessage(message)); } }} 其中,实现了WebSocketHandler接口的afterConnectionEstablished和afterConnectionClosed方法,在afterConnectionEstablished方法中,当WebSocket连接创建后,将连接所对应处理器实例的userId和session注册到静态Map对象中,在 afterConnectionClosed中,当WebSocket连接关闭后,将连接所对应处理器实例的userId和session从静态Map对象中删除。OnlineWebSocketHandler提供了一个静态方法sendMessage,通过调用这个方法,可以向某个指定用户推送消息,其内部是根据userId从静态Map对象查找session,并调用session的sendMessage方法推送消息。 服务端权限校验WebSocket拦截器代码如下:123456789101112131415161718192021222324252627282930public class OnlineHandshakeInterceptor implements HandshakeInterceptor { private static Logger logger = LoggerFactory.getLogger(OnlineHandshakeInterceptor.class); @Autowired private UserService userService; @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception { HttpServletRequest httpRequest = ((ServletServerHttpRequest) request).getServletRequest(); String userIdStr = httpRequest.getParameter(Constant.USER_ID); String session = httpRequest.getParameter(Constant.SESSION); if (StringUtils.isBlank(userIdStr) || StringUtils.isBlank(session)) { logger.info("userId and session needed, now useId={}, session={}", userIdStr, session); throw new BaseException(BaseExceptionEnum.NOT_ALLOWED); } int userId = Integer.valueOf(userIdStr); if (!userService.checkLogin(userId, session)) { throw new BaseException(BaseExceptionEnum.NOT_ALLOWED); } attributes.put(Constant.USER_ID, userId); return true; } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { }} 其中,实现了HandshakeInterceptor接口的beforeHandshake方法,在连接创建开始时,从request中获取url参数userId和session进行权限判断。 服务端启动Spring Boot启动类代码如下:123456789@SpringBootApplication@EnableWebMvc@EnableTransactionManagement@EnableWebSocketpublic class Application { public static void main(String[] args) throws Exception { SpringApplication.run(Application.class, args); } 通过EnableWebSocket注解开启WebSocket。]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>Spring</tag>
<tag>Spring Boot</tag>
<tag>WebSocket</tag>
<tag>小程序</tag>
<tag>微信</tag>
</tags>
</entry>
<entry>
<title><![CDATA[基于nginx-sticky-module-ng实现会话保持]]></title>
<url>%2F2016%2F05%2F15%2Fe5-9f-ba-e4-ba-8enginx-sticky-module-ng-e5-ae-9e-e7-8e-b0-e4-bc-9a-e8-af-9d-e4-bf-9d-e6-8c-81-ef-bc-88sticky-sessions-ef-bc-89%2F</url>
<content type="text"><![CDATA[方案对服务进行集群部署,前端进行负载均衡时,需要实现会话保持,对于同一会话的多个请求,通过集群中的一个节点来提供服务。系统的部署结构如图所示,通过Resin部署Web应用提供服务,通过Nginx进行负载均衡。基于nginx-sticky-module-ng实现会话保持。 安装Nginx已安装(版本为1.7.9),此处介绍如何添加nginx-sticky-module-ng。 从nginx-sticky-module-ng下载源码压缩包并解压; “nginx -V”查看原先安装Nginx时的配置信息: 1234# /usr/local/nginx/sbin/nginx -Vnginx version: nginx/1.7.9built by gcc 4.8.2 20140120 (Red Hat 4.8.2-16) (GCC) configure arguments: --prefix=/usr/local/nginx --with-pcre=/opt/soft/pcre-8.36 --with-zlib=/opt/soft/zlib-1.2.8 在Nginx源码目录下使用上述配置信息进行配置,并添加nginx-sticky-module-ng: 1./configure --prefix=/usr/local/nginx --with-pcre=/opt/soft/pcre-8.36 --with-zlib=/opt/soft/zlib-1.2.8 --add-module=/opt/soft/nginx-sticky-module-1.1 “make”进行编译,若编译时报“src/core/ngx_sha1.h:19:17: fatal error: sha.h: No such file or directory”错误,则先执行“yum install openssl-devel”安装OpenSSL包再配置; 编译成功后,备份/usr/local/nginx/sbin/nginx,并使用objs/nginx覆盖/usr/local/nginx/sbin/nginx: 12mv /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.backupcp objs/nginx /usr/local/nginx/sbin/ 配置使用Resin启动两个Web应用进程,端口分别是8082和8089,两个Web应用中都包含一个jsp页面session_sticky/test.jsp:1234567891011<%@ page contentType="text/html;charset=UTF-8" language="java" %><html><head> <title>测试Session Sticky</title></head><body>Service 1Session Id: <%=request.getSession().getId()%></body></html> test.jsp在两个Web应用中的不同在于一个输出“Service 1”,另一个输出“Service 2”。在Nginx中配置upstream和location,实现反向代理和会话保持:12345678910upstream session_sticky-server { sticky; server xx.xx.xx.xx:8082; server xx.xx.xx.xx:8089;}location ^~/session_sticky/ { proxy_pass http://session_sticky-server;} 测试 删除upstream中的“sticky”,使用默认的轮询方式进行反向代理,访问test.jsp,页面输出中交替出现“Service 1”和“Service 2”,说明请求交替发往两个Web应用,会话未保持: 在upstream中添加“sticky”,访问test.jsp,页面输出中一直出现“Service 1”,说明请求发往第一个应用,会话保持: 在2的基础上,停掉第一个应用,继续访问test.jsp,页面仍有响应,输出中一直出现“Service 2”,说明请求在检测到失败后,会自动发往第二个应用,并在第二个应用会话保持:]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>Nginx</tag>
</tags>
</entry>
<entry>
<title><![CDATA[ActiveMQ基于Zookeeper和LevelDB实现Master/Slave]]></title>
<url>%2F2016%2F03%2F06%2Factivemq-e5-9f-ba-e4-ba-8ezookeeper-e5-92-8cleveldb-e5-ae-9e-e7-8e-b0masterslave%2F</url>
<content type="text"><![CDATA[方案ActiveMQ的Master/Slave目前支持三种实现方式: Shared File System Master Slave; JDBC Master Slave; Replicated LevelDB Store。 对于第三种方式,ActiveMQ使用LevelDB持久化数据,并使用Zookeeper协调集群中各节点选举Master,如图所示:Master启动后接收客户端的连接请求,其他节点作为Slave连接至Master,从Master复制持久化数据,而不接收客户端的连接请求。如果Master停止,则存有最近更新的Slave被选举为Master,原Master恢复后则成为Slave。持久化数据需要复制到规定数量的Slave上。如果设置replicas值为3,则规定数量为(3/2+1)=2(保证超过半数),即需要复制的Slave数为2,Master会同步等待某一个Slave复制持久化数据完成,而其他Slave通过异步方式复制持久化数据。一般建议设置replicas值至少为3,即ActiveMQ集群节点数至少为3,这样即使有一个节点故障,也能保证规定数量,使得集群能够正常对外提供服务。 配置配置3个节点的ActiveMQ集群,修改activemq.xml,保证3个节点的brokerName相同:123<broker xmlns="http://activemq.apache.org/schema/core" brokerName="mp_activemq" dataDirectory="${activemq.data}"> ......</broker> 此处brokerName都设置为“mp_activemq”,这样各节点启动后,通过brokerName能够自动组成集群。另外修改activemq.xml中的持久化配置,使用LevelDB持久化数据,并配置Zookeeper: 1234567891011<persistenceAdapter> <replicatedLevelDB directory="${activemq.data}/leveldb" replicas="3" bind="tcp://0.0.0.0:0" zkAddress="xx.xx.xx.xx:2181,xx.xx.xx.xx:2181,xx.xx.xx.xx:2181" hostname="10.10.25.180" sync="local_disk" zkPath="/activemq/leveldb-stores" /></persistenceAdapter> 测试启动3个节点,其中一个节点被选举为Master,侦听各端口接收客户端的连接请求,对外提供服务,其日志如下:从日志中可以看出,该节点被选举为Master,而另两个节点作为Slave连接至该节点。另两个节点其中一个节点的日志如下:其连接至Master,并从Master复制数据。在Zookeeper的/activemq/leveldb-stores路径下记录着ActiveMQ集群各节点信息,如下:该路径下包含三个Znode,分别对应集群各节点,从中也可以看到有一个节点被选举为Master。客户端连接ActiveMQ集群时,采用fail-over方式,这样能保证始终连接到可用的Master节点上: failover:(tcp://xx.xx.xx.xx:61616,tcp://xx.xx.xx.xx:61616,tcp://xx.xx.xx.xx:61616)注:Replicated LevelDB Store从ActiveMQ 5.9.0开始支持,部署时Java版本为jdk 1.7。]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>ActiveMQ</tag>
<tag>ZooKeeper</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Keepalived+SSDB双主高可用]]></title>
<url>%2F2016%2F03%2F05%2Fkeepalivedssdb-e5-8f-8c-e4-b8-bb-e9-ab-98-e5-8f-af-e7-94-a8%2F</url>
<content type="text"><![CDATA[方案SSDB是一个C/C++语言开发的高性能NoSQL数据库,支持KV,list,map(hash),zset(sorted set)等数据结构,用来替代或者与Redis配合存储十亿级别列表的数据。与Redis相比,SSDB的存储基于文件,因此可以存储更多的数据,而不受限于内存的大小。在实际应用中,我们使用SSDB存储网站实时流量数据。本文主要介绍如何搭建SSDB双主,并使用Keepalived做IP漂移,使得双主对客户端透明。只要双主中有一个服务正常,客户端就可以正常读写,并保证数据不会丢失。其结构如图所示: 安装SSDB在两台服务器上下载SSDB源码包master.zip(下载地址),解压后先后执行“make”和“make install”完成安装:1234unzip master.zip cd ssdb-mastermakemake install SSDB默认安装至/usr/local/ssdb。 配置SSDB修改/usr/local/ssdb中的配置文件ssdb.conf:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556# ssdb-server config# MUST indent by TAB!# 配置目录work_dir = /data/ssdb-shardpidfile = /data/ssdb-shard/ssdb.pid# 配置访问server: # IP配置为0.0.0.0,则任何一个IP都可以访问 ip: 0.0.0.0 port: 8888 # 可以配置访问IP和访问密码,我们使用iptables配置访问IP,因此不在此处配置 # format: allow|deny: all|ip_prefix # multiple allows or denys is supported #deny: all #allow: 127.0.0.1 #allow: 192.168 # auth password must be at least 32 characters #auth: very-strong-password# 配置同步和复制# 双主下,当前SSDB是另一个SSDB的从,需要从另一个SSDB同步和复制数据replication: binlog: yes # Limit sync speed to *MB/s, -1: no limit sync_speed: -1 slaveof: # to identify a master even if it moved(ip, port changed) # if set to empty or not defined, ip:port will be used. id: svc_2 # sync|mirror, default is sync # 由于双主是双向同步,如果使用sync,则会造成双主重复一直同步,死循环 type: mirror ip: xx.xx.xx.xx port: 8888# 配置日志logger: level: error output: log.txt rotate: size: 1000000000# 配置LevelDB,SSDB使用LevelDB作为底层存储引擎leveldb: # in MB cache_size: 500 # in KB block_size: 32 # in MB write_buffer_size: 64 # in MB compaction_speed: 1000 # yes|no compression: yes 启动SSDB启动两台服务器上的SSDB:12# 启动为后台进程(不阻塞命令行)/usr/local/ssdb/ssdb-server -d ssdb.conf 使用/usr/local/ssdb/ssdb-cli进入命令行,使用info命令查看SSDB信息:从replication信息可以看到当前SSDB作为另一个SSDB的从,而在另一个SSDB中通过info命令也可以看到其是当前SSDB的从。而在某个SSDB使用set命令写入一个键值时,在另一个SSDB使用get命令可读出这个键值,说明SSDB双主搭建成功,这两个SSDB只要有一个服务正常,就可以正常对外提供读写,并保证数据不会丢失。 配置Keepalived对于客户端,在访问SSDB双主时,需要知道各个SSDB的地址,并且自己实现fail-over机制,在无法读写某个SSDB时,重新尝试读写另一个SSDB。我们使用Keepalived监控SSDB,对外提供虚IP供客户端连接读写,虚IP初始映射到某个SSDB,当该SSDB不可用时,将虚IP漂移到另一个SSDB,这样,双主对于客户端是透明的,客户端只要连接虚IP即可。此处不再介绍Keepalived的安装和运行,可参见《Keepalived介绍》,Keepalived的主配置keepalived.conf如下:123456789101112131415161718192021222324252627282930! Configuration File for keepalivedglobal_defs { router_id ssdb_1}vrrp_script chk_ssdb { script "/usr/local/keepalived/scripts/ssdb_check.sh 127.0.0.1 8888" interval 5 weight -2}vrrp_instance VI_1 { state BACKUP interface eth0 virtual_router_id 225 priority 101 advert_int 1 nopreempt authentication { auth_type PASS auth_pass 1111 } virtual_ipaddress { xx.xx.xx.xx } track_script { chk_ssdb }} 其中每隔5秒执行一个脚本检查SSDB服务是否可用,如不可用,则将权重减小2至99,而Keepalived从配置中权重为100,因此,从切换为主,并配置虚IP,保证了虚IP所在的SSDB服务一直可用。ssdb_check.sh脚本如下所示:1234567891011121314#!/bin/bashLOGFILE="/var/log/keepalived-ssdb-check.log"if [ "`/usr/local/bin/redis-cli -h $1 -p $2 PING`" == "PONG" ]; then : exit 0else sleep 1 if [ "`/usr/local/bin/redis-cli -h $1 -p $2 PING`" == "PONG" ]; then : exit 0 else date >> $LOGFILE echo "Failed:redis-cli -h $1 -p $2 PING" >> $LOGFILE 2>&1 exit 1 fifi 由于SSDB兼容Redis部分协议,因此此处使用Redis的命令行工具PING SSDB检查服务是否可用。]]></content>
<categories>
<category>服务器</category>
</categories>
<tags>
<tag>Keepalived</tag>
<tag>SSDB</tag>
</tags>
</entry>
<entry>
<title><![CDATA[[转]分布式缓存集群方案特性、应用场景、优缺点对比及选型]]></title>
<url>%2F2016%2F02%2F26%2Fe8-bd-ac-e5-88-86-e5-b8-83-e5-bc-8f-e7-bc-93-e5-ad-98-e9-9b-86-e7-be-a4-e6-96-b9-e6-a1-88-e7-89-b9-e6-80-a7-e3-80-81-e5-ba-94-e7-94-a8-e5-9c-ba-e6-99-af-e3-80-81-e4-bc-98-e7-bc-ba-e7-82-b9-e5-af-b9%2F</url>
<content type="text"><![CDATA[原文地址 分布式缓存特性: 高性能:当传统数据库面临大规模数据访问时,磁盘I/O往往成为性能瓶颈,从而导致过高的响应延迟。分布式缓存将高速内存作为数据对象的存储介质,数据以key/value形式存储,理想情况下可以获得DRAM级的读写性能; 动态扩展性:支持弹性扩展,通过动态增加或减少节点应对变化的数据访问负载,提供可预测的性能与扩展性;同时,最大限度地提高资源利用率; 高可用性:可用性包含数据可用性与服务可用性两方面。基于冗余机制实现高可用性,无单点失效(single point of failure),支持故障的自动发现,透明地实施故障切换,不会因服务器故障而导致缓存服务中断或数据丢失。动态扩展时自动均衡数据分区,同时保障缓存服务持续可用; 易用性:提供单一的数据与管理视图;API接口简单,且与拓扑结构无关;动态扩展或失效恢复时无需人工配置;自动选取备份节点;多数缓存系统提供了图形化的管理控制台,便于统一维护; 分布式代码执行(distributed code execution):将任务代码转移到各数据节点并行执行,客户端聚合返回结果,从而有效避免了缓存数据的移动与传输。最新的Java数据网格规范JSR-347中加入了分布式代码执行与Map/reduce的API支持,各主流分布式缓存产品,如IBM WebSphere eXtreme Scale,VMware GemFire,GigaSpaces XAP和Red Hat Infinispan等也都支持这一新的编程模型。 分布式缓存应用场景: 页面缓存。用来缓存Web页面的内容片段,包括HTML、CSS和图片等,多应用于社交网站等; 应用对象缓存。缓存系统作为ORM框架的二级缓存对外提供服务,目的是减轻数据库的负载压力,加速应用访问; 状态缓存。缓存包括Session会话状态及应用横向扩展时的状态数据等,这类数据一般是难以恢复的,对可用性要求较高,多应用于高可用集群(解决分布式Web部署的session同步问题); 并行处理。通常涉及大量中间计算结果需要共享; 事件处理。分布式缓存提供了针对事件流的连续查询(continuous query)处理技术,满足实时性需求; 极限事务处理。分布式缓存为事务型应用提供高吞吐率、低延时的解决方案,支持高并发事务请求处理,多应用于铁路、金融服务和电信等领域; 云计算领域提供分布式缓存服务(例如:青云、UnitedStack等); 任何需要用到缓存的地方,解决本地缓存数据量太小问题。分布式缓存能有效防止本地缓存失效数据库雪崩现象。 两大开源缓存系统对比,Memcache VS Redis: Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。而Memcache只支持简单数据类型,需要客户端自己处理复杂对象。 Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用(PS:持久化在RDB、AOF)。Redis借助了fork命令的copy on write机制。在生成快照时,将当前进程fork出一个子进程,然后在子进程中循环所有的数据,将数据写成为RDB文件。AOF日志的全称是append only file,从名字上我们就能看出来,它是一个追加写入的日志文件。与一般数据库的binlog不同的是,AOF文件是可识别的纯文本,它的内容就是一个个的Redis标准命令。当然,并不是发送到Redis的所有命令都要记录到AOF日志里面,只有那些会导致数据发生修改的命令才会追加到AOF文件。那么每一条修改数据的命令都生成一条日志。(PS:Memcache不支持数据持久存储)。 由于Memcache没有持久化机制,因此宕机所有缓存数据失效。Redis配置为持久化,宕机重启后,将自动加载宕机时刻的数据到缓存系统中。具有更好的灾备机制。 Memcache可以使用Magent在客户端进行一致性hash做分布式。Redis支持在服务器端做分布式(PS:Twemproxy/Codis/Redis-cluster多种分布式实现方式) Memcache的简单限制就是键(key)和value的限制。最大键长为250个字符。可以接受的储存数据不能超过1MB(可修改配置文件变大),因为这是典型slab的最大值,不适合虚拟机使用。而Redis的key长度支持到512k。 Redis使用的是单线程模型,保证了数据按顺序提交。Memcache需要使用CAS保证数据一致性。CAS(Check and Set)是一个确保并发一致性的机制,属于“乐观锁”范畴;原理很简单:拿版本号,操作,对比版本号,如果一致就操作,不一致就放弃任何操作。 CPU利用。由于Redis只使用单核,而Memcache可以使用多核,所以平均每一个核上Redis在存储小数据时比Memcache性能更高。而在100k以上的数据中,Memcache性能要高于Redis。(PS:Redis可以通过开启多个实例来提高CPU利用率,Memcache默认是单线程,需要编译指定参数才能支持多线程。由于分布式缓存是IO密集型系统,所以性能很大程度受限于网络通信,Memcache使用了libevent网络库,Redis自己实现了一套通信库。线程也不是影响吞吐量的重要因素。如第一点来说,一般情况下,程序处理内存数据的速度远高于网卡接收的速度。使用线程好处是可以同时处理多条连接,在极端情况下,可能会提高响应速度。但是单线程有时候比多线程或多进程更快,不需要考虑并发、锁,也不会增加上下文切换等开销,代码更加简洁,执行效率更高。) Memcache内存管理:使用Slab Allocation。原理相当简单,预先分配一系列大小固定的组,然后根据数据大小选择最合适的块存储。避免了内存碎片。(缺点:不能变长,浪费了一定空间)Memcache默认情况下下一个slab的最大值为前一个的1.25倍。 Redis内存管理:Redis通过定义一个数组来记录所有的内存分配情况, Redis采用的是包装的malloc/free,相较于Memcache的内存管理方法来说,要简单很多。由于malloc首先以链表的方式搜索已管理的内存中可用的空间分配,导致内存碎片比较多。 总结:其实对于企业选型Memcache、Redis而言,更多还是应该看业务使用场景(因为Memcache、Redis两者都具有足够高的性能和稳定性)。假若业务场景需要用到持久化缓存功能、或者支持多种数据结构的缓存功能,那么Redis则是最佳选择。(PS:Redis集群解决方式也优于Memcache,Memcache在客户端一致性hash的集群解决方案,Redis采用无中心的服务器端集群解决方案)综上所述:为了让缓存系统能够支持更多的业务场景,选择Redis会更优。(目前也越来越多的厂商选择Redis)。 Redis三大集群解决方案对比,Twemproxy VS Codis VS Redis-ClusterRedis集群三种常见的解决方案: 客户端分片:这种方案将分片工作放在业务程序端,程序代码根据预先设置的路由规则,直接对多个Redis实例进行分布式访问。这样的好处是,不依赖于第三方分布式中间件,实现方法和代码都自己掌控,可随时调整,不用担心踩到坑。这实际上是一种静态分片技术。Redis实例的增减,都得手工调整分片程序。基于此分片机制的开源产品,现在仍不多见。这种分片机制的性能比代理式更好(少了一个中间分发环节)。但缺点是升级麻烦,对研发人员的个人依赖性强——需要有较强的程序开发能力做后盾。如果主力程序员离职,可能新的负责人,会选择重写一遍。所以,这种方式下,可运维性较差。出现故障,定位和解决都得研发和运维配合着解决,故障时间变长。因此这种方案,难以进行标准化运维,不太适合中小公司(除非有足够的DevOPS)。 代理分片:这种方案,将分片工作交给专门的代理程序来做。代理程序接收到来自业务程序的数据请求,根据路由规则,将这些请求分发给正确的Redis实例并返回给业务程序。这种机制下,一般会选用第三方代理程序(而不是自己研发),因为后端有多个Redis实例,所以这类程序又称为分布式中间件。这样的好处是,业务程序不用关心后端Redis实例,运维起来也方便。虽然会因此带来些性能损耗,但对于Redis这种内存读写型应用,相对而言是能容忍的。这是我们推荐的集群实现方案。像基于该机制的开源产品Twemproxy,Codis便是其中代表,应用非常广泛。 服务器端分片:建立在基于无中心分布式架构之上(没有代理节点性能瓶颈问题)。Redis-Cluster即为官方基于该架构的解决方案。Redis Cluster将所有Key映射到16384个Slot中,集群中每个Redis实例负责一部分,业务程序通过集成的Redis Cluster客户端进行操作。客户端可以向任一实例发出请求,如果所需数据不在该实例中,则该实例引导客户端自动去对应实例读写数据。Redis Cluster的成员管理(节点名称、IP、端口、状态、角色)等,都通过节点之间两两通讯,定期交换并更新。 各解决方案代表产品实现方式优缺点:Twemproxy:Twemproxy是一种代理分片机制,由Twitter开源。Twemproxy作为代理,可接受来自多个程序的访问,按照路由规则,转发给后台的各个Redis服务器,再原路返回。这个方案顺理成章地解决了单个Redis实例承载能力的问题。当然,Twemproxy本身也是单点,需要用Keepalived做高可用方案。这么些年来,Twemproxy是应用范围最广、稳定性最高、最久经考验的分布式中间件。只是,他还有诸多不方便之处。Twemproxy最大的痛点在于,无法平滑地扩容/缩容。这样增加了运维难度:业务量突增,需增加Redis服务器;业务量萎缩,需要减少Redis服务器。但对Twemproxy而言,基本上都很难操作。或者说,Twemproxy更加像服务器端静态sharding。有时为了规避业务量突增导致的扩容需求,甚至被迫新开一个基于Twemproxy的Redis集群。Twemproxy另一个痛点是,运维不友好,甚至没有控制面板。 Codis:Codis由豌豆荚于2014年11月开源,基于Go和C开发,是近期涌现的、国人开发的优秀开源软件之一。现已广泛用于豌豆荚的各种Redis业务场景,从各种压力测试来看,稳定性符合高效运维的要求。性能更是改善很多,最初比Twemproxy慢20%;现在比Twemproxy快近100%(条件:多实例,一般value长度)。Codis具有可视化运维管理界面。Codis无疑是为解决Twemproxy缺点而出的新解决方案。因此综合方面会优于Twemproxy很多。目前也越来越多公司选择Codis。Codis引入了Group的概念,每个Group包括1个Redis Master及至少1个Redis Slave,这是和Twemproxy的区别之一。这样做的好处是,如果当前Master有问题,则运维人员可通过Dashboard“自助式”切换到Slave,而不需要小心翼翼地修改程序配置文件。为支持数据热迁移(Auto Rebalance),出品方修改了Redis Server源码,并称之为Codis Server。Codis采用预先分片(Pre-Sharding)机制,事先规定好了,分成1024个slots(也就是说,最多能支持后端1024个Codis Server),这些路由信息保存在ZooKeeper中。 Redis-Cluster:Reids-Cluster在Redis3.0中推出,支持Redis分布式集群部署模式。采用无中心分布式架构。所有的Redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。节点的fail是通过集群中超过半数的节点检测失效时才生效。客户端与Redis节点直连,不需要中间proxy层。客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可,减少了代理层,大大提高了性能。Redis-Cluster把所有的物理节点映射到[0-16383]slot上,cluster负责维护nodeslotkey之间的关系。目前Jedis已经支持Redis-Cluster。从计算架构或者性能方面无疑Redis-Cluster是最佳的选择方案。(PS:虽然Redis-Cluster从方案选型上面比较占据优势,但是由于Redis-Cluster刚推出不久,虽然官方宣传已经发布的是文档版本,但稳定性方面还有待验证)。]]></content>
<categories>
<category>缓存/NoSQL</category>
</categories>
<tags>
<tag>Codis</tag>
<tag>Memcache</tag>
<tag>Redis</tag>
<tag>Twemproxy</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Keepalived+Redis高可用主从虚IP经常漂移的问题]]></title>
<url>%2F2016%2F02%2F23%2Fkeepalivedredis-e9-ab-98-e5-8f-af-e7-94-a8-e4-b8-bb-e4-bb-8e-e8-99-9aip-e7-bb-8f-e5-b8-b8-e6-bc-82-e7-a7-bb-e7-9a-84-e9-97-ae-e9-a2-98%2F</url>
<content type="text"><![CDATA[配置使用Keepalived+Redis搭建Redis高可用主从,其中Keepalived配置为每隔2秒执行脚本检查Redis服务是否可用:12345vrrp_script chk_redis { script "/usr/local/keepalived/scripts/redis_check.sh 127.0.0.1 6379" interval 2 weight -2} redis_check.sh脚本如下所示:1234567891011121314#!/bin/bashLOGFILE="/var/log/keepalived-redis-check.log"if [ "`/usr/local/bin/redis-cli -h $1 -p $2 PING`" == "PONG" ]; then : exit 0else sleep 1 if [ "`/usr/local/bin/redis-cli -h $1 -p $2 PING`" == "PONG" ]; then : exit 0 else date >> $LOGFILE echo "Failed:redis-cli -h $1 -p $2 PING" >> $LOGFILE 2>&1 exit 1 fifi 问题使用过程中发现虚IP会经常从主服务器漂移至从服务器,而此时主服务器Keepalived和Redis进程都在。查看/var/log/messages中的Keepalived日志:发现是由于检查Redis服务是否可用的脚本超时,减少了主服务器Keepalived的权重,从而导致主从服务器状态切换、虚IP漂移,而脚本执行超时时间默认与执行间隔相等(2秒)。脚本主要执行Redis的PING操作,为什么有时候执行PING操作会超过2秒,进一步检查发现是该Redis在某些时间段读写频率非常高,QPS达到6万,造成PING操作响应慢。针对这个问题,解决方法就是搭建多个Redis主从,通过代理或直接在客户端将请求分摊到各个Redis主从上,减少单个Redis主从压力,避免PING操作响应慢,另外就是增大脚本执行间隔,增大执行超时时间。]]></content>
<categories>
<category>服务器</category>
</categories>
<tags>
<tag>Keepalived</tag>
<tag>Redis</tag>
</tags>
</entry>
<entry>
<title><![CDATA[一个简单的Dubbo示例]]></title>
<url>%2F2016%2F02%2F15%2Fe4-b8-80-e4-b8-aa-e7-ae-80-e5-8d-95-e7-9a-84dubbo-e7-a4-ba-e4-be-8b%2F</url>
<content type="text"><![CDATA[定义服务接口及其实现服务接口TestService:1234567package com.magicwt.service;public interface TestService { public String greet(String name);} 服务实现TestServiceImpl:123456789101112package com.magicwt.service.impl;import com.magicwt.service.TestService;public class TestServiceImpl implements TestService { @Override public String greet(String name) { return "hi, " + name; }} 服务提供者服务提供者TestProvider:1234567891011121314package com.magicwt;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;public class TestProvider { public static void main(String[] args) throws Exception { // 加载Spring上下文环境 ApplicationContext applicationContext = new ClassPathXmlApplicationContext("provider.xml"); // 保持进程,等待输入 System.in.read(); }} 服务提供者Spring上下文配置:1234567891011121314151617181920212223242526<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://code.alibabatech.com/schema/dubbo" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd"> <!-- 配置应用名称,用于表示服务提供者和服务消费者之间的依赖关系 --> <dubbo:application name="test-provider" /> <!-- 配置Zookeeper注册中心 --> <dubbo:registry address="zookeeper://xxx.xxx.xxx.xxx:2181" /> <!-- 配置服务通信协议,采用dubbo协议 --> <dubbo:protocol name="dubbo" port="20880" /> <!-- 配置本地服务实例 --> <bean id="testService" class="com.magicwt.service.impl.TestServiceImpl"/> <!-- 通过Dubbo将本地服务暴露为远程服务 --> <dubbo:service interface="com.magicwt.service.TestService" ref="testService" /></beans> 服务消费者服务消费者TestConsumer:12345678910111213141516171819202122package com.magicwt;import com.magicwt.service.TestService;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;public class TestConsumer { public static void main(String[] args) throws Exception { // 加载Spring上下文环境 ApplicationContext applicationContext = new ClassPathXmlApplicationContext("consumer.xml"); // 从Spring上下文环境获取服务实例 TestService testService = (TestService) applicationContext.getBean("testService"); int count = 0; while(true) { // 每隔2秒调用一次服务方法 System.out.println(testService.greet("admin" + ++count)); Thread.sleep(2000); } }} 服务消费者Spring上下文配置:1234567891011121314151617181920<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://code.alibabatech.com/schema/dubbo" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd"> <!-- 配置应用名称,用于表示服务提供者和服务消费者之间的依赖关系 --> <dubbo:application name="test-consumer" /> <!-- 配置Zookeeper注册中心 --> <dubbo:registry address="zookeeper://xxx.xxx.xxx.xxx:2181" /> <!-- 引用通过Dubbo暴露的远程服务 --> <dubbo:reference id="testService" interface="com.magicwt.service.TestService" /></beans> 运行分别启动服务提供者和服务消费者,服务消费者输出如下:从监控中心可以看出,应用“test-provider”是服务提供者,应用“test-consumer”是服务消费者,因为“test-consumer”调用“test-provider”提供的服务,所以“test-consumer”依赖于“test-provider”。]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>Dubbo</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Dubbo简介与实践]]></title>
<url>%2F2016%2F02%2F14%2Fdubbo-e7-ae-80-e4-bb-8b-e4-b8-8e-e5-ae-9e-e8-b7-b5%2F</url>
<content type="text"><![CDATA[介绍Dubbo 是阿里开源的分布式服务框架,架构如图所示: 其中,节点角色说明: Provider:暴露服务的服务提供者。 Consumer:调用远程服务的服务消费者。 Registry:服务注册与发现的注册中心。 Monitor:统计服务的调用次数和调用时间的监控中心。 Container:服务运行容器。 调用关系说明: 服务容器负责启动,加载,运行服务提供者。 服务提供者在启动时,向注册中心注册自己提供的服务。 服务消费者在启动时,向注册中心订阅自己所需的服务。 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。 Dubbo支持以下4种类型的注册中心: Multicast注册中心,不需要启动任何中心节点,只要广播地址一样,就可以互相发现; Zookeeper注册中心,服务提供者将服务信息写入Zookeeper,服务消费者从Zookeeper获取服务消息; Redis注册中心,服务提供者将服务信息写入Redis,服务消费者从Redis获取服务消息; Simple注册中心,注册中心本身就是一个普通的Dubbo服务,可以减少第三方依赖,使整体通讯方式一致。 实践Dubbo代码目前托管在GitHub上 ,将代码clone至本地目录dubbo下,其中dubbo-demo模块用于示例演示,包含了3个子模块: dubbo-demo-api; dubbo-demo-provider; dubbo-demo-consumer。 dubbo-demo-api说明dubbo-demo-api定义示例服务接口DemoService:1234567package com.alibaba.dubbo.demo;public interface DemoService { String sayHello(String name);} dubbo-demo-provider说明dubbo-demo-provider是示例服务提供者,其中,DemoServiceImpl类实现DemoService接口:12345678910111213141516package com.alibaba.dubbo.demo.provider;import java.text.SimpleDateFormat;import java.util.Date;import com.alibaba.dubbo.demo.DemoService;import com.alibaba.dubbo.rpc.RpcContext;public class DemoServiceImpl implements DemoService { public String sayHello(String name) { System.out.println("[" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "] Hello " + name + ", request from consumer: " + RpcContext.getContext().getRemoteAddress()); return "Hello " + name + ", response form provider: " + RpcContext.getContext().getLocalAddress(); }} 在Spring上下文中配置DemoServiceImpl实例,并将该服务通过Dubbo暴露出来:123456789101112<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://code.alibabatech.com/schema/dubbo" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd"> <bean id="demoService" class="com.alibaba.dubbo.demo.provider.DemoServiceImpl" /> <dubbo:service interface="com.alibaba.dubbo.demo.DemoService" ref="demoService" /></beans> dubbo-demo-consumer说明dubbo-demo-consumer是示例服务消费者,其中,DemoAction类的start方法每隔2秒调用一次DemoService的sayHello方法:12345678910111213141516171819202122232425262728package com.alibaba.dubbo.demo.consumer;import java.text.SimpleDateFormat;import java.util.Date;import com.alibaba.dubbo.demo.DemoService;public class DemoAction { private DemoService demoService; public void setDemoService(DemoService demoService) { this.demoService = demoService; } public void start() throws Exception { for (int i = 0; i < Integer.MAX_VALUE; i ++) { try { String hello = demoService.sayHello("world" + i); System.out.println("[" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "] " + hello); } catch (Exception e) { e.printStackTrace(); } Thread.sleep(2000); } }} 在Spring上下文中配置DemoAction实例,引用Dubbo暴露出来的接口:1234567891011<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://code.alibabatech.com/schema/dubbo" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd"> <dubbo:reference id="demoService" interface="com.alibaba.dubbo.demo.DemoService" /></beans> 12345678910111213<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://code.alibabatech.com/schema/dubbo" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd"> <bean class="com.alibaba.dubbo.demo.consumer.DemoAction" init-method="start"> <property name="demoService" ref="demoService" /> </bean></beans> 启动服务提供者和服务消费者修改dubbo-demo-provider和dubbo-demo-consumer中的src/main/assembly/conf/dubbo.properties,使用Zookeeper注册中心(需要先安装、启动Zookeeper):1dubbo.registry.address=zookeeper://xxx.xxx.xxx.xxx:2181 修改后,在dubbo-demo下执行Maven构建: mvn clean install -Dmaven.test.skip 构建成功后,启动服务提供者: cd dubbo-demo-provider/targettar -zxvf dubbo-demo-provider-2.5.4-SNAPSHOT-assembly.tar.gzcd dubbo-demo-provider-2.5.4-SNAPSHOT/bin./start.sh 启动服务消费者: cd dubbo-demo-consumer/targettar -zxvf dubbo-demo-consumer-2.5.4-SNAPSHOT-assembly.tar.gzcd dubbo-demo-consumer-2.5.4-SNAPSHOT/bin./start.sh 启动后,可以查看Zookeeper目录,如图所示:其中: 服务提供者向/dubbo/com.alibaba.dubbo.demo.DemoService/providers目录下写入自己的URL地址。 服务消费者订阅/dubbo/com.alibaba.dubbo.demo.DemoService/providers目录下的提供者URL地址,并向/dubbo/com.alibaba.dubbo.demo.DemoService/consumers目录下写入自己的URL地址。 查看服务消费者日志文件logs/stdout.log:服务消费者每隔2秒输出一次,远程服务调用正常。 启动监控中心dubbo下的dubbo-simple模块包含dubbo-monitor-simple子模块,修改dubbo-monitor-simple中的src/main/assembly/conf/dubbo.properties,使用Zookeeper注册中心:1dubbo.registry.address=zookeeper://xxx.xxx.xxx.xxx:2181 修改后,在dubbo-monitor-simple下执行Maven构建: mvn clean install -Dmaven.test.skip 构建成功后,启动监控中心: cd targettar -zxvf dubbo-monitor-simple-2.5.4-SNAPSHOT-assembly.tar.gzcd dubbo-monitor-simple-2.5.4-SNAPSHOT/bin./start.sh 启动后,浏览器下打开,如图所示:从中,可以查看到已有的应用及其依赖关系,已有的服务及其提供者、消费者等信息。]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>Dubbo</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Docker简介与实践]]></title>
<url>%2F2016%2F02%2F05%2Fdocker-e7-ae-80-e4-bb-8b-e4-b8-8e-e5-ae-9e-e8-b7-b5%2F</url>
<content type="text"><![CDATA[介绍Docker是一个开源的应用容器引擎。Docker可以将应用程序及其依赖打包成镜像(Image),并在同一个服务器中启动多个容器(Container)共享操作系统内核,分别独立运行镜像(Image)。 安装在OS X中安装Docker(OS X版本需要满足10.8及以上)。从“https://www.docker.com/products/docker-toolbox” 下载DockerToolbox-1.10.0.pkg并运行安装:安装步骤中的配置均采用默认选项,安装成功后,应用程序会新增以下两项,分别是Docker命令行工具和GUI工具: 运行镜像运行“Docker Quickstart Terminal”打开命令行窗口。第一次使用时,Docker会首先执行一些初始化操作。在命令行中,可以执行Docker命令,例如: docker run hello-world 该命令会执行以下操作: 在本地查找有无镜像“hello-world”; 若本地没有镜像“hello-world”,则从Docker Hub下载镜像; 启动容器加载镜像“hello-world”并运行。 以下是命令执行截图:Docker还提供了一个示例镜像“whalesay”,该镜像基于Ubuntu,打包了应用程序“cowsay”,执行以下Docker命令,启动容器加载镜像“whalesay”并运行程序“cowsay”,“boo”是程序传入参数: docker run docker/whalesay cowsay boo 命令执行后,会输出动物图像以及程序传入参数: 创建镜像基于已有镜像,可以创建新的镜像,将应用程序及其依赖打包进新的镜像中。编写Dockerfile文件如下所示:123FROM docker/whalesay:latestRUN apt-get -y update && apt-get install -y fortunesCMD /usr/games/fortune -a | cowsay 其中: “FROM”语句说明基于镜像“whalesay”的最新版本创建新的镜像; “RUN”语句说明在新镜像中安装应用程序”fortunes”; “CMD”语句说明在启动容器加载镜像后执行“/usr/games/fortune -a | cowsay”,即通过程序“fortune”生成句子并通过管道传给程序“cowsay”展示。 在Dockerfile文件所在目录下执行以下Docker命令,根据Dockerfile文件创建新镜像“docker-whale”: docker build -t docker-whale . 命令执行后,开始新镜像的创建过程,会依次输出各步信息:创建镜像成功后,执行以下Docker命令,可以查看本地所有镜像: docker images 其中已新增镜像“docker-whale”:运行镜像“docker-whale”: docker run docker-whale 推送镜像镜像创建成功后,可以将镜像推送到Docker Hub。首先在Docker Hub创建账号并新建仓库“docker-whale”。执行以下Docker命令,将docker-whale重命名为“namespace/image”的格式,其中“namespace”是Docker Hub中的账号: docker tag 8f35c689968c magicwt/docker-whale:lastes 执行以下Docker命令,登录Docker Hub: docker login –username=magicwt --email=xxx@xxx.com 执行以下Docker命令,推送镜像: docker push magicwt/docker-whale 访问“https://hub.docker.com/r/magicwt/docker-whale/” ,Docker Hub上已有push记录:]]></content>
<categories>
<category>服务器</category>
</categories>
<tags>
<tag>Docker</tag>
</tags>
</entry>
<entry>
<title><![CDATA[基于ZooKeeper的服务注册和发现实践]]></title>
<url>%2F2016%2F01%2F24%2Fe5-9f-ba-e4-ba-8ezookeeper-e7-9a-84-e6-9c-8d-e5-8a-a1-e6-b3-a8-e5-86-8c-e5-92-8c-e5-8f-91-e7-8e-b0-e5-ae-9e-e8-b7-b5%2F</url>
<content type="text"><![CDATA[整体架构在分布式架构中,各模块采用多节点部署,对外提供服务,节点数量和服务地址不断会有变化,因此,对于服务调用者,需要动态获取服务地址。ZooKeeper 提供分布式协调服务,服务提供者可以将服务注册至ZooKeeper规定路径中,服务调用者再从ZooKeeper规定路径中动态发现服务,如图所示: 服务注册Curator 进一步封装了ZooKeeper API,提供了许多高级功能,包括服务注册和发现。ZooKeeper使用版本3.4.6,Curator使用版本2.9.1,在pom.xml中增加Curator依赖,如下所示:12345678910<dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>3.2.5.RELEASE</version></dependency><dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-x-discovery</artifactId> <version>2.9.1</version></dependency> 使用Curator进行服务注册,代码如下所示:12345678910111213141516171819202122232425/* 服务注册 start */// 创建CuratorFramework实例,封装与ZooKeeper的操作。CuratorFramework curatorFramework = CuratorFrameworkFactory.newClient("xxx.xxx.xxx.xxx:2181", new RetryNTimes(4, 2000));curatorFramework.start();// 创建ServiceInstanceBuilder实例,指定服务名称和地址,// 由ServiceInstanceBuilder实例构建ServiceInstance实例,ServiceInstance实例即服务。ServiceInstanceBuilder serviceInstanceBuilder = ServiceInstance.builder();serviceInstanceBuilder.uriSpec(new UriSpec("{scheme}://{address}:{port}"));serviceInstanceBuilder.address("xxx.xxx.xxx.xxx");serviceInstanceBuilder.port(8080);serviceInstanceBuilder.name("service1");ServiceInstance serviceInstance = serviceInstanceBuilder.build();// 创建serviceDiscoveryBuilder实例,指定服务和服务在Zookeeper中所在路径,// 由ServiceDiscoveryBuilder实例构建ServiceDiscovery实例,由ServiceDiscovery实例注册服务。ServiceDiscoveryBuilder serviceDiscoveryBuilder = ServiceDiscoveryBuilder.builder(Void.class);serviceDiscoveryBuilder.basePath("test");serviceDiscoveryBuilder.client(curatorFramework);ServiceDiscovery serviceDiscovery = serviceDiscoveryBuilder.build();serviceDiscovery.start();//注册服务serviceDiscovery.registerService(serviceInstance);/* 服务注册 end */// 等待30秒,这时在ZooKeeper中可以查看到注册的服务Thread.sleep(30000);// 30秒后,进程关闭,ZooKeeper中注册的服务因是临时节点而被删除,说明服务已不可用 启动进程,在ZooKeeper中可以查看到注册的服务,在服务“service1”所在路径“/test/service1”下有一个临时节点“ffd66440-e0b0-41a5-b93a-c558c527b3bc”,表示刚启动、注册的服务,如图所示:30秒后进程关闭,在ZooKeeper中可以查看到,注册的服务因是临时节点而被删除,说明服务已不可用,如图所示: 服务发现调用ServiceDiscovery实例的queryForInstances方法可以发现服务,代码如下所示:12345678910111213141516171819/* 服务发现 start */// 创建CuratorFramework实例,封装与ZooKeeper的操作。CuratorFramework curatorFramework = CuratorFrameworkFactory.newClient("xxx.xxx.xxx.xxx:2181", new RetryNTimes(4, 2000));curatorFramework.start();// 创建serviceDiscoveryBuilder实例,指定服务在Zookeeper中所在路径,// 由ServiceDiscoveryBuilder实例构建ServiceDiscovery实例,由ServiceDiscovery实例发现服务。ServiceDiscoveryBuilder serviceDiscoveryBuilder = ServiceDiscoveryBuilder.builder(Void.class);serviceDiscoveryBuilder.basePath("test");serviceDiscoveryBuilder.client(curatorFramework);ServiceDiscovery serviceDiscovery = serviceDiscoveryBuilder.build();serviceDiscovery.start();Collection<ServiceInstance> collection = serviceDiscovery.queryForInstances("service1");if (collection != null && collection.size() > 0) { System.out.println("found " + collection.size() + " service1 services"); for (ServiceInstance serviceInstance : collection) { System.out.println("id: " + serviceInstance.getId()); }}/* 服务发现 end */ 启动进程,发现服务,如图所示:]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>ZooKeeper</tag>
<tag>Curator</tag>
</tags>
</entry>
<entry>
<title><![CDATA[一种基于Nginx+Hessian的高可用分布式架构]]></title>
<url>%2F2016%2F01%2F24%2Fe4-b8-80-e7-a7-8d-e5-9f-ba-e4-ba-8enginxhessian-e7-9a-84-e9-ab-98-e5-8f-af-e7-94-a8-e5-88-86-e5-b8-83-e5-bc-8f-e6-9e-b6-e6-9e-84%2F</url>
<content type="text"><![CDATA[应用场景需要为不同角色的用户分别提供管理系统用于管理资源。 整体架构整体架构如图所示:整体架构由下(数据存储)至上(页面展示): 数据存储于MySQL,并缓存至Redis。 service模块采用多节点部署,封装了数据读写逻辑以及通用业务逻辑,通过Spring MVC+Hessian将通用业务逻辑以接口形式提供给其他模块调用。 service模块通过Nginx反向代理,通过配置Nginx,可以灵活控制service模块节点数量进行水平扩展,以适应请求数量的变化,并且,service模块某个节点故障后,Nginx可以将请求转发到其他节点,从而实现service模块的高可用。 Nginx也采用多节点部署,使用Keepalived实现高可用,对外以虚IP作为地址提供HTTP请求服务,通过Keepalived管理虚IP,初始时将虚IP分配至主Nginx所在服务器,并监控Nginx进程,当进程失败时,将虚IP漂移至从Nginx所在服务器,保证服务可用性。 为不用角色的用户分别提供管理系统,各管理系统各自作为独立的web模块部署,各web模块也采用多节点部署,接受用户请求并调用service模块提供的接口处理请求。 各web模块也通过Nginx反向代理,从而实现web模块的高可用。由于web模块需要权限验证,管理session,因此web模块的反向代理采用“ip_hash”方式,同一用户的访问请求都将转发到同一个节点上。 Nginx配置通过配置location和upstream,将HTTP请求根据URL转发至各个模块,例如,将URL以“/remoting/”开头的HTTP请求转发至service模块:123456789101112upstream service-server { server IP1:port1; server IP2:port2;}server { listen 80; server_name localhost; location ^~/remoting/ { proxy_pass http://service-server; } ...} Keepalived配置在主、从Nginx所在服务器部署Keepalived,主Nginx所在服务器的Keepalived配置如下所示:12345678910111213141516171819202122232425262728global_defs { router_id web_nginx}vrrp_script chk_nginx { script "/usr/local/keepalived/scripts/nginx_check.sh" interval 2 weight -2}vrrp_instance VI_1 { state BACKUP interface eth0 virtual_router_id 179 priority 101 advert_int 1 nopreempt authentication { auth_type PASS auth_pass 1111 } virtual_ipaddress { 虚IP } track_script { chk_nginx }} 其中,priority为101,从Nginx所在服务器的Keepalived配置类似,priority为100,Keepalived每隔2秒执行脚本nginx_check.sh,监控Nginx进程,若进程失败,则减少priority,从而将虚IP漂移至从Nginx所在服务器,nginx_check.sh脚本如下所示:1234567#!/bin/shA=`ps -C nginx --no-header | wc -l`if [ $A -eq 0 ]then exit 1fiexit 0 Hessian配置Hessian 是一个二进制web服务协议,能够提供轻量级的web服务,Spring对Hessian作了进一步的封装,基于Spring MVC+Hessian实现服务接口提供和调用。 提供服务接口service模块的web.xml作如下配置:1234567891011121314151617181920212223242526272829<!-- 上下文配置文件 --><context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:/spring/context*.xml</param-value></context-param><!-- 加载上下文环境 --><listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class></listener><!-- 配置Spring MVC servlet --><servlet> <servlet-name>Remoting</servlet-name> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> <init-param> <!-- 该servlet的spring上下文采用WebApplicationContext,即listener加载的上下文 --> <param-name>contextAttribute</param-name> <param-value>org.springframework.web.context.WebApplicationContext.ROOT</param-value> </init-param> <load-on-startup>1</load-on-startup></servlet><servlet-mapping> <servlet-name>Remoting</servlet-name> <url-pattern>/remoting/*</url-pattern></servlet-mapping> 其中,先设置listener加载Spring的上下文环境,再配置Spring MVC的servlet,该servlet处理“/remoting/”开头的HTTP请求。在service模块的Spring配置文件中配置服务实例以及web服务接口,如下所示:12345678<bean id="machineService" class="xxx.xxx.xxx.service.impl.MachineServiceImpl" /><bean name="/machineService" class="org.springframework.remoting.caucho.HessianServiceExporter"> <property name="service" ref="machineService" /> <property name="serviceInterface"> <value>xxx.xxx.xxx.service.MachineService</value> </property></bean> 部署service模块,通过“/remoting/machineService”可以调用该web服务。 调用服务接口在web模块的Spring配置文件中配置web服务接口的调用,如下所示:12345<bean id="machineService" class="org.springframework.remoting.caucho.HessianProxyFactoryBean"> <property name="serviceUrl" value="http://xxx.xxx.xxx.xxx/remoting/machineService" /> <property name="serviceInterface" value="xxx.xxx.xxx.service.MachineService" /> <property name="chunkedPost" value="false"/></bean>]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>Hessian</tag>
<tag>Keepalived</tag>
<tag>Nginx</tag>
</tags>
</entry>
<entry>
<title><![CDATA[工作总结]]></title>
<url>%2F2016%2F01%2F10%2Fe5-b7-a5-e4-bd-9c-e6-80-bb-e7-bb-93%2F</url>
<content type="text"><![CDATA[从14年5月至今,转眼一年多过去了,希望通过这篇文章对这段时间的主要工作做个简单的梳理、总结。 主要工作搜狐公众平台(mp.sohu.com)是在搜狐门户改革背景下全新打造的分类内容的入驻、发布和分发全平台,通过搜狐网、手机搜狐网和搜狐新闻客户端三端来推广媒体和自媒体优质内容。搜狐公众平台主要流程如下图所示: 在整个流程中,我主要负责文章发文后的推荐工作,围绕文章推荐和流量分发,我主要完成了下述工作: 统计,对用户、文章流量进行离线、实时统计; 优选,对文章进行筛选、排序,选取优质内容自动更新至三端入口; 本地,对文章按地域进行归并,自动更新至客户端本地流; 搜索,对文章构建索引,提供按关键词进行文章搜索; 相关,计算文章之间的相关度,为每一篇文章提供相关文章列表; 个性化,计算文章和读者之间的相关度,通过“猜你喜欢”为读者提供个性化推荐文章列表。 技术架构整体技术架构如下图所示: 接口接入,使用Nginx作反向代理,并基于OpenResty在Nginx中通过Lua读取数据实现高可用接口; App,使用Java和Python作应用开发,Java应用基于Spring实现上下文管理、MVC和数据库读写等; Web容器,使用Resin; 任务调度,使用Cronhub进行集中式管理; 日志收集,使用Flume读取Nginx日志并写入Kafka; 消息队列,使用Kafka; 数据计算,使用MapReduce作离线计算,使用Storm作实时计算; 数据存储,使用MySQL作关系存储,使用Redis/SSDB作Key-Value存储,使用HDFS/Hive存储海量日志数据; 搜索引擎,使用ElasticSearch搭建分布式高可用搜索集群。 具体实现统计:使用Nginx集群收集访问日志,使用HDFS存储访问数据,使用MapReduce离线处理访问数据,使用Flume+Kafka+Storm实时处理访问数据。通过离线和实时处理,统计平台流量数据。基于AngularJS+RESTful API实现内部数据运营系统。基于OpenResty实现高可用、高吞吐量的流量数据访问接口供前端页面调用。 优选:基于流量反馈、作者平台表现、文章格式与时效选取优质文章作为热点推荐。 本地:对平台海量文章基于地名关键词识别本地文章,并基于SimHash算法对文章按内容滤重。开发本地文章运营系统,对识别、滤重后的本地文章进一步人工筛选,提供本地文章接口供客户端调用展示。 搜索:搭建Elasticsearch集群,对文章按照标题和内容构建索引,并对外提供文章搜索接口。 相关:使用机器学习库Gensim库基于TF-IDF模型计算文章标题之间的相似度,选取相似度最高的若干篇文章作为相关文章,并对外提供相关文章接口。 个性化:通过文章分类、关键词构建文章模型,使用Storm实时分析用户访问行为,通过其所访问文章的分类、关键词构建用户模型,计算用户模型与文章模型的余弦相似度,选取相似度最高的若干篇文章作为用户个性化推荐。 未来工作未来计划深入算法方面的工作,搭建完整的算法开发框架,包括特征提取、离线训练、离线评测、模型上线、用户分桶、线上评测,目标是通过这套框架,使得算法开发流程规范化、流程化。]]></content>
<categories>
<category>生活点滴</category>
</categories>
</entry>
<entry>
<title><![CDATA[基于ActiveMQ的消息推送实现]]></title>
<url>%2F2016%2F01%2F04%2Fe5-9f-ba-e4-ba-8eactivemq-e7-9a-84-e6-b6-88-e6-81-af-e6-8e-a8-e9-80-81-e5-ae-9e-e7-8e-b0%2F</url>
<content type="text"><![CDATA[应用场景应用场景是当用户在餐饮、娱乐场所消费时,可以用手机关注商户微信公众号,并从公众号中访问商户评论页面,发表评论,评论会实时推送至现场的各个显示屏终端上,因此就需要开发一个评论消息推送系统,该系统具体需要满足: 能够向所有显示屏终端发送评论消息; 能够指定向某台显示屏终端发送评论消息。 实现方案基于ActiveMQ实现评论消息推送系统,Producer使用OpenWire协议将评论消息发送至Topic,Consumer通过订阅Topic,使用MQTT协议从Topic上取到评论消息。Topic是广播模式,同一个评论消息可以被所有订阅Topic的Consumer接收,这样就可以实现向所有显示屏终端发送评论消息。在conf/activemq.xml中配置ActiveMQ支持使用OpenWire协议和MQTT协议连接,如下所示:12345<transportConnectors> <!-- DOS protection, limit concurrent connections to 1000 and frame size to 100MB --> <transportConnector name="openwire" uri="tcp://0.0.0.0:61616?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/> <transportConnector name="mqtt" uri="mqtt://0.0.0.0:1883?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/></transportConnectors> 如何向某台显示屏终端发送消息呢?这里参考了博客《使用ActiveMQ+MQTT实现Android点对点消息通知》 :通过在评论消息中设置一个属性记录这个消息需要发往Consumer的id,然后当消息在ActiveMQ中被分发至Consumer时,采用自定义的分发策略,该策略取出当前所有连接的Consumer,判断Consumer的id是否与消息中记录的值相等,若相等,则将消息发往这个Consumer。自定义分发策略代码如下所示:123456789101112131415161718192021222324252627282930313233343536373839404142public class ClientIdFilterDispatchPolicy extends SimpleDispatchPolicy { private String ptpClientId = "PTP_CLIENTID"; private String ptpSuffix = ".PTP"; public boolean dispatch(MessageReference node, MessageEvaluationContext msgContext, List<Subscription> consumers) throws Exception { Object _clientId = node.getMessage().getProperty(this.ptpClientId); if (_clientId == null) super.dispatch(node, msgContext, consumers); ActiveMQDestination _destination = node.getMessage().getDestination(); int count = 0; for (Subscription sub : consumers) { if (!sub.getConsumerInfo().isBrowser()) { if (!sub.matches(node, msgContext)) { sub.unmatched(node); } else if ((_clientId != null) && (_destination.isTopic()) && (_clientId.equals(sub.getContext().getClientId())) && (_destination.getQualifiedName().endsWith(this.ptpSuffix))) { sub.add(node); count++; } else { sub.unmatched(node); } } } return count > 0; } public String getPtpClientId() { return this.ptpClientId; } public void setPtpClientId(String ptpClientId) { this.ptpClientId = ptpClientId; } public String getPtpSuffix() { return this.ptpSuffix; } public void setPtpSuffix(String ptpSuffix) { this.ptpSuffix = ptpSuffix; }} 从代码中可以看出,若消息中包含属性ptpClientId(默认属性名为“PTP_CLIENTID”)且Topic的后缀为ptpSuffix(默认后缀为“.PTP”),则判断是否有Consumer,其id与ptpClientId属性值相等,若有,则将该消息分发给该Consumer。在conf/activemq.xml中配置分发策略,如下所示:1234567891011121314<destinationPolicy> <policyMap> <policyEntries> <policyEntry topic=">" > <dispatchPolicy> <clientIdFilterDispatchPolicy ptpSuffix=".push" ptpClientId="machineId" /> </dispatchPolicy> <pendingMessageLimitStrategy> <constantPendingMessageLimitStrategy limit="1000"/> </pendingMessageLimitStrategy> </policyEntry> </policyEntries> </policyMap></destinationPolicy> 从中可以看出,ptpClientId取值为“machineId”,ptpSuffix取值为“.push”。 消息发送评论消息发送端代码如下所示:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990public class PushProducer { private static Logger logger = LoggerFactory.getLogger(PushProducer.class); private String brokerUrl; private Connection connection; private Session session; private MessageProducer messageProducer; public void init() { try { ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(brokerUrl); connection = connectionFactory.createConnection(); connection.start(); session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); Destination destination = session.createTopic(Constants.PUSH_TOPIC_NAME); messageProducer = session.createProducer(destination); } catch (Exception e) { logger.error("init producer error", e); } } public void destroy() { try { if (session != null) { session.close(); } if (connection != null) { connection.close(); } } catch (Exception e) { logger.error("destroy producer error", e); } } public boolean pushToOne(String text, int machineId) { try { Message message = session.createTextMessage(text); message.setStringProperty(Constants.PUSH_CLIENT_ID_KEY, String.valueOf(machineId)); messageProducer.send(message); return true; } catch (Exception e) { logger.error("push message error", e); } return false; } public boolean pushToAll(String text) { try { Message message = session.createTextMessage(text); messageProducer.send(message); return true; } catch (Exception e) { logger.error("push message error", e); } return false; } public String getBrokerUrl() { return brokerUrl; } public void setBrokerUrl(String brokerUrl) { this.brokerUrl = brokerUrl; } public Connection getConnection() { return connection; } public void setConnection(Connection connection) { this.connection = connection; } public Session getSession() { return session; } public void setSession(Session session) { this.session = session; } public MessageProducer getMessageProducer() { return messageProducer; } public void setMessageProducer(MessageProducer messageProducer) { this.messageProducer = messageProducer; }} 其中: init方法使用JMS API建立与ActiveMQ的连接与会话,并创建MessageProducer实例。 destroy方法关闭与ActiveMQ的连接与会话。 pushToOne方法创建文本消息包含评论,并设置属性“machineId”,使用MessageProducer实例发送消息。 pushToAll方法创建文本消息包含评论,使用MessageProducer实例发送消息。 消息接收评论消息接收端代码如下所示:12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788public abstract class AuthPushConsumer implements MqttCallback { private static Logger logger = LoggerFactory.getLogger(AuthPushConsumer.class); private String brokerUrl; private MqttClient mqttClient; private int machineId; private String password; public AuthPushConsumer() {} public AuthPushConsumer(String brokerUrl, int machineId, String password) { this.brokerUrl = brokerUrl; this.machineId = machineId; this.password = password; } public void start() throws Exception { mqttClient = new MqttClient(brokerUrl, String.valueOf(machineId), new MemoryPersistence()); MqttConnectOptions options= new MqttConnectOptions(); options.setCleanSession(true); options.setKeepAliveInterval(30); options.setUserName(String.valueOf(machineId)); options.setPassword(password.toCharArray()); mqttClient.setCallback(this); mqttClient.connect(options); mqttClient.subscribe(new String[]{Constants.PUSH_TOPIC_NAME, String.valueOf(machineId)}); logger.info("start mqtt client success"); } @Override public void connectionLost(Throwable throwable) { logger.error("lost connection"); if (mqttClient != null) { try { mqttClient.close(); } catch (Exception e) { logger.error("close error", e); } } while (true) { try { start(); break; } catch (Exception e) { logger.error("exception", e); if (e.getCause() instanceof ConnectException || "代理程序不可用".equals(e.getMessage())) { try { Thread.sleep(2000); } catch (Exception e1) { } continue; } else { break; } } } } @Override public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { logger.info("delivery complete"); } @Override public void messageArrived(String s, MqttMessage mqttMessage) throws Exception { System.out.println(s); processMessage(new String(mqttMessage.getPayload())); } public abstract void processMessage(String message); public int getMachineId() { return machineId; } public void setMachineId(int machineId) { this.machineId = machineId; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; }} 其中: start方法创建MqttClient实例,建立与ActiveMQ的连接,创建时指定id。 connectionLost方法在连接丢失时被调用,循环调用start方法直至连接恢复。 messageArrived方法在消息接收时被调用,调用processMessage方法执行具体的业务逻辑,例如在屏幕上显示评论。 权限验证为了验证尝试连接的Consumer是否具有权限,开发了权限验证插件,该插件调用远程接口进行权限验证。插件配置如下所示:1234567891011<plugins> <bean xmlns="http://www.springframework.org/schema/beans" id="AuthPlugin" class="com.vshangping.server.activemq.auth.plugin.AuthPlugin"> <property name="machineService"> <bean xmlns="http://www.springframework.org/schema/beans" id="machineService" name="machineService" class="org.springframework.remoting.caucho.HessianProxyFactoryBean"> <property name="serviceUrl" value="http://xxx.xxx.xxx.xxx/remoting/machineService" /> <property name="serviceInterface" value="xxx.xxx.server.service.MachineService" /> <property name="chunkedPost" value="false"/> </bean> </property> </bean></plugins> 插件AuthPlugin代码如下所示:12345678910111213141516171819public class AuthPlugin implements BrokerPlugin { private static Logger logger = LoggerFactory.getLogger(AuthPlugin.class); private MachineService machineService; public Broker installPlugin(Broker broker) throws Exception { logger.info("install auth plugin"); return new AuthBroker(broker, machineService); } public MachineService getMachineService() { return machineService; } public void setMachineService(MachineService machineService) { this.machineService = machineService; }} 从代码中可以看出,该插件主要功能是新建并返回AuthBroker实例,AuthBroker代码如下所示:1234567891011121314151617181920212223242526272829303132333435public class AuthBroker extends BrokerFilter { private static Logger logger = LoggerFactory.getLogger(AuthBroker.class); private MachineService machineService; public AuthBroker(Broker next) { super(next); } public AuthBroker(Broker next, MachineService machineService) { super(next); this.machineService = machineService; } @Override public void addConnection(ConnectionContext context, ConnectionInfo info) throws Exception { if (context.getConnector().toString().equals("mqtt")) { if (!machineService.checkPassword(Integer.parseInt(info.getUserName()), info.getPassword())) { throw new SecurityException("invalid machine " + info.getUserName()); } } logger.info("connect machine " + info.getUserName()); super.addConnection(context, info); } public MachineService getMachineService() { return machineService; } public void setMachineService(MachineService machineService) { this.machineService = machineService; }} 从代码中可以看出,AuthBroker继承自BrokerFilter,重写了addConnection方法,在创建连接时,对于使用MQTT协议的连接,调用远程接口的checkPassword方法,判断账号和密码是否正确,若正确则允许连接。]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>ActiveMQ</tag>
<tag>MQTT</tag>
</tags>
</entry>
<entry>
<title><![CDATA[使用Hive存储数据实践]]></title>
<url>%2F2016%2F01%2F03%2Fe4-bd-bf-e7-94-a8hive-e5-ad-98-e5-82-a8-e6-95-b0-e6-8d-ae-e5-ae-9e-e8-b7-b5%2F</url>
<content type="text"><![CDATA[数据存储需求是:每天会生成大量文章数据,每条文章数据包含标题、内容、URL、发表时间等多个字段,数据后续不会更新,因此考虑使用Hive作为数据仓库存储这些数据。以下介绍使用Hive存储数据的实践步骤以及注意事项。 创建表创建外部表toutiao_category,建表语句如下所示,使用外部表是为了考虑数据存储的灵活性,对于外部表,若后续删除表,仅删除表元数据,不会删除表数据,可以继续读取数据进行分析。123456789101112131415161718192021222324252627282930313233343536373839CREATE EXTERNAL TABLE `toutiao_category`( `toutiao_has_mp4_video` int, `toutiao_repin_count` int, `abstract` string, `article_ptime` int, `toutiao_recommend` int, `toutiao_article_type` int, `category` string, `docid` bigint, `bury_count` int, `title` string, `content` string, `source` string, `comment_count` int, `article_url` string, `toutiao_middle_mode` string, `toutiao_datetime` string, `toutiao_aggr_type` int, `toutiao_article_sub_type` int, `toutiao_external_visit_count` int, `ctime` int, `toutiao_favorite_count` int, `toutiao_impression_count` int, `toutiao_keywords` string, `digg_count` int, `toutiao_more_mode` string, `toutiao_go_detail_count` int, `origin_url` string) PARTITIONED BY ( `date` string) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t' LINES TERMINATED BY '\n' STORED AS INPUTFORMAT 'com.hadoop.mapred.DeprecatedLzoTextInputFormat' OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat' LOCATION 'hdfs://heracles/user/mediadata/hive/warehouse/news/toutiao_category'; 建表语句中: “PARTITIONED BY (`date` string)”是按照天进行分区,类似于关系数据库中的分表操作,这样在实际存储表数据时,某一天的数据会单独存储于某个目录下,查询条件包含天时,就会只读取满足条件的天所对应目录下的数据,这样可以加快查询速度; “ROW FORMAT DELIMITED FIELDS TERMINATED BY ‘\t’ LINES TERMINATED BY ‘\n’”表示对于存储的数据,按照“\n”划分成行,并对于行,按照“\t”划分出字段,划分出的字段的个数、顺序、属性应与建表语句中的字段说明一致; “STORED AS INPUTFORMAT ‘com.hadoop.mapred.DeprecatedLzoTextInputFormat’”,因为数据采用LZO算法压缩,所以存储格式指定为“com.hadoop.mapred.DeprecatedLzoTextInputFormat”; “LOCATION ‘hdfs://heracles/user/mediadata/hive/warehouse/crawl_news/toutiao_category’”表示数据在HDFS中存储的根目录。 压缩数据按行并且每行按照“\t”分割字段导出某一天的文章数据,并使用lzop命令压缩数据: lzop toutiao_category_20160101.txt -odata.lzo 压缩后的数据文件和原有文件相比,大小可以减小50%左右,节约了存储空间。 导入数据公司集群中的Hive采用BeeLine连接Hive Server,使用BeeLine向toutiao_category表导入数据: /usr/lib/hive/bin/beeline -u “jdbc:hive2://xxx.xxx.xxx.xxx:xxx/mediadata_news;principal=xxx” -e “load data local inpath ‘/data/rsync_dir/news/data.lzo’ overwrite into table toutiao_category partition (date=’20160101’);” 执行后,本地文件“/data/rsync_dir/news/data.lzo”被导入到toutiao_category表的“20160101”分区中: /user/mediadata/hive/warehouse/crawl_news/toutiao_category/date=20160101/data.lzo 为压缩数据创建索引对Hive表进行查询时,Hive实际上是将查询SQL转化为MapReduce Job,而对于导入到toutiao_category表的data.lzo文件,由于其是lzo格式,因此MapReduce Job在读取、分析该文件时,只会分配一个Mapper任务,因此为了提高查询速度,对lzo文件创建索引,这样MapReduce Job会对一个lzo文件分配多个Mapper任务: hadoop jar /usr/lib/hadoop/lib/hadoop-lzo-0.6.0.jar com.hadoop.compression.lzo.LzoIndexer /user/mediadata/hive/warehouse/news/toutiao_category/date=20160101/data.lzo 执行后,会增加index文件: /user/mediadata/hive/warehouse/news/toutiao_category/date=20160101/data.lzo.index 查询数据使用BeeLine执行SQL查询: 0: jdbc:hive2://xxx.xxx.xxx.xxx:xxx/mediadata_n> select ctime from toutiao_category where date=20160101 limit 10;+————-+| ctime |+————-+| 1451663999 || 1451663998 || 1451663980 || 1451663967 || 1451663956 || 1451663956 || 1451663955 || 1451663945 || 1451663933 || 1451663933 |+————-+10 rows selected (19.087 seconds)]]></content>
<categories>
<category>大数据</category>
</categories>
<tags>
<tag>Hive</tag>
</tags>
</entry>
<entry>
<title><![CDATA[基于Jenkins+Maven+Git实现自动化构建和部署]]></title>
<url>%2F2015%2F12%2F06%2Fe5-9f-ba-e4-ba-8ejenkinsmavengit-e5-ae-9e-e7-8e-b0-e8-87-aa-e5-8a-a8-e5-8c-96-e6-9e-84-e5-bb-ba-e5-92-8c-e9-83-a8-e7-bd-b2%2F</url>
<content type="text"><![CDATA[安装jdk1.7 下载jdk-7u79-linux-x64.tar.gz至/usr/local目录,解压并创建软连接: tar -zxvf jdk-7u79-linux-x64.tar.gzln -s jdk1.7.0_79 jdk 在/etc/profile中增加环境变量: export JAVA_HOME=/usr/local/jdkexport CLASSPATH=./:$JAVA_HOME/lib:$JAVA_HOME/lib/tools.jarexport PATH=$JAVA_HOME/bin:$PATH 重新打开shell,输入“java -version”,如下图所示,说明jdk1.7安装成功。 安装Maven 下载apache-maven-3.3.9-bin.tar.gz至/usr/local目录,解压并创建软连接: tar -zxvf apache-maven-3.3.9-bin.tar.gzln -s apache-maven-3.3.9 maven 在/etc/profile中增加环境变量: export M2_HOME=/usr/local/mavenexport M2=$M2_HOME/binexport PATH=$M2:$PATH Maven默认本地库地址是用户目录下的.m2/repository,可以通过修改用户目录下的.m2/settings.xml或Maven安装目录下的conf/settings.xml来修改本地库地址: \<localRepository>/usr/local/maven/repo\</localRepository> .m2/settings.xml优先级高于conf/settings.xml,.m2/settings.xml适用于某个用户,conf/settings.xml适用于所有用户。 重新打开shell,输入“mvn -version”,如下图所示,说明Maven安装成功。 安装Git 直接使用yum方式安装: yum install git 安装后输入“git”,如下图所示,说明Git安装成功。 安装启动Jenkins下载Jenkins.war,可以将该war包置于Web应用容器(如Tomcat)的应用目录下,通过Tomcat启动Jenkins,也可以直接使用“java -jar”启动Jenkins,使用Jetty作为Web应用容器。启动前,需要在/etc/profile增加Jenkins的环境变量: export JENKINS_HOME=/usr/local/jenkins 使用如下命令启动Jenkins: nohup java -jar /usr/local/jenkins.war & Jenkins默认使用8080端口,在浏览器下打开如图所示: 配置Jenkins 配置用户登录在“系统管理->Configure Global Security”中设置“启用安全”,如图所示:安全域可以选择“Jenkins专有用户数据库”,并允许用户注册。保存后页面右上角会出现“注册”选项,注册用户后再进入“系统管理->Configure Global Security”,设置用户的授权策略,如图所示:授权策略可以选择“安全矩阵”,取消匿名用户的所有权限,为“test”用户赋予所有权限。 安装Git插件Jenkins默认没有安装Git插件,可以在“系统管理->管理插件”中选择Git插件进行安装,如图所示: 配置jdk和Maven在“系统管理->系统设置”中配置jdk和Maven,如图所示: 自动构建和部署项目web-test是一个Java Web项目,使用Maven构建,代码托管在GitHub上,使用Jenkins进行自动构建和部署。 新建项目选择“构建一个maven项目”,如图所示: Git配置在“源码管理”中选择“Git”,并输入项目的GitHub库地址(事先需要将Jenkins所在服务器的公钥添加到项目的“Deploy Keys”中),如图所示: Maven配置在“构建触发器->Build”中设置Maven构建目标和选项,在“构建触发器->Post Steps”中设置构建成功后执行的Shell,Shell的功能是通过scp命令将war包拷贝至部署服务器,解压并重启Web应用进程,如图所示:Shell代码如下,配置完成后,点击“保存”: 123456789101112131415161718192021222324252627282930313233#!/bin/shhudson_basedir=/usr/local/jenkins/workspace/web-test/module_name=web-testwar_name=testip=xxx.xxx.xxx.xxxresin_sh=/usr/local/resin/bin/httpd_test.shwebapp_dir=/opt/appwar_source=$hudson_basedir/target/$war_name.warwar_goal=$webapp_dir/$war_name.warwar_goal_dir=$webapp_dir/$war_nameuser=rootfunction deploy(){ echo " ---------------------------- stop resin in ${ip} ..." ssh ${user}@${ip} "$resin_sh stop"; while [ `ssh ${user}@${ip} " $resin_sh status | grep java.net.ConnectException | wc -l " ` = "0" ];do echo "waiting for resin stop " sleep 1 done; echo "stop resin ok !" ssh ${user}@${ip} "rm -rf $war_goal_dir*"; scp $war_source ${user}@${ip}:$war_goal; ssh ${user}@${ip} "unzip -oq $war_goal -d $war_goal_dir "; echo "start resin ..." ssh ${user}@${ip} "$resin_sh start"; echo " ----------------------------- start resin ok !"}function main(){ deploy}main 构建、部署项目在项目中,点击“立即构建”,在“Console Output”可以实时观察构建、部署过程,如图所示:构建、部署成功后,浏览器下访问Web应用,访问正常。]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>Maven</tag>
<tag>Git</tag>
<tag>Jenkins</tag>
</tags>
</entry>
<entry>
<title><![CDATA[使用pypiserver搭建pypi服务器]]></title>
<url>%2F2015%2F12%2F03%2Fe4-bd-bf-e7-94-a8pypiserver-e6-90-ad-e5-bb-bapypi-e6-9c-8d-e5-8a-a1-e5-99-a8%2F</url>
<content type="text"><![CDATA[使用Python开发时,经常需要用到“pip install”安装依赖,但是“pip install”需要访问外网,并且默认源pypi在国内访问并不是很稳定。为了便于在公司内网服务器上安装依赖,我们使用pypiserver搭建pypi服务器。 安装pypiserver从pypiserver下载pypiserver-1.1.7.zip,解压后进入目录,执行: python setup.py install 即可完成安装。 启动pypiserver启动命令是: /usr/bin/pypi-server -p 8082 /opt/python-packages 其中: “-p 8082”,表示使用端口8082; “/opt/python-packages”,表示依赖包的存储目录。 /opt/python-packages目录下的依赖包如图所示:在浏览器下访问如图所示: 使用pypiserver若使用搭建的pypiserver作为源安装依赖BeautifulSoup,命令如下: pip install BeautifulSoup -i http://ip:port/simple/]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>pypiserver</tag>
</tags>
</entry>
<entry>
<title><![CDATA[前端包管理工具Bower简介]]></title>
<url>%2F2015%2F11%2F28%2Fe5-89-8d-e7-ab-af-e5-8c-85-e7-ae-a1-e7-90-86-e5-b7-a5-e5-85-b7bower-e7-ae-80-e4-bb-8b%2F</url>
<content type="text"><![CDATA[类似于在Java中使用Maven管理依赖,在前端可以使用Bower管理包。类似于Maven中的pom.xml文件,Bower使用bower.json文件记录所需管理的包。 安装Bower是一个命令行工具,可以使用node的npm命令进行安装: npm install -g bower Bower依赖node、npm和git,所以需要先安装node和git。 使用安装包可以使用“bower install”命令安装所需要的包,格式如下: bower install \<package> 其中package既可以直接使用包名,也可以使用包的GitHub地址,例如JQuery的安装可以使用以下命令: # registered packagebower install jquery# GitHub shorthandbower install jquery/jquery# Git endpointbower install git://github.com/jquery/jquery.git 执行命令后,JQuery的相关文件会被下载到bower_components目录下。 保存包可以使用“bower init”命令保存所需要的包的信息,信息会被保存在bower.json文件中,bower.json示例如下:12345678910111213141516171819{ name: 'test', authors: [ 'magicwt' ], description: 'this is a tset', main: 'index.html', license: 'MIT', homepage: 'http://magicwt.com', ignore: [ '**/.*' ], dependencies: { jquery: '~2.1.4' }, keywords: [ 'bower' ]} 使用包Bower官方建议基于Bower API,结合其他前端工具(如Grunt,RequireJS,Yeoman等)来使用所管理的包,另外,也可以在页面中直接使用已安装的包,如直接引用已安装的JQuery:1<script src="bower_components/jquery/dist/jquery.min.js"></script>]]></content>
<categories>
<category>前端</category>
</categories>
<tags>
<tag>Bower</tag>
</tags>
</entry>
<entry>
<title><![CDATA[storm-kafka KafkaSpout原理分析]]></title>
<url>%2F2015%2F10%2F06%2Fstorm-kafka-kafkaspout-e5-8e-9f-e7-90-86-e5-88-86-e6-9e-90%2F</url>
<content type="text"><![CDATA[Storm Spout通过实现Storm中的ISpout接口,重写其中的nextTuple、ack和fail方法,可以实现tuple流的发送、成功确认、失败重发。ISpout接口代码如下所示。1234567891011121314151617181920212223242526272829303132public interface ISpout extends Serializable { /** * work进程初始化spout时运行该方法 */ void open(Map conf, TopologyContext context, SpoutOutputCollector collector); /** * 关闭时调用,但不保证一定调用(kill -9) */ void close(); /** * activate topology后调用 */ void activate(); /** * deactivate topology后调用 */ void deactivate(); /** * 在nextTuple中通过调用collector的emit方法发送tuple * nextTuple、ack和fail这三个方法在一个线程中被循环调用 */ void nextTuple(); /** * msgId标识的消息处理成功后调用该方法 */ void ack(Object msgId); /** * msgId标识的消息处理失败后调用该方法 * 可以进行消息的重新发送 */ void fail(Object msgId);} storm-kafkastorm-kafka模块目前集成中在storm中,为storm提供读取kafka消息队列的支持,其中,KafkaSpout类继承自BaseRichSpout类,用于从kafka消息队列中读取消息发送tuple,对tuple进行成功确认和失败重发,保证kafka消息至少被处理一次。 KafkaSpoutKafkaSpout类核心逻辑的序列图如下所示。从中可以看出,发送tuple,成功确认和失败重发都是由PartitionManager这个类完成。在storm中,可以设置spout的并行度,也就是说可以有多个KafkaSpout实例从一个kafka队列中读取消息,而kafka作为大数据应用场景下的消息队列,其每个队列可以配置为多个partition,每个partition可以由一个消费者读取消息,这样可以保证消息消费的大吞吐量。当在storm集群中使用KafkaSpout类读取消息时,需要控制每个KafkaSpout实例读取kafka队列的哪几个partition,如果实例数等于partition数,那么每个KafkaSpout实例从一个partiton中读取消息。在KafkaSpout类中有一个成员变量PartitionCoordinator _coordinator,_coordinator中有一个成员变量Map\<Partition, PartitionManager> _managers。通过在zookeeper上保存信息,_coordinator可以在storm集群中协调各个KafkaSpout实例读取哪几个partition,并通过_managers管理当前所在KafkaSpout实例负责的partition的读取,而PartitionManager类则具体负责某个partition的读取。当KafkaSpout实例执行nextTuple方法时,会从_coordinator中获取到PartitonManager实例,并调用该实例的next方法,而当KafkaSpout实例执行ack方法时,实际调用了PartitonManager的ack方法,而当KafkaSpout实例执行fail方法时,实际调用了PartitonManager的fail方法。PartitionManager的next、ack和fail方法具体流程如下所示。 next ack fail]]></content>
<categories>
<category>大数据</category>
</categories>
<tags>
<tag>Storm</tag>
</tags>
</entry>
<entry>
<title><![CDATA[使用daemontools实现Storm进程监控]]></title>
<url>%2F2015%2F10%2F01%2Fe4-bd-bf-e7-94-a8daemontools-e5-ae-9e-e7-8e-b0storm-e8-bf-9b-e7-a8-8b-e7-9b-91-e6-8e-a7%2F</url>
<content type="text"><![CDATA[Storm集群如图所示:其中包含一个nimbus节点和多个supervisor节点: nimbus,负责在集群中分发代码,分配计算任务,监控失败等; supervisor,负责在集群中按照nimbus的分配,启动和停止计算任务; worker,实际执行spout和bolt任务的进程。 在Storm安装目录下,可通过执行以下命令启动nimbus和supervisor进程(需先安装Python): bin/storm nimbus|supervisor nimbus和supervisor进程都是无状态和fail-fast的,状态保存在zookeeper和本地磁盘,当任何一个进程失败时,通过重启进程,可以快速恢复。Storm官方建议使用daemontools 监控nimbus和supervisor进程,当有进程失败时,daemontools可以重启进程。 安装daemontools摘自http://cr.yp.to/daemontools/install.html 。 创建/package目录: mkdir -p /packagechmod 1755 /packagecd /package 下载daemontools-0.76.tar.gz至/package,解压: gunzip daemontools-0.76.tartar -xpf daemontools-0.76.tarrm -f daemontools-0.76.tarcd admin/daemontools-0.76 编译、启动daemontools: package/install daemontools会启动2个进程:其中svscan默认扫描/service目录,可以在/service中配置需要监控的进程。daemontools使用supervise命令启动需要监控的进程,并在进程失败时,重启进程。 启动nimbus和supervisor进程启动nimbus进程在nimbus节点的/service目录,创建nimbus和ui子目录,并在nimbus和ui目录中分别创建名称为run的脚本。nimbus目录中的run脚本为:123#!/bin/sh. /etc/profile/usr/local/bin/python2.7 /opt/software/storm/bin/storm nimbus ui目录中的run脚本为:123#!/bin/sh. /etc/profile/usr/local/bin/python2.7 /opt/software/storm/bin/storm ui run脚本其实就是使用storm命令启动nimbus和ui进程。daemontools扫描到上述配置后,使用supervise命令执行run脚本,启动nimbus和ui进程: 启动supervisor进程与nimbus节点类似,在supervisor节点的/service目录,创建supervisor子目录,并在supervisor目录中创建名称为run的脚本:123#!/bin/sh. /etc/profile/usr/local/bin/python2.7 /opt/software/storm/bin/storm supervisor 配置完成后,可以通过ui看到所有节点已启动:]]></content>
<categories>
<category>大数据</category>
</categories>
<tags>
<tag>daemontools</tag>
<tag>Storm</tag>
</tags>
</entry>
<entry>
<title><![CDATA[PostgreSQL+PostGIS安装]]></title>
<url>%2F2015%2F08%2F24%2Fpostgresqlpostgis-e5-ae-89-e8-a3-85%2F</url>
<content type="text"><![CDATA[PostgreSQL与MySQL类似,也是一款开源、免费数据库。PostGIS是一款PostgreSQL插件,为PostgreSQL提供GIS支持。在开发页面点击热力图时,需要以(x,y)坐标方式存储点击位置,所以使用PostgreSQL+PostGIS存储点击数据。 安装PostgreSQL groupadd postgresuseradd -g postgres postgrespasswd postgrestar -zxvf postgresql-9.5alpha2.tar.gzcd postgresql-9.5alpha2./configure –prefix=/opt/postgresqlmakemake installcd /opt/chown -R postgres:postgres postgresqlsu - postgres/opt/postgresql/bin/initdb -D /opt/postgresql/data/opt/postgresql/bin/pg_ctl -D /opt/postgresql/data -l /opt/postgresql/logfile start 使用“/opt/postgresql/bin/psql -U postgres”可以登录数据库,执行相关操作。 安装PostGIS从源码安装,需要先安装GEOS,Proj.4,GDAL,LibXML2和JSON-C(http://postgis.net/source/)。 tar -jxvf geos-3.5.0.tar.bz2cd geos-3.5.0./configuremakemake installtar -zxvf proj.4-4.9.1.tar.gzcd proj.4-4.9.1./configuremakemake installtar -zxvf gdal-1.10.0.tar.gzcd gdal-1.10.0./configuremakemake installtar -zxvf libxml2-2.9.2.tar.gzcd libxml2-2.9.2./configuremakemake installtar -zxvf json-c-json-c-0.11-20130402.tar.gzcd json-c-json-c-0.11-20130402./configuremakemake installtar -zxvf postgis-2.1.8.tar.gzcd postgis-2.1.8./configure –with-pgconfig=/opt/postgresql/bin/pg_configmakemake installsu - postgres/opt/postgresql/bin/createdb postgis/opt/postgresql/bin/psql -d postgis -U postgres -f /opt/postgresql/share/contrib/postgis-2.1/postgis.sql 执行上述语句有可能报错,打开日志文件/opt/postgresql/logfile,会有如下错误: ERROR: could not load library “/opt/postgresql/lib/postgis-2.1.so”: libgeos_c.so.1: cannot open shared object file: No such file or directory 执行“ldd /opt/postgresql/lib/postgis-2.1.so”,缺少两个so文件: linux-vdso.so.1 => (0x00007fff3d1fe000)libgeos_c.so.1 => not foundlibproj.so.9 => not foundlibjson-c.so.2 => /lib64/libjson-c.so.2 (0x00007fb8f08f1000)libxml2.so.2 => /lib64/libxml2.so.2 (0x00007fb8f0588000)libm.so.6 => /lib64/libm.so.6 (0x00007fb8f0285000)libc.so.6 => /lib64/libc.so.6 (0x00007fb8efec4000)libdl.so.2 => /lib64/libdl.so.2 (0x00007fb8efcc0000)libz.so.1 => /lib64/libz.so.1 (0x00007fb8efaa9000)liblzma.so.5 => /lib64/liblzma.so.5 (0x00007fb8ef884000)/lib64/ld-linux-x86-64.so.2 (0x00007fb8f0dac000)libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fb8ef668000) 执行以下语句可解决: ln -s /usr/local/lib/libgeos_c.so.1 /lib64/libgeos_c.so.1ln -s /usr/local/lib/libproj.so.9 /lib64/libproj.so.9 使用 postgis=# CREATE TABLE IF NOT EXISTS click (id int primary key,pt geometry);postgis=# insert into click values (1,’POINT(30 40)’);postgis=# select * from click;id | pt—-+——————————————– 1 | 01010000000000000000003E400000000000004440]]></content>
<categories>
<category>关系数据库</category>
</categories>
<tags>
<tag>PostGIS</tag>
<tag>PostgreSQL</tag>
</tags>
</entry>
<entry>
<title><![CDATA[storm读书笔记]]></title>
<url>%2F2015%2F08%2F15%2Fstorm-e8-af-bb-e4-b9-a6-e7-ac-94-e8-ae-b0%2F</url>
<content type="text"><![CDATA[什么是storm 分布式实时计算系统; 与hadoop为批处理提供map和reduce这两种操作原语类似,storm为实时处理也提供了spout和bolt这两种操作原语。 storm的特点: 可扩展性,通过增加集群机器、调整计算并行度,即可以扩展计算性能; 保证数据不丢失,每条消息至少能被执行一次; 健壮性,集群状态保存在zookeeper中,节点不保存状态,节点故障不影响系统运行; 容错性,计算任务错误时,能够及时重新分配、运行计算任务,保证计算任务永远运行; 支持多种开发语言,java、python等。 storm的计算模型 tuple,元组,strom中的数据模型,tuple由多个key-value组成; stream,流,多个tuple组成的序列,storm的计算过程就是输入流,对流进行转换、计算的过程; spout,输入流的操作; bolt,转换流的操作; topology,拓扑,由多个spout和bolt操作组成,实现某个具体计算任务。 以word count为例,其spout生成sentence流,bolt 1接收sentence流,进行分词操作,生成word流,bolt 2接收word流,计算每个词出现的次数 storm的部署 一个nimbus节点,多个supervisor节点,所有的节点都是fail-fast和无状态,状态保存在zookeeper和本地磁盘; nimbus,类似于hadoop中的JobTracker,负责在集群中分发代码,分配计算任务,监控失败等; supervisor,类似于hadoop中的的TaskTracker,负责在集群中按照nimbus的分配,启动和停止 计算任务。 通过storm提供的ui模块可以查看集群信息,从上述图中可以看出集群包含4个supervisor节点。 storm的apiISpout:1234567public interface ISpout extends Serializable { void open(Map conf, TopologyContext context, SpoutOutputCollector collector); //work进程初始化spout时运行该方法; void nextTuple(); //使用collector发送tuple; void ack(Object msgId); //某个tuple处理成功后,运行该方法; void fail(Object msgId); //某个tuple处理失败后,运行该方法; void close();//关闭spout时运行该方法} IBolt:12345public interface IBolt extends Serializable { void prepare(Map stormConf, TopologyContext context, OutputCollector collector); //work进程初始化bolt时运行该方法; void execute(Tuple input);//处理tuple void cleanup();//关闭bolt时运行该方法} TopologyBuilder:123456789public class TopologyBuilder { public StormTopology createTopology() {…}//创建topology public BoltDeclarer setBolt(String id, IRichBolt bolt) {…}//设置bolt public BoltDeclarer setBolt(String id, IRichBolt bolt, Number parallelism_hint) {...} public BoltDeclarer setBolt(String id, IBasicBolt bolt) {...} public BoltDeclarer setBolt(String id, IBasicBolt bolt, Number parallelism_hint) {...} public SpoutDeclarer setSpout(String id, IRichSpout spout) {…}//设置spout public SpoutDeclarer setSpout(String id, IRichSpout spout, Number parallelism_hint) {...}} StormSubmitter:1234public class StormSubmitter { public static void submitTopology(String name, Map stormConf, StormTopology topology) throws AlreadyAliveException, InvalidTopologyException {…}//向集群中提交topology public static void submitTopology(String name, Map stormConf, StormTopology topology, SubmitOptions opts, ProgressListener progressListener) throws AlreadyAliveException, InvalidTopologyException {...}} LocalCluster:123public class LocalCluster implements ILocalCluster { public void submitTopology(String var1, Map var2, StormTopology var3) {…}//在本地提交运行toplogy,多用于测试} storm的数据可靠性什么是storm的数据可靠性(数据完全被处理)?仍以word count为例,spout从外部队列取消息生成sentence tuple,bolt 1生成word tuple,bolt2 生成word count tuple,这些tuple组成tuple树,sentence tuple是树的根节点。当tuple树的所有叶子节点都被确认成功处理时,根节点才会被确认成功处理,这时可以向外部队列发送消息确认,否则会重新从外部队列获取消息再次处理。 代码中如何保证数据可靠性?12345678910111213141516171819public class SplitSentence extends BaseRichBolt { OutputCollector _collector; public void prepare(Map conf, TopologyContext context, OutputCollector collector) { _collector = collector; } public void execute(Tuple tuple) { String sentence = tuple.getString(0); for(String word: sentence.split(" ")) { _collector.emit(tuple, new Values(word));//发送新tuple } _collector.ack(tuple);//确认原tuple } public void declareOutputFields(OutputFieldsDeclarer declarer) { declarer.declare(new Fields("word")); } } 其中_collector.ack(tuple)用于确认输入tuple是否被正常处理。 _collector.emit(oldTuple, newTuple)与_collector.emit(newTuple)的区别前者会执行anchor操作,即在tuple树中构建oldTuple和newTuple的父子关系,newTuple及其后续tuple需要被确认成功处理,tuple树根节点才会被确认成功处理。后者不会执行anchor操作,即在tuple树中只要oldTuple及其前续tuple被确认成功处理,tuple树根节点就会被确认成功处理,而不用监控后续tuple。 数据可靠性的实现原理topology在负责spout和bolt的进程外,还有一个负责ack的进程,用于在emit和ack操作时,更新tuple树,ack进程确认tuple被成功处理是采用异或来实现的。 tuple1 xor tuple1 xor tuple2 xor tuple2 = 0 异或结果为0说明所有的tuple都被正常处理。 storm的并行计算三类实体: worker:集群中的每台机器运行多个worker进程(默认值4); executor:每个worker processe进程运行多个executor线程; task:每个executor运行多个task(即spout和bolt)。 并行计算实例:该topology由1个spout、2个bolt组成,需要设置blue-spout并行度为2,green-bolt并行度为2,yellow-bolt并行度为6,以下是在并行度配置代码:123456Config conf = new Config();conf.setNumWorkers(2); //使用两个worker进程运行topologytopologyBuilder.setSpout("blue-spout", new BlueSpout(), 2); //使用2个executor线程运行blue-spouttopologyBuilder.setBolt("green-bolt", new GreenBolt(), 2).setNumTasks(4).shuffleGrouping(“blue-spout”);//使用2个executor线程运行green-bolt,并使用4个tasktopologyBuilder.setBolt("yellow-bolt", new YellowBolt(), 6).shuffleGrouping(“green-bolt”);//使用6个executor线程运行yellow-boltStormSubmitter.submitTopology("mytopology",conf,topologyBuilder.createTopology()); 运行的进程、线程状态如图所示,共分配2个worker进程,10个executor线程,12个task,每个进程各运行5个线程。 storm的分组策略 shuffle grouping:随机分组; fields grouping:按字段分组; all grouping:广播分组,发送到bolt的所有task中; global grouping:发送到bolt的同一个task(task id最小)中; none grouping:同shuffle grouping; direct grouping:制定发送给某个task。]]></content>
<categories>
<category>大数据</category>
</categories>
<tags>
<tag>Storm</tag>
</tags>
</entry>
<entry>
<title><![CDATA[基于Selenium+PhantomJS模拟浏览器访问]]></title>
<url>%2F2015%2F08%2F07%2Fe5-9f-ba-e4-ba-8eseleniumphantomjs-e6-a8-a1-e6-8b-9f-e6-b5-8f-e8-a7-88-e5-99-a8-e8-ae-bf-e9-97-ae%2F</url>
<content type="text"><![CDATA[介绍SeleniumSelenium 是一个Web应用自动化测试工具集,可以在多个浏览器平台上执行测试用例。Selenium包含以下几个工具: Selenium 1 (Selenium RC or Remote Control):Selenium 1的核心是一个JavaScript库,通过该库可以和浏览器中的页面进行交互。Selenium RC基于该库,可以使用多种语言开发测试用例,在多个浏览器平台上执行测试用例。 Selenium 2 (Selenium WebDriver):Selenium 1基于JavaScript库,因此受限于浏览器对于JavaScript的安全限制,Selenium 1对于浏览器中的页面交互也存在着很多限制。Selenium 2基于WebDriver,而WebDriver基于浏览器和操作系统提供的原生方法,因此Selenium 2可以直接操作浏览器,避免了JavaScript的安全限制。Selenium 2也可以使用多种语言开发测试用例,在多个浏览器平台上执行测试用例。 Selenium IDE:Selenium IDE是一个FireFox插件,可以记录用户操作并导出成可重复执行的脚本。 Selenium-Grid:Selenium-Grid可以用来扩展Selenium RC,例如可以在不同的机器上并行执行测试用例。 由于存在较多限制,已不推荐使用Selenium RC,而推荐使用Selenium WebDriver。Selenium WebDriver支持的浏览器有: Google Chrome; Internet Explorer 6, 7, 8, 9, 10; Firefox; Safari; Opera; HtmlUnit; PhantomJS; Android; iOS。 PhantomJSPhantomJS 是一个基于WebKit的JavaScript API,它原生支持各种Web标准,例如DOM解析、CSS选择器、JSON、Canvas、SVG等。PhantomJS可用于: 无GUI浏览器下的Web站点测试; 网页截屏; 基于DOM API访问和操作网页; 监控网页加载。 安装SeleniumSelenium支持的语言有: Java; C#; Python; Ruby; PHP; Perl; JavaScript。 这里介绍Python Selenium包的安装。操作系统为Linux,并已安装Python 2.7.5和pip,直接使用pip安装Selenium: pip install selenium PhantomJS下载PhantomJS的可执行包phantomjs-1.9.8-linux-x86_64.bz2,解压后将bin/phantomjs拷贝至/bin下即可。Shell中输入phantomjs可打开REPL,也可以使用phantomjs执行代码文件: phantomjs hello.js hello.js代码如下所示:12console.log('Hello, world!');phantom.exit(); 应用基于Selenium+PhantomJS模拟浏览器访问网页,输入用户名和密码并点击登录按钮,网页HTML代码如下所示:1234567891011<tr> <td>用户名:</td> <td><input type="text" id="username" placeholder="用户名"></td></tr><tr> <td>密码:</td> <td><input type="password" id="password" placeholder="密码"></td></tr><tr> <td colspan="2"><button id="logon">登录</button></td></tr> Python代码如下所示:12345678910# -*- coding: utf-8 -*-from selenium import webdriverdriver = webdriver.PhantomJS()driver.set_page_load_timeout(300)driver.get('http://xxx.xxx.xxx.xxx/login.html')driver.find_element_by_id('username').send_keys(u'username')driver.find_element_by_id('password').send_keys(u'password')driver.find_element_by_id('logon').click()]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>PhantomJS</tag>
<tag>Selenium</tag>
</tags>
</entry>
<entry>
<title><![CDATA[SimpleDateFormat线程安全问题]]></title>
<url>%2F2015%2F08%2F05%2Fsimpledateformat-e7-ba-bf-e7-a8-8b-e5-ae-89-e5-85-a8-e9-97-ae-e9-a2-98%2F</url>
<content type="text"><![CDATA[在需要进行日期与字符串相互转换的类中,经常会声明一个静态的SimpleDateFormat变量,如下所示:1private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") 然后就可以直接使用sdf的format和parse方法进行日期与字符串的相互转换,但是SimpleDateFormat并不是线程安全的,我们使用如下代码验证SimpleDateFormat的线程安全问题:1234567891011121314151617181920212223242526public class Test { private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); private static String[] dates = new String[]{"2015-07-01 10:45:52", "2015-07-02 09:34:11", "2015-07-01 07:12:32", "2015-07-03 15:06:54", "2015-07-02 21:12:38", "2015-07-01 01:59:01", "2015-07-02 13:51:18", "2015-07-03 10:05:08"}; public static class MyThread extends Thread { public void run() { for (int i=0; i<100; i++) { try { System.out.println(Thread.currentThread().getName() + "\t" + sdf.format(sdf.parse(dates[new Random().nextInt(8)]))); } catch (Exception e) { e.printStackTrace(); } } } } public static void main(String[] args) throws ParseException { MyThread t1 = new MyThread(); MyThread t2 = new MyThread(); t1.start(); t2.start(); }} MyThread线程循环100次,每次从dates数组中随机取出一个日期字符串,依次进行parse和format操作后再输出最终的日期字符串,若声明一个MyThread线程运行,则输出如图所示:输出的日期字符串都在dates数组中,说明parse和format操作均正常。若声明两个MyThread线程运行,则输出如图所示:输出的日期字符串有不在dates数组中的,且还存在异常,说明parse和format操作并不是线程安全的。在多线程下若要使用SimpleDateFormat,一种方法是每次进行日期与字符串转换时,声明一个SimpleDateFormat变量,但这样会产生过多的变量实例,另一种方法是使用ThreadLocal为每个线程保存一个SimpleDateFormat变量实例,如下所示:123456789101112131415161718192021public class DateUtil { private static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>() { protected synchronized SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); } }; public static SimpleDateFormat get() { return threadLocal.get(); } public static Date parse(String s) throws ParseException { return get().parse(s); } public static String format(Date d) { return get().format(d); }} 同时修改MyThread,使用DateUtil进行日期与字符串转换,如下所示:1234567891011public static class MyThread extends Thread { public void run() { for (int i=0; i<100; i++) { try { System.out.println(Thread.currentThread().getName() + "\t" + DateUtil.format(DateUtil.parse(dates[new Random().nextInt(8)]))); } catch (Exception e) { e.printStackTrace(); } } } } 再声明两个MyThread线程运行,则输出正常。]]></content>
<categories>
<category>后端</category>
</categories>
</entry>
<entry>
<title><![CDATA[OpenResty简介与实践]]></title>
<url>%2F2015%2F04%2F20%2Fopenresty-e7-ae-80-e4-bb-8b-e4-b8-8e-e5-ae-9e-e8-b7-b5%2F</url>
<content type="text"><![CDATA[简介OpenResty(亦称ngx_openresty)是一个基于Nginx的 Web 应用服务器。它集成了Nginx内核、LuaJIT、Lua语言实现的库以及许多第三方的Nginx模块。开发者可以使用Lua语言对Nginx中的C模块和Lua模块进行脚本编程,从而构建高性能的Web应用。 安装 tar xvf ngx_openresty-1.7.7.2.tar.gzcd ngx_openresty-1.7.7.2/./configuremakemake install OpenResty依赖perl 5.6.1+、libreadline、libpcre、libssl,因此在configure时,可能会由于缺少依赖报如下错误: ./configure: error: the HTTP rewrite module requires the PCRE library. 需要安装libpcre: yum install pcre-devel.x86_64 ./configure: error: SSL modules require the OpenSSL library. 需要安装libssl: yum install openssl-devel.x86_64 安装成功后,OpenResty默认安装至/usr/local/openresty。 使用使用OpenResty开发接口,该接口实现以下逻辑: 接口根据传入的key返回value; 接口先尝试从Redis中读取key所对应的value,若读取到value,则直接返回; 若读取不到value,则接口再尝试从MySQL中读取key所对应的value,若读取到value,则将key-value写入Redis,并返回value。 在OpenResty安装目录的nginx/conf中新建Lua脚本文件value.lua,代码如下所示,其实现了上述逻辑:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980-- ################## 解析参数key ########################local cjson = require "cjson"local key = tostring(ngx.var.arg_key)if key == 'nil' then ngx.say(cjson.encode({status = false, msg = 'need parameter key'})) returnend-- ################## 初始化Redis和MySQL ########################local mysql = require 'resty.mysql'local conn = mysql:new()local props = { host = "xxx.xxx.xxx.xxx", port = 3306, database = "xxx", user = "xxx", password = "xxx"}-- 使用connect方法创建连接,该方法会先尝试从连接池中获取已有连接local res, err, errno, sqlstate = conn:connect(props)if not res then print("connect to mysql error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate) ngx.say(cjson.encode({status = false, msg = 'internal error'})) returnendlocal redis = require 'resty.redis'local redis_client = redis:new()-- 使用connect方法创建连接,该方法会先尝试从连接池中获取已有连接local ok, err = redis_client:connect('xxx.xxx.xxx.xxx', 6379)if not ok then print("connect to redis error:", err) ngx.say(cjson.encode({status = false, msg = 'internal error'})) returnend-- 释放Redis连接function close_redis() local pool_max_idle_time = 10000 local pool_size = 1024 if redis_client then -- 使用set_keepalived方法将连接释放到连接池中,并可以指定连接池中连接的最长空闲时间和连接池中的连接数 redis_client:set_keepalive(pool_max_idle_time, pool_size) endend-- 释放MySQL连接function close_mysql() local pool_max_idle_time = 10000 local pool_size = 5 if conn then -- 使用set_keepalived方法将连接释放到连接池中,并可以指定连接池中连接的最长空闲时间和连接池中的连接数 conn:set_keepalive(pool_max_idle_time, pool_size) endendfunction get(key) -- 先尝试从Redis中根据key获取value local value = redis_client:get(key) if not value or value == ngx.null then -- 若Redis中不存在该key,则尝试从MySQL中根据key获取value local rows = conn:query("select v from key_value where k='" .. key .. "'") if rows and rows[1] then value = rows[1].v if value and value ~= ngx.null then -- 若MySQL中存在该key,则将该key和value添加到Redis中 redis_client:set(key, value) end end end return valueendlocal value = get(key)if not value or value == ngx.null then ngx.say(cjson.encode({status = false, msg = 'no value'}))else ngx.say(cjson.encode({status = true, msg = 'success', value = value})) endclose_redis()close_mysql() 配置nginx/conf/nginx.conf,对于地址为“/api/value”的请求使用value.lua处理:12345678server { listen 8080; server_name localhost; location /api/value { content_by_lua_file conf/value.lua; } ...} 启动Nginx,浏览器中访问“api/value?key=v1”,成功返回value:]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>Nginx</tag>
<tag>Lua</tag>
<tag>OpenResty</tag>
</tags>
</entry>
<entry>
<title><![CDATA[使用Resin部署Web Service]]></title>
<url>%2F2015%2F04%2F15%2Fe4-bd-bf-e7-94-a8resin-e9-83-a8-e7-bd-b2web-service%2F</url>
<content type="text"><![CDATA[Resin是一款非常流行的Web Application服务器,可以部署servlet和JSP,同时,Resin也可以作为Web Service服务器。使用Resin部署Web Service的步骤如下。 配置Resin在Resin配置文件中添加以下语句使其支持Web Service:1<system-property javax.xml.stream.XMLInputFactory="com.sun.xml.internal.stream.XMLInputFactoryImpl" /> 配置web.xml配置servlet,对Http请求使用WSSpringServlet实例进行处理:123456789<servlet> <servlet-name>JAXWSServlet</servlet-name> <servlet-class>com.sun.xml.ws.transport.http.servlet.WSSpringServlet</servlet-class> <load-on-startup>1</load-on-startup></servlet><servlet-mapping> <servlet-name>JAXWSServlet</servlet-name> <url-pattern>/*</url-pattern></servlet-mapping> 配置Spring上下文绑定URL和Web Service:123456789101112131415161718192021222324252627<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:ws="http://jax-ws.dev.java.net/spring/core" xmlns:wss="http://jax-ws.dev.java.net/spring/servlet" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd http://jax-ws.dev.java.net/spring/core http://jax-ws.dev.java.net/spring/core.xsd http://jax-ws.dev.java.net/spring/servlet http://jax-ws.dev.java.net/spring/servlet.xsd"> <context:component-scan base-package="com.vshangping.server.webservice" /> <wss:binding url="/testWebService"> <wss:service> <ws:service bean="#testWebService" /> </wss:service> </wss:binding></beans> 实现Web Service123456789101112@Service("testWebService")@WebService(targetNamespace = "com.vshangping.server.webservice", serviceName = "TestWebService")public class TestWebService { @Resource private MachineService machineService; public Machine getMachine(Integer id) { return machineService.get(id); }} 编译打包后部署到Resin中,访问/webservice/testWebService,如下所示:]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>Spring</tag>
<tag>Resin</tag>
</tags>
</entry>
<entry>
<title><![CDATA[使用MyBatis Generator逆向生成数据库读写代码]]></title>
<url>%2F2015%2F04%2F12%2Fe4-bd-bf-e7-94-a8mybatis-generator-e7-94-9f-e6-88-90-e4-bb-a3-e7-a0-81%2F</url>
<content type="text"><![CDATA[简介MyBatis(前身是iBATIS)是一个基于Java的持久层框架,而MyBatis Generator (MBG)是一个根据数据库表结构为MyBatis和iBATIS逆向生成相关Java代码和配置文件的工具。MBG可以生成: Java POJO,与数据库表结构对应; MyBatis和iBATIS规范的SQL映射配置文件,每个配置文件包含对一个表实现简单CRUD操作的SQL语句; MyBatis和iBATIS规范的Java客户端接口,提供对数据的增、删、查、改方法。 示例数据库示例数据库包含3张表: 课程表(course),存储课程名称、学分信息,主键为自增整数; 学生表(student),存储学生姓名、性别、年龄等信息,主键为自增整数; 得分表(score),存储某位学生某门课程的分数,使用课程id和学生id作为联合主键。 使用MBG逆向生成代码使用MBG生成代码时,需要指定配置文件(generatorConfig.xml),如下所示,其中指定了数据库连接方式,Java POJO、SQL映射配置文件、Java客户端接口的生成方式和生成路径等:1234567891011121314151617181920212223242526272829<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"><generatorConfiguration> <classPathEntry location="/Users/tao/develop/mysql-connector-java-5.1.12.jar" /> <context id="test" targetRuntime="MyBatis3"> <commentGenerator> <property name="suppressAllComments" value="true" /> </commentGenerator> <jdbcConnection driverClass="com.mysql.jdbc.Driver" connectionURL="jdbc:mysql://xxx.xxx.xxx.xxx:3306/test" userId="xxx" password="xxx"> </jdbcConnection> <javaTypeResolver> <property name="forceBigDecimals" value="false" /> </javaTypeResolver> <javaModelGenerator targetPackage="com.magicwt.bean" targetProject="/Users/tao/develop/test/src"> <property name="enableSubPackages" value="false" /> <property name="trimStrings" value="true" /> </javaModelGenerator> <sqlMapGenerator targetPackage="com.magicwt.mapper" targetProject="/Users/tao/develop/test/src"> <property name="enableSubPackages" value="false" /> </sqlMapGenerator> <javaClientGenerator type="XMLMAPPER" targetPackage="com.magicwt.dao" targetProject="/Users/tao/develop/test/src"> <property name="enableSubPackages" value="false" /> </javaClientGenerator> <table tableName="%" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false"></table> </context></generatorConfiguration> 指定配置文件后,MBG支持以下4种生成代码方式: 使用命令行生成代码; 作为Ant任务生成代码; 作为Maven插件生成代码; 使用Java接口编程生成代码。 这里使用命令行方式,从https://github.com/mybatis/generator/releases 下载jar包,命令行下执行jar包: java -jar mybatis-generator-core-1.3.2.jar -configfile generatorConfig.xml 执行成功后,便会生成如下代码: Java POJO:Course.java,Student.java,ScoreKey.java,Score.java,其中Course.java对应于course表,如下所示: 123456789101112131415161718192021222324252627282930313233package com.magicwt.bean;public class Course { private Integer id; private String name; private Byte credit; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name == null ? null : name.trim(); } public Byte getCredit() { return credit; } public void setCredit(Byte credit) { this.credit = credit; }} SQL映射配置文件:CourseMapper.xml,StudentMapper.xml,ScoreMapper.xml,其中CourseMapper.xml包含对course表实现CRUD操作的SQL语句,如下所示: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" ><mapper namespace="com.magicwt.dao.CourseMapper" > <resultMap id="BaseResultMap" type="com.magicwt.bean.Course" > <id column="id" property="id" jdbcType="INTEGER" /> <result column="name" property="name" jdbcType="VARCHAR" /> <result column="credit" property="credit" jdbcType="TINYINT" /> </resultMap> <sql id="Base_Column_List" > id, name, credit </sql> <select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer" > select <include refid="Base_Column_List" /> from course where id = #{id,jdbcType=INTEGER} </select> <delete id="deleteByPrimaryKey" parameterType="java.lang.Integer" > delete from course where id = #{id,jdbcType=INTEGER} </delete> <insert id="insert" parameterType="com.magicwt.bean.Course" > insert into course (id, name, credit ) values (#{id,jdbcType=INTEGER}, #{name,jdbcType=VARCHAR}, #{credit,jdbcType=TINYINT} ) </insert> <insert id="insertSelective" parameterType="com.magicwt.bean.Course" > insert into course <trim prefix="(" suffix=")" suffixOverrides="," > <if test="id != null" > id, </if> <if test="name != null" > name, </if> <if test="credit != null" > credit, </if> </trim> <trim prefix="values (" suffix=")" suffixOverrides="," > <if test="id != null" > #{id,jdbcType=INTEGER}, </if> <if test="name != null" > #{name,jdbcType=VARCHAR}, </if> <if test="credit != null" > #{credit,jdbcType=TINYINT}, </if> </trim> </insert> <update id="updateByPrimaryKeySelective" parameterType="com.magicwt.bean.Course" > update course <set > <if test="name != null" > name = #{name,jdbcType=VARCHAR}, </if> <if test="credit != null" > credit = #{credit,jdbcType=TINYINT}, </if> </set> where id = #{id,jdbcType=INTEGER} </update> <update id="updateByPrimaryKey" parameterType="com.magicwt.bean.Course" > update course set name = #{name,jdbcType=VARCHAR}, credit = #{credit,jdbcType=TINYINT} where id = #{id,jdbcType=INTEGER} </update></mapper> Java客户端接口,CourseMapper.java,StudentMapper.java,ScoreMapper.java,其中CourseMapper.java提供对Course对象的增、删、查、改方法,如下所示: 1234567891011121314151617package com.magicwt.dao;import com.magicwt.bean.Course;public interface CourseMapper { int deleteByPrimaryKey(Integer id); int insert(Course record); int insertSelective(Course record); Course selectByPrimaryKey(Integer id); int updateByPrimaryKeySelective(Course record); int updateByPrimaryKey(Course record);} 使用所生成代码读写数据库示例工程使用Maven构建,并使用Spring管理上下文,代码目录如图所示:除MBG生成的代码和配置文件外,还需要context.xml和configuration.xml分别配置Spring和MyBatis,context.xml和configuration.xml如下所示:1234567891011121314151617181920212223242526272829303132333435363738<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd"> <!-- 配置数据库连接池 --> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="driverClass" value="com.mysql.jdbc.Driver" /> <property name="jdbcUrl" value="jdbc:mysql://xx.xx.xx.xx:3306/test?characterEncoding=UTF-8&autoReconnect=true" /> <property name="user" value="xx" /> <property name="password" value="xx" /> <property name="initialPoolSize" value="1" /> </bean> <!-- 配置会话工厂 --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="configLocation" value="classpath:configuration.xml" /> </bean> <bean id="courseDao" class="org.mybatis.spring.mapper.MapperFactoryBean"> <property name="sqlSessionFactory" ref="sqlSessionFactory" /> <property name="mapperInterface" value="com.magicwt.dao.CourseMapper" /> </bean> <bean id="studentDao" class="org.mybatis.spring.mapper.MapperFactoryBean"> <property name="sqlSessionFactory" ref="sqlSessionFactory" /> <property name="mapperInterface" value="com.magicwt.dao.StudentMapper" /> </bean> <bean id="scoreDao" class="org.mybatis.spring.mapper.MapperFactoryBean"> <property name="sqlSessionFactory" ref="sqlSessionFactory" /> <property name="mapperInterface" value="com.magicwt.dao.ScoreMapper" /> </bean></beans> 123456789101112131415161718<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration> <typeAliases> <typeAlias alias="Course" type="com.magicwt.bean.Course"/> <typeAlias alias="Student" type="com.magicwt.bean.Student"/> <typeAlias alias="Score" type="com.magicwt.bean.Score"/> </typeAliases> <mappers> <mapper resource="com/magicwt/mapper/CourseMapper.xml"/> <mapper resource="com/magicwt/mapper/StudentMapper.xml"/> <mapper resource="com/magicwt/mapper/ScoreMapper.xml"/> </mappers></configuration> 编写测试类对course表执行插入操作:12345678910111213141516171819public class DaoTest { private ApplicationContext context; @Before public void prepare() { context = new ClassPathXmlApplicationContext(new String[] {"context.xml"}); } @Test public void test() { CourseMapper courseDao = (CourseMapper) context.getBean("courseDao"); Course course = new Course(); course.setName("微积分"); course.setCredit(new Byte("4")); courseDao.insert(course); }} 执行后,在course表中成功插入一条记录:]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>Maven</tag>
<tag>MyBatis</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Move Zeroes]]></title>
<url>%2F2015%2F03%2F13%2F283-move-zeroes%2F</url>
<content type="text"><![CDATA[Given an array nums, write a function to move all 0’s to the end of it while maintaining the relative order of the non-zero elements.For example, given nums = [0, 1, 0, 3, 12], after calling your function, nums should be [1, 3, 12, 0, 0].Note:1.You must do this in-place without making a copy of the array.2.Minimize the total number of operations 给定一个数组,把所有0移到数组末尾,同时保证非0数的相对顺序不变。不能使用额外的空间,且保证操作次数最小。 比如,数组[0,1,0,3,12],处理后得到的结果应该为[1,3,12,0,0]。1234567891011121314public class Solution { public void moveZeroes(int[] nums) { int index = 0; for (int i=0;i<nums.length;i++) { if (nums[i]!=0) { nums[index] = nums[i]; index++; } } for (int i=index;i<nums.length;i++) { nums[i] = 0; } }}]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
<tags>
<tag>LeetCode</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Kafka简介]]></title>
<url>%2F2015%2F02%2F16%2Fkafka-e7-ae-80-e4-bb-8b%2F</url>
<content type="text"><![CDATA[节选自Kafka官方文档(http://kafka.apache.org/documentation.html) 介绍Kafka是一个分布式、分区消息服务,基本概念包括: topic,消息流; producer,向topic发布消息; consumer,订阅topic,接收、处理消息; broker,Kafka集群由多个broker组成。 Topictopic包含多个分区,如图所示:每个分区是一个有序的消息队列,消息按照从旧到新的顺序排列,新的消息不断追加到尾部。每个消息使用递增的id(offset)来唯一标识。消息在Kafka中会被保存一段时间,而不管它是否被消费,这个保存时间可以设置。如果保存时间设置为2天,那么在2天内,消息都可以被消费,2天后消息将被删除。每个消费者需要保存当前所消费消息在分区中的位置(offset)。通常情况下,消费者在消费消息时递增offset表示消息已被消费。另外,消费者也可以灵活地设置offset,例如,设置到较旧的offset,重复消费已消费过的消息。]]></content>
<categories>
<category>大数据</category>
</categories>
<tags>
<tag>Kafka</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Infobright简介与实践]]></title>
<url>%2F2014%2F11%2F21%2Finfobright-e7-ae-80-e4-bb-8b-e4-b8-8e-e5-ae-9e-e8-b7-b5%2F</url>
<content type="text"><![CDATA[infobright是一款基于MySQL的数据仓库,它采用列式存储,压缩比高,查询速度快。infobright提供社区(ICE)和商业(IEE)两个版本,ICE版本不支持DML,无法执行insert、update、delete和alter操作。在工作中,我们每天会通过hadoop统计出前一天全站每个页面的PV和UV,约有数几千万条记录。我们使用infobright来存储这些数据并提供快速查询。 安装在Linux 64位系统中,下载infobright-4.0.7-0-x86_64-ice.rpm,执行以下语句进行安装: rpm -ivh infobright-4.0.7-0-x86_64-ice.rpm infobright默认安装在/usr/local/infobright-4.0.7-x86_64,执行该目录下的postconfig.sh可以修改相关配置,默认配置如下所示: Current config file: [/etc/my-ib.cnf]Current brighthouse.ini file: [/usr/local/infobright-4.0.7-x86_64/data/brighthouse.ini]Current datadir: [/usr/local/infobright-4.0.7-x86_64/data]Current CacheFolder in brighthouse.ini file: [/usr/local/infobright-4.0.7-x86_64/cache]Current socket: [/tmp/mysql-ib.sock]Current port: [5029] 修改相关配置后,执行以下语句启动服务: /usr/local/infobright-4.0.7-x86_64/bin/mysqld_safe & 使用创建表123456789create table if not exists url_stat_20141117 ( refer_type int , id int , id_type int , url_type int , url text , uv int , pv int ); infobright的存储引擎为BRIGHTHOUSE。 装载数据hadoop统计前一天全站每个页面的PV和UV,统计结果存储在文件中,每行包含7列,每列使用“\t”分隔,并且与表字段一一对应。原始文件大小一般接近10G,使用以下语句加载数据至infobright:1load data infile '/opt/data/url_stat_20141117.txt' into table url_stat_20141117 fields terminated by '\t' enclosed by 'NULL' lines terminated by '\n'; 对于表url_stat_20141117,infobright在data目录下使用文件url_stat_20141117.frm保存schema信息,使用目录url_stat_20141117.bht保存数据。装载数据后,url_stat_20141117.bht中的文件总大小约为1.3G,与原始文件相比,压缩比接近10:1。表中记录约有7000多万条。]]></content>
<categories>
<category>关系数据库</category>
</categories>
<tags>
<tag>MySQL</tag>
<tag>Infobright</tag>
</tags>
</entry>
<entry>
<title><![CDATA[使用GraphicsMagick+im4java处理图片]]></title>
<url>%2F2014%2F11%2F10%2Fe4-bd-bf-e7-94-a8graphicsmagickim4java-e5-a4-84-e7-90-86-e5-9b-be-e7-89-87%2F</url>
<content type="text"><![CDATA[GraphicsMagick被称为图片处理的瑞士军刀,它的功能包括: Convert an image from one format to another (e.g. TIFF to JPEG) Resize, rotate, sharpen, color reduce, or add special effects to an image Create a montage of image thumbnails Create a transparent image suitable for use on the Web Compare two images Turn a group of images into a GIF animation sequence Create a composite image by combining several separate images Draw shapes or text on an image Decorate an image with a border or frame Describe the format and characteristics of an image Linux下安装GraphicsMagick 安装libpng: tar -zxvf libpng-1.6.14.tar.gzcd libpng-1.6.14./configuremakemake install 安装jpeg-9a: tar -zxvf jpegsrc.v9a.tar.gzcd jpeg-9a./configuremakemake install 安装GraphicsMagick: tar -zxvf GraphicsMagick-1.3.20.tar.gzcd GraphicsMagick-1.3.20./configuremakemake install 使用GraphicsMagick输入gm可以得到该命令的使用说明,例如对以下490326的图片先缩放到原来的50%至245164再居中裁剪出164*164的正方形图片的命令是: gm convert -quality 100 -resize 245x164 -crop 164x164+40+0 +profile “*” old.jpg new.jpg 其中,“-quality 100”表示图片质量,“-resize 245x164”表示将图片尺寸调整至245*164,“-crop 164x164+40+0”表示从坐标(40,0)开始裁剪出164*164的图片,+profile “*“表示不保留exif信息。 在Java中处理图片在Java中可以通过im4java调用gm命令处理图片,上述对图片进行缩放再裁剪的操作在Java中可以通过以下代码实现:123456789IMOperation op = new IMOperation();op.quality(100d);op.resize(245, 164);op.crop(164, 164, 40, 0);op.p_profile("*");op.addImage("old.jpg");op.addImage("new.jpg"); ConvertCmd convert = new ConvertCmd(true);convert.run(op);]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>GraphicsMagick</tag>
<tag>im4java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Flume简介与实践]]></title>
<url>%2F2014%2F11%2F05%2Fflume-e7-ae-80-e4-bb-8b-e4-b8-8e-e5-ae-9e-e8-b7-b5%2F</url>
<content type="text"><![CDATA[Flume是一个分布式、高可用日志收集系统,可以收集不同来源的日志并集中存储。目前Flume是Apache顶级项目。 架构数据流模型Flume Agent是一个JVM进程,包含三个基本组件: Source,用于从外部数据源获取数据; Channel,用于暂存数据; Sink,用于向目标发送数据。 SourceFlume支持以下Source: Avro Source,侦听Avro端口,接收外部Avro客户端发送的数据; Thrift Source,侦听Thrift端口,接收外部Thrift客户端发送的数据; Exec Source,执行Unix命令,并将标准输出作为外部数据源,例如,可以使用“cat [named pipe]”或“tail -F [file]”命令持续地输出数据作为外部数据源; JMS Source,从JMS消息队列或主题中读取消息作为外部数据源; Spooling Directory Source,将目录下的文件作为外部数据源,当有新文件加入目录时,则读取文件中的数据; Kafka Source,从Kafka topic中读取消息作为外部数据源; NetCat Source,侦听某个端口,按行接收数据作为外部数据源; Sequence Generator Source,产生从0开始,按1递增的序列作为外部数据源,主要用于测试; Syslog Sources,读取syslog作为外部数据源; HTTP Source,将HTTP GET和POST请求作为外部数据源; Stress Source,内部产生数据用于压力测试; Legacy Sources,接收Flume 0.9.4发送的数据并作适配,作为Flume 1.x的外部数据源; Scribe Source,适配Scribe,将Scribe作为外部数据源。 ChannelFlume支持以下Channel: Memory Channel,将数据暂存在内存队列中; JDBC Channel,将数据暂存在数据库中; Kafka Channel,将数据暂存在Kafka集群中; File Channel,将数据暂存在文件中; Spillable Memory Channel,同时使用内存和文件暂存数据。 SkinFlume支持以下Sink: HDFS Sink,将数据发送至HDFS; Hive Sink,将数据发送至Hive; Logger Sink,以日志方式输出数据,主要用于测试; Avro Sink,以Avro方式将数据发送到其他的Avro服务器端; Thrift Sink,以Thrift方式将数据发送到其他的Thrift服务器端; File Roll Sink,将数据发送至本地文件; Null Sink,不再发送数据; HBaseSinks,将数据发送至HBase; MorphlineSolrSink,将数据发送至Solr; ElasticSearchSink,将数据发送至ElasticSearch; Kafka Sink,将数据发送至Kafka Topic。 安装直接下载和解压可执行包: tar -zxvf apache-flume-1.5.0.1-bin.tar.gzln -s apache-flume-1.5.0.1-bin flume 配置以tail -F方式读取nginx日志,使用内存暂存并发送到Kafka topic,配置mp_pv_producer.properties如下:1234567891011121314151617181920212223242526272829#agent sectionmp_pv_producer.sources=smp_pv_producer.channels=cmp_pv_producer.sinks=r#source sectionmp_pv_producer.sources.s.type=execmp_pv_producer.sources.s.channels=cmp_pv_producer.sources.s.restart=truemp_pv_producer.sources.s.restartThrottle=500mp_pv_producer.sources.s.command=tail -f /data/logs/nginx/access.log# Each sink's type must be definedmp_pv_producer.sinks.r.type=org.apache.flume.plugins.KafkaSinkmp_pv_producer.sinks.r.metadata.broker.list=10.16.3.97:9092,10.16.3.172:9092mp_pv_producer.sinks.r.partition.key=0mp_pv_producer.sinks.r.partitioner.class=org.apache.flume.plugins.SinglePartitionmp_pv_producer.sinks.r.serializer.class=kafka.serializer.StringEncodermp_pv_producer.sinks.r.request.required.acks=0mp_pv_producer.sinks.r.max.message.size=1000000mp_pv_producer.sinks.r.producer.type=syncmp_pv_producer.sinks.r.custom.encoding=UTF-8mp_pv_producer.sinks.r.custom.topic.name=mp_pv#Specify the channel the sink should usemp_pv_producer.sinks.r.channel=c# Each channel's type is defined.mp_pv_producer.channels.c.type=memorymp_pv_producer.channels.c.capacity=1000 启动 nohup bin/flume-ng agent –conf conf –conf-file conf/mp_pv_producer.properties –name mp_pv_producer -Dflume.root.logger=INFO,console &]]></content>
<categories>
<category>大数据</category>
</categories>
<tags>
<tag>Flume</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Elasticsearch简介与实践]]></title>
<url>%2F2014%2F10%2F16%2Felasticsearch-e7-ae-80-e4-bb-8b-e4-b8-8e-e5-ae-9e-e8-b7-b5%2F</url>
<content type="text"><![CDATA[Elasticsearch是一个基于Lucene的开源搜索引擎,使用Elasticsearch可以搭建分布式、可扩展、高可用的搜索集群,并提供RESTful API。Elasticsearch包含的数据结构及其与关系数据库的类比如下所示: Elasticsearch 关系数据库 index database mapping table document row field field 工作中,我们使用Elasticsearch搭建搜索集群,对文章构建索引并提供文章搜索接口。使用了Elasticsearch中文发行版elasticsearch-rtf,该版本针对中文集成了相关插件。从Github上下载压缩包https://github.com/medcl/elasticsearch-rtf/archive/1.0.0.tar.gz 并解压。 部署集群集群配置修改config/elasticsearch.yml,设置集群名称: cluster.name: mp_production_es 设置节点名称: node.name: “production1” 对于同一集群中的各个节点,要保证各节点的集群名称相同,节点名称不同,这样在启动集群中的各节点时,能通过广播发现同一网段中具有相同集群名称的其他节点组成集群。 Analyzer配置搜索引擎构建索引时需要先对文档进行分析,从文档中提取出token(词元),实现此操作的是tokenizer,提取出的token会被进一步处理(如转成小写等),实现此操作的是filter, 被处理后的结果被称为term(词),搜索引擎使用这些term构建倒排索引。tokenizer+filter被称为analyzer(分析器)。Elasticsearch内置了很多analyzer, 还有很多第三方的analyzer插件, 比如用于中文分词的analyzer。在elasticsearch.yml中可以配置所支持的tokenizer、filter和analyzer。elasticsearch-rtf已集成了很多第三方的analyzer插件,并在elasticsearch.yml中已配置,其中“string2int ”用于将字符串转化为整数,从而减小索引文件大小,节约内存,这个插件使用Redis存储字符串和整数的映射关系,所以如果需要使用这个插件,需要搭建Redis并配置Redis访问地址,如果不使用这个插件,可以直接删除该插件配置。“ansj”是基于ansj的中文分词插件,这个插件可选择使用Redis的pub/sub方式更新词典,如果不使用这个插件,也可以直接删除该插件配置。实际使用中,针对analyzer,我们默认使用“keyword”,即不分词,内容整体作为一个Token,并配置了“ik”,用于对标题和正文进行中文分词,配置如下:123456789101112131415index: analysis: analyzer: ik: alias: - ik_analyzer type: org.elasticsearch.index.analysis.IkAnalyzerProvider ik_max_word: type: ik use_smart: false ik_smart: type: ik use_smart: true index.analysis.analyzer.default.type: keyword JVM配置修改bin/service/elasticsearch.conf,设置堆大小: set.default.ES_HEAP_SIZE=8192 运行执行以下命令启动各节点,各节点通过广播发现同一网段中具有相同集群名称的其他节点自动组成集群。 bin/service/elasticsearch start 基于RESTful API创建索引创建index: curl -XPUT 127.0.0.1:9200/mp 创建mapping,设置文章各字段,其中主键是“_id”,“title”和“content”使用“ik”进行中文分词:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889curl -XPUT '127.0.0.1:9200/mp/news/_mapping' -d '{ "news":{ "_all" : { "enabled" : false }, "_id" : { "index": "not_analyzed", "store" : "yes", "type":"integer"}, "properties" :{ "cmsId" :{ "type" : "integer", "index": "no", "store" : "yes" }, "title" :{ "type" : "string", "store": "yes", "term_vector": "with_positions_offsets", "indexAnalyzer": "ik", "searchAnalyzer": "ik", "include_in_all": "true" }, "mobileTitle" :{ "type" : "string", "index": "no", "store" : "yes" }, "brief" :{ "type" : "string", "index": "no", "store" : "yes" }, "content": { "type" : "string", "store": "yes", "term_vector": "with_positions_offsets", "indexAnalyzer": "ik", "searchAnalyzer": "ik", "include_in_all": "true" }, "time": { "type" : "long", "index": "not_analyzed", "store" : "yes" }, "mediaId": { "type" : "integer", "index": "not_analyzed", "store" : "yes" }, "channelId": { "type" : "integer", "index": "not_analyzed", "store" : "yes" }, "categoryId": { "type" : "integer", "index": "not_analyzed", "store" : "yes" }, "img": { "type" : "string", "index": "no", "store" : "yes" }, "url": { "type" : "string", "index": "no", "store" : "yes" }, "tags": { "type" : "string", "index": "no", "store" : "yes" }, "tagList": { "type" : "string", "index": "no", "store" : "yes" }, "json": { "type" : "string", "index": "no", "store" : "yes" } } }}' 索引创建成功后,通过浏览器可查看到相关信息:其中,有5个shard(分片),每个shard有一个副本。 基于Java API更新索引和搜索文章添加依赖12345<dependency> <groupId>org.elasticsearch</groupId> <artifactId>elasticsearch</artifactId> <version>1.0.0</version></dependency> 建立连接创建ElasticsearchClientManager类,用于维护Client实例,建立并保持和搜索集群的连接。123456789101112131415161718192021222324252627282930313233343536package com.sohu.cms.mp.es.search;import org.elasticsearch.client.Client;import org.elasticsearch.client.transport.TransportClient;import org.elasticsearch.common.settings.ImmutableSettings;import org.elasticsearch.common.settings.Settings;import org.elasticsearch.common.transport.InetSocketTransportAddress;import org.slf4j.Logger;import org.slf4j.LoggerFactory;public class ElasticsearchClientManager { private static Logger logger = LoggerFactory.getLogger(ElasticsearchClientManager.class); private Client client; private String clusterName; private String clusterIps; private int clusterPort; public void init() { Settings settings = ImmutableSettings.settingsBuilder().put("cluster.name", clusterName).build(); client = new TransportClient(settings); for (String clusterIp : clusterIps.split(";")) { client = ((TransportClient) client).addTransportAddress(new InetSocketTransportAddress(clusterIp, clusterPort)); } logger.info("init cluster [" + clusterName + "|" + clusterIps + "] success"); } public void destroy() { logger.info("destroy success"); client.close(); } // 省略get/set方法} 12345<bean id="elasticsearchClientManager" class="com.sohu.cms.mp.es.search.ElasticsearchClientManager" init-method="init" destroy-method="destroy"> <property name="clusterName" value="mp_production_es"/> <property name="clusterIps" value="xxx.xxx.xxx.xxx;xxx.xxx.xxx.xxx"/> <property name="clusterPort" value="9300"/></bean> 更新索引123456789101112131415161718192021222324252627282930313233public boolean index(News news) { try { Client client = elasticsearchClientManager.getClient(); XContentBuilder contentBuilder = XContentFactory.jsonBuilder() .startObject() .field("cmsId", news.getCmsId()) .field("title", news.getTitle()) .field("mobileTitle", news.getMobileTitle()) .field("brief", news.getBrief()) .field("content", news.getContent()) .field("time", news.getTime()) .field("mediaId", news.getMediaId()) .field("channelId", news.getChannelId()) .field("categoryId", news.getCategoryId()) .field("img", news.getImg()) .field("url", news.getUrl()) .field("tags", news.getTags()) .field("tagList", news.getTagList()) .field("json", news.getJson()) .endObject(); BulkRequestBuilder requestBuilder = client.prepareBulk(); requestBuilder.add(client.prepareIndex("mp", "news", String.valueOf(news.getId())).setSource(contentBuilder)); BulkResponse bulkResponse = requestBuilder.execute().actionGet(); if(bulkResponse.hasFailures()) { return false; } else { return true; } } catch (Exception e) { logger.error("updateIndex error", e); return false; }} 搜索文章12345678910111213141516171819202122232425262728/** * 在指定频道、指定类别中,根据搜索词查询,为了支持分页,需要设置查询结果的起始和数目 * @param wd * @param channels * @param categories * @param from * @param size * @return */public SearchHits search(String wd, List<Integer> channels, List<Integer> categories, int from, int size) { Client client = elasticsearchClientManager.getClient(); QueryBuilder query = QueryBuilders.boolQuery().must(QueryBuilders.multiMatchQuery(wd, "title", "content")) .must(QueryBuilders.termsQuery("channelId", channels)) .must(QueryBuilders.termsQuery("categoryId", categories)); SearchResponse searchResponse= client.prepareSearch("mp") .setQuery(query) .addSort("_score", SortOrder.DESC) .addSort("time", SortOrder.DESC) .setTypes("news") .setFrom(from).setSize(size).setExplain(true) .addHighlightedField("content") .addHighlightedField("title") .setHighlighterPreTags("<span style='color:red'>") .setHighlighterPostTags("</span>") .execute() .actionGet(); return searchResponse.getHits();}]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>Elasticsearch</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Odd Even Linked List]]></title>
<url>%2F2014%2F05%2F13%2Fodd-even-linked-list%2F</url>
<content type="text"><![CDATA[Given a singly linked list, group all odd nodes together followed by the even nodes. Please note here we are talking about the node number and not the value in the nodes. You should try to do it in place. The program should run in O(1) space complexity and O(nodes) time complexity. Example:Given 1->2->3->4->5->NULL,return 1->3->5->2->4->NULL. Note:The relative order inside both the even and odd groups should remain as it was in the input.The first node is considered odd, the second node even and so on … 给定一个单向链表,将奇数节点放在前面,偶数节点放在后面。所有奇数节点或偶数节点的相对位置保持不变。空间复杂度O(1),时间复杂度O(n)。所有奇数节点或偶数节点的相对位置保持不变。123456789101112131415161718192021222324/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */public class Solution { public ListNode oddEvenList(ListNode head) { ListNode n1 = head; if (n1 == null) return head; ListNode head2 = head.next; ListNode n2 = head.next; while (n2 != null && n2.next != null) { n1.next = n2.next; n1 = n2.next; n2.next = n1.next; n2 = n1.next; } n1.next = head2; return head; }}]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
<tags>
<tag>LeetCode</tag>
</tags>
</entry>
<entry>
<title><![CDATA[聚簇索引(摘自《高性能MySQL》)]]></title>
<url>%2F2014%2F04%2F01%2Fe8-81-9a-e7-b0-87-e7-b4-a2-e5-bc-95-ef-bc-88-e6-91-98-e8-87-aa-e3-80-8a-e9-ab-98-e6-80-a7-e8-83-bdmysql-e3-80-8b-ef-bc-89%2F</url>
<content type="text"><![CDATA[InnoDB的聚簇索引实际上在同一个结构中保存了B-Tree索引和数据行。当表有聚簇索引时,它的数据行实际上存放在索引的叶子页(leaf page)中。术语“聚簇”表示数据行和相邻的键值紧凑地存储在一起。因为无法同时把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。聚集的数据有一些重要的优点: 可以把相关数据保存在一起。例如实现电子邮箱时,可以根据用户ID来聚集数据,这样只需要从磁盘读取少数的数据页就能获取某个用户的全部邮件。如果没有使用聚簇索引,则每封邮件都可能导致一次磁盘I/O。 数据访问更快。聚簇索引将索引和数据保存在同一个B-Tree中,因此从聚簇索引中获取数据通常比在非聚簇索引中查找要快。 使用覆盖索引扫描的查询可以直接使用页节点中的主键值。]]></content>
<categories>
<category>关系数据库</category>
</categories>
<tags>
<tag>MySQL</tag>
</tags>
</entry>
<entry>
<title><![CDATA[解决NoClassDefFoundError: Could not initialize class net.sf.json.JsonConfig]]></title>
<url>%2F2014%2F04%2F01%2Fe8-a7-a3-e5-86-b3noclassdeffounderror-could-not-initialize-class-net-sf-json-jsonconfig%2F</url>
<content type="text"><![CDATA[今天在开发时遇到了如下错误: Caused by: java.lang.NoClassDefFoundError: Could not initialize class net.sf.json.JsonConfig NoClassDefFoundError不同于ClassNotFoundException,而且在pom.xml里面已经加上了net.sf.json.JsonConfig的jar包依赖,所以不是缺少该类,而是在类加载时发生了错误,进一步看JsonConfig的源码,发现import了以下两个类:12import org.apache.commons.collections.map.MultiKeyMap;import org.apache.commons.lang.StringUtils; 查看pom.xml中的依赖,发现commons-collections包的版本是2.1.1,里面并没有org.apache.commons.collections.map.MultiKeyMap这个类,于是在pom.xml里面修改了commons-collections的依赖,使用版本3.2.1,如下:12345<dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version></dependency> 重新编译部署后,不再报错。]]></content>
<categories>
<category>后端</category>
</categories>
</entry>
<entry>
<title><![CDATA[Remove Duplicates from Sorted Array]]></title>
<url>%2F2014%2F03%2F13%2Fremove-duplicates-from-sorted-array%2F</url>
<content type="text"><![CDATA[Given a sorted array, remove the duplicates in place such that each element appear only once and return the new length. Do not allocate extra space for another array, you must do this in place with constant memory. For example,Given input array nums = [1,1,2], Your function should return length = 2, with the first two elements of nums being 1 and 2 respectively. It doesn’t matter what you leave beyond the new length. 给定一有序数组,删除所有重复元素,保证每个元素只出现一次,并返回新数组的长度。不能额外申请空间。如给定数组[1,1,2],处理后应返回长度2,数组的前两个元素分别为1,2。其中,剩余元素如何排序没有要求。 12345678910111213public class Solution { public int removeDuplicates(int[] nums) { if (nums.length == 1) return 1; int index = 0; for (int i=1;i<nums.length;i++) { if (nums[index] != nums[i]) { index++; nums[index] = nums[i]; } } return index+1; }}]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
<tags>
<tag>LeetCode</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Thrift简介与实践]]></title>
<url>%2F2014%2F03%2F11%2Fthrift-e7-ae-80-e4-bb-8b-e4-b8-8e-e5-ae-9e-e8-b7-b5%2F</url>
<content type="text"><![CDATA[Thrift是一个支持多种语言的远程服务调用框架,最初由Facebook开发,目前由Apache基金会负责维护。 安装 tar -zxvf thrift-0.9.1.tar.gzcd thrift-0.9.1/./configuremakemake install 安装成功后,可以使用“thrift”命令。 定义远程服务接口Thrift提供了接口描述语言,用于定义远程服务接口,例如,“TestService.thrift”中定义了“greet”方法:1234namespace java com.magicwt.serviceservice TestService { string greet(1:string name)} 使用“thrift”命令根据“TestService.thrift”生成相应的Java代码文件: thrift -r -gen java TestService.thrift 执行成功后,会在gen-java/com/magicwt/service目录下生成TestService.java。 远程服务调用测试创建Maven工程,增加对Thrift的依赖:12345<dependency> <groupId>org.apache.thrift</groupId> <artifactId>libthrift</artifactId> <version>0.9.1</version></dependency> 导入TestService.java,创建TestServiceImpl类实现TestService中的Iface接口,实现其中的greet方法:12345678910111213package com.magicwt.service.impl;import com.magicwt.service.TestService;import org.apache.thrift.TException;public class TestServiceImpl implements TestService.Iface { @Override public String greet(String name) throws TException { return "hi, " + name; }} 创建TestServer和TestClient类分别作为测试服务端和测试客户端。 TestServer: 1234567891011121314151617181920212223242526package com.magicwt;import com.magicwt.service.TestService;import com.magicwt.service.impl.TestServiceImpl;import org.apache.thrift.TProcessor;import org.apache.thrift.protocol.TBinaryProtocol;import org.apache.thrift.server.TServer;import org.apache.thrift.server.TThreadPoolServer;import org.apache.thrift.server.TThreadPoolServer.Args;import org.apache.thrift.transport.TServerSocket;import org.apache.thrift.transport.TServerTransport;import java.net.InetSocketAddress;public class TestServer { public static void main(String[] array) throws Exception { TProcessor processor = new TestService.Processor(new TestServiceImpl()); TServerTransport transport = new TServerSocket(new InetSocketAddress("127.0.0.1", 9001)); Args args = new Args(transport); args.processor(processor); args.protocolFactory(new TBinaryProtocol.Factory()); TServer server = new TThreadPoolServer(args); server.serve(); }} TestClient: 12345678910111213141516171819package com.magicwt;import com.magicwt.service.TestService.Client;import org.apache.thrift.protocol.TBinaryProtocol;import org.apache.thrift.protocol.TProtocol;import org.apache.thrift.transport.TSocket;import org.apache.thrift.transport.TTransport;public class TestClient { public static void main(String[] args) throws Exception { TTransport transport = new TSocket("127.0.0.1", 9001); TProtocol protocol = new TBinaryProtocol(transport); Client client = new Client(protocol); transport.open(); System.out.println(client.greet("admin")); transport.close(); }} 先后启动TestServer和TestClient,TestClient的控制台输出如下:说明远程服务调用成功。]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>Thrift</tag>
</tags>
</entry>
<entry>
<title><![CDATA[使用iptables限制redis端口的访问]]></title>
<url>%2F2014%2F02%2F14%2Fe4-bd-bf-e7-94-a8iptables-e9-99-90-e5-88-b6redis-e7-ab-af-e5-8f-a3-e7-9a-84-e8-ae-bf-e9-97-ae%2F</url>
<content type="text"><![CDATA[redis默认端口是6379,使用iptables限制该端口访问。 设置规则链 新建规则链: iptables -N RH-Firewall-1-INPUT 在规则链中添加可以访问6379端口的规则,表示ip XXX.XXX.XXX.XXX可以访问redis端口,可以多次添加,为不同的ip设置访问权限: iptables -A RH-Firewall-1-INPUT -p tcp –dport 6379 -s XXX.XXX.XXX.XXX -j ACCEPT 在规则链中添加禁止访问6379端口的规则,表示任意ip都禁止访问redis端口: iptables -A RH-Firewall-1-INPUT -p tcp –dport 6379 -s 0.0.0.0/0 -j DROP 规则链设置完成后,如下所示:对于该规则链,若ip在前面4条规则中,则允许访问,否则禁止访问。 在INPUT中增加规则链 iptables -A INPUT -j RH-Firewall-1-INPUT 在FORWARD中增加规则链 iptables -A FORWARD -j RH-Firewall-1-INPUT]]></content>
<categories>
<category>服务器</category>
</categories>
</entry>
<entry>
<title><![CDATA[ProcessBuilder中“Too many open files”错误的解决]]></title>
<url>%2F2014%2F01%2F10%2Fprocessbuilder-e4-b8-adtoo-many-open-files-e9-94-99-e8-af-af-e7-9a-84-e8-a7-a3-e5-86-b3%2F</url>
<content type="text"><![CDATA[在一个系统中程序每隔5分钟会通过ProcessBuilder创建进程执行shell脚本。系统运行一段时间后,会报以下错误: java.io.IOException: Cannot run program “ssh”: java.io.IOException: error=24, Too many open files at java.lang.ProcessBuilder.start(ProcessBuilder.java:460) 通过排查发现是由于进程执行shell脚本后,没有再调用进程的destroy方法。修改后的程序如下:1234567891011121314151617181920212223242526String cmd = "xxx";Process process = null;try { //创建ProcessBuilder实例 ProcessBuilder processBuilder = new ProcessBuilder("ssh", "root@xxx.xxx.xxx", cmd); processBuilder.redirectErrorStream(); //创建进程并启动 process = processBuilder.start(); //输入流 BufferedReader input = new BufferedReader(new InputStreamReader(process.getInputStream())); String line = ""; //读取进程返回的结果 while((line = input.readLine())!= null) { //处理返回的结果 } process.waitFor(); //关闭输入流 input.close();} catch (Exception e) { logger.error("execute [" + cmd + "] fail", e);} finally { //此处作了修改,调用进程的destroy方法销毁进程 if (process != null) { process.destroy(); }} 修改后系统运行不再报错。关于此问题,具体原因如下:Linux系统对于每个进程能打开的最大文件数有限制(一般为65535,可使用“ulimit -n”命令查看该值)。当通过ProcessBuilder创建进程时,STDIN、STDOUT、STDERR会和Java主进程通过输入、输出流建立管道连接,从而占用文件描述符,当进程执行结束后,若不关闭输入、输出流和销毁进程,则会造成Java主进程文件描述符的泄露。因此,需要在进程执行结束后,关闭输入、输出流,销毁进程。]]></content>
<categories>
<category>后端</category>
</categories>
</entry>
<entry>
<title><![CDATA[TinyMCE高级编辑器中的一个乱码问题]]></title>
<url>%2F2013%2F12%2F24%2Ftinymce-e9-ab-98-e7-ba-a7-e7-bc-96-e8-be-91-e5-99-a8-e4-b8-ad-e7-9a-84-e4-b8-80-e4-b8-aa-e4-b9-b1-e7-a0-81-e9-97-ae-e9-a2-98%2F</url>
<content type="text"><![CDATA[系统使用了TinyMCE,最近修改了前端的一个功能,即在提交前,取出TinyMCE编辑器中的内容,使用Ajax方式传入后台,修改内容,再传回前台填入TincyMCE编辑器中。测试时发现内容传回前台后,会经常在有些地方多个“?”号,感觉是内容在传入后台、传回前台时发生了乱码问题,但排查了一下,系统的编码并没有问题,后来再排查并通过从网上查找资料,找到了原因。 原因是在向TinyMCE编辑器中填入内容时,直接拷贝了一段网页上的内容,而这段内容中包含空格符号“ ”(&nbsp;),填入内容后,这个字符为\u00A0,在使用Ajax将内容传回后台时,对这段内容按照UTF-8作URL编码,而这个符号被编码成%C2%A0,在后台解码时,由于后台程序为GBK编码,而\u00A0已经超出了GBK的编码范围,所以解码后这个字符就变为了“?”号。 既然\u00A0在后台不能正常表示,为什么系统之前没有出现“?”号的问题呢?原因是当表单向后台提交内容时,浏览器会自动对\u00A0这个字符进行转化处理,IE浏览器会自动转化为“ ”(&nbsp;),FireFox浏览器会自动转化为“ ”(&#160;)。]]></content>
<categories>
<category>前端</category>
</categories>
</entry>
<entry>
<title><![CDATA[基于Java NIO实现服务端与客户端的非阻塞通信]]></title>
<url>%2F2013%2F10%2F30%2Fe5-9f-ba-e4-ba-8ejava-nio-e5-ae-9e-e7-8e-b0-e6-9c-8d-e5-8a-a1-e7-ab-af-e4-b8-8e-e5-ae-a2-e6-88-b7-e7-ab-af-e7-9a-84-e9-9d-9e-e9-98-bb-e5-a1-9e-e9-80-9a-e4-bf-a1%2F</url>
<content type="text"><![CDATA[Java从JDK 1.4开始支持NIO(New IO),与传统IO相比,NIO是非阻塞的,它包含三个核心组件: Channel,类似于Stream,但不同于Stream单向,Channel是双向的,从同一个Channel既可以读取数据,也可以写入数据; Buffer,向Channel写入数据或从Channel读取数据时,数据需先写入Buffer或读入Buffer; Selector,多个Channel可以注册到一个Selector中,Selector可以通过单线程侦听这些Channel,当有读、写、连接事件时(而不是阻塞等待读、写或连接),执行相应的操作。 基于Java NIO实现服务端与客户端的非阻塞通信如图所示: 服务端代码1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950package com.magicwt;import java.net.InetSocketAddress;import java.nio.ByteBuffer;import java.nio.channels.SelectionKey;import java.nio.channels.Selector;import java.nio.channels.ServerSocketChannel;import java.nio.channels.SocketChannel;import java.util.Iterator;import java.util.concurrent.atomic.AtomicInteger;public class Server { private static AtomicInteger count = new AtomicInteger(1); public static void main(String[] args) throws Exception { // 新建ServerSocketChannel实例,设置为非阻塞,绑定端口8081; ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.configureBlocking(false); serverSocketChannel.socket().bind(new InetSocketAddress(8081)); // 新建Selector实例,并将ServerSocketChannel实例注册到Selector实例中,侦听ACCEPT事件 Selector selector = Selector.open(); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { // 从Selector实例中获取事件 selector.select(); Iterator iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()) { SelectionKey selectionKey = (SelectionKey) iterator.next(); iterator.remove(); if (selectionKey.isAcceptable()) { // 对于ACCEPT事件,新建SocketChannel实例,并将SocketChannel实例注册到Selector实例中,侦听READ事件,通过SocketChannel实例向客户端返回消息 serverSocketChannel = (ServerSocketChannel) selectionKey.channel(); SocketChannel socketChannel = serverSocketChannel.accept(); socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ); System.out.println("连接请求已收到,服务端建立连接"); socketChannel.write(ByteBuffer.wrap(new String("服务端消息" + count.getAndIncrement()).getBytes())); } else if (selectionKey.isReadable()) { // 对于READ事件,通过SocketChannel实例读取客户端消息并向客户端返回消息 SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(128); socketChannel.read(byteBuffer); System.out.println("已读取信息:" + new String(byteBuffer.array())); socketChannel.write(ByteBuffer.wrap(new String("服务端消息" + count.getAndIncrement()).getBytes())); } } } }} 客户端代码123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051package com.magicwt;import java.net.InetSocketAddress;import java.nio.ByteBuffer;import java.nio.channels.SelectionKey;import java.nio.channels.Selector;import java.nio.channels.SocketChannel;import java.util.Iterator;import java.util.concurrent.atomic.AtomicInteger;public class Client { private static AtomicInteger count = new AtomicInteger(1); public static void main(String[] args) throws Exception { // 新建SocketChannel实例,设置为非阻塞,连接127.0.0.1:8080; SocketChannel socketChannel = SocketChannel.open(); socketChannel.configureBlocking(false); socketChannel.connect(new InetSocketAddress("127.0.0.1", 8081)); // 新建Selector实例,并将SocketChannel实例注册到Selector实例中,侦听CONNECT事件 Selector selector = Selector.open(); socketChannel.register(selector, SelectionKey.OP_CONNECT); while (true) { // 从Selector实例中获取事件 selector.select(); Iterator iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()) { SelectionKey selectionKey = (SelectionKey) iterator.next(); iterator.remove(); if (selectionKey.isConnectable()) { // 对于CONNECT事件,新建SocketChannel实例,并将SocketChannel实例注册到Selector实例中,侦听READ事件,通过SocketChannel实例向服务端返回消息 socketChannel = (SocketChannel) selectionKey.channel(); if (socketChannel.isConnectionPending()) { socketChannel.finishConnect(); } socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ); System.out.println("客户端建立连接"); socketChannel.write(ByteBuffer.wrap(new String("客户端消息" + count.getAndIncrement()).getBytes())); } else if (selectionKey.isReadable()) { // 对于READ事件,通过SocketChannel实例读取服务端消息并向服务端返回消息 socketChannel = (SocketChannel) selectionKey.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(128); socketChannel.read(byteBuffer); System.out.println("已读取信息:" + new String(byteBuffer.array())); socketChannel.write(ByteBuffer.wrap(new String("客户端信息" + count.getAndIncrement()).getBytes())); } } } }} 分别启动服务端和客户端程序,输出如下所示。服务端:客户端:]]></content>
<categories>
<category>后端</category>
</categories>
</entry>
<entry>
<title><![CDATA[Java开发问题总结]]></title>
<url>%2F2013%2F10%2F12%2Fjava-e5-bc-80-e5-8f-91-e9-97-ae-e9-a2-98-e6-80-bb-e7-bb-93%2F</url>
<content type="text"><![CDATA[最近在做开发时,遇到了以下四个问题,总结一下。 Nginx+Struts中上传文件的大小限制。在上传文件时,若文件太大会出现上传失败。我们的Web应用前端代理使用了Nginx,MVC使用了Struts,通过检查,发现在Nginx和Struts中对于上传文件的大小都做了限制。在Nginx的http模块配置中,client_max_body_size这个参数用于表示http请求body最大值,由于文件也是以二进制形式存储于body中,因此client_max_body_size也限制了上传文件的大小,当上传文件较大时,可适当调大该值。12345http { ... client_max_body_size 20m; ...} 在Struts中,对于上传文件大小也有限制,默认大小为2M,可以在Struts配置文件中修改该默认配置。1<constant name="struts.multipart.maxSize" value="104857600"/> Nginx代理对于请求响应超时时间的设置。外部应用调用我们提供的http接口时,有时会返回504错误,但查看我们的应用日志,请求是被正确执行了,再查看Nginx配置,发现在Nginx代理配置中,proxy_read_timeout这个参数用于表示Nginx转发请求至实际应用后等待响应的时间,若超过该时间,则向客户端返回超时错误,通过适当调大该值解决问题。 同一个应用部署在不同服务器上导致的乱码问题。为了实现高可用,我们将同一个应用部署在两台服务器上,前端通过Nginx实现负载均衡。启动后,发现应用有一个功能存在乱码问题,该功能是接收客户端传来的GBK编码字符串,并调用另一文件服务接口写到另一个服务器的磁盘中。我们发现该乱码问题只在客户端请求转发至这两台服务器上中的其中一台时存在。通过检查发现,应用在接收客户端传来的GBK编码字符串时,会调用字符串的getByte()方法,进行解码,而getByte()方法在不指定编码格式时,会使用服务器的默认编码,而这两台服务器中,一台服务器的默认编码是GBK,能正确解码,另一台服务器的默认编码是UTF-8,则不能正确解码。后来的解决方法是:由于应用是部署在resin容器中,因此修改resin的配置文件,在jvm参数配置中,指定服务器的默认编码为GBK。 \<jvm-arg>-Dfile.encoding=GBK\</jvm-arg> Web应用负载过高有一个Java Web应用部署在resin容器中,每隔一端时间,会导致服务器负载过高,但查看后台并没有报错信息,因而以前每次只能通过重启应用来解决该问题。后来在网上找到一个解决该类问题的方法: 使用ps查看应用的进程id; 使用“top -H -p 进程id”查看进程中的线程列表,排在前面的线程占CPU较高; 使用“jstack 进程id”dump Java进程使用情况; 从Java进程dump中找到在2)中排在前面占CPU较高的线程,需要注意的是2)中的线程号是十进制,而3)中的线程号是十六进制,需要做一下转换,我们找到的占CPU较高的线程在dump中如下: “http–8080-37$65442827” daemon prio=10 tid=0x00002aaabc0f3000 nid=0x2c53 runnable [0x000000004517b000]java.lang.Thread.State: RUNNABLEat java.util.HashMap.get(HashMap.java:303)at com.sohu.cms.auth.impl.AuthenticationImpl.hasAction(AuthenticationImpl.java:170)at com.sohu.cms.webapp.util.AuthenticationInterceptor.intercept(AuthenticationInterceptor.java:62) 发现问题发生在HashMap,这里我们定义了一个static HashMap对象,在多个客户端发起请求时,会并发访问该HashMap对象时,由于HashMap并不是线程安全的,因而可能会引起问题。我们在网上找到一个问题现象和原因和我们类似的例子:http://shuaijie506.iteye.com/blog/1815213 ,其中提到问题根本原因是HashMap在resize时会造成死循环。后来的解决方案是我们使用线程安全的ConcurrentHashMap来替代HashMap。]]></content>
<categories>
<category>后端</category>
</categories>
</entry>
<entry>
<title><![CDATA[JMS和ActiveMQ介绍(4)_ActiveMQ]]></title>
<url>%2F2013%2F09%2F30%2Fjms-e5-92-8cactivemq-e4-bb-8b-e7-bb-8d4-activemq%2F</url>
<content type="text"><![CDATA[ActiveMQ高可用性下面,我们再看看ActiveMQ是如何实现高可用的。ActiveMQ实现高可用有两类方案: 第一类方案是构建服务器网络,消息在服务器网络中进行传递,客户端通过failover或discovery连接网络中的一个服务器发送或接收消息,当服务器失效时,客户端自动重连另一个服务器。 第二类方案是构建服务器主从集群,在某个时间只有一个服务器作为主对外提供服务,当主服务器失效时,从服务器切换成主服务器对外提供服务,客户端自动重连至新的主服务器。 MasterSlave有4种具体实现方案,下面将介绍一下前3种,第4种在官网上已有介绍,是基于HahaDBA和Zookeeper的,但目前还不支持该功能。 PureMaster SlavePureMaster Slave需要两个启动两个ActiveMQbroker,主broker不需要做额外配置,从broker中需要作如下配置,在其中设定主broker的连接,这样在从broker启动后,会建立与主broker的连接,并对主broker的消息进行同步。当主broker失败时,从broker自动切换成主broker。另外,在使用PureMaster Slave时,需要客户端使用failover方式连接ActiveMQbroker,并设定randomize的值为false,这样,客户端在连接主broker失败后,会自动尝试连接从broker。PureMaster Slave配置简单,易于实现,之前是ActiveMQ比较推荐的高可用解决方案,但在Pure Master Slave中,消息传送必须经过主、从服务器的确认,另外Pure Master Slave还存在以下不足: 主服务器只能有一个从服务器; 主服务器失败重启后,还必须重启从服务器才能恢复主服务器; 主、从服务器之间没有自动同步机制。 我们在实践中也遇到过由没有自动同步机制导致的问题:有一次使用中我们创建持久订阅者连接服务器,开始时,持久订阅者的状态在主从上是同步的,均为活跃,但在做了一次重启从服务器的操作后,从服务器中原先活跃的持久订阅者变更为了非活跃,与主服务器状态不一致,所以发往主从的消息由于从服务器上订阅者为非活跃状态不消费造成消息阻塞。由于PureMaster Slave具有以上不足,因此在ActiveMQ5.8.0版本中已去掉了该功能。 JDBC Master Slave使用基于JDBC的Master Slave时,多个消息服务器实例尝试获取数据库排他锁(exclusive lock),只有一个成功获取锁,对外提供服务,成为主服务器,其他等待。客户端连接采用failover方式,从多个地址中找到当前可用的消息服务器实例并连接。当Master失去排他锁或失去数据库连接时,Master将会关闭,其他服务器实例通过竞争,由一个实例获取锁成为新Master,客户端通过failover重新连接至新Master。原Master在重新启动后成为Slave等待锁。 SharedFile System Master SlaveSharedFile System MasterSlave的实现原理与JDBCMaster Slave类似,多个服务器共用一个存储消息的文件系统(SAN);需要确认文件系统存在文件读写锁,获取读写锁的服务器自动成为主服务器,直至失败放弃读写锁;客户端也是使用failover进行连接。 ActiveMQ安全机制下面是关于ActiveMQ的安全机制。ActiveMQ的安全机制包括两部分: 验证,通过访问者的用户名和密码实现用户身份的验证; 授权,为消息目标(队列或主题)的读、写、管理指定具有相应权限的用户组,并为用户分配权限。 ActiveMQ的安全机制基于插件实现,可以灵活配置。ActiveMQ提供两种验证插件,分别是: 简单验证插件; JAAS验证插件。 ActiveMQ提供一种授权插件。 验证插件配置以上是简单验证和JAAS验证插件的配置示例,均需要配置用户名、用户组和密码。设置插件后,在创建连接时,需要输入账号和密码。1connectionFactory.createConnection(“user”,”password”); 授权插件配置以上是授权插件的配置示例,可以设置某一个队列、主题或某一类队列、主题的读、写、管理权限组。 简单验证插件源码分析下面再通过简单验证插件源码介绍一下ActiveMQ的插件机制以及验证是如何实现的ActiveMQ的插件机制如图所示,主要涉及BrokerPlugin和BrokerFilter这两个接口和类。BrokerPlugin中的installPlugin方法用于安装插件,在installPlugin方法中会传入Broker对象,而BrokerFilter是Broker的实现,在BrokerFilter中包含着一个对Broker的引用next,可以在installPlugin方法中,新建BrokerFilter对象,并将installPlugin方法传入Broker对象赋值给next,这样多个BrokerFilter对象就通过next引用形成链。这与struts中Interceptor的原理是类似的。对于简单验证插件,在SimpleAuthenticationBroker类中,重写了BrokerFilter的addConnnection方法,在新建连接时,若允许匿名访问,则不进行验证,若不允许匿名访问,则验证连接的用户名和密码是否与配置文件中的一致,若不一致,则抛出安全异常。 通知消息通知信息机制是ActiveMQ服务器通过发布特定的通知消息记录服务器和客户端的操作日志,便于系统监控。另外,ActiveMQ服务器网络之间的通信也是基于通知消息的。发送通知消息的操作有: 消费者、生产者和连接的创建和关闭; 队列、主题的创建和销毁; 消息的过期; 消息发送至无消费者的目的地址。 通知消息发布至相应的主题上,通知消息的主题有两类: 第一类是基于客户端的主题,包括连接、生产者、消费者的创建和关闭,例如连接的创建和关闭消息发布在Connection主题上; 第二类是基于目的地址和消息的主题,包括队列、主题的创建和销毁,消息的过期等,例如,队列、主题的创建和销毁消息发布至Queue和Topic主题上。 ActiveMQ消息组这一页介绍一下有关排他消费者和消息组的相关概念。对于队列,当有多个消费者时,消息服务器将把消息平均地发送给每个消费者,实现负载均衡,但是多个消费者接收、处理消息时并不会在线程上同步,这就导致消息在并发处理时,无法保证消息原有的顺序。排他消费者(Exclusive Consumer)是在多消费者情况下,消息服务器指定一个消费者接收消息,这样可以保证消息的有序性。其他消费者处于等待状态。当原先指定的消费者无法接收消息时,消息服务器将从等待的消费者中再指定一个消费者接收消息。排他消费者可以在多个消费者的情况下,保证消息的有序性,且在单个消费者故障时,能够保证消息继续被消费,但由于每次只有一个消费者消费消息,造成吞吐量低,其他消费者的资源浪费。使用消息组(message group)可以避免排他消费者的不足。消息组是指通过设定消息的JMSXGroupID属性来为消息进行分组,而同一组消息将被发送至同一个消费者进行处理,因此可以保证在多消费者情况下,消息能够有序处理。 ActiveMQ虚主题之前我们讨论过,对于主题,若订阅者不连接,则存在消息丢失,若需要保证消息全部接收,则需要创建持久订阅者,但对于持久订阅者,由于其需要指定ClientID,因此只能保证同一个ClientID只有一个消费者、一个线程访问主题,这就导致以下两个问题: 无法实现消息消费的负载均衡; 消费者失败时,不能由其他的消费者恢复。 如何同时既能实现主题的特性,同一个消息由多个消费者消费,也能实现队列的特性,多消费者实现负载均衡?可以通过使用虚主题解决上述问题。虚主题以VirtualTopic.主题名的形式命名,当配置虚主题后,ActiveMQ可以将该主题映射到以Consumer.队列名.VirtualTopic.主题名的形式命名的队列上,消息生产者仍将消息发送至主题,而ActiveMQ会将消息再转发至队列上,消息消费者通过使用队列接收来自主题的消息,这样就可以启动多个消费者,实现负载均衡。虚主题的配置如图所示。 ActiveMQ多语言支持ActiveMQ支持除Java以外的多种语言,包括C/C++,.NET,Perl,PHP,Python,Ruby等,对于脚本语言,一般采用STOMP协议连接ActiveMQ。STOMP是一种类似于HTTP的协议,支持命令有:SEND,SUBSCIRBE,UNSUBSCRIBE等,在使用STOMP连接ActiveMQ时,首先需要在ActiveMQ配置STOMP连接,通常一般占用61613端口。以下分别是Python和PHP使用STOMP连接消息服务器并接收主题消息的示例代码。对于C,ActiveMQ提供C++ API和.NET API,对于Web编程,ActiveMQ提供RESTAPI和AjaxAPI,大家有兴趣可以去尝试一下。 在Java应用中嵌入ActiveMQ服务器在使用消息服务器时,有时候我们不希望ActiveMQ在单独的JVM中运行,而是集成在已有的Java应用中。在Java应用中嵌入ActiveMQ服务器有如下几种方法:创建BrokerService实例;使用BrokerFactory方法创建BrokerService实例。]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>ActiveMQ</tag>
</tags>
</entry>
<entry>
<title><![CDATA[JMS和ActiveMQ介绍(3)_ActiveMQ]]></title>
<url>%2F2013%2F09%2F30%2Fjms-e5-92-8cactivemq-e4-bb-8b-e7-bb-8d3-activemq%2F</url>
<content type="text"><![CDATA[ActiveMQ介绍首先简单介绍一下ActiveMQ。ActiveMQ是由Apache软件基金会提供的开源免费消息服务器,目前版本是5.8.0。ActiveMQ具有以下特点: 基于JMS 1.1和J2EE 1.4规范; 支持多种连接协议:HTTP/S,IP组播,SSL,STOMP,TCP,UDP,XMPP等; 支持多种消息持久化机制:文件形式持久化(KahaDB),关系数据库形式持久化(JDBC); 插件化的安全机制:ActiveMQ支持插件开发,并且它的安全机制就是以插件形式实现灵活配置的; 支持嵌入Java应用,ActiveMQ就可以作为一个单独服务,也可以直接嵌入到其他Java应用中; 支持与应用服务器进行集成:支持Apache Tomcat,Jetty,ApacheGeronimo,Jboss; 多种语言的客户端API:支持C/C++,.NET,Perl,PHP,Python,Ruby等; 集群; 动态化的简单管理。 ActiveMQ文件包ActiveMQ使用Java开发,使用Spring配置bean,使用Maven构建。ActiveMQ 5.6.0的源代码目录结构如图所示,从pom文件中可以看出,共包含23个模块,比如核心模块activemq-core,web控制台模块activemq-web-console,文件形式的消息持久化模块kahadb等。使用Maven编译、打包后,在assembly/target下ActiveMQ的生成可执行包。ActiveMQ的执行包目录结构如图所示: bin目录下是启动脚本; conf目录下是配置文件; data目录下是消息持久化的文件; webapps是web控制台目录。 ActiveMQ的启动、配置通过bin目录下的脚本可以启动ActiveMQ服务,从配置文件可以看出,ActiveMQ服务也是通过Spring来进行配置的,整个配置文件实质就是对一个broker实例进行相关配置,从命名空间可以看出,ActiveMQ broker实例其实就是一个org.apache.activemq.xbean. XBeanBrokerService类的实例,而XBeanBrokerService继承自org.apache.activemq.broker.BrokerService,BrokerService是ActiveMQ中的一个核心类,用于对外提供消息服务。ActiveMQbroker的配置包含以下部分: 目的地策略:用来对目的地(队列、主题)的相关策略进行配置,包括流量控制、分发策略等; JMX管理:用来配置JMX; 持久化设置: 存储设置: 连接器设置: Web控制台设置:ActiveMQ通过内嵌Jetty,用来提供Web控制台,能够对连接、主题、队列、消费者等进行可视化的管理。 ActiveMQ通配符在ActiveMQ中,队列或主题的地址名称可以使用“.”号分割的方式来表示,例如queue.news.subject0-10。并且ActiveMQ支持使用通配符表示某一类队列和主题的地址,通配符包括: .,用于分割地址中的名称; *,用于匹配“.”之间的任意字符; >,用于匹配任意字符。 目的地策略在目的地策略中,包含多个策略实体,策略实体既可以描述某一个队列或主题的策略,也可以通过通配符描述某一类队列或主题的策略。目的地策略配置示例如上所示,该策略实体对所有主题进行配置,对每个主题进行流量控制,内存限制为1M,将消息引用保存在内存中。图中列出了策略实体的部分属性,全部属性可以从ActiveMQ官网查看到。 策略实体子标签策略实体中还包含一些子标签用于设置一些具体的策略,以下列出部分子标签: 消息分发策略; 死信队列策略:当消息重复发送多次仍未成功时,ActiveMQ将向死信队列发送一个消息,死信队列策略对该操作进行配置,比如设置使用共享的死信队列或单独的死信队列; 消息撤销策略:当消息消费较慢需要删除消息时使用,比如撤销最久的消息; 消息数量限制策略:用于设置持久化消息数量最大值; 订阅恢复策略:当订阅者重新连接时使用,比如只恢复最后一个消息; 持久订阅这的消息引用策略,队列的消息引用策略,订阅者的消息引用策略:用于设置消息应用存储位置,比如将消息引用存储于内存中; 慢消费者策略:当消息消费较慢时使用,比如直接退出。 分发策略下面具体再介绍一下分发策略。首先介绍一下预读取机制,为了提高消息消费速度,在一次消息接收过程中,ActiveMQ通过预读取机制将消息尽可能多地推送给消费者,在消费者客户端缓存。但为了防止消费者缓存溢出,ActiveMQ通过prefetchlimit控制当前推送给消费者且未收到确认的消息数量。prefetch limit的默认值是:持久化消息队列为1000,非持久化消息队列为1000,持久化消息主题为100,非持久化消息主题为-1。若prefetch limit=1,则消费者每次只会接收一个消息,相当于关闭预读取机制。若prefetchlimit=0,则消息只有在消费者主动拉取时才会被接收,而不会被推送给消费者。对于分发策略,有如下具体的策略,其中roundRobin策略是当有多个消费者时,将消息平均地发给各个消费者,而不是采用预读取机制先将消息全部发往某个消费者直至达到prefetch limit。而strictOrder策略相反,是将消息先全部发往某个消费者,但该策略可以保证当主题中有多个消息生产者,且有多个消息消费者时,每个消息消费者接收到的消息顺序是一致的。 流量控制流量控制是为了防止消息生产较快,而消费较慢,导致队列或主题堵塞。在ActiveMQ5.0之前,ActiveMQ使用TCP协议本身的流量控制机制,这种方法的不足是只能对整个连接进行流量控制,而不能对单个生产者进行流量控制,而且当多个消息生产者和消费者使用同一个连接时可能会造成死锁。从ActiveMQ5.0开始,ActiveMQ支持对单个生产者进行流量控制。流量控制可在生产者客户端和服务器端进行配置。如果生产者客户端异步发送消息(useAsyncSend置为true),发送消息时线程不会阻塞等待消息服务器返回确认,此时就需要在生产者客户端配置流量控制,通过setProducerWindowSize设置一个最大值,即生产者发送的未接收到确认的消息不能超过该最大值,若超过,则等待。服务器端的流量控制配置在两个地方: 首先在目的地策略中,通过producerFlowControl可以对每个目的地设置是否进行流量控制,memoryLimit表示消息存储在内存中的最大量,vmCursor表示在内存中仅保存消息的游标,这样可以在内存中存储尽可能多的消息。 在存储设置中,可以对整个消息服务器的存储用度进行配置,memoryUsage表示ActiveMQ使用的内存,storeUsage表示持久化消息存储文件的大小,tempUsage表示非持久化消息存储的文件大小。在存储配置中还可以设置当存储用度不足时系统如何处理,除默认等待外还支持sendFailIfNoSpace,sendFailIfNoSpaceAfterTimeout。 JMX管理ActiveMQ支持JMX,在ActiveMQ中配置JMX的示例如下所示。在启动后,可以通过Java工具jconsole连接ActiveMQ,对其进行监控。 ActiveMQ消息持久化模型对于消息持久化,消息以先进先出的方式存储于队列中。只有接收到消费者的确认,消息才会从队列中出队。对于具有持久订阅者的主题,主题中只保存一份消息。每个持久订阅者保存一个指向最后一个接收消息的指针。只有接收到所有持久订阅者对于消息的确认,消息才会从主题中删除。 消息存储机制下面将介绍4种消息存储机制。 KahaDBKahaDB是ActiveMQ推荐的消息存储机制,它基于文件,是最快的一种消息存储机制。KahaDB的实现机制如图所示,首先所有消息数据追加写入log文件,log文件的大小有限制,若达到限制,则创建一个新文件,若log文件中的消息已全部发送出去,则该文件被删除。队列、主题使用B树数据结构存储,这样能够快速查询到其中的消息,B树中的消息实际存储对log文件中数据的引用。同时队列、主题的消息还保存在缓存中,以提高访问速度。 AMQ与KahaDB类似,基于文件,但与KahaDB不同的是,每个队列有独立的索引文件,多用于消息量大的场景,但不适用于队列多的场景。 ActiveMQ连接在客户端连接服务器或服务器之间互连时,ActiveMQ支持多种连接协议,以下是这些协议以及使用说明,其中,TCP是ActiveMQ默认使用的网络协议,STOMP是一种面向简单文本的消息协议,主要用于多语言支持,实践中,我们在PHP和Python的客户端连接服务器时,使用了该协议,VM主要用于访问在同一个JVM中运行的服务器。在客户端使用vm连接消息服务器时,如vm://brokerName,若同一个JVM内存在以该brokerName命名的消息服务器实例,则连接至该实例,若不存在,则创建一个以该brokerName命名的消息服务器实例,并连接。在连接时采用的URI格式如下所示,第一种是单一URI,表示一个消息服务器连接地址,第二种是组合URI,即将多个消息服务器地址组合起来。在消息服务器配置中,连接有两种配置:transportconnector,用于配置客户端与服务器之间的连接,向客户端提供连接端口,ActiveMQ通常占有61616端口对外提供tcp连接;networkconnector,用于配置服务器与服务器之间的连接,实现服务器网络,下面将具体介绍ActiveMQ的两种服务器网络。 静态网络首先是静态网络。在网络中服务器配置已知的情况下,可以使用static创建静态网络,例如已知网络中已有一消息服务器BrokerB,则在BrokerA的配置中可以添加以上配置,在启动BrokerA时,BrokerA会创建与BrokerB的连接,当生产者将消息发送至BrokerA,且消费者从BrokerB接收该消息时,BrokerA会自动将消息转发至BrokerB。在这种配置下,BrokerA至BrokerB的连接是单向的,即消息只能从BrokerA转发至BrokerB。如果需要将连接配置成双向的,可以将duplex属性置成true,这样,BrokerA和BrokerB即可以向对方转发消息,也可以从对方接收消息。 动态网络静态网络需要已知服务配置情况,且不易于进行后期扩展,通过使用动态网络可解决以上问题。在动态网络中,每个消息服务器需要进行以上配置。在服务启动后,会自动使用IP组播在网络中寻找其他消息服务器实例,并创建连接。 客户端连接在客户端连接消息服务器时,既可以使用单一URI连接单个服务器,也可以使用组合URI从多个服务器中选择一个进行连接。在组合URL中,failover是一种比较常用的客户端连接方式,使用failover时,客户端会从多个服务器地址中随机选择一个进行连接,当连接失效时,会尝试连接其他的服务器。如果只连接一个服务器,也建议在服务器地址前再加上failover,这样可以建立重连机制,提高系统健壮性。discovery与failover类似,但是通过组播从动态网络中查询可用的服务器,并从中随机选择一个进行连接,当连接失效时,也会尝试连接其他的服务器。peer与vm类似,在使用peer连接时,会自动在JVM内创建服务器,另外,还会在建立此服务器与网络中同组服务器的连接。peer的应用场景是客户端与服务器经常会有连接失效发生,但又需要在连接失效时,客户端仍可以正常工作。使用peer,客户端可以在本地JVM内创建服务器并与其通信,当与远程服务器连接正常时,本地服务器会再与远程服务器进行通信。fanout可以向静态网络或动态网络发送消息,网络中的每个服务器都会接收到消息。]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>ActiveMQ</tag>
</tags>
</entry>
<entry>
<title><![CDATA[JMS和ActiveMQ介绍(2)_JMS]]></title>
<url>%2F2013%2F09%2F30%2Fjms-e5-92-8cactivemq-e4-bb-8b-e7-bb-8d2-jms%2F</url>
<content type="text"><![CDATA[JMS APIJMSAPI可以分为3个主要部分:公共API、队列API和主题API。JMSAPI中,ConnectionFactory和Destination既可以作为受管对象,由JMS提供者创建,并使用JNDI从提供者获得,也可以直接动态创建;其他接口通过工厂方法创建,比如Session可以通过Connection创建;消息生产者和消费者一般仅创建一个连接(Connection),而可以创建多个会话(Session);由Session保存用于消息发送的事务性工作单元。通过Session可以创建Message,MessageProducer,MessageConsumer。 JMS消息消息可分为3部分:消息头、属性和有效负载。其中消息又分为自动分配消息头和开发者分配消息头。自动分配的消息头: JMSDestination,消息目的地。 JMSDeliveryMode,消息传送模式,默认设置为持久性模式。 JMSMessageID,消息ID。 JMSTimestamp,JMS提供者接收消息的时间。 JMSExpiration,消息过期时间,默认值为0,即永不过期。 JMSRedelivered,是否是重发的消息。 JMSPriority,0~4级是普通优先级,而5~9级则是加急优先级。 开发者分配的消息头: JMSReplyTo,接收消息后发送响应消息的目的地。 JMSCorrelationID,与消息相关联的特定ID,在大多数情况下,用于接收消息后发送响应消息时,记录上一个消息的ID。 JMSType,消息类型。 属性:与消息头类似,由开发者添加,支持String、Int、Boolean、Double、Float、Byte、Long、Short、Object等类型,可用于消息选择;有效负载,即实际传输的消息内容。根据要携带的有效负载类型,JMS定义了6种消息接口: Message,不含有效负载; TextMessage,携带一个String作为有效负载; ObjectMessage,携带一个可序列化Java对象作为有效负载; BytesMessage,携带一组原始类型字节流; StreamMessage,携带一个Java原始数据类型流; MapMessage,携带一组键值对作为有效负载。 消息持久化,通过JMSDeliveryMode设定。在持久性模式下,消息在服务器端被持久化保存,直至消费者接收到该消息,因此可以保证消息成功发送有且仅有一次,而非持久性模式只能保证消息最多发送一次,而不能保证消息被成功接收。 生产者对于消息生产者相关API的使用,我们分队列和主题分别介绍。在队列下:通过ActiveMQ提供的连接工厂类创建queueConnection。queueConnection调用start方法真正建立与消息服务器的长连接。调用queueConnectoin的createQueueSession方法创建负责消息相关操作的会话单元。调用queueSession的createQueue、createSender、createMessage方法分别创建队列、发送者和消息。最后调用queueSender的send方法发送消息,消息发送是一个同步操作,在send方法中线程会被阻塞,直到接收到来自消息服务器的确认。在主题下,消息生产过程中各接口的调用方法与队列下的基本类似,只是使用了与主题相关的一些接口。因此,对于消息生产,可以使用公共API中的接口来完成。使用公共API中的接口实现消息生产者,其中使用了公共API中的接口Connection、Session、Destination和MessageProducer。在创建destination时,调用createTopic方法和createQueue方法来分别创建主题和队列。 消费者对于消息消费者相关API的使用,我们也分队列和主题分别介绍。在队列下,调用queueSession的createReceiver方法创建接收者。调用queueReceiver的receive方法接收消息,在receive方法中,线程将被阻塞直至接收到消息。也可以在调用receive方法时,设置最长延时,若该时间内未收到消息,receive方法将直接返回。在主题下,调用topicSession的createSubscriber方法创建订阅者。另外需要通过实现MessageListener接口,实现消息侦听器,并重写侦听器的onMessage方法。通过topicSubscriber的setMessageListener将侦听器注册到订阅者中。当消息服务器将一条消息推向topicSubscriber时,topicSubscriber将调用侦听器的onMessage方法对消息进行具体处理。使用公共API中的接口实现消息消费者。对于队列和主题,除了在创建destination时的方法不同外,在消息接收时,队列下的consumer是通过receive方法阻塞线程主动接收消息,主题下的comsumer是通过设置侦听器侦听推送过来的消息。 持久订阅者对于持久订阅者,它与普通订阅者的区别是:对于普通订阅者,在不连接时,发送的消息将被丢失,连接后这些消息不会被接收,而对于持久订阅者,在不连接时,发送的消息将被保存,连接后这些消息会被正常接收。持久订阅者的创建与普通订阅者的创建有两点不同:一是在连接中需要设置ClientID,二是在创建订阅者时调用createDurableSubscriber方法创建持久订阅者,并设置订阅者名称。消息服务器将ClientID和订阅者名称作为持久订阅者的唯一标识符,在不连接时,为其保留一份消息副本,在连接时,将消息发送至该持久订阅者。若要彻底关闭持久订阅者,使消息服务器不再为其保留消息副本,则需要调用unsubscribe方法。持久订阅者的优点是不管订阅者是否连接,消息都能被正常接收。持久订阅者的缺点是若订阅者一直不连接,消息将被一直保存,造成存储压力。因此是否选用持久订阅者要根据具体应用场景来决定,若需要保证订阅者在有连接丢失的情况下仍能接收到所有消息,则采用持久订阅者,若只需要订阅者在连接时接收消息,且允许有一定的消息丢失,则采用普通订阅者。 QueueBrowser队列中的消息只能被一个消费者消费,若查看队列中的消息,但不消费消息,可以使用QueueBrowser。通过调用getEnumeration方法可以按照消息接收顺序获取到当前队列中的所有消息。 消息选择器对于消费者,有时候需要处理满足特定条件的消息,一个可行的解决方案是在处理消息前,增加条件判断,对于不满足条件的消息不作处理,但这个方案存在的一个问题是由于消息已经被该消费者接收,但不会被处理,从而造成网络带宽的浪费,另外不作处理的消息也不能再被其他可能会处理这些消息的消费者接收。我们可以通过消息选择器解决上述问题。使用消息选择器可以实现消息的过滤,只接收满足特定要求的消息。消息选择器可以增加的条件包括消息头和消息属性。消息头中可以用作选择条件的有JMSDeliveryMode,JMSPriority,JMSMessageID,JMSTimestamp,JMSCorrelationID,JMSType,例如可以选择权重值较高的消息。在消息属性中可以设置业务属性,这样就可以选择满足特定业务需求的消息。消息选择器的基本表达式由标识符、比较运算符和常量组成,例如,JMSPriority> 5,即选择权重大于5的消息。多个基本表达式可以由AND、OR组成复杂表达式,例如,JMSPriority> 5 AND Oper= ‘Delete’,即选择权重大于5且业务上操作类型为“Delete”的消息。消息选择器在创建消费者时设置,如图所示。 消息确认机制对于持久化消息,消息服务器会对消息进行确认,以保证消息成功发送。消息确认机制有如下几种:AUTO_ACKNOWLEDGE,DUPS_OK_ACKNOWLEDGE,CLIENT_ACKNOWLEDGE,消息确认机制可以在创建session时指定。AUTO_ACKNOWLEDGE是由消息服务器自动完成消息确认。消息的发送和接收是两个异步过程,因此分别讨论这两个过程中AUTO机制的消息确认。消息发送时: 生产者发送消息,生产者阻塞线程等待消息服务器接收到消息的确认。 消息服务器接收到消息后,对消息进行持久化。 持久化成功后,消息服务器向生成者发送确认通知。 生产者接收到确认通知后,从发送方法返回。 消息接收时: 消费者接收消息。 消费者发送确认通知。 消息服务器从存储中删除消息。 DUPS_OK_ACKNOWLEDGE不同于AUTO对单条消息进行确认,是一种延时、批量确认机制,这样做的一个好处是可以减少因单条消息确认而带来的系统开销,但带来的一个问题是可能将一条消息向同一目的地发送两次以上,因此,适用于可以重复接收消息的应用场景。 CLIENT_ACKNOWLEDGE是由消费者通过调用消息的acknowledge方法进行确认,如果消费者使用CLIENT机制,且调用acknowledge方法,那么在上次确认之间所发的未被确认的消息都将被确认。 INDIVIDUAL_ACKNOWLEDGE也是由消费者通过调用消息的acknowledge方法进行确认,但不同于CLIENT机制,每条消息都需要调用acknowledge方法进行确认。 事务消息事务可以保证: 如果在一个会话中,只有消息生产者或只有消息消费者,则对于消息生产者,事务将保证其所有消息都发送至服务器或都不发送,而对于消息消费者,事务将保证其接收所有消息或都不接收。 如果在一个会话中,既有消息生产者,又有消息消费者,则事务将保证所有消息都发送并接收或都不发送和接收。 创建会话时可以设置是否使用事务。多次操作后通过调用commit()提交。在出现异常时,通过调用rollback()回滚。 JMS和SpringSpring对JMS提供支持,在客户端可以通过Spring配置连接、生产者和消费者。通过spring提供的DefaultMessageListenerContainer可以设置消息的侦听,通过spring提供的JmsTemplate可以进行消息的发送和接收。]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>ActiveMQ</tag>
</tags>
</entry>
<entry>
<title><![CDATA[JMS和ActiveMQ介绍(1)_消息传送机制]]></title>
<url>%2F2013%2F09%2F30%2Fjms-e5-92-8cactivemq-e4-bb-8b-e7-bb-8d1-e6-b6-88-e6-81-af-e4-bc-a0-e9-80-81-e6-9c-ba-e5-88-b6%2F</url>
<content type="text"><![CDATA[JMS,java消息服务是由Sun提出的一套消息传送API规范,而ActiveMQ是一款开源的消息服务器产品,前一段在组内做过一次有关《JMS和ActiveMQ介绍》的技术分享,今天把幻灯片整理了一下,希望和大家多交流。 什么是消息传送机制消息传送机制是在多个系统之间引入消息服务器,由消息服务器接收来自各系统的消息并将消息转发到相应的系统上,以实现各系统之间的通信。而在每个系统中,应用程序通过调用消息传送API来进行消息的发送和接收,消息传送API再通过各消息服务器所对应的消息传送客户端将消息发送至服务器或从服务器接收消息。目前,消息服务器产品中,属于商业软件的有:IBM WebSphere MQ、SonicMQ、Microsoft Message Queuing(mSMQ);属于开源软件的有:ActiveMQ、OpenMQ、RabbitMQ。虽然消息服务器产品较多,但基本遵循同一个消息传送API规范,即JMS(Java Message Service,Java消息服务): JMS是由Sun发起创建; 它与JDBC类似,只定义消息传送的相关接口,由消息服务器产品自己实现相应的接口功能; 因此,在使用不同的消息服务器产品时,系统中的应用程序可以使用同一个消息传送API。 消息传送机制的优点消息传送机制具有以下优点: 可以实现异构集成,不同平台、不同语言的系统可以通过消息进行通信,实现集成。例如,ActiveMQ支持多语言,除Java以外,还包括C、PHP、Python。 可以缓解系统瓶颈,当系统同步处理的请求数量增大时,会造成请求阻塞,如果使用消息传送机制,可以将请求以消息方式发送至消息服务器,并由多个请求处理模块接收消息进行并发处理。 可以提高可伸缩性,这个与缓解系统瓶颈类似,通过增加或减少消息接收者来控制并发处理的能力,提高可伸缩性。 可以提高最终用户生产率,这是因为使用消息传送机制时,可以对请求进行异步处理,请求以消息方式发送至消息服务器后,最终用户无需同步等待请求返回结果。 体系结构灵活性和敏捷性,我们知道系统设计的一个基本原则就是高内聚、低耦合,通过引入消息传送机制,各系统服务以消息的形式抽象出来,减少系统之间的耦合,提高系统结构灵活性和敏捷性。 消息传送模型首先,先引入以下概念: JMS提供者(Provider),消息服务器; 目的地(Destination、Queue、Topic),消息在JMS提供者中的目的地; 生产者(Producer、Sender、Publisher),发送消息; 消费者(Consumer、Receiver、Subscriber),接收消息。 两种消息传送模型: 队列(一对一): 基于拉取(Pull)或基于轮询(polling); 发送到队列的消息被一个而且仅仅一个接收者所接收,即使有多个接收者在一个队列中侦听同一消息; 既支持异步“即发即弃”消息传送方式,又支持同步请求/应答消息传送方式; 支持负载均衡。 主题(一对多): 基于推送(push)的模型,其中消息自动地向消费者广播,它们无须请求或轮询主题来获得新消息。]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>ActiveMQ</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Maven中-DskipTests和-Dmaven.test.skip=true的区别]]></title>
<url>%2F2013%2F09%2F27%2Fmaven-e4-b8-ad-dskiptests-e5-92-8c-dmaven-test-skiptrue-e7-9a-84-e5-8c-ba-e5-88-ab%2F</url>
<content type="text"><![CDATA[在使用mvn package进行编译、打包时,Maven会执行src/test/java中的JUnit测试用例,有时为了跳过测试,会使用参数-DskipTests和-Dmaven.test.skip=true,这两个参数的主要区别是: -DskipTests,不执行测试用例,但编译测试用例类生成相应的class文件至target/test-classes下。 -Dmaven.test.skip=true,不执行测试用例,也不编译测试用例类。]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>Maven</tag>
</tags>
</entry>
<entry>
<title><![CDATA[AOP简介及Spring AOP的4种实现]]></title>
<url>%2F2013%2F09%2F15%2Faop-e7-ae-80-e4-bb-8b-e5-8f-8aspring-aop-e7-9a-844-e7-a7-8d-e5-ae-9e-e7-8e-b0%2F</url>
<content type="text"><![CDATA[AOPAOP(Aspect Oriented Programing,面向切面编程)用于在不侵入原有代码的基础上,为代码统一添加新功能,例如性能监控、日志记录、事务管理等。AOP的原理如图所示: 其中: 连接点(Jointpoint),表示方法执行的某个位置,如方法调用前、方法调用后、方法抛出异常等; 通知(Advice),表示需要在某个连接点加入的新功能,通知包括以下5种类型:Before advice,After returning advice,After throwing advice,After (finally) advice,Around advice; 切入点(Pointcut),用于描述某些类的某些方法; 切面(Aspect),通知和切入点组成切面,通过切面实现在指定类的指定方法上,加入通知。 Spring AOP的4种实现Spring AOP的实现基于动态代理,有以下4种实现方式: 基于Aspect注解; 基于aop标签配置; 基于ProxyFactoryBean; 基于DefaultAdvisorAutoProxyCreator。 通过一个例子分别说明上述4种实现方式。例子中包含TestService接口及其实现TestServiceImpl,TestServiceImpl的test方法输出“execute test method”:1234567package com.magicwt.service;public interface TestService { public void test();} 1234567891011package com.magicwt.service.impl;import com.magicwt.service.TestService;public class TestServiceImpl implements TestService { public void test() { System.out.println("execute test method"); }} main方法初始化Spring上下文,获取TestService实例,并执行test方法:1234567891011121314package com.magicwt;import com.magicwt.service.TestService;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;public class Test { public static void main(String[] args) { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("context.xml"); TestService testService = (TestService) applicationContext.getBean("testService"); testService.test(); }} Spring上下文配置文件context.xml:12345678910<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd"> <bean id="testService" class="com.magicwt.service.impl.TestServiceImpl"/></beans> 执行main方法,输出: execute test method 以下通过Spring AOP,在test方法调用前和调用后,增加切面。 基于Aspect注解新建类TestAspect:1234567891011121314151617181920212223242526272829303132333435package com.magicwt.aop;import org.aspectj.lang.annotation.*;import org.springframework.stereotype.Component;/** * 配置切面和组件 */@Component@Aspectpublic class TestAspect { /** * 配置切入点,com.magicwt.service包下所有类的所有方法 */ @Pointcut("execution(* com.magicwt.service..*(..))") public void pointcut() {} /** * 方法调用前通知 */ @Before("pointcut()") public void before() { System.out.println("execute before advice"); } /** * 方法调用后通知 */ @After("pointcut()") public void after() { System.out.println("execute after advice"); }} context.xml修改为:12345678910111213141516171819202122<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd"> <bean id="testService" class="com.magicwt.service.impl.TestServiceImpl"/> <!-- 扫描com.magicwt.aop包下的组件,进行初始化 --> <context:component-scan base-package="com.magicwt.aop"/> <!-- 根据注解自动创建代理,织入切面 --> <aop:aspectj-autoproxy /></beans> 执行main方法,输出: execute before adviceexecute test methodexecute after advice 基于aop标签配置TestAspect:12345678910111213package com.magicwt.aop;public class TestAspect { public void before() { System.out.println("execute before advice"); } public void after() { System.out.println("execute after advice"); }} context.xml修改为:123456789101112131415161718192021222324252627<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd"> <bean id="testService" class="com.magicwt.service.impl.TestServiceImpl"/> <bean id="testAspect" class="com.magicwt.aop.TestAspect"/> <aop:config> <!-- 配置切面 --> <aop:aspect ref="testAspect"> <!-- 配置切入点,com.magicwt.service包下所有类的所有方法 --> <aop:pointcut id="pointcut" expression="execution(* com.magicwt.service..*(..))"/> <!-- 配置方法调用前通知 --> <aop:before method="before" pointcut-ref="pointcut"/> <!-- 配置方法调用后通知 --> <aop:after method="after" pointcut-ref="pointcut"/> </aop:aspect> </aop:config></beans> 执行main方法,输出: execute before adviceexecute test methodexecute after advice 基于ProxyFactoryBean新建类TestAdvice,实现接口MethodBeforeAdvice、AfterReturningAdvice,重写before、afterReturning方法:12345678910111213141516171819package com.magicwt.aop;import org.springframework.aop.AfterReturningAdvice;import org.springframework.aop.MethodBeforeAdvice;import java.lang.reflect.Method;public class TestAdvice implements MethodBeforeAdvice, AfterReturningAdvice { @Override public void before(Method method, Object[] args, Object target) throws Throwable { System.out.println("execute before advice"); } @Override public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable { System.out.println("execute after advice"); }} context.xml修改为:123456789101112131415161718192021222324252627282930<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd"> <bean id="realTestService" class="com.magicwt.service.impl.TestServiceImpl"/> <bean id="testAdvice" class="com.magicwt.aop.TestAdvice"/> <!-- 配置切入点,com.magicwt.service包下所有类的所有方法 --> <bean id="pointcut" class="org.springframework.aop.aspectj.AspectJExpressionPointcut"> <property name="expression" value="execution(* com.magicwt.service..*(..))"/> </bean> <!-- 配置切面 --> <bean id="advisor" class="org.springframework.aop.support.DefaultPointcutAdvisor"> <property name="advice" ref="testAdvice"/> <property name="pointcut" ref="pointcut"/> </bean> <!-- 创建代理,织入切面 --> <bean id="testService" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="target" ref="realTestService"/> <property name="interceptorNames" value="advisor" /> <property name="proxyInterfaces" value="com.magicwt.service.TestService" /> </bean></beans> 执行main方法,输出: execute before adviceexecute test methodexecute after advice 基于DefaultAdvisorAutoProxyCreator与基于ProxyFactoryBean的实现相比,只需修改context.xml:1234567891011121314151617181920212223242526<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd"> <bean id="testService" class="com.magicwt.service.impl.TestServiceImpl"/> <bean id="testAdvice" class="com.magicwt.aop.TestAdvice"/> <!-- 配置切入点,com.magicwt.service包下所有类的所有方法 --> <bean id="pointcut" class="org.springframework.aop.aspectj.AspectJExpressionPointcut"> <property name="expression" value="execution(* com.magicwt.service..*(..))"/> </bean> <!-- 配置切面 --> <bean id="advisor" class="org.springframework.aop.support.DefaultPointcutAdvisor"> <property name="advice" ref="testAdvice"/> <property name="pointcut" ref="pointcut"/> </bean> <!-- 通过DefaultAdvisorAutoProxyCreator自动创建代理,织入切面 --> <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/></beans> 执行main方法,输出: execute before adviceexecute test methodexecute after advice]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>Spring</tag>
</tags>
</entry>
<entry>
<title><![CDATA[ActiveMQ与业务系统的集成]]></title>
<url>%2F2013%2F09%2F12%2F40%2F</url>
<content type="text"><![CDATA[ActiveMQ是一款基于Java的开源消息服务器产品,因此,我们可以将其集成到通过Java实现的业务系统中。下面将对集成方法做一个简单的总结。首先,我们看一下ActiveMQ中的部分核心类: org.apache.activemq.Service是ActiveMQ中的一个接口,定义了start和stop方法。org.apache.activemq.broker.BrokerService是ActiveMQ中的核心类,其实现自Service接口,用于对外提供消息服务。org.apache.activemq.broker.XBeanBrokerService是BrokerService的一个子类,XBeanBrokerService实例可以通过xbean xml配置文件生成。ActiveMQ作为单独服务启动时,其配置文件activemq.xml就是xbean xml格式的,启动后生成的就是一个XBeanBrokerService实例。 通过VM协议实现集成:客户端连接ActiveMQ消息服务器时,可以使用多种协议(TCP、VM、STOMP等),其中VM协议是指客户端会从当前JVM中查找ActiveMQ消息服务器实例,若查找到,则与其建立连接,若查找不到,则自动创建ActiveMQ消息服务器实例。如果客户端和服务器端都在一个JVM中,则可以通过VM协议实现ActiveMQ消息服务器的自动创建、与业务系统的集成。 直接创建BrokerService实例:12345678910111213141516171819public static void main(String[] args)throws Exception { BrokerService broker = new BrokerService(); broker.setBrokerName("myBroker"); broker.setDataDirectory("data/"); SimpleAuthenticationPlugin authentication = newSimpleAuthenticationPlugin(); List<AuthenticationUser> users = newArrayList<AuthenticationUser>(); users.add(new AuthenticationUser("admin","password", "admins,publishers,consumers")); users.add(new AuthenticationUser("publisher","password", "publishers,consumers")); users.add(new AuthenticationUser("consumer","password", "consumers")); users.add(new AuthenticationUser("guest","password", "guests")); authentication.setUsers(users); broker.setPlugins(new BrokerPlugin[]{authentication}); broker.addConnector("tcp://localhost:61616"); broker.start(); System.out.println(); System.out.println("Press any key to stop the broker"); System.out.println(); System.in.read();} 使用工厂方法创建BrokerService实例:1234567891011public static void main(String[] args)throws Exception { System.setProperty("activemq.base",System.getProperty("user.dir")); String configUri ="xbean:activemq.xml" URI brokerUri = new URI(configUri); BrokerService broker = BrokerFactory.createBroker(brokerUri); broker.start(); System.out.println(); System.out.println("Press any key to stop the broker"); System.out.println(); System.in.read();} 通过Spring配置直接创建BrokerService实例1234567891011121314151617181920<bean id="simpleAuthPlugin" class="org.apache.activemq.security.SimpleAuthenticationPlugin"> <property name="users"> <util:list> <ref bean="admins" > <ref bean="publishers" > <ref bean="consumers" > <ref bean="guests" > </util:list> </property></bean><bean id="broker" class="org.apache.activemq.broker.BrokerService" init-method="start"destroy-method="stop"> <property name="brokerName"value="myBroker" > <property name="persistent"value="false" > <property name="transportConnectorURIs"> <list><value>tcp://localhost:61616</value></list> </property> <property name="plugins"> <list><ref bean="simpleAuthPlugin"/></list> </property></bean> 通过Spring配置使用BrokerFactoryBean创建BrokerService实例:1234<bean id="broker" class="org.apache.activemq.xbean.BrokerFactoryBean"> <property name="config" value="classpath:activemq.xml"> <property name="start"value="true" /></bean>]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>ActiveMQ</tag>
</tags>
</entry>
<entry>
<title><![CDATA[SWFUpload文件上传时从后台返回中文乱码的问题]]></title>
<url>%2F2013%2F09%2F11%2Fswfupload-e6-96-87-e4-bb-b6-e4-b8-8a-e4-bc-a0-e6-97-b6-e4-bb-8e-e5-90-8e-e5-8f-b0-e8-bf-94-e5-9b-9e-e4-b8-ad-e6-96-87-e4-b9-b1-e7-a0-81-e7-9a-84-e9-97-ae-e9-a2-98%2F</url>
<content type="text"><![CDATA[SWFUpload是一款使用Flash和Javascript开发的文件上传工具,最近在使用其实现文件上传功能时,发现从后台返回的中文经常乱码。SWFUpload上传成功后,对后台返回结果的处理方法是:123function uploadSuccess(file, serverData) { ......} 其中file是上传文件信息,serverData是从后台返回的数据。我们发现serverData中的中文出现乱码。而后台处理图片上传的Struts2 Action类返回时的代码如下:1servletResponse.getOutputStream().println(serverData); 后来发现就是上面这个地方出了问题。之前使用的是ServletOutputStream字节流的println方法,这个方法会将serverData字符串按照默认编码转化为字节流,而SWFUpload使用的是UTF-8编码,从而导致编码不一致,引起中文乱码。后来想了两个解决方法: 对于字节流,手工对serverData按照UTF-8进行编码,并采用write方法输出字节流; 1servletResponse.getOutputStream().write(serverData.getBytes("UTF-8")); 采用字符流,并指定字符流的编码为UTF-8。 12servletResponse.setCharacterEncoding("UTF-8");servletResponse.getWriter().print(serverData);]]></content>
<categories>
<category>后端</category>
</categories>
</entry>
<entry>
<title><![CDATA[Spring Struts2 Hessian集成的一个问题]]></title>
<url>%2F2013%2F09%2F07%2Fspring-struts2-hessian-e9-9b-86-e6-88-90-e7-9a-84-e4-b8-80-e4-b8-aa-e9-97-ae-e9-a2-98%2F</url>
<content type="text"><![CDATA[最近在做系统改造的时候,还遇到了一个问题是,如何集成Spring Struts2和Hessian。当配置Spring和Struts2的时候,在web.xml做了如下配置:123456789<context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:/spring/*.xml</param-value></context-param> <listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class></listener> 通过设置listener加载Spring的上下文环境,并在struts.xml中设置对象工厂为Spring:1<constant name="struts.objectFactory" value="spring" /> 这样,Struts2就可以使用Spring上下文环境中的action bean了。但在配置Hessian的时候,以前在web.xml中是这样配置的:123456789101112131415<servlet> <servlet-name>Remoting</servlet-name> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:/spring/*.xml</param-value> </init-param> <load-on-startup>1</load-on-startup></servlet><servlet-mapping> <servlet-name>Remoting</servlet-name> <url-pattern>/remoting/*</url-pattern></servlet-mapping> 在初始化Hessian的servlet的时候又一次把Spring配置文件作为参数,这样又会重新生成一个Spring上下文环境,导致Spring中bean的重复。为了解决这个问题,在配置Hessian时,做了一下修改,如下:12345678910111213141516<servlet> <servlet-name>Remoting</servlet-name> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> <init-param> <!-- <param-name>contextConfigLocation</param-name> <param-value>classpath:/spring/*.xml</param-value> --> <!-- 该servlet的spring上下文采用WebApplicationContext,不再重复生成上下文 --> <param-name>contextAttribute</param-name> <param-value> org.springframework.web.context.WebApplicationContext.ROOT </param-value> </init-param> <load-on-startup>1</load-on-startup></servlet> 即在初始化Hessian时不再传入Spring配置文件,而是传入通过listener初始化的Spring WebApplicationContext上下文环境,即使用同一个上下文环境。]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>Spring</tag>
<tag>Hessian</tag>
<tag>Struts</tag>
</tags>
</entry>
<entry>
<title><![CDATA[本地jar包添加到maven依赖中]]></title>
<url>%2F2013%2F09%2F05%2Fe6-9c-ac-e5-9c-b0jar-e5-8c-85-e6-b7-bb-e5-8a-a0-e5-88-b0maven-e4-be-9d-e8-b5-96-e4-b8-ad%2F</url>
<content type="text"><![CDATA[对于本地jar包,如果要添加到maven依赖中,可以使用如下方式:1234567<dependency> <groupId>groupId</groupId> <artifactId>artifactId</artifactId> <version>version</version> <scope>system</scope> <systemPath>本地jar包路径</systemPath></dependency> 指定scope为system,maven会直接使用systemPath指定路径下的jar包。]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>Maven</tag>
</tags>
</entry>
<entry>
<title><![CDATA[SSH错误解决]]></title>
<url>%2F2013%2F08%2F22%2Fssh-e9-94-99-e8-af-af-e8-a7-a3-e5-86-b3%2F</url>
<content type="text"><![CDATA[今天在修改系统的一个Action类时(系统架构是struts2+spring+hibernate),报了以下错误: JSONWriter can not access a member of class org.springframework.aop.TruePointcut with modifiers “public” 后来发现是由于我在该Action类中新增加了一个对象属性,并添加了get和set方法,通过spring注入,但由于该Action类使用了struts2支持的json格式字符串返回,会将我新增加的这个对象属性也添加到返回的json字符串,导致struts2报错,该错误的解决方法有2个: 在新增对象属性的get方法上添加标注“@JSON(serialize=false)”; 去掉新增对象属性的get方法; 这样,在返回json格式字符串时,不会再添加这个对象属性,也就不会再报错了。]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>Spring</tag>
<tag>Hibernate</tag>
<tag>Struts</tag>
</tags>
</entry>
<entry>
<title><![CDATA[开源压力测试工具JMeter使用介绍]]></title>
<url>%2F2013%2F08%2F10%2Fe5-bc-80-e6-ba-90-e5-8e-8b-e5-8a-9b-e6-b5-8b-e8-af-95-e5-b7-a5-e5-85-b7jmeter-e4-bd-bf-e7-94-a8-e4-bb-8b-e7-bb-8d%2F</url>
<content type="text"><![CDATA[最近需要对改造的redis缓存接口做压力测试,使用了开源压力测试工具JMeter,分享一下自己的使用经验,希望能对需要进行压力测试的开发同学有所帮助。 JMeter介绍JMeter是Apache软件基金会下的一款开源压力测试工具,官方网址是:http://jmeter.apache.org/ 。JMeter可以测试静态、动态资源的性能,这些资源包括文件、Servlets 、Perl脚本、Java对象、数据库、FTP服务器等,并生成图形报告。JMeter使用Java开发,既支持可视化界面操作,也支持命令行操作。 Java请求测试(界面操作)由于需要对改造的redis缓存接口测试,因此使用了JMeter的Java请求测试,安装和使用步骤如下所示(以Windows操作系统为例,并默认已安装、配置Java运行环境)。 从官网上下载JMeter并解压,例如解压至C:\apache-jmeter-2.9。 配置JMeter环境变量,增加变量JMETER_HOME,值为“C:\apache-jmeter-2.9”,修改变量CLASSPATH,增加“%JMETER_HOME%\lib\ext\ApacheJMeter_core.jar;% JMETER_HOME%\lib\jorphan.jar;%JMETER_HOME%\lib\logkit-1.2.jar;” 执行JMeter目录下的bin\jmeter.bat,显示JMeter界面说明安装成功。 新建Java工程,在该工程中,引入JMeter目录下lib中的jar包,继承JMeter的AbstractJavaSamplerClient类,在子类中重写setupTest、teardownTest、getDefaultParameters、runTest方法,实现对缓存接口的get、set、hGet、hSet方法进行测试,子类代码如下所示。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164package com.sohu.cms.test;import org.apache.jmeter.config.Arguments; import org.apache.jmeter.protocol.java.sampler.AbstractJavaSamplerClient; import org.apache.jmeter.protocol.java.sampler.JavaSamplerContext; import org.apache.jmeter.samplers.SampleResult; import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;import com.sohu.cms.datacache.DataCache;import com.sohu.cms.datacache.DataCacheManager;/** * 继承jmeter的AbstractJavaSamplerClient类, * 重写setupTest、teardownTest、getDefaultParameters、runTest方法 * 对缓存接口的get、set、hGet、hSet方法进行测试 * @author wang * */public class TestDataCacheClient extends AbstractJavaSamplerClient{ private static long start = 0; private static long end = 0; private static DataCacheManager dataCacheManager; private static DataCache dataCache; private static int size = 8192; private static String method = "get"; private static Object key = "TEST_KEY"; private static Object field = "TEST_FIELD"; private static Object value = new Byte[size]; //此处初始化我们改造的redis缓存接口dataCache static { ApplicationContext context = new ClassPathXmlApplicationContext(new String[]{"applicationContext.xml"}); dataCacheManager = (DataCacheManager) context.getBean("dataCacheManager"); dataCache = dataCacheManager.getObjectDataCache(); } /** * 测试开始 */ public void setupTest(JavaSamplerContext arg0) { start = System.currentTimeMillis(); } /** * 测试结束 */ public void teardownTest(JavaSamplerContext arg0) { end = System.currentTimeMillis(); System.out.println("[method=" + method + "] [size=" + size + "] [time:" + (end - start) / 1000); } /** * 添加参数默认值,参数默认值会在界面中显示 */ public Arguments getDefaultParameters() { //添加两个参数, //size为测试value值的字节大小,默认为8192字节 //method为需要进行测试的缓存接口方法,默认为get Arguments args = new Arguments(); args.addArgument("size", "8192"); args.addArgument("method","get"); return args; } /** * 测试 */ public SampleResult runTest(JavaSamplerContext arg0) { //获取界面或测试计划配置文件(jmx)中传入的参数 if (arg0.getParameter("method") != null) method = arg0.getParameter("method"); if (arg0.getIntParameter("size") > 0) size = arg0.getIntParameter("size"); value = new Byte[size]; SampleResult sr = new SampleResult(); try { boolean result = true; //根据传入的method参数测试相应的缓存接口方法 if (method.equals("get")){ //测试用例的核心逻辑 //测试用例开始 sr.sampleStart(); //测试用例执行 result = get(); //测试用例结果 sr.setSuccessful(result); //测试用例结束 sr.sampleEnd(); } else if (method.equals("set")) { sr.sampleStart(); result = set(); sr.setSuccessful(result); sr.sampleEnd(); } else if (method.equals("hGet")) { sr.sampleStart(); result = hGet(); sr.setSuccessful(result); sr.sampleEnd(); } else if (method.equals("hSet")) { sr.sampleStart(); result = hSet(); sr.setSuccessful(result); sr.sampleEnd(); } } catch (Exception e) { e.printStackTrace(); } return sr; } /** * 缓存接口get方法测试用例 * @return */ public boolean get() { boolean result = true; try { dataCache.get(key); } catch (Exception e) { System.out.println(e); result = false; } return result; } /** * 缓存接口set方法测试用例 * @return */ public boolean set() { boolean result = true; try { dataCache.put(key, value); } catch (Exception e) { System.out.println(e); result = false; } return result; } /** * 缓存接口hGet方法测试用例 * @return */ public boolean hGet() { boolean result = true; try { dataCache.hGet(key, field); } catch (Exception e) { System.out.println(e); result = false; } return result; } /** * 缓存接口hSet方法测试用例 * @return */ public boolean hSet() { boolean result = true; try { result = dataCache.hSet(key, field, value); } catch (Exception e) { System.out.println(e); result = false; } return result; }} 将工程以jar包形式导出,置于JMeter目录lib\ext下,并将工程依赖的jar包置于JMeter目录lib下,启动JMeter。右键“测试计划”添加“线程组”。右键“线程组”添加“Java请求”,右键“Java请求”添加“聚合报告”。 在“Java请求”中选择所写的测试类,并可以设置相关参数,在“线程组”中可以设置并发线程数和循环次数,点击上方“启动”按钮开始测试,测试完成后,可以在“聚合报告”中查看到测试结果。 Java请求测试(命令行操作)JMeter也支持命令行操作,在服务器上进行测试时,我们就采用了这种方式。一个简单的JMeter命令行操作如下所示(在JMeter目录bin下执行)。 Windows下: JMeter-n –t test.jmx -l result.jtlLinux下: ./jmeter.sh -n –t test.jmx -l result.jtl 其中-n 表示命令行执行,test.jmx为测试计划配置文件,result.jtl为测试结果。可在界面中进行测试计划配置,然后保存便可生成jmx格式文件,如下所示。1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586<?xml version="1.0" encoding="UTF-8"?><jmeterTestPlan version="1.2" properties="2.4" jmeter="2.9 r1437961"> <hashTree> <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="测试计划" enabled="true"> <stringProp name="TestPlan.comments"></stringProp> <boolProp name="TestPlan.functional_mode">false</boolProp> <boolProp name="TestPlan.serialize_threadgroups">false</boolProp> <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="用户定义的变量" enabled="true"> <collectionProp name="Arguments.arguments"/> </elementProp> <stringProp name="TestPlan.user_define_classpath"></stringProp> </TestPlan> <hashTree> <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="线程组" enabled="true"> <stringProp name="ThreadGroup.on_sample_error">continue</stringProp> <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="循环控制器" enabled="true"> <boolProp name="LoopController.continue_forever">false</boolProp> <!-- 循环次数 --> <stringProp name="LoopController.loops">100</stringProp> </elementProp> <!-- 并发线程数 --> <stringProp name="ThreadGroup.num_threads">100</stringProp> <stringProp name="ThreadGroup.ramp_time">100</stringProp> <longProp name="ThreadGroup.start_time">1376103961000</longProp> <longProp name="ThreadGroup.end_time">1376103961000</longProp> <boolProp name="ThreadGroup.scheduler">false</boolProp> <stringProp name="ThreadGroup.duration"></stringProp> <stringProp name="ThreadGroup.delay"></stringProp> </ThreadGroup> <hashTree> <JavaSampler guiclass="JavaTestSamplerGui" testclass="JavaSampler" testname="Java请求" enabled="true"> <elementProp name="arguments" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" enabled="true"> <!-- 自定义参数 --> <collectionProp name="Arguments.arguments"> <elementProp name="size" elementType="Argument"> <stringProp name="Argument.name">size</stringProp> <stringProp name="Argument.value">8192</stringProp> <stringProp name="Argument.metadata">=</stringProp> </elementProp> <elementProp name="method" elementType="Argument"> <stringProp name="Argument.name">method</stringProp> <stringProp name="Argument.value">set</stringProp> <stringProp name="Argument.metadata">=</stringProp> </elementProp> </collectionProp> </elementProp> <stringProp name="classname">com.sohu.cms.test.TestDataCacheClient</stringProp> </JavaSampler> <hashTree> <ResultCollector guiclass="StatVisualizer" testclass="ResultCollector" testname="聚合报告" enabled="true"> <boolProp name="ResultCollector.error_logging">false</boolProp> <objProp> <name>saveConfig</name> <value class="SampleSaveConfiguration"> <time>true</time> <latency>true</latency> <timestamp>true</timestamp> <success>true</success> <label>true</label> `true` <message>true</message> <threadName>true</threadName> <dataType>true</dataType> <encoding>false</encoding> <assertions>true</assertions> <subresults>true</subresults> <responseData>false</responseData> <samplerData>false</samplerData> <xml>false</xml> <fieldNames>false</fieldNames> <responseHeaders>false</responseHeaders> <requestHeaders>false</requestHeaders> <responseDataOnError>false</responseDataOnError> <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage> <assertionsResultsToSave>0</assertionsResultsToSave> <bytes>true</bytes> </value> </objProp> <stringProp name="filename"></stringProp> </ResultCollector> <hashTree/> </hashTree> </hashTree> </hashTree> </hashTree></jmeterTestPlan> result.ftl为按行输出的测试执行结果,如下所示,可在界面操作的聚合报告导入该文件从而生成聚合报告。 …1376040889436,48,Java请求,,,线程组1-40,,true,0,01376040889792,9,Java请求,,,线程组1-7,,true,0,01376040889526,167,Java请求,,,线程组1-57,,true,0,01376040889791,10,Java请求,,,线程组 1-54,,true,0,0…]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>JMeter</tag>
</tags>
</entry>
<entry>
<title><![CDATA[[转]Hibernate 缓存机制]]></title>
<url>%2F2013%2F07%2F10%2Fe8-bd-achibernate-e7-bc-93-e5-ad-98-e6-9c-ba-e5-88-b6%2F</url>
<content type="text"><![CDATA[原文地址 why(为什么要用Hibernate缓存?)Hibernate是一个持久层框架,经常访问物理数据库。为了降低应用程序对物理数据源访问的频次,从而提高应用程序的运行性能。缓存内的数据是对物理数据源中的数据的复制,应用程序在运行时从缓存读写数据,在特定的时刻或事件会同步缓存和物理数据源的数据。 what(Hibernate缓存原理是怎样的?)Hibernate缓存包括两大类:Hibernate一级缓存和Hibernate二级缓存。 Hibernate一级缓存又称为“Session的缓存”。Session内置不能被卸载,Session的缓存是事务范围的缓存(Session对象的生命周期通常对应一个数据库事务或者一个应用事务)。一级缓存中,持久化类的每个实例都具有唯一的OID。 Hibernate二级缓存又称为“SessionFactory的缓存”。由于SessionFactory对象的生命周期和应用程序的整个过程对应,因此Hibernate二级缓存是进程范围或者集群范围的缓存,有可能出现并发问题,因此需要采用适当的并发访问策略,该策略为被缓存的数据提供了事务隔离级别。第二级缓存是可选的,是一个可配置的插件,默认下SessionFactory不会启用这个插件。Hibernate提供了org.hibernate.cache.CacheProvider接口,它充当缓存插件与Hibernate之间的适配器。什么样的数据适合存放到第二级缓存中? 很少被修改的数据 不是很重要的数据,允许出现偶尔并发的数据 不会被并发访问的数据 常量数据 不适合存放到第二级缓存的数据? 经常被修改的数据 绝对不允许出现并发访问的数据,如财务数据,绝对不允许出现并发 与其他应用共享的数据。 Session的延迟加载实现要解决两个问题:正常关闭连接和确保请求中访问的是同一个session。Hibernate session就是java.sql.Connection的一层高级封装,一个session对应了一个Connection。http请求结束后正确的关闭session(过滤器实现了session的正常关闭);延迟加载必须保证是同一个session(session绑定在ThreadLocal)。 Hibernate查找对象如何应用缓存?当Hibernate根据ID访问数据对象的时候,首先从Session一级缓存中查;查不到,如果配置了二级缓存,那么从二级缓存中查;如果都查不到,再查询数据库,把结果按照ID放入到缓存删除、更新、增加数据的时候,同时更新缓存。 一级缓存与二级缓存的对比图。 一级缓存 二级缓存 存放数据的形式 相互关联的持久化对象 对象的散装数据 缓存的范围 事务范围,每个事务都拥有单独的一级缓存 进程范围或集群范围,缓存被同一个进程或集群范围内所有事务共享 并发访问策略 由于每个事务都拥有单独的一级缓存不会出现并发问题,因此无须提供并发访问策略 由于多个事务会同时访问二级缓存中的相同数据,因此必须提供适当的并发访问策略,来保证特定的事务隔离级别 数据过期策略 处于一级缓存中的对象永远不会过期,除非应用程序显示清空或者清空特定对象 必须提供数据过期策略,如基于内存的缓存中对象的最大数目,允许对象处于缓存中的最长时间,以及允许对象处于缓存中的最长空闲时间 物理介质 内存 内存和硬盘,对象的散装数据首先存放到基于内存的缓存中,当内存中对象的数目达到数据过期策略的maxElementsInMemory值,就会把其余的对象写入基于硬盘的缓存中 缓存软件实现 在Hibernate的Session的实现中包含 由第三方提供,Hibernate仅提供了缓存适配器,用于把特定的缓存插件集成到Hibernate中 启用缓存的方式 只要通过Session接口来执行保存,更新,删除,加载,查询,Hibernate就会启用一级缓存,对于批量操作,如不希望启用一级缓存,直接通过JDBCAPI来执行 用户可以再单个类或类的单个集合的粒度上配置第二级缓存,如果类的实例被经常读,但很少被修改,就可以考虑使用二级缓存,只有为某个类或集合配置了二级缓存,Hibernate在运行时才会把它的实例加入到二级缓存中 用户管理缓存的方式 一级缓存的物理介质为内存,由于内存的容量有限,必须通过恰当的检索策略和检索方式来限制加载对象的数目,Session的evit()方法可以显示的清空缓存中特定对象,但不推荐 二级缓存的物理介质可以使内存和硬盘,因此第二级缓存可以存放大容量的数据,数据过期策略的maxElementsInMemory属性可以控制内存中的对象数目,管理二级缓存主要包括两个方面:选择需要使用第二级缓存的持久化类,设置合适的并发访问策略;选择缓存适配器,设置合适的数据过期策略。SessionFactory的evit()方法也可以显示的清空缓存中特定对象,但不推荐 how(Hibernate的缓存机制如何应用?)一级缓存的管理:evit(Object obj) 将指定的持久化对象从一级缓存中清除,释放对象所占用的内存资源,指定对象从持久化状态变为脱管状态,从而成为游离对象。clear() 将一级缓存中的所有持久化对象清除,释放其占用的内存资源。contains(Object obj) 判断指定的对象是否存在于一级缓存中。flush() 刷新一级缓存区的内容,使之与数据库数据保持同步。 一级缓存应用:save()。当session对象调用save()方法保存一个对象后,该对象会被放入到session的缓存中。get()和load()。当session对象调用get()或load()方法从数据库取出一个对象后,该对象也会被放入到session的缓存中。 使用HQL和QBC等从数据库中查询数据。1234567891011121314151617181920212223242526272829303132333435363738394041424344public class Client{ public static void main(String[] args) { Session session = HibernateUtil.getSessionFactory().openSession(); Transaction tx = null; try { /*开启一个事务*/ tx = session.beginTransaction(); /*从数据库中获取id="402881e534fa5a440134fa5a45340002"的Customer对象*/ Customer customer1 = (Customer)session.get(Customer.class, "402881e534fa5a440134fa5a45340002"); System.out.println("customer.getUsername is"+customer1.getUsername()); /*事务提交*/ tx.commit(); System.out.println("-------------------------------------"); /*开启一个新事务*/ tx = session.beginTransaction(); /*从数据库中获取id="402881e534fa5a440134fa5a45340002"的Customer对象*/ Customer customer2 = (Customer)session.get(Customer.class, "402881e534fa5a440134fa5a45340002"); System.out.println("customer2.getUsername is"+customer2.getUsername()); /*事务提交*/ tx.commit(); System.out.println("-------------------------------------"); /*比较两个get()方法获取的对象是否是同一个对象*/ System.out.println("customer1 == customer2 result is "+(customer1==customer2)); } catch (Exception e) { if(tx!=null) { tx.rollback(); } } finally { session.close(); } }} 结果 Hibernate:selectcustomer0_.id as id0_0_,customer0_.username as username0_0_,customer0_.balance as balance0_0_fromcustomer customer0_wherecustomer0_.id=?customer.getUsername islisi-————————————customer2.getUsername islisi-————————————customer1 == customer2 result is true 输出结果中只包含了一条SELECT SQL语句,而且customer1 == customer2 result is true说明两个取出来的对象是同一个对象。其原理是:第一次调用get()方法, Hibernate先检索缓存中是否有该查找对象,发现没有,Hibernate发送SELECT语句到数据库中取出相应的对象,然后将该对象放入缓存中,以便下次使用,第二次调用get()方法,Hibernate先检索缓存中是否有该查找对象,发现正好有该查找对象,就从缓存中取出来,不再去数据库中检索。 二级缓存的管理:evict(Class arg0, Serializable arg1)将某个类的指定ID的持久化对象从二级缓存中清除,释放对象所占用的资源。1sessionFactory.evict(Customer.class, new Integer(1)); evict(Class arg0) 将指定类的所有持久化对象从二级缓存中清除,释放其占用的内存资源。1sessionFactory.evict(Customer.class); evictCollection(String arg0) 将指定类的所有持久化对象的指定集合从二级缓存中清除,释放其占用的内存资源。1sessionFactory.evictCollection("Customer.orders"); 二级缓存的配置常用的二级缓存插件 EHCache org.hibernate.cache.EhCacheProvider OSCache org.hibernate.cache.OSCacheProvider SwarmCahe org.hibernate.cache.SwarmCacheProvider JBossCache org.hibernate.cache.TreeCacheProvider 12345678910111213<!-- EHCache的配置,hibernate.cfg.xml --> <hibernate-configuration> <session-factory> <!-- 设置二级缓存插件EHCache的Provider类--> <property name="hibernate.cache.provider_class"> org.hibernate.cache.EhCacheProvider </property> <!-- 启动"查询缓存" --> <property name="hibernate.cache.use_query_cache"> true </property> </session-factory> </hibernate-configuration> 123456789101112131415161718192021222324<!-- ehcache.xml --><?xml version="1.0" encoding="UTF-8"?><ehcache> <!-- 缓存到硬盘的路径 --> <diskStore path="d:/ehcache"></diskStore> <!-- 默认设置 maxElementsInMemory : 在內存中最大緩存的对象数量。 eternal : 缓存的对象是否永远不变。 timeToIdleSeconds :可以操作对象的时间。 timeToLiveSeconds :缓存中对象的生命周期,时间到后查询数据会从数据库中读取。 overflowToDisk :内存满了,是否要缓存到硬盘。 --> <defaultCache maxElementsInMemory="200" eternal="false" timeToIdleSeconds="50" timeToLiveSeconds="60" overflowToDisk="true"></defaultCache> <!-- 指定缓存的对象。 下面出现的的属性覆盖上面出现的,没出现的继承上面的。 --> <cache name="com.suxiaolei.hibernate.pojos.Order" maxElementsInMemory="200" eternal="false" timeToIdleSeconds="50" timeToLiveSeconds="60" overflowToDisk="true"></cache></ehcache> 1234567891011<!-- *.hbm.xml --><?xml version="1.0" encoding='UTF-8'?><!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd" ><hibernate-mapping> <class> <!-- 设置该持久化类的二级缓存并发访问策略 read-only read-write nonstrict-read-write transactional--> <cache usage="read-write"/> </class></hibernate-mapping> 若存在一对多的关系,想要在在获取一方的时候将关联的多方缓存起来,需要在集合属性下添加\<cache>子标签,这里需要将关联的对象的hbm文件中必须在存在\<class>标签下也添加\<cache>标签,不然Hibernate只会缓存OID。12345678910111213141516171819<hibernate-mapping> <class name="com.suxiaolei.hibernate.pojos.Customer" table="customer"> <!-- 主键设置 --> <id name="id" type="string"> <column name="id"></column> <generator class="uuid"></generator> </id> <!-- 属性设置 --> <property name="username" column="username" type="string"></property> <property name="balance" column="balance" type="integer"></property> <set name="orders" inverse="true" cascade="all" lazy="false" fetch="join"> <cache usage="read-only"/> <key column="customer_id" ></key> <one-to-many class="com.suxiaolei.hibernate.pojos.Order"/> </set> </class></hibernate-mapping>]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>Hibernate</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Hibernate的延迟加载]]></title>
<url>%2F2013%2F07%2F09%2Fhibernate-e7-9a-84-e5-bb-b6-e8-bf-9f-e5-8a-a0-e8-bd-bd%2F</url>
<content type="text"><![CDATA[简介Hibernate通过延迟加载在真正需要使用数据时从数据库表中加载,这样可以加快程序运行速度,减少内存开销。Hibernate在以下三种情况会默认使用延迟加载: load方法延迟加载; 实体属性延迟加载; 集合属性延迟加载。 数据库示例示例数据库包含3张表: 文章表(post),存储文章标题、内容,主键为自增整数; 分类表(category),存储分类名称,主键为自增整数; 标签表(score),存储标签名称,主键为自增整数; 文章标签表(post_tag),存储文章包含的标签,使用文章id和标签id作为联合主键。 每篇文章属于一个分类,并包含多个标签。 延迟加载示例创建Post类与post表映射。Post.java:12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758package com.magicwt.bean;import java.util.Set;public class Post { //与主键id映射 private int id; //与字段title映射 private String title; //与字段content映射 private String content; //关联Category实体 private Category category; //关联Tag实体 private Set<Tag> tags; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public Category getCategory() { return category; } public void setCategory(Category category) { this.category = category; } public Set<Tag> getTags() { return tags; } public void setTags(Set<Tag> tags) { this.tags = tags; }} Post.hbm.xml:123456789101112131415161718192021222324<?xml version='1.0' encoding='utf-8'?><!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd"><hibernate-mapping> <class name="com.magicwt.bean.Post" table="post"> <id name="id"> <column name="id" sql-type="int" not-null="true"/> </id> <property name="title"> <column name="title" sql-type="varchar" length="16"/> </property> <property name="content"> <column name="content" sql-type="text" length="65535"/> </property> <!-- 实体属性,对应于一个分类实体 --> <one-to-one name="category" class="com.magicwt.bean.Category" constrained="true"/> <!-- 集合属性,对应于多个标签实体 --> <set name="tags" table="post_tag" inverse="true"> <key column="post_id"/> <many-to-many column="tag_id" class="com.magicwt.bean.Tag"/> </set> </class></hibernate-mapping> 创建Category类与category表映射。Category.java:1234567891011121314151617181920212223242526package com.magicwt.bean;public class Category { //与主键id映射 private int id; //与字段name映射 private String name; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; }} Category.hbm.xml:1234567891011121314<?xml version='1.0' encoding='utf-8'?><!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd"><hibernate-mapping> <class name="com.magicwt.bean.Category" table="category"> <id name="id"> <column name="id" sql-type="int" not-null="true"/> </id> <property name="name"> <column name="name" sql-type="varchar" length="16"/> </property> </class></hibernate-mapping> 创建Tag类与tag表映射。Tag.java:1234567891011121314151617181920212223242526package com.magicwt.bean;public class Tag { //与主键id映射 private int id; //与字段name映射 private String name; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; }} Tag.hbm.xml:1234567891011121314<?xml version='1.0' encoding='utf-8'?><!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd"><hibernate-mapping> <class name="com.magicwt.bean.Tag" table="tag"> <id name="id"> <column name="id" sql-type="int" not-null="true"/> </id> <property name="name"> <column name="name" sql-type="varchar" length="16"/> </property> </class></hibernate-mapping> Hibernate配置文件hibernate.cfg.xml:123456789101112131415161718<?xml version='1.0' encoding='utf-8'?><!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd"><hibernate-configuration> <session-factory> <property name="connection.driver_class">com.mysql.jdbc.Driver</property> <property name="connection.url">jdbc:mysql://xxx.xxx.xxx.xxx:3306/test</property> <property name="connection.username">xxx</property> <property name="connection.password">xxx</property> <property name="dialect">org.hibernate.dialect.MySQL5Dialect</property> <property name="show_sql">true</property> <property name="hibernate.format_sql">true</property> <mapping resource="mapper/Category.hbm.xml"/> <mapping resource="mapper/Post.hbm.xml"/> <mapping resource="mapper/Tag.hbm.xml"/> </session-factory></hibernate-configuration> load方法延迟加载创建测试类Test:123456789101112131415161718192021package com.magicwt;import com.magicwt.bean.Post;import org.hibernate.SessionFactory;import org.hibernate.cfg.Configuration;import org.hibernate.classic.Session;public class Test { public static void main(String[] args) { SessionFactory sessionFactory = (new Configuration()).configure().buildSessionFactory(); Session session = sessionFactory.openSession(); //使用load方法加载id为1的文章 Post post = (Post)session.load(Post.class, Integer.valueOf(1)); //输出post实例的类名 System.out.println("class of post: " + post.getClass().getCanonicalName()); //取出文章标题并输出 System.out.println(post.getTitle()); }} 执行main方法,输出如下: class of post: com.magicwt.bean.Post$$EnhancerByCGLIB$$70a21d16Hibernate: select post0_.id as id1_0_, post0_.title as title1_0_, post0_.content as content1_0_ from post post0_ where post0_.id=?title of post: 测试标题1 从中可以看出,load方法返回的是通过动态代理实现的代理类实例。通过断点可以观察到,执行“post.getTitle()”前,post实例各属性都是默认值(null或0),执行“post.getTitle()”后,才真正从数据库表中取出数据并输出。将load方法修改为get方法并重新执行,输出如下: Hibernate: select post0_.id as id1_0_, post0_.title as title1_0_, post0_.content as content1_0_ from post post0_ where post0_.id=?class of post: com.magicwt.bean.Posttitle of post: 测试标题1 从中可以看出,get方法返回的是Post类实例。通过断点可以观察到,get方法返回的post实例各属性已从数据库表中取出对应的值,get方法并没有延迟加载。 实体属性延迟加载修改测试类Test:1234567891011121314151617181920212223package com.magicwt;import com.magicwt.bean.Post;import org.hibernate.SessionFactory;import org.hibernate.cfg.Configuration;import org.hibernate.classic.Session;public class Test { public static void main(String[] args) { SessionFactory sessionFactory = (new Configuration()).configure().buildSessionFactory(); Session session = sessionFactory.openSession(); //使用load方法加载id为1的文章 Post post = (Post)session.get(Post.class, Integer.valueOf(1)); //输出post实例的类名 System.out.println("class of post: " + post.getClass().getCanonicalName()); //输出post实例category属性的类名 System.out.println("class of category: " + post.getCategory().getClass().getCanonicalName()); //输出分类名称 System.out.println("name of category:" + post.getCategory().getName()); }} 执行main方法,输出如下: Hibernate: select post0_.id as id1_0_, post0_.title as title1_0_, post0_.content as content1_0_ from post post0_ where post0_.id=?class of post: com.magicwt.bean.Postclass of category: com.magicwt.bean.Category$$EnhancerByCGLIB$$483428f6Hibernate: select category0_.id as id0_0_, category0_.name as name0_0_ from category category0_ where category0_.id=?name of category: 测试分类1 从中可以看出,get方法返回的post实例,其属性category是代理类实例,并没有真正读取category表中的数据,只有在输出分类名称时,才读取category表中的数据。 集合属性延迟加载修改测试类Test:1234567891011121314151617181920212223242526package com.magicwt;import com.magicwt.bean.Post;import com.magicwt.bean.Tag;import org.hibernate.SessionFactory;import org.hibernate.cfg.Configuration;import org.hibernate.classic.Session;public class Test { public static void main(String[] args) { SessionFactory sessionFactory = (new Configuration()).configure().buildSessionFactory(); Session session = sessionFactory.openSession(); //使用load方法加载id为1的文章 Post post = (Post)session.get(Post.class, Integer.valueOf(1)); //输出post实例的类名 System.out.println("class of post: " + post.getClass().getCanonicalName()); //输出post实例tags属性的类名 System.out.println("class of tags: " + post.getTags().getClass().getCanonicalName()); //输出标签名称 for (Tag tag : post.getTags()) { System.out.println("name of tag: " + tag.getName()); } }} 执行main方法,输出如下: Hibernate: select post0_.id as id1_0_, post0_.title as title1_0_, post0_.content as content1_0_ from post post0_ where post0_.id=?class of post: com.magicwt.bean.Postclass of tags: org.hibernate.collection.PersistentSetHibernate: select tags0_.post_id as post1_1_, tags0_.tag_id as tag2_1_, tag1_.id as id3_0_, tag1_.name as name3_0_ from post_tag tags0_ left outer join tag tag1_ on tags0_.tag_id=tag1_.id where tags0_.post_id=?name of tag: 测试标签1name of tag: 测试标签2 从中可以看出,get方法返回的post实例,其属性tags是PersistentSet实例,并没有真正读取tag表中的数据,只有在输出标签名称时,才读取tag表中的数据。]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>Hibernate</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Keepalived介绍]]></title>
<url>%2F2013%2F06%2F30%2Fkeepalived-e4-bb-8b-e7-bb-8d%2F</url>
<content type="text"><![CDATA[Keepalived介绍Keepalived是一款高可用软件,它的功能主要包括两方面: 通过IP漂移,实现服务的高可用:服务器集群共享一个虚拟IP,同一时间只有一个服务器占有虚拟IP并对外提供服务,若该服务器不可用,则虚拟IP漂移至另一台服务器并对外提供服务; 对LVS应用服务层的应用服务器集群进行状态监控:若应用服务器不可用,则keepalived将其从集群中摘除,若应用服务器恢复,则keepalived将其重新加入集群中。 Keepalived可以单独使用,即通过IP漂移实现服务的高可用,也可以结合LVS使用,即一方面通过IP漂移实现LVS负载均衡层的高可用,另一方面实现LVS应用服务层的状态监控,如图所示: Keepalived原理Keepalived的实现基于VRRP(Virtual Router Redundancy Protocol,虚拟路由器冗余协议),而VRRP是为了解决静态路由的高可用。VRRP的基本架构如图所示:虚拟路由器由多个VRRP路由器组成,每个VRRP路由器都有各自的IP和共同的VRID(0-255),其中一个VRRP路由器通过竞选成为MASTER,占有VIP,对外提供路由服务,其他成为BACKUP,MASTER以IP组播(组播地址:224.0.0.18)形式发送VRRP协议包,与BACKUP保持心跳连接,若MASTER不可用(或BACKUP接收不到VRRP协议包),则BACKUP通过竞选产生新的MASTER并继续对外提供路由服务,从而实现高可用。 Keepalived安装配置下载地址:http://www.keepalived.org/download.htmlLinux下以默认配置安装: tar –zxvf keepalived-1.2.7.tar.gzcd keepalived-1.2.7./configuremakemake install 或: yum install keepalived 执行脚本: /etc/init.d/keepalived start|stop|restart 由于keepalived服务之间需要使用VRRP协议进行通信,因此需要进行防火墙配置: iptables –I INPUT –i eth0 –d 224.0.0.0/8 –j ACCEPTiptables –A INPUT –i eth0 –p vrrp –j ACCEPTiptables –A OUTPUT –p vrrp –o eth0 –j ACCEPT 配置文件: /etc/keepalived/keepalived.conf Keeaplived的配置包含三部分: 全局配置,配置邮件等; VRRPD配置,配置VRRP实例; LVS配置,配置LVS的应用服务器; 若只是单独使用keepalived,通过IP漂移实现服务的高可用,则只需要配置前两部分就可以,若结合LVS使用,实现LVS负载均衡层的高可用、应用服务层的状态监控,则还需要配置第三部分。 Keepalived应用示例单独使用(IP漂移)如图所示,两台机器192.168.80.128、192.168.80.129共享虚拟IP 192.168.80.130,192.168.80.128、192.168.80.129的keepalived配置分别如下所示:123456789101112131415161718global_defs { router_id LVS_DEVEL}vrrp_instance_VI_1{ state MASTER interface eth0 virtual_router_id 201 priority 100 advert_int 1 authentication { auth_type PASS auth_pass 1111 } virtual_ipaddress { 192.168.80.130/24 dev eth0 }} 123456789101112131415161718global_defs { router_id LVS_DEVEL}vrrp_instance_VI_1{ state BACKUP interface eth0 virtual_router_id 201 priority 50 advert_int 1 authentication { auth_type PASS auth_pass 1111 } virtual_ipaddress { 192.168.80.130/24 dev eth0 }} 启动192.168.80.128、192.168.80.129上的keepalived192.168.80.128成为MASTER,占有VIP 192.168.80.130并对外组播VRRP协议包192.168.80.129无变化 关闭192.168.80.128上的keepalived192.168.80.128不再占有VIP 192.168.80.130192.168.80.129成为MASTER,占有VIP 192.168.80.130并对外组播VRRP协议包 结合LVS使用LVS高可用集群通过VIP 192.168.80.130对外提供服务,负载均衡层有两台服务器192.168.80.132、192.168.80.135,负责分发服务请求至应用服务层,通过keepalived实现这两台服务器的负载均衡高可用(和单独使用keepalived的应用示例相同),应用服务层也有两台服务器192.168.80.133、192.168.80.134,负责对外提供Web应用服务,通过keepalived实现这两台服务器的状态监控。192.168.80.132、192.168.80.135上的keepalived配置在全局配置、VRRPD配置上和单独使用keepalived的应用示例相同,但需要添加LVS配置,如下所示:1234567891011121314151617181920212223virtual_server 192.168.80.130 80 { delay_loop 6 lb_algowlc lb_kind DR persistence_timeout 50 protocolTCP real_server 192.168.80.133 80 { weight 1 TCP_CHECK { connect_timeout 3 nb_get_retry 3 delay_before_retry 3 } } real_server 192.168.80.134 80 { weight 1 TCP_CHECK { connect_timeout 3 nb_get_retry 3 delay_before_retry 3 } }} 启动服务后,负载均衡层通过VIP对外提供服务,并将服务请求转发至133和134这两台应用服务器上:132和135这两台负载均衡服务器中,132占有VIP 130,并对133和134进行状态监控: 应用服务状态监控若133和134这两台应用服务器中,134服务不可用,则keepalived将其从应用服务器集群中摘除,待其恢复后,又重新加入,在其恢复前,负载均衡层将应用请求均转发至133,保证服务可用。 负载均衡服务高可用若132和135这两台负载均衡服务器中,132服务不可用,则keepalived将VIP 130漂移至135,由135负责应用请求转发,保证服务可用。]]></content>
<categories>
<category>服务器</category>
</categories>
<tags>
<tag>Keepalived</tag>
</tags>
</entry>
<entry>
<title><![CDATA[ActiveMQ的PHP、Python客户端]]></title>
<url>%2F2013%2F01%2F06%2F157%2F</url>
<content type="text"><![CDATA[ActiveMQ这款开源消息服务器提供了多语言支持,除了一般的Java客户端以外,还可以使用C/C++、PHP、Python、JavaScript(Ajax)等语言开发客户端。最近由于项目需要,需要提供PHP和Python的主题订阅客户端。这里作为总结,列出这两种语言客户端的简单安装和使用。对于PHP和Python,可以通过使用STOMP协议与消息服务器进行通讯。在ActiveMQ的配置文件activemq.xml中,需要添加以下语句,来提供基于STOMP协议的连接器。1234<transportConnectors> <transportConnector name="openwire" uri="tcp://0.0.0.0:61616"/> <transportConnector name="stomp" uri="stomp://0.0.0.0:61613"/><!--添加stomp连接器--> </transportConnectors> Python安装Python27,并安装stomppy这一客户端库:基于stomppy访问ActiveMQ的Python代码:1234567891011121314151617181920212223import time, sys import stomp #消息侦听器 class MyListener(object): def on_error(self, headers, message): print 'received an error %s' % message def on_message(self, headers, message): print '%s' % message #创建连接 conn = stomp.Connection([('127.0.0.1',61613)]) #设置消息侦听器 conn.set_listener('', MyListener()) #启动连接 conn.start() conn.connect() #订阅主题,并采用消息自动确认机制 conn.subscribe(destination='/topic/all_news', ack='auto') PHP安装PHP5,并安装STOMP的客户端库(http://php.net/manual/zh/book.stomp.php ):123456tar -zxf stomp-1.0.5.tgz cd stomp-1.0.5//usr/local/php/bin/phpize ./configure --enable-stomp --with-php-config=/usr/local/php/bin/php-configmakemake install 安装完成后,将生成的stomp.so移入php.ini中指定的extension_dir目录下,并在php.ini中添加该客户端库:1extension=stomp.so 访问ActiveMQ的PHP代码:123456789101112131415161718192021222324252627282930<?php $topic = '/topic/all_news'; /* connection */ try { $stomp = new Stomp('tcp://127.0.0.1:61613'); } catch(StompException $e) { die('Connection failed: ' . $e->getMessage()); } /* subscribe to messages from the queue 'foo' */ $stomp->subscribe($topic); /* read a frame */ while(true) { $frame = $stomp->readFrame(); if ($frame != null) { echo $frame->body; /* acknowledge that the frame was received */ $stomp->ack($frame); } } /* close connection */ unset($stomp); ?>]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>ActiveMQ</tag>
</tags>
</entry>
<entry>
<title><![CDATA[ActiveMQ的安全机制使用及其源代码分析]]></title>
<url>%2F2012%2F09%2F23%2Factivemq-e7-9a-84-e5-ae-89-e5-85-a8-e6-9c-ba-e5-88-b6-e4-bd-bf-e7-94-a8-e5-8f-8a-e5-85-b6-e6-ba-90-e4-bb-a3-e7-a0-81-e5-88-86-e6-9e-90%2F</url>
<content type="text"><![CDATA[ActiveMQ是目前较为流行的一款开源消息服务器。最近在项目开发中,需要为ActiveMQ开发基于IP的验证和授权机制,因此,对ActiveMQ的安全机制进行了了解,以下将介绍ActiveMQ的安全机制使用及其源代码分析。本文开发环境介绍:操作系统:Windows XPJava:jdk 1.6.0_12maven:maven 3.0.4ActiveMQ:ActiveMQ 5.6.0 ActiveMQ安全机制的介绍安全机制一般包含验证(Authentication)和授权(Authorization)两部分。在ActiveMQ中,验证指通过访问者的用户名和密码实现用户身份的验证,授权指为消息目标(队列或主题)的读、写、管理指定具有相应权限的用户组,并为用户分配权限。ActiveMQ的安全机制基于插件实现。ActiveMQ提供两种验证插件,分别是: Simple authentication plugin-in; JAAS(Java Authentication and Authorization Service)authentication plugin-in。 ActiveMQ提供一种授权插件:Authorization plugin-in。 ActiveMQ安全机制的使用ActiveMQ的使用可从ActiveMQ官网“http://activemq.apache.org/” 下载ActiveMQ的源代码包或二进制分发包。由于ActiveMQ使用Java开发,因此需要预先安装jdk,另外,由于ActiveMQ的开发使用了maven,因此,若下载的是源代码包,需要预先安装maven。解压源代码包,并在源代码包目录下执行“mvn install -Dmaven.test.skip=true ”完成编译、打包和安装,成功后,会在assembly\target下生成二进制分发包。若下载的是二进制分发包,解压即可。ActiveMQ的二进制分发包目录如下所示:进入bin文件,执行脚本,即可运行ActiveMQ。 Simple authentication plugin-in的使用在activemq.xml中如下配置:123456789<plugins> <simpleAuthenticationPlugin> <users> <authenticationUser username="system" password="password" groups="users,admins"/> <authenticationUser username="user" password="password" groups="users"/> <authenticationUser username="guest" password="password" groups="guests"/> </users> </simpleAuthenticationPlugin> </plugins> JAAS authentication plugin-in的使用在activemq.xml中如下配置:123<plugins> <jaasAuthenticationPlugin configuration="activemq-domain" /> </plugins> 创建login.config文件:123456activemq-domain { org.apache.activemq.jaas.PropertiesLoginModule required debug=true org.apache.activemq.jaas.properties.user="users.properties" org.apache.activemq.jaas.properties.group="groups.properties"; }; 创建users.properties和groups.properties文件,包含用户和用户组信息。users.properties:123system=password user=password guest=password groups.properties:123admins=system users=system,user guests=guest Authorization plugin-in的使用在activemq.xml中如下配置:123456789101112131415161718<plugins> <authorizationPlugin> <map> <authorizationMap> <authorizationEntries> <authorizationEntry queue=">" read="admins" write="admins" admin="admins" /> <authorizationEntry queue="USERS.>" read="users" write="users" admin="users" /> <authorizationEntry queue="GUEST.>" read="guests" write="guests,users" admin="guests,users" /> <authorizationEntry queue="TEST.Q" read="guests" write="guests" /> <authorizationEntry topic=">" read="admins" write="admins" admin="admins" /> <authorizationEntry topic="USERS.>" read="users" write="users" admin="users" /> <authorizationEntry topic="GUEST.>" read="guests" write="guests,users" admin="guests,users" /> <authorizationEntry topic="ActiveMQ.Advisory.>" read="guests,users" write="guests,users" admin="guests,users"/> </authorizationEntries> </authorizationMap> </map> </authorizationPlugin> </plugins> ActiveMQ安全机制的源代码分析ActiveMQ在其maven工程的activemq-core模块中实现安全机制。ActiveMQ原有安全机制均基于插件实现,实现思路如图所示。其中,Broker接口是ActiveMQ的核心接口,ActiveMQ消息服务器对象即该接口的实现。接口BrokerPlugin通过installPlugin方法传入Broker对象,为其创建插件。BrokerFilter类也实现自Broker接口,其与Broker的关系,类似于Struts中Interceptor与Action的关系,多个BrokerFilter对象以及消息服务器Broker对象通过指向下一个对象的引用next构成链状结构,当创建连接、消息生产者、消息消费者时,先后执行BrokerFilter中的相应方法,直至执行消息服务器中的方法,而安全机制类即继承自BrokerFilter。ActiveMQ原有安全机制的相关类均继承或实现自上述类或接口,安全机制的类包为activemq-core中的org.apache.activemq.security。 Simple authentication plugin-in的源代码分析Simple authentication plugin-in主要包含两个基本类:SimpleAuthenticationPlugin(实现自BrokerPlugin)和SimpleAuthenticationBroker(继承自BrokerFilter)。SimpleAuthenticationPlugin部分代码:12345678910111213141516171819public class SimpleAuthenticationPlugin implements BrokerPlugin { private Map<String, String> userPasswords; private Map<String, Set<Principal>> userGroups; private static final String DEFAULT_ANONYMOUS_USER = "anonymous"; private static final String DEFAULT_ANONYMOUS_GROUP = "anonymous"; private String anonymousUser = DEFAULT_ANONYMOUS_USER; private String anonymousGroup = DEFAULT_ANONYMOUS_GROUP; private boolean anonymousAccessAllowed = false; //...... //安装插件时,根据activemq.xml中的配置,新建 SimpleAuthenticationBroker对象, 并返回该对象 public Broker installPlugin(Broker parent) { SimpleAuthenticationBroker broker = new SimpleAuthenticationBroker(parent, userPasswords, userGroups); broker.setAnonymousAccessAllowed(anonymousAccessAllowed); broker.setAnonymousUser(anonymousUser); broker.setAnonymousGroup(anonymousGroup); return broker; } //...... } SimpleAuthenticationBroker部分代码:12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758public class SimpleAuthenticationBroker extends BrokerFilter { private boolean anonymousAccessAllowed = false; private String anonymousUser; private String anonymousGroup; private final Map<String,String> userPasswords; private final Map<String,Set<Principal>> userGroups; private final CopyOnWriteArrayList<SecurityContext> securityContexts = new CopyOnWriteArrayList<SecurityContext>(); //...... //由于验证需要在创建连接时进行,因此重写BrokerFilter的addConnection方法 public void addConnection(ConnectionContext context, ConnectionInfo info) throws Exception { SecurityContext s = context.getSecurityContext(); if (s == null) { //若允许匿名访问,则不进行验证 // Check the username and password. if (anonymousAccessAllowed && info.getUserName() == null && info.getPassword() == null) { info.setUserName(anonymousUser); s = new SecurityContext(info.getUserName()) { public Set<Principal> getPrincipals() { Set<Principal> groups = new HashSet<Principal>(); groups.add(new GroupPrincipal(anonymousGroup)); return groups; } }; //若不允许匿名访问,则验证连接的用户名和密码是否与配置文件中的一致,若不一致,则抛出安全异常 } else { String pw = userPasswords.get(info.getUserName()); if (pw == null || !pw.equals(info.getPassword())) { throw new SecurityException( "User name [" + info.getUserName() + "] or password is invalid."); } final Set<Principal> groups = userGroups.get(info.getUserName()); s = new SecurityContext(info.getUserName()) { public Set<Principal> getPrincipals() { return groups; } }; } context.setSecurityContext(s); securityContexts.add(s); } //调用父对象的addConnection方法,即调用next引用的Broker对象的addConnection方法 try { super.addConnection(context, info); } catch (Exception e) { securityContexts.remove(s); context.setSecurityContext(null); throw e; } } //...... } JAAS authentication plugin-in的源代码分析JAAS authentication plugin-in主要包含两个基本类:JaasAuthenticationPlugin(实现自BrokerPlugin)JaasAuthenticationBroker(继承自BrokerFilter)。JaasAuthenticationPlugin部分代码:1234567891011public class JaasAuthenticationPlugin implements BrokerPlugin { protected String configuration = "activemq-domain"; //...... public Broker installPlugin(Broker broker) { //读取配置文件, 初始化JAAS initialiseJaas(); //创建JaasAuthenticationBroker对象并返回 return new JaasAuthenticationBroker(broker, configuration); } //...... } JaasAuthenticationBroker部分代码:12345678910111213141516171819202122232425262728293031323334353637383940public class JaasAuthenticationBroker extends BrokerFilter { private final String jassConfiguration; private final CopyOnWriteArrayList<SecurityContext> securityContexts = new CopyOnWriteArrayList<SecurityContext>(); //...... //由于验证需要在创建连接时进行,因此重写BrokerFilter的addConnection方法 public void addConnection(ConnectionContext context, ConnectionInfo info) throws Exception { if (context.getSecurityContext() == null) { // Set the TCCL since it seems JAAS needs it to find the login // module classes. ClassLoader original = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(JaasAuthenticationBroker.class.getClassLoader()); try { // Do the login. try { JassCredentialCallbackHandler callback = new JassCredentialCallbackHandler(info .getUserName(), info.getPassword()); LoginContext lc = new LoginContext(jassConfiguration, callback); lc.login(); Subject subject = lc.getSubject(); //基于JAAS判断用户名和密码是否正确 SecurityContext s = new JaasSecurityContext(info.getUserName(), subject); context.setSecurityContext(s); securityContexts.add(s); } catch (Exception e) { throw (SecurityException)new SecurityException("User name [" + info.getUserName() + "] or password is invalid.") .initCause(e); } } finally { Thread.currentThread().setContextClassLoader(original); } } //调用父对象的addConnection方法,即调用next引用的Broker对象的addConnection方法 super.addConnection(context, info); } //...... } Authorization plugin-in的源代码分析Authorization plugin-in主要包含两个基本类:AuthorizationPlugin(实现自BrokerPlugin)AuthorizationBroker(继承自BrokerFilter)。AuthorizationPlugin部分代码:123456789101112131415public class AuthorizationPlugin implements BrokerPlugin { //AuthorizationMap对象存储activemq.xml中消息目标、读、写、管理用户组信息 private AuthorizationMap map; //...... //创建 AuthorizationBroker 对象并返回 public Broker installPlugin(Broker broker) { if (map == null) { throw new IllegalArgumentException("You must configure a 'map' property"); } return new AuthorizationBroker(broker, map); } //...... } AuthorizationBroker部分代码:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859public class AuthorizationBroker extends BrokerFilter implements SecurityAdminMBean { private final AuthorizationMap authorizationMap; //...... //由于需要授权是否可管理消息目标,因此重写BrokerFilter的 addDestinationInfo 方法 @Override public void addDestinationInfo(ConnectionContext context, DestinationInfo info) throws Exception { addDestination(context, info.getDestination(),true); super.addDestinationInfo(context, info); } //由于需要授权是否可管理消息目标,因此重写BrokerFilter的 addDestination 方法 @Override public Destination addDestination(ConnectionContext context, ActiveMQDestination destination,boolean create) throws Exception { final SecurityContext securityContext = context.getSecurityContext(); if (securityContext == null) { throw new SecurityException("User is not authenticated."); } Destination existing = this.getDestinationMap().get(destination); if (existing != null) { return super.addDestination(context, destination,create); } //从访问控制列表中查看是否具有授权 if (!securityContext.isBrokerContext()) { Set<?> allowedACLs = null; if (!destination.isTemporary()) { allowedACLs = authorizationMap.getAdminACLs(destination); } else { allowedACLs = authorizationMap.getTempDestinationAdminACLs(); } if (allowedACLs != null && !securityContext.isInOneOf(allowedACLs)) { throw new SecurityException("User " + securityContext.getUserName() + " is not authorized to create: " + destination); } } //调用next引用的addDestination方法 return super.addDestination(context, destination,create); } //由于需要授权是否可读消息,因此重写BrokerFilter的 addConsumer 方法,在该方法中,从访问控制列表中查看是否具有读授权,并调用next引用的addConsumer方法 @Override public Subscription addConsumer(ConnectionContext context, ConsumerInfo info) throws Exception { //...... return super.addConsumer(context, info); } //由于需要授权是否可写消息,因此重写BrokerFilter的 addProducer 方法,在该方法中,从访问控制列表中查看是否具有写授权,并调用next引用的 addProducer 方法 @Override public void addProducer(ConnectionContext context, ProducerInfo info) throws Exception { //...... super.addProducer(context, info); } //...... } 总结ActiveMQ提供了便利的插件开发方式,并基于插件实现了包含验证和授权的安全机制。参考ActiveMQ的源代码,可以进行插件开发,实现个性化的安全机制,如基于IP的验证和授权。]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>ActiveMQ</tag>
</tags>
</entry>
<entry>
<title><![CDATA[MyEclipse下Spring+Hibernate整合]]></title>
<url>%2F2012%2F07%2F27%2Fmyeclipse-e4-b8-8bspringhibernate-e6-95-b4-e5-90-88%2F</url>
<content type="text"><![CDATA[目前,SSH(Struts+Spring+Hibernate)是Web开发的一种常用框架组合,Struts实现了MVC,Hibernate实现了关系对象映射,Spring实现了基于Bean的配置管理。本文使用 MyEclipse实现Spring+Hibernate的整合。软件和框架使用版本: MyEclipse:8.0 Spring:2.5 Hibernate:3.2 MySQL:5.1 创建数据库连接 首先在MyEclipse下切换Perspective至MyEclipse Database Explorer,在左侧DB Browser中右键点击,在对话框中选择“New”,创建数据库连接MySQL。数据库连接的配置如图1所示,需要填写URL、用户名、密码、驱动等信息。 数据库连接配置成功后,左侧DB Browser中会显示该连接,右键点击该连接,在对话框中选择“Open connection”,显示数据库中的详细信息,如图2所示。在数据库中已有一user表,在下面将使用Spring+Hibernate实现对该表的操作。 新建Web工程,并配置Spring 切换Perspective至MyEclipse Java Enterprise,新建Web工程ssh,如图3所示。如果在随后的工作中还需整合Struts 2,则J2EE Specification Level应选择Java EE 5.0,此处暂选用默认配置J2EE 1.4。 右键点击该工程,在对话框中选择“MyEclipse->Add Spring Capabilities…”,添加Spring,并进行相关配置,如图4所示,采用默认配置即可。 配置Hibernate 右键点击该工程,在对话框中选择“MyEclipse->Add Hibernate Capabilities…”,添加Hibernate,并进行相关配置,如图5、图6、图7、图8、图9所示。由于需要使用Spring配置Hibernate,因此选中所有的Library。 选择Spring的配置文件applicationContext.xml进行配置。 选择已有的Spring配置文件,并使用Spring配置Hibernate中的SessionFactory,SessionFactory的bean id为sessionFactory。 配置数据源DataSource的bean id为dataSource,且其配置信息采用数据库连接MySQL。 不另写SessionFactory类,而采用Spring为Hibernate已设计的SessionFactory类。 数据库逆向工程 切换Perspective至MyEclipse Hibernate,右键点击数据表user,在对话框中选择“Hibernate Reverse Engineering…”,对user表的关系对象映射进行配置,如图10所示,其中第一个红框用于选择Java源文件目录,第二个红框用于选择是否创建关系对象映射文件,以hbm.xml结尾,第三个红框用于选择是否创建数据对象类,第四个红框用于选择是否创建数据访问对象类,均选择是,其他采用默认配置即可。 分析和测试按上述步骤配置后就可以在工程中实现Spring和Hibernate的整合,以及user表的关系对象映射,工程目录如图11所示,其中src目录下的applicationContext.xml是Spring和Hibernate的核心配置文件,pojo包中的三个文件是与user表对应的数据对象类User.java、数据访问对象类UserDAO.java、关系对象映射文件User.hbm.xml。applicationContext.xml内容如下所示:123456789101112131415161718192021222324252627282930313233343536<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"></property> <property name="url" value="jdbc:mysql://localhost:3306/web"></property> <property name="username" value="root"></property> <property name="password" value="root"></property> </bean> <bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"> <property name="dataSource"> <ref bean="dataSource" /> </property> <property name="hibernateProperties"> <props> <prop key="hibernate.dialect"> org.hibernate.dialect.MySQLDialect </prop> </props> </property> <property name="mappingResources"> <list> <value>pojo/User.hbm.xml</value> </list> </property> </bean> <bean id="UserDAO" class="pojo.UserDAO"> <property name="sessionFactory"> <ref bean="sessionFactory" /> </property> </bean></beans> 其中,bean dataSource实现数据库连接配置,bean sessionFactory实现Hibernate的SessionFactory,并使用bean dataSource的连接配置,bean UserDAO实现User数据对象访问,并使用bean sessionFactory创建会话。User.java代码部分如下:12345678910111213141516171819202122232425public class User implements java.io.Serializable { // Fields private Integer id; private String password; private String name; private String school; // Constructors /** default constructor */ public User() { } /** full constructor */ public User(String password, String name, String school) { this.password = password; this.name = name; this.school = school; } // Property accessors ....} 其中,对User对象类进行了定义,包括与user表字段对应的属性值、构造函数、Get/Set函数等。UserDAO.java代码部分如下所示:1234567891011121314151617181920212223public class UserDAO extends HibernateDaoSupport { private static final Log log = LogFactory.getLog(UserDAO.class); // property constants public static final String PASSWORD = "password"; public static final String NAME = "name"; public static final String SCHOOL = "school"; protected void initDao() { // do nothing } public void save(User transientInstance) { log.debug("saving User instance"); try { getHibernateTemplate().save(transientInstance); log.debug("save successful"); } catch (RuntimeException re) { log.error("save failed", re); throw re; } } ...} 该类继承了Spring的HibernateDaoSupport类,调用对象实例本身的getHibernateTemplate获取HibernateTemplate对象,进而调用其save、delete等方法实现数据的增加、删改等操作。User.hbm.xml内容如下所示:1234567891011121314151617181920212223<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <!-- Mapping file autogenerated by MyEclipse Persistence Tools --> <hibernate-mapping> <class name="pojo.User" table="user" catalog="web"> <id name="id" type="java.lang.Integer"> <column name="id" /> <generator class="identity" /> </id> <property name="password" type="java.lang.String"> <column name="password" length="10" not-null="true" /> </property> <property name="name" type="java.lang.String"> <column name="name" length="10" not-null="true" /> </property> <property name="school" type="java.lang.String"> <column name="school" length="30" not-null="true" /> </property> </class> </hibernate-mapping> 其中定义了user表和User类的关系对象映射。在UserDAO中编写一个主函数实现向user表中写入数据,如下所示:1234567891011public static void main(String args[]) { //从src/applicationContext.xml装载BeanFactory Resource res =new FileSystemResource("src/applicationContext.xml"); BeanFactory factory = new XmlBeanFactory(res); //从BeanFactory获取UserDAO UserDAO userDAO = (UserDAO) factory.getBean("UserDAO"); //添加新User User user = new User("123","Tom","Tsinghua"); userDAO.save(user); } 执行后,查看user表,如果该表已新增一条记录,说明配置成功。]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>Spring</tag>
<tag>Hibernate</tag>
</tags>
</entry>
<entry>
<title><![CDATA[浅谈Java多线程(二)]]></title>
<url>%2F2012%2F07%2F21%2Fe6-b5-85-e8-b0-88java-e5-a4-9a-e7-ba-bf-e7-a8-8b-ef-bc-88-e4-ba-8c-ef-bc-89%2F</url>
<content type="text"><![CDATA[线程的协调在Java多线程中,对于线程之间共享的实例资源,可以通过synchronized修饰符修饰其方法,实现线程之间的同步。另外,在多线程设计中,还需考虑到线程之间的协调。关于协调的一个典型设计模式便是Producer-Consumer(生产者-消费者)模式。 Producer-Consumer(生产者-消费者)模式在这一模式中,存在Producer和Consumer两类线程,Producer线程用于生成Data(共享数据资源),而Consumer线程用于消费Data,在Producer和Consumer之间,存在对于Data生成和消费的协调,即当不存在Data时,Consumer线程需要等待Producer线程生成新的Data,而当Data过多时,Producer线程需要等待Consumer线程消费过多的Data。在Producer-Consumer模式中,引入了Channel(管道)类,负责Data在各线程之间的协调。Producer-Consumer模式的UML类图如下所示。在上图中,Producer线程类和Consumer线程类均包含对Channel类对象的引用,而Channel类对象封装了Data类,并分别实现了生产和消费Data的同步方法produce和consume。在produce和consume方法中,通过使用wait和notifyAll方法进一步实现Data的协调。 wait、notify、notifyAll在Java中,wait、notify、notifyAll是Object类的方法,用于实现对调用对象方法的线程的暂停和唤醒。wait用于暂停线程,将其放入对象的wait set(线程等待集合),而notify、notifyAll方法用于唤醒wait set中的线程,使其继续执行。notify和notifyAll的不同是,当wait set中存在多个线程时,notify只会从中随机唤醒一个线程,而notifyAll会从中唤醒所有线程,由其进行竞争,获得同步锁并继续执行。一个关于wait、notify、notifyAll使用的简单示例如图所示。 Producer-Consumer(生产者-消费者)模式的实现示例Producer和Consumer线程类123456789101112131415161718192021222324252627package com.wt.pc; public class Producer extends Thread{ //对Channel对象的引用 Channel channel = null; //Consumer类构造函数 public Producer(String producerName,Channel channel) { super(producerName); this.channel = channel; } //Producer线程每隔500ms尝试生成新的data public void run() { try{ while(true) { channel.produce(Integer.toString(Channel.dataId++)); Thread.sleep(500); } }catch(Exception e){} } } 12345678910111213141516171819202122232425262728package com.wt.pc; public class Consumer extends Thread{ //对Channel对象的引用 Channel channel = null; //Consumer类构造函数 public Consumer(String consumerName,Channel channel) { super(consumerName); this.channel = channel; } //Consumer线程每隔500ms尝试消费data public void run() { try{ while(true) { channel.consume(); Thread.sleep(500); } }catch(Exception e){} } } Channel和Data类1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859package com.wt.pc; public class Channel { //静态变量,用于生成data名称 static int dataId = 0; //存储data的数组 Data dataList[]; //当前未消费data的头序号 int head; //当前未消费data的尾序号的下一个 int tail; //当前未消费data的数目 int count; //Channel类构造函数 //数组容量为3,其他值初始化为0 public Channel() { dataList = new Data[3]; head = 0; tail = 0; count = 0; } //produce方法,用于生成data public synchronized void produce(String dataName)throws Exception{ //当数组容量已满时,即不能再生成新data时,当前线程进入wait set while (count>=3) { wait(); } //生成新的data System.out.println(Thread.currentThread().getName()+" is producing "+dataName); dataList[tail] = new Data(); dataList[tail].setDataName(dataName); tail = (tail+1)%3; count++; Thread.sleep(400); //唤醒wait set中的线程 notifyAll(); } //consume方法,用于消费data public synchronized void consume()throws Exception{ //当数组容量为空时,即不能再消费data时,当前线程进入wait set while (count<=0) { wait(); } //消费data System.out.println(Thread.currentThread().getName()+" is consuming "+dataList[head].getDataName()); head = (head+1)%3; count--; Thread.sleep(300); //唤醒wait set中的线程 notifyAll(); } } 123456789101112package com.wt.pc; public class Data { String dataName; public String getDataName() { return dataName; } public void setDataName(String dataName) { this.dataName = dataName; } } 主函数1234567891011public static void main(String args[]) { Channel channel = new Channel(); new Producer("p1",channel).start(); new Producer("p2",channel).start(); new Producer("p3",channel).start(); new Consumer("c1",channel).start(); new Consumer("c2",channel).start(); new Consumer("c3",channel).start(); } 执行结果 p2 is producing 0c2 is consuming 0p3 is producing 2p1 is producing 1c3 is consuming 2c1 is consuming 1p1 is producing 5p3 is producing 4c2 is consuming 5p2 is producing 3p3 is producing 7c1 is consuming 4c3 is consuming 3p3 is producing 9p1 is producing 6c2 is consuming 7p2 is producing 8c3 is consuming 9c1 is consuming 6p2 is producing 12p3 is producing 10c2 is consuming 8…… 从执行结果中可以看出,生成Data线程的执行次数p和消费Data线程的执行次数c始终满足: p>=c且p<=c+3 即保证Data数组在消费时存在Data但也不超过数组容量,从而有效实现Producer和Consumer线程类关于Data的协调。 总结在Java多线程设计中,需要充分考虑线程之间的同步和协调。针对不同的应用场景,可以采用不同的设计模式,已有的设计模式有Single Threaded Execution、Immutable、Guarded Suspension、Balking、Producer-Consumer、Read-Write Lock、Thread-Per-Message、Worker Thread等,具体可进一步参考网上有关“Java多线程设计模式”的教程。]]></content>
<categories>
<category>后端</category>
</categories>
</entry>
<entry>
<title><![CDATA[浅谈Java多线程(一)]]></title>
<url>%2F2012%2F07%2F18%2Fe6-b5-85-e8-b0-88java-e5-a4-9a-e7-ba-bf-e7-a8-8b-ef-bc-88-e4-b8-80-ef-bc-89%2F</url>
<content type="text"><![CDATA[Java支持多线程,可以通过继续Thread类或实现Runnable接口定义线程类,并在该类中重写run方法。一个简单的Java线程类MyThread如下所示。12345678910111213141516171819202122package com.wt.testThread; public class MyThread extends Thread{ public void run() { int i = 0; while (true) { i++; System.out.println("MyThread Processing --> "+i); try { Thread.sleep(500); } catch (InterruptedException e) {} } } public static void main(String args[]) { MyThread thread = new MyThread(); thread.start(); } } 在MyThread的run方法中,每隔500ms(Thread.sleep(500);)输出一次信息。在主函数中,新建MyThread实例对象,并使用对象的start方法启动该线程实例。程序输出结果部分如下所示。 MyThread Processing –> 1MyThread Processing –> 2MyThread Processing –> 3 多线程的同步假设现在有两件工具,三个工人,每个工人工作时需要同时使用这两件工具。使用Java多线程模拟上述情景,使用线程类WorkThread表示工人,使用ToolBox表示两件工具所在的工具箱,如下所示。1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950class ToolBox{ //t1Owner,t2Owner分别表示正在使用该工具的工人名 private String t1Owner; private String t2Owner; //useTool表示某位工人取得工具并使用 public void useTool(String workerName) throws InterruptedException { t1Owner = workerName; Thread.sleep(100); t2Owner = workerName; Thread.sleep(100); checkTool(); } //useTool检查工具是否正在被同一个工人使用 public void checkTool() { if (!t1Owner.equals(t2Owner)){ System.out.println("*Wrong*: Tool1:"+t1Owner+"\t Tool2:"+t2Owner); } else{ System.out.println("*Right*: Tool1:"+t1Owner+"\t Tool2:"+t2Owner); } } } public class WorkerThread extends Thread{ private ToolBox toolBox; private String workerName; public WorkerThread(String workerName,ToolBox toolBox) { super(); this.workerName = workerName; this.toolBox = toolBox; } public void run() { //工人每次休息1秒,然后使用工具工作 while(true) { try { Thread.sleep(1000); toolBox.useTool(workerName); } catch (InterruptedException e) {} } } } 主函数如下所示,其中ToolBox类实例toolBox表示工人共用的工具箱,通过WorkerThread类的构造函数构造三个工人线程类,并传入工人姓名和toolBox,启动线程。123456789public static void main(String args[]){ //工人共用的工具箱 ToolBox toolBox = new ToolBox(); //三个工人开始工作 new WorkerThread("Tom",toolBox).start(); new WorkerThread("Jack",toolBox).start(); new WorkerThread("Bob",toolBox).start();} 执行结果部分如下所示。 *Right*: Tool1:Bob Tool2:Bob*Right*: Tool1:Bob Tool2:Bob*Wrong*: Tool1:Bob Tool2:Jack*Wrong*: Tool1:Bob Tool2:Jack*Wrong*: Tool1:Bob Tool2:Jack*Right*: Tool1:Jack Tool2:Jack*Right*: Tool1:Jack Tool2:Jack*Right*: Tool1:Jack Tool2:Jack*Wrong*: Tool1:Jack Tool2:Tom*Wrong*: Tool1:Jack Tool2:Tom*Wrong*: Tool1:Jack Tool2:Tom*Right*: Tool1:Tom Tool2:Tom 可以看出,由于三个工人线程类共享使用工具箱中的工具,导致存在对于工具使用的竞争。由于ToolBox中useTool方法的代码(即工人使用工具)存在上述多线程之间的资源竞争,因此将其称为临界区(Critical Section)。临界区如下所示。12345t1Owner = workerName; Thread.sleep(100); t2Owner = workerName; Thread.sleep(100); checkTool(); 对于上述临界区中的代码,需要实现同步,即在一个线程中,要么都执行,要么都不执行。在Java中,可以使用关键字synchronized实现同步。synchronized的使用方法有两种: 对于需要同步的共享实例对象,将synchronized添加为方法的修饰符,如下所示。 1public synchronized void useTool(String workerName) throws InterruptedException 将需要同步的临界区代码放入synchronized(this){}中,如下所示。 12345678synchronized(this) { t1Owner = workerName; Thread.sleep(100); t2Owner = workerName; Thread.sleep(100); checkTool(); } 使用synchronized同步后,重新执行原主函数,得到正确执行结果。 *Right*: Tool1:Tom Tool2:Tom*Right*: Tool1:Bob Tool2:Bob*Right*: Tool1:Jack Tool2:Jack*Right*: Tool1:Tom Tool2:Tom*Right*: Tool1:Bob Tool2:Bob*Right*: Tool1:Jack Tool2:Jack*Right*: Tool1:Tom Tool2:Tom*Right*: Tool1:Bob Tool2:Bob*Right*: Tool1:Jack Tool2:Jack*Right*: Tool1:Tom Tool2:Tom*Right*: Tool1:Bob Tool2:Bob*Right*: Tool1:Jack Tool2:Jack*Right*: Tool1:Tom Tool2:Tom*Right*: Tool1:Bob Tool2:Bob*Right*: Tool1:Jack Tool2:Jack*Right*: Tool1:Tom Tool2:Tom]]></content>
<categories>
<category>后端</category>
</categories>
</entry>
<entry>
<title><![CDATA[Java的类加载机制]]></title>
<url>%2F2012%2F07%2F15%2Fjava-e7-9a-84-e7-b1-bb-e5-8a-a0-e8-bd-bd-e6-9c-ba-e5-88-b6%2F</url>
<content type="text"><![CDATA[Java包含两种基本的类加载器,分别是启动类加载器和用户自定义类加载器。启动类加载器负责加载核心Java API的class文件,而用户自定义类加载器负责加载其他class文件,如用于安装或下载标准扩展class文件的标准扩展类加载器,在类路径中发现类库class文件的类路径类加载器。Java中类加载机制采用委派双亲模式。启动类加载器、标准扩展类加载器、类路径类加载器等组成双亲-孩子关系链,如图所示。在委派双亲模式下,当需要加载一个类时,类加载器首先将加载请求传递给它的双亲加载器,如此直至将加载请求向上传递给启动类加载器。若启动类加载器可以加载,则由其加载,若启动类加载器无法加载,则将加载请求传递给它的孩子加载器,如此直至将加载请求向下传递给可以加载的类加载器并实现加载。例如:当需要加载java.util.HashMap类时,首先将加载请求向上传递给启动类加载器,由于核心Java API包含java.util.HashMap,因此由启动类加载器完成加载即可。而当需要加载com.wt.Test类时,首先仍将加载请求向上传递给启动类加载器,由于核心Java API不包含com.wt.Test,因此加载请求又向下传递给标准扩展类加载器,而由于属于标准扩展也不包含com.wt.Test,因此加载请求又向下传递给类路径类加载器。若com.wt.Test在类路径下,则由类路径类加载器完成加载,若com.wt.Test不在类路径下,则再由网络类加载器尝试从网络中实现加载。采用委派双亲模式实现类加载有利于类安全。例如,对于java.util.HashMap,可以保证由启动类加载器加载,采用核心Java API中的类,而避免由用户自定义类加载器加载时,可能采用被恶意修改的同名类。参考:《深入Java虚拟机》,Bill Venners 著]]></content>
<categories>
<category>后端</category>
</categories>
</entry>
<entry>
<title><![CDATA[八皇后问题与回溯算法]]></title>
<url>%2F2012%2F07%2F11%2Fe5-85-ab-e7-9a-87-e5-90-8e-e9-97-ae-e9-a2-98-e4-b8-8e-e5-9b-9e-e6-ba-af-e7-ae-97-e6-b3-95%2F</url>
<content type="text"><![CDATA[八皇后问题是在8*8的棋盘上放置8枚皇后,使得棋盘中每个纵向、横向、左上至右下斜向、右上至左下斜向均只有一枚皇后。八皇后的一个可行解如图所示: 思路对于八皇后的求解可采用回溯算法,从上至下依次在每一行放置皇后,进行搜索,若在某一行的任意一列放置皇后均不能满足要求,则不再向下搜索,而进行回溯,回溯至有其他列可放置皇后的一行,再向下搜索,直到搜索至最后一行,找到可行解,输出。可以使用递归函数实现上述回溯算法,递归函数用于求解在某一行放置皇后,具体代码如下所示。 代码123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081#include <stdlib.h>#include <stdio.h> int m[8][8] = {0};//表示棋盘,初始为0,表示未放置皇后int num = 0;//解数目 //对于棋盘前row-1行已放置好皇后//检查在第row行、第column列放置一枚皇后是否可行bool check(int row,int column){ if(row==1) return true; int i,j; //纵向只能有一枚皇后 for(i=0;i<=row-2;i++) { if(m[i][column-1]==1) return false; } //左上至右下只能有一枚皇后 i = row-2; j = i-(row-column); while(i>=0&&j>=0) { if(m[i][j]==1) return false; i--; j--; } //右上至左下只能有一枚皇后 i = row-2; j = row+column-i-2; while(i>=0&&j<=7) { if(m[i][j]==1) return false; i--; j++; } return true;} //当已放置8枚皇后,为可行解时,输出棋盘void output(){ int i,j; num++; printf("answer %d:\n",num); for(i=0;i<8;i++) { for(j=0;j<8;j++) printf("%d ",m[i][j]); printf("\n"); }} //采用递归函数实现八皇后回溯算法//该函数求解当棋盘前row-1行已放置好皇后,在第row行放置皇后void solve(int row){ int j; //考虑在第row行的各列放置皇后 for (j=0;j<8;j++) { //在其中一列放置皇后 m[row-1][j] = 1; //检查在该列放置皇后是否可行 if (check(row,j+1)==true) { //若该列可放置皇后,且该列为最后一列,则找到一可行解,输出 if(row==8) output(); //若该列可放置皇后,则向下一行,继续搜索、求解 else solve(row+1); } //取出该列的皇后,进行回溯,在其他列放置皇后 m[row-1][j] = 0; }} //主函数int main(){ //求解八皇后问题 solve(1); return 0;}]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
</entry>
<entry>
<title><![CDATA[Java笔试题]]></title>
<url>%2F2012%2F06%2F23%2Fjava-e7-ac-94-e8-af-95-e9-a2-98%2F</url>
<content type="text"><![CDATA[OOP三特性 Java中如何实现多继承 Java对象生成过程 HashTable和HashMap的区别 接口的成员域具有什么属性 经常使用的三种设计模式,画出UML图,并写出Java代码 final、finally、finalize的区别 SOAP、WSDL、UDDI的作用 Java输入输出流的两种类型是什么 ArrayList如何实现自定义的排序 下列代码如何优化(代码非原题,应是String到StringBuffer的优化) 123456public String test(String s1,String s2){ String s = s1; s+=s2; return s;} 写出一个JSP/Servlet,实现传入A和B,计算A*B,并显示结果]]></content>
<categories>
<category>后端</category>
</categories>
</entry>
<entry>
<title><![CDATA[Java中对象的初始化生成过程]]></title>
<url>%2F2012%2F06%2F22%2Fjava-e4-b8-ad-e5-af-b9-e8-b1-a1-e7-9a-84-e5-88-9d-e5-a7-8b-e5-8c-96-e7-94-9f-e6-88-90-e8-bf-87-e7-a8-8b%2F</url>
<content type="text"><![CDATA[Java是面向对象的一种语言,在Java对象生成的过程,涉及子类和父类的加载、静态成员变量的初始化、子类和父类对象的初始化等过程,其具体过程通过下述代码来说明。12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455class A { public A(String s) { System.out.println(s+" Constructor A"); } } class B { public B(String s) { System.out.println(s+ " Constructor B"); } } class C { public C(String s) { System.out.println(s+ " Constructor C"); } } class Base { static A a1 = new A("a1: static"); A a2 = new A("a2: normal"); public Base() { A a3 = new A("a3: normal"); System.out.println("Constructor Base"); } } class Derived extends Base { static B b1 = new B("b1: static"); B b2 = new B("b2: normal"); public Derived() { B b3 = new B("b3: normal"); System.out.println("Constructor Derived"); } } public class Test { static C c1 = new C("c1: static"); C c2 = new C("c2: normal"); public static void main(String[] args) { C c3 = new C("c3: normal"); Derived derived = new Derived(); System.out.println("end"); } } 该段代码的执行结果为: c1: static Constructor Cc3: normal Constructor Ca1: static Constructor Ab1: static Constructor Ba2: normal Constructor Aa3: normal Constructor AConstructor Baseb2: normal Constructor Bb3: normal Constructor BConstructor Derivedend 对上述执行结果进行分析,其生成过程为: 程序执行时,首先加载main函数所在的类Test,由于Test类包含静态成员变量c1,因此对该变量进行初始化,调用其构造函数。 由于c2是Test类的对象成员变量,且此处并没有初始化Test类对象,因此不需要初始化c2。 执行main函数。 main函数声明且初始化变量c3,因此调用其构造函数。 main函数声明且初始化变量derived,此过程可具体划分为以下步骤: 加载Derived类,由于其继承自Base类,因此还需加载Base类。 类加载完成后,由父类至子类,先后完成其中静态成员变量的初始化,因此先后调用a1,b1的构造函数。 静态成员变量初始化后,由父类至子类,先后完成类对象的初始化。在类对象的初始化过程中,首先初始化对象成员变量,再调用构造函数。因此,在Base类,首先调用a2的构造函数,再调用Base的构造函数,在构造函数中,调用a3的构造函数;而在Derived类,首先调用b2的构造函数,再调用Derived的构造函数,在构造函数中,调用b3的构造函数。 综上,完成了整个对象的初始化生成过程和程序运行。 参考:《Java编程思想》]]></content>
<categories>
<category>后端</category>
</categories>
</entry>
<entry>
<title><![CDATA[1000瓶药,需要多少只小白鼠试验(二)]]></title>
<url>%2F2012%2F06%2F17%2F1000-e7-93-b6-e8-8d-af-e9-9c-80-e8-a6-81-e5-a4-9a-e5-b0-91-e5-8f-aa-e5-b0-8f-e7-99-bd-e9-bc-a0-e8-af-95-e9-aa-8c-ef-bc-88-e4-ba-8c-ef-bc-89%2F</url>
<content type="text"><![CDATA[1000瓶水,其中有一瓶有毒,小白鼠喝后24小时会死忙。请问最少多少只老鼠,可以在24小时测出哪瓶有毒。答案:最少需要[log21000]=10只小白鼠 试验方法:1000瓶水,分别编号从1到1000,并用10位二进制表示。10只老鼠,编号从0到9。喝药策略:对于编号为X的水, 二进制表示为b9b8b7b6b5b4b3b2b1b0。如果bi=1(i=0,…,9),则表示第i个小老鼠要喝这瓶水;否则,不喝。如:ID=7的水,二进制表示为0000000111,则表示第0,1,2只老鼠要喝这瓶水,6-9小老鼠不喝。结果:假设编号为X的水为毒水(二进制表示为b9b8b7b6b5b4b3b2b1b0)。如果第i个小白鼠活着,则说明第i个小白鼠没喝这瓶水,则bi=0;同理,如果第i个小白鼠死了,则说明第i个小白鼠喝了这瓶毒水,则bi=1。根据10只小白鼠的活(0)或死(1)的状态,我们就可以唯一确定毒水的编号。]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
</entry>
<entry>
<title><![CDATA[1000瓶药有1瓶有毒,需要多少只小白鼠才能试验找出]]></title>
<url>%2F2012%2F06%2F17%2F1000-e7-93-b6-e8-8d-af-e6-9c-891-e7-93-b6-e6-9c-89-e6-af-92-ef-bc-8c-e9-9c-80-e8-a6-81-e5-a4-9a-e5-b0-91-e5-8f-aa-e5-b0-8f-e7-99-bd-e9-bc-a0-e6-89-8d-e8-83-bd-e8-af-95-e9-aa-8c-e6-89-be-e5-87-ba%2F</url>
<content type="text"><![CDATA[1000瓶药有1瓶有毒,如果小白鼠服用有毒的药,则24小时后死亡。现在需设计一种策略,使用尽可能少的小白鼠,在24小时内找出有毒的药。 思路:令1000瓶药的序号为1,2,…,999,1000,小白鼠的数目为n。a(k)表示小白鼠k在24小时后的状态:a(k) = 1, 小白鼠k存活;a(k) = 0,小白鼠k死亡。如此,所有n只小白鼠在24小时后的状态为a(1)a(2)…a(k)…a(n-1)a(n),相当于n位2进制数,若n = 10,则2^10 = 1024 > 1000,也就是说,可设计一种策略,使得某一瓶有毒映射一个10位二进制数,这样,需要10只小白鼠就可以在24小时内找出哪一瓶有毒。 具体策略:令瓶号表示为b(1)b(2)…b(k)…b(9)b(10),对于第k只小白鼠,它尝试b(k) = 0的药。对于第1只小白鼠,它尝试b(1) = 0的药,即尝试1至512的药。对于第10只小白鼠,它尝试b(10) = 0的药,即尝试瓶号为偶数的药。这样,当24小时后,得到10只小白鼠的状态a(1)a(2)…a(k)…a(9)a(10),且由10个状态组成的10位二进制数即有毒药瓶序号。 举例分析:若10只小白鼠的状态为1001001001,换算成10进制即585。进行验证:a(1) = 1,由于第1只小白鼠尝试1至512的药,且存活,所以有毒药瓶在513至1000之间。a(2) = 0,由于第2只小白鼠尝试1至256,513至768的药,且死亡,所以有毒药瓶在513至768之间。a(3) = 0,由于第3只小白苏尝试1至128,257至384,513至640,769至896的药,且死亡,所以有毒药瓶在513至640之间。a(4) = 1,有毒药瓶在577至640之间。a(5) = 0,有毒药瓶在577至608之间。a(6) = 0,有毒药瓶在577至592之间。a(7) = 1,有毒药瓶在585至592之间。a(8) = 0,有毒药瓶在585至588之间。a(9) = 0,有毒药瓶在585至586之间。a(10) = 1,有毒药瓶是585。]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
</entry>
<entry>
<title><![CDATA[一道有关飞机绕赤道飞行的面试题]]></title>
<url>%2F2012%2F06%2F12%2Fe4-b8-80-e9-81-93-e6-9c-89-e5-85-b3-e9-a3-9e-e6-9c-ba-e7-bb-95-e8-b5-a4-e9-81-93-e9-a3-9e-e8-a1-8c-e7-9a-84-e9-9d-a2-e8-af-95-e9-a2-98%2F</url>
<content type="text"><![CDATA[假设赤道上有且仅有一座机场,并有若干架飞机,每架飞机的油量可保证飞半个赤道的距离。现在要求一架飞机从机场起飞,绕赤道一圈后,返回机场。在该飞机飞行过程中,不能中途降落、返回。其他飞机可返回,也可在空中给飞机加油,但不能降落,且必须返回机场。求飞机飞行和加油策略。 思路假设完成环球飞行的飞机为A,由于A的油量仅满足飞行赤道半圈,所以必须通过其他飞机给其加油。而其他飞机给A加油时,也需要使自己剩余的油量能够保证返回机场,因此,尽量在离机场近的空中给A加油,这样的策略才较优,因此,首先初步的策略是:A飞行至1/4圈处,其他同向飞行的飞机给A加满油返回,A独自飞行1/2圈,至3/4圈处,从机场逆向起飞的飞机飞行至3/4圈处,给A加适量油,保证其继续飞行至机场。图中,矩形框表示A的油箱。针对上述初步策略,下面需要求解的问题是:从0至1/4圈和从3/4圈至1圈,如何安排飞机给A加油并保证其他飞机返回。从0至1/4圈的解决方案是: A和B1、B2从机场起飞,飞至1/8圈处,B2分别给A、B1加满油,剩下1/4的油可返回机场。 A和B1飞至1/4圈处,B1给A加满油,剩下1/2的油可返回机场。 从3/4至1圈的解决方案是: A独自飞行至3/4圈处,迎面飞来的C1给A加1/4油,剩下1/4油。 A、C1飞行至7/8圈处,迎面飞来的C2分别给A、C1加1/4油,剩下1/4油,从而三架飞机均可返回机场。]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
</entry>
<entry>
<title><![CDATA[Lucene简介]]></title>
<url>%2F2012%2F06%2F09%2Flucene-e7-ae-80-e4-bb-8b%2F</url>
<content type="text"><![CDATA[Lucene是Apache基金会下的一个开源项目,提供能够实现全文索引和检索的Java API。Lucene包含索引引擎和检索引擎两部分。对于包含多个字段(Field)的文档(Document),可以通过Lucene的索引引擎对文档字段中的文本内容进行分词处理,构建关键字索引。当索引构建完成后,可以通过Lucene的检索引擎对特定字段进行基于关键字的查询。Lucene支持多种查询方式,包括模糊检索、分组查询等。对于查询结果,Lucene使用基于向量空间模型的排名算法计算得出查询结果的排名。 倒排索引倒排索引(Inverted Index)是一种索引数据结构。在倒排索引中,词语被映射到包含该词语的文档。通过使用倒排索引,可以实现快速的全文搜索。一个简单的倒排索引及其构建过程如图1.1所示,其中文档d1和d2的内容分别是“home sales rise in July”和“increase in home sales in July”。对于文档d1和d2,首先进行分词处理,将文本内容划分为词语集。因为在英文文本中,单词之间均有空格,所以使用空格作为分隔符进行分词处理,得到词语集,如图1.1中左侧一列。对于划分后的词语集,进行统计,统计词语及其出现的次数和位置,如图1.1中右侧一列,构成倒排索引。 1.2 Lucene工作原理Lucene中包含了以下6个核心包: org.apache.lucene.document包,包含了用于表示文档及其内容的类,如表示文档的Document类,表示文档中字段的Field类。 org.apache.lucene.index包,包含了用于构建、读取索引的类。 org.apache.lucene.analysis包,包含了用于对文档中的自然语言文本进行分词处理的类。 org.apache.lucene.store包,包含了用于存储索引的类。 org.apache.lucene.search包,包含了用于查询索引的类。 org.apache.lucene.queryParser包,包含了用于构建、解析查询条件的类。 在Lucene的倒排索引中,包含字段(Field)、文档(Document)、关键字(Term)这三个部分。每一个关键字均与一个集合相映射。集合中的每一个元素为一个二元组(Document,Field),表示该文档的该字段包含此关键字。Lucene的工作原理如图所示,主要分为以下6个步骤: 为每一个待检索的文件构建Document类对象,将文件中各部分内容作为Field类对象。 使用Analyzer类实现对文档中的自然语言文本进行分词处理,并使用IndexWriter类构建索引。 使用FSDirectory类设定索引存储的方式和位置,实现索引的存储。 使用IndexReader类读取索引。 使用Term类表示用户所查找的关键字以及关键字所在的字段,使用QueryParser类表示用户的查询条件。 使用IndexSearcher类检索索引,返回符合查询条件的Document类对象。 Lucene应用示例1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192package com.wt.testlucene.main; import java.io.File; import java.io.FileReader; import java.io.IOException; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.index.CorruptIndexException; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriter.MaxFieldLength; import org.apache.lucene.queryParser.ParseException; import org.apache.lucene.queryParser.QueryParser; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.Query; import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.TopDocs; import org.apache.lucene.store.FSDirectory; import org.apache.lucene.store.LockObtainFailedException; import org.apache.lucene.util.Version; public class TestLucene { private static final String indexDir = "J:\\tempIndex"; private static final String dataDir = "J:\\tempData"; public static void main(String[] args) throws CorruptIndexException, LockObtainFailedException, IOException, ParseException { /******************* 写入索引 *******************/ // IndexWriter用于写入索引 // FSDirectory表示索引存储于磁盘 // StandardAnalyzer表示采用标准的词法分析器进行分词处理 IndexWriter indexWriter = new IndexWriter(FSDirectory.open(new File( indexDir)), new StandardAnalyzer(Version.LUCENE_CURRENT), true, MaxFieldLength.UNLIMITED); indexWriter.setUseCompoundFile(false); File[] files = new File(dataDir).listFiles(); for (int i = 0; i < files.length; i++) { // Document表示索引中的文档 Document document = new Document(); // Field表示文档中的域,对于域有不同的处理方法 // Field.Store用于设置存储属性 // YES:存储 // NO :不存储 // Field.Index用于设置存储属性 // NO :不索引 // ANALYZED :索引且保存NORMS信息 // ANALYZED_NO_NORMS :索引但不保存NORMS信息 // NOT_ANALYZED :不索引但保存NORMS信息 // NOT_ANALYZED_NO_NORMS:不索引且不保存NORMS信息 // 存储name的域保存、索引但不分词 document.add(new Field("name", files[i].getName(), Field.Store.YES, Field.Index.NOT_ANALYZED)); // 存储content的域保存、索引且分词 document.add(new Field("content", new FileReader(files[i]))); // 存储path的域保存、不索引 document.add(new Field("path", files[i].getAbsolutePath(), Field.Store.YES, Field.Index.NO)); // 加入文档 indexWriter.addDocument(document); } // 优化 indexWriter.optimize(); // 提交 indexWriter.commit(); // 关闭 indexWriter.close(); /******************* 查询索引 *******************/ // IndexSearcher用于查询索引 IndexSearcher indexSearcher = new IndexSearcher(FSDirectory .open(new File(indexDir))); String queryString = "TEST"; // QueryParser用于解析查询语句生成相应的查询 QueryParser parser = new QueryParser(Version.LUCENE_CURRENT, "content", new StandardAnalyzer(Version.LUCENE_CURRENT)); Query query = parser.parse(queryString); // 返回最符合查询条件的前10个结果 TopDocs topDocs = indexSearcher.search(query, 10); ScoreDoc[] list = topDocs.scoreDocs; for (int i = 0; i < list.length; i++) { ScoreDoc scoreDoc = list[i]; Document document = indexSearcher.doc(scoreDoc.doc); // 输出文件序号、得分和名称 System.out.println(scoreDoc.doc + "\t" + scoreDoc.score + "\t" + document.get("name")); } indexSearcher.close(); } }]]></content>
<categories>
<category>后端</category>
</categories>
<tags>
<tag>Lucene</tag>
</tags>
</entry>
<entry>
<title><![CDATA[求解二叉树的深度]]></title>
<url>%2F2012%2F06%2F06%2Fe6-b1-82-e8-a7-a3-e4-ba-8c-e5-8f-89-e6-a0-91-e7-9a-84-e6-b7-b1-e5-ba-a6%2F</url>
<content type="text"><![CDATA[思路采用递归求解,对于树tree的深度,其值为: Depth(tree) =max(Depth(tree的左子树),Depth(tree的右子树)),tree != NULL0,tree == NULL 代码1234567891011121314151617181920212223242526272829303132333435363738#include <stdlib.h> #include <stdio.h> typedef struct TNode { int value; TNode* lchild; TNode* rchild; }TNode,*BTree; //采用递归思想求解树的深度 int CalDepth(BTree tree) { //若树为空,则树的深度为0 if (tree==NULL) return 0; int left,right; //仅考虑左子树,则树的深度等于左子树的深度加1 left = CalDepth(tree->lchild)+1; //仅考虑右子树,则树的深度等于右子树的深度加1 right = CalDepth(tree->rchild)+1; //树的深度取上述两个值中的较大值 return (left>right?left:right); } int main() { BTree tree = (BTree)malloc(sizeof(TNode)); tree->lchild = (TNode*)malloc(sizeof(TNode)); tree->lchild->lchild = NULL; tree->lchild->rchild = NULL; tree->rchild = (TNode*)malloc(sizeof(TNode)); tree->rchild->lchild = (TNode*)malloc(sizeof(TNode)); tree->rchild->rchild = NULL; tree->rchild->lchild->lchild = NULL; tree->rchild->lchild->rchild = NULL; printf("the depth of the tree is:%d\n",CalDepth(tree)); return 0; }]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
</entry>
<entry>
<title><![CDATA[字典树]]></title>
<url>%2F2012%2F06%2F06%2Fe5-ad-97-e5-85-b8-e6-a0-91%2F</url>
<content type="text"><![CDATA[字典树,又名Trier树,可以用于单词的查找和统计。字典树的示例如下图所示,其中,若根节点为第0层,则第k层节点表示字典中单词前k个字符,从根节点至树中标黄节点的路径表示以该节点字符为末尾的单词,例如,too、tooth、tea、two等。字典树中的节点可用以下结构体表示:1234567typedef struct TNode { char value;//表示该节点存储的字符 int count;//表示以该节点字符为末尾的单词出现的次数 bool endFlag;//表示该节点是否是单词的末尾 TNode* childList[26];//表示下一个字符 }TNode,*DTree; 字典树的初始化和插入单词操作:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647//初始化字典树 void Init(DTree &tree) { int i; tree=(DTree)malloc(sizeof(TNode)); tree->value = ' ';//根节点的字符为空 tree->count = 0; tree->endFlag = false; for (i=0;i<26;i++) tree->childList[i] = NULL; } //将单词插入字典树中 //从根节点开始从上至下,将单词字符从左至右逐步插入到字典树中 void Insert(DTree tree,char string[],int len) { int i,j; TNode* node = tree; for (i=0;i<len;i++) { //若节点存在,则直接访问下一个节点 //若该节点是单词最后一个字符,则将endFlag置为true,并将count加1 if (node->childList[string[i]-'a']!=NULL) { node = node->childList[string[i]-'a']; if (i==len-1) { node->endFlag = true; node->count++; } } //若节点不存在,则新建节点并初始化 //若该节点是单词最后一个字符,则将endFlag置为true,否则置为false,并将count加1 else { node->childList[string[i]-'a'] = (TNode*)malloc(sizeof(TNode)); node = node->childList[string[i]-'a']; node->value = string[i]; for (j=0;j<26;j++) node->childList[j] = NULL; node->endFlag = false; if (i==len-1) { node->endFlag = true; node->count++; } } } } 字典树可用于计算某个文本中出现次数最多的前k个单词:遍历文本中的单词,将每个单词插入到字典树中,重复的单词实际不再添加,而是将原有单词末尾节点中的count加1,最后,字典树中根节点到所有endFlag为true的节点的路径为文本中出现的所有单词,节点中的count值为这些单词出现的次数,通过排序可以得到出现次数最多的前k个单词。]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
</entry>
<entry>
<title><![CDATA[求解最长单调递增子串]]></title>
<url>%2F2012%2F06%2F05%2Fe6-b1-82-e8-a7-a3-e6-9c-80-e9-95-bf-e5-8d-95-e8-b0-83-e9-80-92-e5-a2-9e-e5-ad-90-e4-b8-b2%2F</url>
<content type="text"><![CDATA[求解最长递增子串可分为两种情况,即子串连续或非连续。例如,对于整数串{1,3,5,1,-1,4,5,3,1,8,3,4,6,2,4,6,7,8,6,4}其连续递增子串为{2,4,6,7,8},非连续递增子串为{ {-1},{1},{2,4,6,7,8}} 连续递增子串的求解思路:采用动态规划思想,令lengthOfSubList[k]表示子串{list[0]…list[k]}中最长的连续递增子串长度lengthOfSubListIncludeK[k]表示子串{list[0]…list[k]}中以list[k]结尾的最长连续递增子串长度indexOfLastElement[k]表示子串{list[0]…list[k]}中最长的连续递增子串的最后一个元素的位置则lengthOfSubListIncludeK[k] =lengthOfSubListIncludeK[k-1]+1,list[k]>=list[k-1]1,list[k]<list[k-1]lengthOfSubList[k] = max(lengthOfSubListIncludeK[k],lengthOfSubList[k-1])indexOfLastElement[k] =k,lengthOfSubListIncludeK[k]>lengthOfSubList[k-1]indexOfLastElement[k-1],lengthOfSubListIncludeK[k]<=lengthOfSubList[k-1] 代码:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253#include <stdlib.h> #include <stdio.h> void MaxIncrementSubList(int list[],int length) { //lengthOfSubList[k]表示子串{list[0]...list[k]}中最长的连续递增子串长度 //lengthOfSubListIncludeK[k]表示子串{list[0]...list[k]}中以list[k]结尾的最长连续递增子串长度 int* lengthOfSubList = (int*)malloc(sizeof(int)*length); int* lengthOfSubListIncludeK = (int*)malloc(sizeof(int)*length); int* indexOfLastElement = (int*)malloc(sizeof(int)*length); int i; //初始化 for (i=0;i<length;i++) { lengthOfSubList[i] = 1; lengthOfSubListIncludeK[i] = 1; indexOfLastElement[i] = i; } //按照动态规划思想,计算lengthOfSubList[k]和lengthOfSubListIncludeK[k] for (i=1;i<length;i++) { if (list[i]>=list[i-1]) lengthOfSubListIncludeK[i] = lengthOfSubListIncludeK[i-1]+1; else lengthOfSubListIncludeK[i] = 1; if (lengthOfSubListIncludeK[i]>lengthOfSubList[i-1]) { lengthOfSubList[i] = lengthOfSubListIncludeK[i]; indexOfLastElement[i] = i; } else { lengthOfSubList[i] = lengthOfSubList[i-1]; indexOfLastElement[i] = indexOfLastElement[i-1]; } } //逆序输出最长连续递增子串 printf("longest sub list is:\n"); int idx = indexOfLastElement[length-1]; while (list[idx]>=list[idx-1]) { printf("[%d]=%d ",idx,list[idx]); idx--; } printf("[%d]=%d ",idx,list[idx]); } int main() { int list[20] = {1,3,5,1,-1,4,5,3,1,8,3,4,6,2,4,6,7,8,6,4}; MaxIncrementSubList(list,20); int a; scanf("%d",&a); return 0; } 非连续递增子串的求解思路:采用动态规划思想,令lengthOfSubList[k]表示子串{list[0]…list[k]}中最长的非连续递增子串长度lengthOfSubListIncludeK[k]表示子串{list[0]…list[k]}中以list[k]结尾的最长非连续递增子串长度indexOfLastElement[k]表示子串{list[0]…list[k]}中最长的非连续递增子串的最后一个元素的位置则lengthOfSubListIncludeK[k] = max(lengthOfSubListIncludeK[i]+1, list[k]>=list[i], i=0..k-1)lengthOfSubList[k] = max(lengthOfSubListIncludeK[k],lengthOfSubList[k-1])indexOfLastElement[k] =k, lengthOfSubListIncludeK[k]>lengthOfSubList[k-1]indexOfLastElement[k-1],lengthOfSubListIncludeK[k]<=lengthOfSubList[k-1] 代码:12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364#include <stdlib.h> #include <stdio.h> void MaxIncrementSubList(int list[],int length) { //lengthOfSubList[k]表示子串{list[0]...list[k]}中最长的非连续递增子串长度 //lengthOfSubListIncludeK[k]表示子串{list[0]...list[k]}中以list[k]结尾的最长非连续递增子串长度 int* lengthOfSubList = (int*)malloc(sizeof(int)*length); int* lengthOfSubListIncludeK = (int*)malloc(sizeof(int)*length); int* indexOfLastElement = (int*)malloc(sizeof(int)*length); int i,j,max; //初始化 for (i=0;i<length;i++) { lengthOfSubList[i] = 1; lengthOfSubListIncludeK[i] = 1; indexOfLastElement[i] = i; } //按照动态规划思想,计算lengthOfSubList[k]和lengthOfSubListIncludeK[k] for (i=1;i<length;i++) { max = lengthOfSubListIncludeK[i]; for (j=0;j<i;j++) { if (list[i]>list[j]) { lengthOfSubListIncludeK[i] = lengthOfSubListIncludeK[j]+1; if (max<lengthOfSubListIncludeK[i]) max = lengthOfSubListIncludeK[i]; } } lengthOfSubListIncludeK[i] = max; if (lengthOfSubListIncludeK[i]>lengthOfSubList[i-1]) { lengthOfSubList[i] = lengthOfSubListIncludeK[i]; indexOfLastElement[i] = i; } else { lengthOfSubList[i] = lengthOfSubList[i-1]; indexOfLastElement[i] = indexOfLastElement[i-1]; } } //逆序输出最长非连续递增子串 printf("longest sub list is:\n"); int idx = indexOfLastElement[length-1]; int tmp = list[idx]; for (i=idx;i>=0;i--) { if (list[i]<=tmp) { printf("[%d]=%d ",i,list[i]); tmp = list[i]; } } } int main() { int list[20] = {1,3,5,1,-1,4,5,3,1,8,3,4,6,2,4,6,7,8,6,4}; MaxIncrementSubList(list,20); int a; scanf("%d",&a); return 0; }]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
</entry>
<entry>
<title><![CDATA[求和最大的连续子串]]></title>
<url>%2F2012%2F06%2F05%2Fe6-b1-82-e5-92-8c-e6-9c-80-e5-a4-a7-e7-9a-84-e8-bf-9e-e7-bb-ad-e5-ad-90-e4-b8-b2%2F</url>
<content type="text"><![CDATA[思路:采用动态规划进行求解,令:maxSum[k]表示子串list{0,k}的最大连续子串和,maxSumIncludeK[k]表示子串list{0,k}中末尾为k的最大连续子串和,则maxSumIncludeK[k] = max(list[k],list[k]+maxSumIncludeK[k-1]),maxSum[k] = max(maxSumIncludeK[k],maxSum[k-1])。 代码:123456789101112131415161718192021222324252627282930313233#include <stdlib.h>#include <stdio.h> int MaxSubList(int list[],int length){ int i; //maxSum[k]表示子串list{0,k}的最大连续子串和 //maxSumIncludeK[k]表示子串list{0,k}中末尾为k的最大连续子串和 int* maxSum = (int*)malloc(sizeof(int)*length); int* maxSumIncludeK = (int*)malloc(sizeof(int)*length); for (i=0;i<length;i++) { maxSum[i] = 0; maxSumIncludeK[i] = 0; } if (list[0]>0) maxSum[0] = list[0]; maxSumIncludeK[0] = list[0]; for (i=1;i<length;i++) { maxSumIncludeK[i] = list[i]; if (maxSumIncludeK[i-1]>0) maxSumIncludeK[i]+=maxSumIncludeK[i-1]; if (maxSumIncludeK[i]>maxSum[i-1]) maxSum[i] = maxSumIncludeK[i]; else maxSum[i] = maxSum[i-1]; } return maxSum[length-1];} int main(){ int list[20] = {4,9,-4,3,2,-1,-6,7,2,-3,1,5,3,-2,7,-3,9,2,-4,2}; printf("max sum:%d \n",MaxSubList(list,20)); return 0;}]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
</entry>
<entry>
<title><![CDATA[不使用加减乘除实现加法]]></title>
<url>%2F2012%2F06%2F04%2Fe4-b8-8d-e4-bd-bf-e7-94-a8-e5-8a-a0-e5-87-8f-e4-b9-98-e9-99-a4-e5-ae-9e-e7-8e-b0-e5-8a-a0-e6-b3-95%2F</url>
<content type="text"><![CDATA[思路:例如:a=5,b=9,a+b=14a转换为二进制形式为101,b转换为二进制形式为1001,其和转换为二进制形式为1110。对于二进制形式的相加,可分两步进行操作: 先不考虑进位,则0101+1001=1100,从中可以看出,不考虑进位求和即对两个加数进行按位异或操作。 再考虑进位,则0101+1001=1110=1100+0010,即第一步所得结果再加上进位0010,而0010可通过下述方法进行计算:两个加数进行按为与操作得到0001,再将0001左移一位得到0010。采用递归思想,重复进行上述两步操作,直至无进位,可实现不使用加减乘除进行加法操作。 代码:123456789101112131415161718192021#include <stdlib.h> #include <stdio.h> int add(int a,int b) { if (a == 0) return b; if (b == 0) return a; int sum1,sum2; sum1 = a^b; sum2 = (a&b)<<1; return add(sum1,sum2); } int main() { printf("input a and b:\n"); int a,b; scanf("%d %d",&a,&b); printf("sum:%d \n",add(a,b)); return 0; }]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
</entry>
<entry>
<title><![CDATA[线索二叉树:二叉搜索树转换为双向链表]]></title>
<url>%2F2012%2F06%2F03%2Fe7-ba-bf-e7-b4-a2-e4-ba-8c-e5-8f-89-e6-a0-91-ef-bc-9a-e4-ba-8c-e5-8f-89-e6-90-9c-e7-b4-a2-e6-a0-91-e8-bd-ac-e6-8d-a2-e4-b8-ba-e5-8f-8c-e5-90-91-e9-93-be-e8-a1-a8%2F</url>
<content type="text"><![CDATA[对于二叉搜索树,可以将其转换为双向链表,其中,节点的左子树指针在链表中指向前一个节点,右子树指针在链表中指向后一个节点。 思路:采用递归思想,对于二叉搜索树,将左、右子树分别转换为双向链表,左子树转换所得链表的头结点即整个树的头结点,左子树转换所得链表的尾节点与根节点相邻;右子树转换所得链表的尾节点即整个树的尾节点,右子树转换所得链表的头结点与根节点相邻。 代码:12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485#include <stdlib.h> #include <stdio.h> typedef struct TNode { int value; TNode* lchild; TNode* rchild; }TNode,*BTree; //二叉树转换为双向链表 TNode* TreeToList(BTree tree,TNode* &lastNode) { TNode* head; //若树为空,返回空 if (tree == NULL) { lastNode = NULL; return NULL; } //若无左子树,则该根节点为链表的头结点 if (tree->lchild==NULL) { head = tree; } //若有左子树,递归调用转换函数将左子树转换为双向链表 //左子树转换所得链表的头结点是整个树的头结点 //左子树链表的尾结点与根节点相邻 else { head = TreeToList(tree->lchild,lastNode); tree->lchild = lastNode; lastNode->rchild = tree; } //若无右子树,则该根节点为链表的尾结点 if (tree->rchild==NULL) { lastNode = tree; } //若有右子树,递归调用转换函数将左子树转换为双向链表 //右子树转换所得链表的尾结点是整个树的尾结点 //右子树链表的头结点与根节点相邻 else { tree->rchild = TreeToList(tree->rchild,lastNode); tree->rchild->lchild = tree; } return head; } int main() { BTree tree = (BTree)malloc(sizeof(TNode)); tree->value = 4; tree->lchild = (TNode*)malloc(sizeof(TNode)); tree->lchild->value = 2; tree->lchild->lchild = (TNode*)malloc(sizeof(TNode)); tree->lchild->lchild->value = 1; tree->lchild->lchild->lchild = NULL; tree->lchild->lchild->rchild = NULL; tree->lchild->rchild = (TNode*)malloc(sizeof(TNode)); tree->lchild->rchild->value = 3; tree->lchild->rchild->lchild = NULL; tree->lchild->rchild->rchild = NULL; tree->rchild = (TNode*)malloc(sizeof(TNode)); tree->rchild->value = 6; tree->rchild->lchild = (TNode*)malloc(sizeof(TNode)); tree->rchild->lchild->value = 5; tree->rchild->lchild->lchild = NULL; tree->rchild->lchild->rchild = NULL; tree->rchild->rchild = NULL; TNode* lastNode; TNode* listHead = TreeToList(tree,lastNode); TNode* node = listHead; while(node) { printf("%d ",node->value); node = node->rchild; } printf("\n"); return 0; }]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
</entry>
<entry>
<title><![CDATA[根据二叉树的先序、中序遍历结果重建二叉树]]></title>
<url>%2F2012%2F06%2F03%2Fe6-a0-b9-e6-8d-ae-e4-ba-8c-e5-8f-89-e6-a0-91-e7-9a-84-e5-85-88-e5-ba-8f-e3-80-81-e4-b8-ad-e5-ba-8f-e9-81-8d-e5-8e-86-e7-bb-93-e6-9e-9c-e9-87-8d-e5-bb-ba-e4-ba-8c-e5-8f-89-e6-a0-91%2F</url>
<content type="text"><![CDATA[先序遍历为:1 2 4 5 3 6,中序遍历为:4 2 5 1 6 3 思路:先序遍历的第一个元素为根节点,在中序遍历中找到这个根节点,从而可以将中序遍历分为左右两个部分,左边部分为左子树的中序遍历,右边部分为右子树的中序遍历,进而也可以将先序遍历除第一个元素以外的剩余部分分为两个部分,第一个部分为左子树的先序遍历,第二个部分为右子树的先序遍历。由上述分析结果,可以递归调用构建函数,根据左子树、右子树的先序、中序遍历重建左、右子树。 代码:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263#include <stdlib.h>#include <stdio.h>typedef struct TNode{ int value; TNode* lchild; TNode* rchild;}TNode,*BTree; //根据先序遍历、中序遍历构建二叉树BTree rebuild(int preOrder[],int startPre,int endPre,int inOrder[],int startIn,int endIn){ //先序遍历和中序遍历长度应相等 if (endPre - startPre != endIn - startIn) return NULL; //起始位置不应大于末尾位置 if (startPre > endPre) return NULL; //先序遍历的第一个元素为根节点 BTree tree = (BTree)malloc(sizeof(TNode)); tree->value = preOrder[startPre]; tree->lchild = NULL; tree->rchild = NULL; //先序遍历和中序遍历只有一个元素时,返回该节点 if (startPre == endPre) return tree; //在中序遍历中找到根节点 int index,length; for (index=startIn;index<=endIn;index++) { if (inOrder[index] == preOrder[startPre]) break; } //若未找到,返回空 if (index > endIn) return NULL; //有左子树,递归调用构建左子树 if (index > startIn) { length = index-startIn; tree->lchild = rebuild(preOrder,startPre+1,startPre+1+length-1,inOrder,startIn,startIn+length-1); } //有右子树,递归调用构建右子树 if (index < endIn) { length = endIn - index; tree->rchild = rebuild(preOrder,endPre-length+1,endPre,inOrder,endIn-length+1,endIn); } return tree;}//后序遍历二叉树void postTraverse(BTree tree){ if (tree->lchild != NULL) postTraverse(tree->lchild); if (tree->rchild != NULL) postTraverse(tree->rchild); printf("%d ",tree->value);} int main(){ int preOrder[] = {1,2,4,5,3,6}; int inOrder[] = {4,2,5,1,6,3}; BTree tree = rebuild(preOrder,0,5,inOrder,0,5); postTraverse(tree); printf("\n"); return 0;} 重建二叉树后后序遍历的结果如下:]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
</entry>
<entry>
<title><![CDATA[验证栈的出栈序列是否正确]]></title>
<url>%2F2012%2F06%2F03%2Fe9-aa-8c-e8-af-81-e6-a0-88-e7-9a-84-e5-87-ba-e6-a0-88-e5-ba-8f-e5-88-97-e6-98-af-e5-90-a6-e6-ad-a3-e7-a1-ae%2F</url>
<content type="text"><![CDATA[思路:遍历出栈序列,对于其中任一元素k,查看当前栈是否为空,若为空或栈顶元素不等于k,则根据入栈序列进行入栈,直至入栈序列中的元素k入栈。若直至入栈序列末尾仍无k,说明出栈序列错误。入栈完成后,将k出栈。如上述操作,直至完成出栈序列遍历,说明出栈序列正确。 代码:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102#include <stdlib.h> #include <stdio.h> struct Stack { int* base; int* top; int size; }; //初始化栈 void Init(Stack &s) { s.base = (int*)malloc(sizeof(int)*100); s.top = s.base; s.size = 100; } //入栈 bool Push(Stack &s,int n) { if (s.top-s.base >= s.size) { s.size = s.size + 10; s.base = (int*)realloc(s.base,sizeof(int)*s.size); } *s.top = n; s.top++; return true; } //出栈 bool Pop(Stack &s,int &n) { if (s.top==s.base) return false; s.top--; n = *s.top; return true; } bool Top(Stack &s,int &n) { if (s.top==s.base) return false; n = *(s.top-1); return true; } bool Empty(Stack s) { if (s.top == s.base) return true; else return false; } int main() { int i,number; int* input; int* output; printf("input the number of the elements:\n"); scanf("%d",&number); input = (int*)malloc(sizeof(int)*number); output = (int*)malloc(sizeof(int)*number); printf("input the sequence of the elements:\n"); for (i=0;i<number;i++) scanf("%d",&output[i]); for (i=0;i<number;i++) input[i] = i+1; Stack s; Init(s); int inIndex = 0; int outIndex = 0; //遍历出栈序列 while (outIndex<number) { int temp; if (!Empty(s)) Top(s,temp); //若栈为空或栈顶元素不等于出栈序列中当前元素,执行入栈操作 if (Empty(s)||temp!=output[outIndex]) { while(inIndex<number&&input[inIndex]!=output[outIndex]) { Push(s,input[inIndex]); inIndex++; } if (inIndex == number) { printf("wrong output sequece\n"); return -1; } Push(s,input[inIndex]); inIndex++; } Pop(s,temp); if (temp!=output[outIndex]) { printf("wrong output sequece\n"); return -1; } outIndex++; } printf("rignt output sequece\n"); return 0; }]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
</entry>
<entry>
<title><![CDATA[求二叉树的镜像]]></title>
<url>%2F2012%2F06%2F02%2Fe6-b1-82-e4-ba-8c-e5-8f-89-e6-a0-91-e7-9a-84-e9-95-9c-e5-83-8f%2F</url>
<content type="text"><![CDATA[思路:采用递归的思想。对于根节点,若其左子树或右子树不为空,则互换左、右子树,然后对于左、右子树,分别递归上述处理方法,直至叶节点。示例: 代码:123456789101112131415161718typedef struct TNode{ int value; TNode* lchild; TNode* rchild;}TNode,*BTree; //采用递归进行镜像转换void MirrorTree(BTree tree){ if (tree == NULL) return; if (tree->lchild == NULL && tree->rchild == NULL) return; TNode* temp = tree->lchild; tree->lchild = tree->rchild; tree->rchild = temp; MirrorTree(tree->lchild); MirrorTree(tree->rchild);}]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
</entry>
<entry>
<title><![CDATA[链表反转的递归和非递归]]></title>
<url>%2F2012%2F06%2F01%2Fe9-93-be-e8-a1-a8-e5-8f-8d-e8-bd-ac-e7-9a-84-e9-80-92-e5-bd-92-e5-92-8c-e9-9d-9e-e9-80-92-e5-bd-92%2F</url>
<content type="text"><![CDATA[如题,给出链表反转的递归和非递归算法:1234567891011121314151617Node *reverse (Node *head){ Node *p1=NULL,*p2=NULL,*p3=NULL; if(NULL == head || NULL == head->next) return head; p1=head; p2=p1->next; head->next=NULL; while(!p2) { p3=p2->next; p2->next=p1; p1=p2; p2=p3; } return p2;}; 1234567891011Node *reverse_recursive(Node *head){ if(NULL==head || NULL==head->next) return head; Node *p,*q; p=head->next; q=reverse_recursive(head->next); p->next=head; head->next=NULL; return q;}]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
</entry>
<entry>
<title><![CDATA[多叉树的宽度优先搜索BFS]]></title>
<url>%2F2012%2F06%2F01%2Fe5-a4-9a-e5-8f-89-e6-a0-91-e7-9a-84-e5-ae-bd-e5-ba-a6-e4-bc-98-e5-85-88-e6-90-9c-e7-b4-a2bfs%2F</url>
<content type="text"><![CDATA[首先,选择一个合适的数据结构存储多叉树,我使用了“左孩子右兄弟”的方法,使用二叉树来存储多叉树,便于实现和遍历。其次,宽度优先搜索时: 用队列(先进先出)保存遍历路径。 搜索顺序:对节点A,先访问节点的左节点(第一个孩子节点),从该左节点开始一直向右遍历直到最右叶子节点(所有兄弟节点),此时遍历完A的所有直接孩子。遍历的同时,节点入队列。 遍历完A的孩子后,取队列中的下一个节点,继续遍历其孩子节点。直到队列为空。 代码用C++实现,省略了类Queue, Node的实现:1234567891011121314151617181920212223242526272829303132333435363738394041class Tree { public: Node *root; Tree() { root=NULL; } void BFS() { Queue *queue=new Queue(); Node *p=root; if(p == NULL) return; //首先打印根节点 printf("%d\t",p->data); //第一个孩子节点 p = p->left; while(p != NULL) { //p打印,并入栈 queue->push(p); printf("%d\t",p->data); //如果有右孩子,一直入栈 p=p->right; while(p!= NULL) { queue->push(p); printf("%d\t",p->data); p=p->right; } p=(Node *)queue->getFront(); if(p == NULL) break; //从队列中取出下一个节点,访问它的所有孩子节点 queue->pop(); p=p->left; } } };]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
</entry>
<entry>
<title><![CDATA[最大公因子-辗转相除法]]></title>
<url>%2F2012%2F06%2F01%2Fe6-9c-80-e5-a4-a7-e5-85-ac-e5-9b-a0-e5-ad-90-e8-be-97-e8-bd-ac-e7-9b-b8-e9-99-a4-e6-b3-95%2F</url>
<content type="text"><![CDATA[求两个数的最大公因子,使用“辗转相除法”。 原理 若r=a%b,则gcd(a,b)=gcd(b,r)。 推导因为r=a%b,所以a=bq+r,r=a-bq。a=bq+r,能被b,r整除的,则一定能被a整除,自然也能被a,b整除r=a-bq,能被a,b整除的,则一定可以被r整除,自然也能被b,r整除显然gcd(a,b)=gcd(b,r)。 代码代码很简单:12345678910111213int gcd(int a,int b) { int m=a,n=b; if (a < b) { m=b; n=a; } if(m%n==0) return b; else return gcd(n,r); }]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
</entry>
<entry>
<title><![CDATA[求二叉树中两节点的最小公共父节点]]></title>
<url>%2F2012%2F06%2F01%2Fe6-b1-82-e4-ba-8c-e5-8f-89-e6-a0-91-e4-b8-ad-e4-b8-a4-e8-8a-82-e7-82-b9-e7-9a-84-e6-9c-80-e5-b0-8f-e5-85-ac-e5-85-b1-e7-88-b6-e8-8a-82-e7-82-b9%2F</url>
<content type="text"><![CDATA[思路:采用递归,深度优先遍历,找到一个节点时,返回,逐层记录遍历方向,另一个节点同,这样深度优先遍历后,可以找到这两个节点由根节点访问的路径,然后沿着这个路径找到分叉的地方就行了。 代码:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677#include <stdlib.h> #include <stdio.h> typedef struct TNode { int data; int pathA; int pathB; TNode* lchild; TNode* rchild; }TNode,*BTree; //采用递归进行深度优先遍历 //计算出路径 int traverse(BTree t,int x,int flag) { if (t == NULL) return 0; if (t->data == x) return 1; if (traverse(t->lchild,x,flag)==1) { if (flag == 1) t->pathA = 1; else t->pathB = 1; return 1; } if (traverse(t->rchild,x,flag)==1) { if (flag == 1) t->pathA = 2; else t->pathB = 2; return 1; } } int main() { BTree tree = (BTree)malloc(sizeof(TNode)); tree->data = 1; tree->pathA = 0; tree->pathB = 0; tree->lchild = (TNode*)malloc(sizeof(TNode)); tree->lchild->data = 2; tree->lchild->pathA = 0; tree->lchild->pathB = 0; tree->rchild = (TNode*)malloc(sizeof(TNode)); tree->rchild->data = 3; tree->rchild->pathA = 0; tree->rchild->pathB = 0; tree->rchild->lchild = NULL; tree->rchild->rchild = NULL; tree->lchild->lchild = (TNode*)malloc(sizeof(TNode)); tree->lchild->lchild->data = 4; tree->lchild->lchild->pathA = 0; tree->lchild->lchild->pathB = 0; tree->lchild->lchild->lchild = NULL; tree->lchild->lchild->rchild = NULL; tree->lchild->rchild = (TNode*)malloc(sizeof(TNode)); tree->lchild->rchild->data = 5; tree->lchild->rchild->pathA = 0; tree->lchild->rchild->pathB = 0; tree->lchild->rchild->lchild = NULL; tree->lchild->rchild->rchild = NULL; int a,b; printf("please input a,b:"); scanf("%d %d",&a,&b); if (traverse(tree,a,1)!=1) printf("no a"); if (traverse(tree,b,2)!=1) printf("no b"); //从根节点开始向下遍历,找到路径分叉点 TNode* node = tree; while(node&&(node->pathA==node->pathB)) { if (node->pathA == 1) node = node->lchild; else node = node->rchild; } printf("the common father node is:%d",node->data); return 0; } 示例: please input a,b:4 3the common father node is:1]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
</entry>
<entry>
<title><![CDATA[Huffman编码]]></title>
<url>%2F2012%2F06%2F01%2Fhuffman-e7-bc-96-e7-a0-81%2F</url>
<content type="text"><![CDATA[编码字符:”w”,”o”,”r”,”l”,”d”,权重值4,2,1,5,7。12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576#include <stdlib.h>#include <stdio.h> typedef struct Node{ int value; int parent;}Node,*HTree; void printCode(HTree tree,int l,int i); int main(){ int i,j; int w[5]={4,2,1,5,7}; int n = 5; HTree tree = (Node*)malloc(sizeof(Node)*(2*n-1)); //贪心策略构造Huffman树 for (i=0;i<n;i++) { tree[i].value = w[i]; tree[i].parent = 0; } for (i=n;i<2*n-1;i++) { int min1 = -1; int min2 = -1; for (j=0;j<i;j++) { if (tree[j].parent == 0) { if (min1 == -1) min1 = j; else if (min2 == -1) { min2 = j; if (tree[min1].value>tree[min2].value) { int temp = min1; min1 = min2; min2 = temp; } } else { if (tree[j].value<tree[min1].value) { min2 = min1; min1 = j; } else if (tree[j].value<tree[min2].value) { min2 = j; } } } } tree[min1].parent = i; tree[min2].parent = i; tree[i].parent = 0; tree[i].value = tree[min1].value+tree[min2].value; } //递归输出Huffman编码 for (i=0;i<n;i++) { printCode(tree,2*n-1,i); printf("\r\n"); } return 0; } void printCode(HTree tree,int l,int i) { if (tree[i].parent == 0) return; printCode(tree,l,tree[i].parent); if ((tree[tree[i].parent].value-tree[i].value)>tree[i].value) printf("0"); else printf("1");} 结果:010010001011]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
</entry>
<entry>
<title><![CDATA[树的宽度优先遍历]]></title>
<url>%2F2012%2F05%2F31%2Fe6-a0-91-e7-9a-84-e5-ae-bd-e5-ba-a6-e4-bc-98-e5-85-88-e9-81-8d-e5-8e-86%2F</url>
<content type="text"><![CDATA[1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374struct Node{ int id; int childNum; Node* childList[10];};struct Queue{ Node* list[100]; int front; int rear; int num;};void InitQueue(Queue &q){ q.front = q.rear = 99; q.num = 0;}int EnQueue(Queue &q,Node* n){ if (q.num == 100) return -1; q.rear = (q.rear+1)%100; q.list[q.rear] = n; q.num = q.num+1; return 1;}int DeQueue(Queue &q,Node* &n){ if (q.num == 0) return -1; q.front = (q.front+1)%100; n = q.list[q.front]; q.num = q.num-1; return 1;}int main(){ int i; Node* tree; tree = (Node*)malloc(sizeof(Node)); tree->id = 1; tree->childNum = 3; for (i=1;i<=tree->childNum;i++) { tree->childList[i-1] = (Node*)malloc(sizeof(Node)); tree->childList[i-1]->id = i+1; tree->childList[i-1]->childNum = 0; } tree->childList[0]->childNum = 2; tree->childList[0]->childList[0] = (Node*)malloc(sizeof(Node)); tree->childList[0]->childList[0]->id = 5; tree->childList[0]->childList[0]->childNum = 0; tree->childList[0]->childList[1] = (Node*)malloc(sizeof(Node)); tree->childList[0]->childList[1]->id = 6; tree->childList[0]->childList[1]->childNum = 0; Queue q; InitQueue(q); EnQueue(q,tree); while(q.num!=0) { Node* n; DeQueue(q,n); cout<id<<' '; if (n->childNum!=0) { for (i=0;ichildNum;i++) EnQueue(q,n->childList[i]); } } cout<<endl; return 0;} 输出为:1 2 3 4 5 6]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
</entry>
<entry>
<title><![CDATA[快速排序和堆排序]]></title>
<url>%2F2012%2F05%2F31%2Fe5-bf-ab-e9-80-9f-e6-8e-92-e5-ba-8f-e5-92-8c-e5-a0-86-e6-8e-92-e5-ba-8f%2F</url>
<content type="text"><![CDATA[快速排序:1234567891011121314151617181920212223242526272829303132333435363738#include <iostream> using namespace std; int n[5] = {4,3,1,5,2}; void quickSort(int s, int t); int sort(int s, int t); int main() { quickSort(0,4); for (int i=0;i<5;i++) cout<<n[i]; return 0; } void quickSort(int s, int t) { if(s>=t) return; int k = sort(s, t); quickSort(s,k-1); quickSort(k+1,t); return; } int sort(int s, int t) { int key = n[s]; while(s<t) { while(n[t]>=key&&t>s) t--; n[s] = n[t]; s++; while(n[s]<=key&&t>s) s++; n[t] = n[s]; t--; } n[s] = key; return s; } 堆排序:12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273#include <iostream> using namespace std; int n[10] = {5,1,4,9,8,7,6,2,10,3}; int l = 10; void creatHeap(); void backHeap(int t); int main() { creatHeap(); int i; for (i=1;i<=l;i++) { cout<<n[0]; n[0] = n[l-i]; backHeap(l-i); } return 0; } void creatHeap() { int i; for (i=l/2;i>=1;i--) { if (2*i==l||n[2*i-1]>n[2*i]) { if (n[i-1]<n[2*i-1]) { int temp = n[2*i-1]; n[2*i-1] = n[i-1]; n[i-1] = temp; } } else if (n[2*i-1]<=n[2*i]) { if (n[i-1]<n[2*i]) { int temp = n[2*i]; n[2*i] = n[i-1]; n[i-1] = temp; } } } } void backHeap(int t) { int i; for (i=1;i<=t/2;i++) { if (2*i==t||n[2*i-1]>n[2*i]) { if (n[i-1]<n[2*i-1]) { int temp = n[2*i-1]; n[2*i-1] = n[i-1]; n[i-1] = temp; } } else if (n[2*i-1]<=n[2*i]) { if (n[i-1]<n[2*i]) { int temp = n[2*i]; n[2*i] = n[i-1]; n[i-1] = temp; } } } }]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
</entry>
<entry>
<title><![CDATA[二叉树先序遍历非递归算法]]></title>
<url>%2F2012%2F05%2F31%2Fe4-ba-8c-e5-8f-89-e6-a0-91-e5-85-88-e5-ba-8f-e9-81-8d-e5-8e-86-e9-9d-9e-e9-80-92-e5-bd-92-e7-ae-97-e6-b3-95%2F</url>
<content type="text"><![CDATA[1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889#include <stdio.h>#include <stdlib.h> struct TNode{ int number; TNode* lchild; TNode* rchild;}; struct Stack{ TNode** top; TNode** base; int size;}; void Init(Stack* s){ s->size = 100; s->base = (TNode**)malloc(sizeof(TNode*)*s->size); s->top = s->base;} void Push(Stack* s,TNode* n){ if(s->top-s->base>=s->size) { s->size = s->size+10; s->base = (TNode**)realloc(s->base,sizeof(TNode*)*s->size); } *s->top = n; s->top = s->top+1;} void Pop(Stack* s,TNode* &n){ if(s->top-s->base<=0) return; s->top = s->top-1; n = *s->top;} int Empty(Stack* s){ if (s->top-s->base<=0) return 1; else return 0; } int main() { Stack *s; s = (Stack*)malloc(sizeof(Stack)); Init(s); TNode* tree; tree = (TNode*)malloc(sizeof(TNode)); tree->number = 1; tree->lchild = (TNode*)malloc(sizeof(TNode)); tree->lchild->number = 2; tree->lchild->lchild = NULL; tree->lchild->rchild = NULL; tree->rchild = (TNode*)malloc(sizeof(TNode)); tree->rchild->number = 3; tree->rchild->lchild = (TNode*)malloc(sizeof(TNode)); tree->rchild->lchild->number = 4; tree->rchild->lchild->lchild = NULL; tree->rchild->lchild->rchild = NULL; tree->rchild->rchild = (TNode*)malloc(sizeof(TNode)); tree->rchild->rchild->number = 5; tree->rchild->rchild->lchild = NULL; tree->rchild->rchild->rchild = NULL; TNode* node; node = tree; while(node) { printf("%d ",node->number); Push(s,node); node = node->lchild; } while(Empty(s)!=1) { Pop(s,node); node = node->rchild; while(node) { printf("%d ",node->number); Push(s,node); node = node->lchild; } } return 0;}]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
</entry>
<entry>
<title><![CDATA[单链表之:如何快速找到倒数第n个节点]]></title>
<url>%2F2011%2F01%2F05%2F613%2F</url>
<content type="text"><![CDATA[题目:如何查找单链表的倒数第n个指针。算法一:第一次遍历到链表末尾,找到链表长度N;第二遍遍历,找到第N-n个节点。算法二:设立两个指针,p1指向头节点,p2往前走n步,这样,p2与p1之间间隔n个指针。这样,当p2到达末尾是,p1则为倒数第N-n个节点。12345678910111213141516Node *lastN(Node *head) { Node *p1=head,*p2=head; for(int i=0;i<n;i++) { if(p2==NULL) return NULL; p2=p2->next; } while(p2) { p1=p1->next; p2=p2->next; } return p1; }]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
</entry>
<entry>
<title><![CDATA[数组循环移位]]></title>
<url>%2F2010%2F12%2F31%2Fe6-95-b0-e7-bb-84-e5-be-aa-e7-8e-af-e7-a7-bb-e4-bd-8d%2F</url>
<content type="text"><![CDATA[题目:给定数组str[],循环左移m位。即如果str=”ABCDEF”,循环左移2位得到 “CDEFAB”。算法:使用两个倒序,倒序AB得到BA,倒序CDEF得到FEDC,最后全部BAFEDC全部倒序CDEFAB。1234567891011121314151617181920#include <string.h> #include <iostream> using namespace std; void reverse(char *str,int left,int right) { char tmp; for(int i=left,j=right;i<j;i++,j--) { str[i]=str[j]; str[j]=tmp; } } void shift(char *str,int m, int len) { reverse(str,0,m-1); reverse(str,m,len-1); reverse(str,0,len-1); }]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
</entry>
</search>