在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

鍍金池/ 教程/ Linux/ HTTP 服務(wù)
Web 開發(fā)實(shí)戰(zhàn)2——商品詳情頁
流量復(fù)制 /AB 測(cè)試/協(xié)程
常用 Lua 開發(fā)庫 1-redis、mysql、http 客戶端
Lua 模塊開發(fā)
常用 Lua 開發(fā)庫 3-模板渲染
HTTP 服務(wù)
Nginx+Lua 開發(fā)入門
安裝 Nginx+Lua 開發(fā)環(huán)境
Redis/SSDB+Twemproxy 安裝與使用
JSON 庫、編碼轉(zhuǎn)換、字符串處理

HTTP 服務(wù)

此處我說的 HTTP 服務(wù)主要指如訪問京東網(wǎng)站時(shí)我們看到的熱門搜索、用戶登錄、實(shí)時(shí)價(jià)格、實(shí)時(shí)庫存、服務(wù)支持、廣告語等這種非 Web 頁面,而是在 Web 頁面中異步加載的相關(guān)數(shù)據(jù)。這些服務(wù)有個(gè)特點(diǎn)即訪問量巨大、邏輯比較單一;但是如實(shí)時(shí)庫存邏輯其實(shí)是非常復(fù)雜的。在京東這些服務(wù)每天有幾億十幾億的訪問量,比如實(shí)時(shí)庫存服務(wù)曾經(jīng)在沒有任何 IP 限流、DDos 防御的情況被刷到600多萬/分鐘的訪問量,而且能輕松應(yīng)對(duì)。支撐如此大的訪問量就需要考慮設(shè)計(jì)良好的架構(gòu),并很容易實(shí)現(xiàn)水平擴(kuò)展。

架構(gòu)

此處介紹下我曾使用過 Nginx+JavaEE 的架構(gòu)。

單 DB 架構(gòu)

http://wiki.jikexueyuan.com/project/nginx-lua/images/1.png" alt="" />

早期架構(gòu)可能就是 Nginx 直接 upstream 請(qǐng)求到后端 Tomcat,擴(kuò)容時(shí)基本是增加新的 Tomcat 實(shí)例,然后通過 Nginx 負(fù)載均衡 upstream 過去。此時(shí)數(shù)據(jù)庫還不是瓶頸。當(dāng)訪問量到一定級(jí)別,數(shù)據(jù)庫的壓力就上來了,此處單純的靠單個(gè)數(shù)據(jù)庫可能扛不住了,此時(shí)可以通過數(shù)據(jù)庫的讀寫分離或加緩存來實(shí)現(xiàn)。

DB+Cache/ 數(shù)據(jù)庫讀寫分離架構(gòu)

http://wiki.jikexueyuan.com/project/nginx-lua/images/2.png" alt="" />

此時(shí)就通過使用如數(shù)據(jù)庫讀寫分離或者 Redis 這種緩存來支撐更大的訪問量。使用緩存這種架構(gòu)會(huì)遇到的問題諸如緩存與數(shù)據(jù)庫數(shù)據(jù)不同步造成數(shù)據(jù)不一致(一般設(shè)置過期時(shí)間),或者如 Redis 掛了,此時(shí)會(huì)直接命中數(shù)據(jù)庫導(dǎo)致數(shù)據(jù)庫壓力過大;可以考慮 Redis 的主從或者一致性Hash 算法做分片的 Redis 集群;使用緩存這種架-構(gòu)要求應(yīng)用對(duì)數(shù)據(jù)的一致性要求不是很高;比如像下訂單這種要落地的數(shù)據(jù)不適合用 Redis 存儲(chǔ),但是訂單的讀取可以使用緩存。

Nginx+Lua+Local Redis+Mysql 集群架構(gòu)

http://wiki.jikexueyuan.com/project/nginx-lua/images/3.png" alt="" />

首先 Nginx 通過 Lua 讀取本機(jī) Redis 緩存,如果不命中才回源到后端 Tomcat 集群;后端Tomcat 集群再讀取 Mysql 數(shù)據(jù)庫。Redis 都是安裝到和 Nginx 同一臺(tái)服務(wù)器,Nginx 直接讀本機(jī)可以減少網(wǎng)絡(luò)延時(shí)。Redis 通過主從方式同步數(shù)據(jù),Redis 主從一般采用樹的方式實(shí)現(xiàn):

http://wiki.jikexueyuan.com/project/nginx-lua/images/4.png" alt="" />

在葉子節(jié)點(diǎn)可以做 AOF 持久化,保證在主 Redis 掛時(shí)能進(jìn)行恢復(fù);此處假設(shè)對(duì) Redis 很依賴的話,可以考慮多主 Redis 架構(gòu),而不是單主,來防止單主掛了時(shí)數(shù)據(jù)的不一致和擊穿到后端Tomcat 集群。這種架構(gòu)的缺點(diǎn)就是要求 Redis 實(shí)例數(shù)據(jù)量較小,如果單機(jī)內(nèi)存不足以存儲(chǔ)這么多數(shù)據(jù),當(dāng)然也可以通過如尾號(hào)為 1 的在 A 服務(wù)器,尾號(hào)為 2 的在 B 服務(wù)器這種方式實(shí)現(xiàn);缺點(diǎn)也很明顯,運(yùn)維復(fù)雜、擴(kuò)展性差。

Nginx+Lua+Redis 集群 +Mysql 集群架構(gòu)

http://wiki.jikexueyuan.com/project/nginx-lua/images/5.png" alt="" />

和之前架構(gòu)不同的點(diǎn)是此時(shí)我們使用一致性 Hash 算法實(shí)現(xiàn) Redis 集群而不是讀本機(jī) Redis,保證其中一臺(tái)掛了,只有很少的數(shù)據(jù)會(huì)丟失,防止擊穿到數(shù)據(jù)庫。Redis 集群分片可以使用Twemproxy;如果 Tomcat 實(shí)例很多的話,此時(shí)就要考慮 Redis 和 Mysql 鏈接數(shù)問題,因?yàn)榇蟛糠?Redis/Mysql 客戶端都是通過連接池實(shí)現(xiàn),此時(shí)的鏈接數(shù)會(huì)成為瓶頸。一般方法是通過中間件來減少鏈接數(shù)。

http://wiki.jikexueyuan.com/project/nginx-lua/images/6.png" alt="" />

Twemproxy 與 Redis 之間通過單鏈接交互,并 Twemproxy 實(shí)現(xiàn)分片邏輯;這樣我們可以水平擴(kuò)展更多的 Twemproxy 來增加鏈接數(shù)。

此時(shí)的問題就是 Twemproxy 實(shí)例眾多,應(yīng)用維護(hù)配置困難;此時(shí)就需要在之上做負(fù)載均衡,比如通過 LVS/HAProxy 實(shí)現(xiàn) VIP(虛擬 IP ),可以做到切換對(duì)應(yīng)用透明、故障自動(dòng)轉(zhuǎn)移;還可以通過實(shí)現(xiàn)內(nèi)網(wǎng) DNS 來做其負(fù)載均衡。

http://wiki.jikexueyuan.com/project/nginx-lua/images/7.png" alt="" />

本文沒有涉及 Nginx 之上是如何架構(gòu)的,對(duì)于 Nginx、Redis、Mysql 等的負(fù)載均衡、資源的CDN 化不是本文關(guān)注的點(diǎn),有興趣可以參考

很早的 Taobao CDN 架構(gòu)

Nginx/LVS/HAProxy 負(fù)載均衡軟件的優(yōu)缺點(diǎn)詳解

實(shí)現(xiàn)

接下來我們來搭建一下第四種架構(gòu)。

http://wiki.jikexueyuan.com/project/nginx-lua/images/8.png" alt="" />

以獲取如京東商品頁廣告詞為例,如下圖

http://wiki.jikexueyuan.com/project/nginx-lua/images/9.png" alt="" />

假設(shè)京東有10億商品,那么廣告詞極限情況是10億;所以在設(shè)計(jì)時(shí)就要考慮:

  1. 數(shù)據(jù)量,數(shù)據(jù)更新是否頻繁且更新量是否很大;
  2. 是K-V還是關(guān)系,是否需要批量獲取,是否需要按照規(guī)則查詢。

而對(duì)于本例,廣告詞更新量不會(huì)很大,每分鐘可能在幾萬左右;而且是 K-V 的,其實(shí)適合使用關(guān)系存儲(chǔ);因?yàn)閺V告詞是商家維護(hù),因此后臺(tái)查詢需要知道這些商品是哪個(gè)商家的;而對(duì)于前臺(tái)是不關(guān)心商家的,是 KV 存儲(chǔ),所以前臺(tái)顯示的可以放進(jìn)如 Redis 中。 即存在兩種設(shè)計(jì):

  1. 所有數(shù)據(jù)存儲(chǔ)到 Mysql,然后熱點(diǎn)數(shù)據(jù)加載到 Redis;
  2. 關(guān)系存儲(chǔ)到 Mysql,而數(shù)據(jù)存儲(chǔ)到如SSDB這種持久化KV存儲(chǔ)中。

基本數(shù)據(jù)結(jié)構(gòu):商品 ID、廣告詞、所屬商家、開始時(shí)間、結(jié)束時(shí)間、是否有效。

后臺(tái)邏輯

  1. 商家登錄后臺(tái);
  2. 按照商家分頁查詢商家數(shù)據(jù),此處要按照商品關(guān)鍵詞或商品類目查詢的話,需要走商品系統(tǒng)的搜索子系統(tǒng),如通過 Solr或elasticsearch 實(shí)現(xiàn)搜索子系統(tǒng);
  3. 進(jìn)行廣告詞的增刪改查;
  4. 增刪改時(shí)可以直接更新 Redis 緩存或者只刪除 Redis 緩存(第一次前臺(tái)查詢時(shí)寫入緩存);

前臺(tái)邏輯

  1. 首先 Nginx 通過 Lua 查詢 Redis 緩存;
  2. 查詢不到的話回源到 Tomcat,Tomcat 讀取數(shù)據(jù)庫查詢到數(shù)據(jù),然后把最新的數(shù)據(jù)異步寫入 Redis(一般設(shè)置過期時(shí)間,如5分鐘);此處設(shè)計(jì)時(shí)要考慮假設(shè) Tomcat 讀取 Mysql 的極限值是多少,然后設(shè)計(jì)降級(jí)開關(guān),如假設(shè)每秒回源達(dá)到 100,則直接不查詢 Mysql 而返回空的廣告詞來防止 Tomcat 應(yīng)用雪崩。

為了簡(jiǎn)單,我們不進(jìn)行后臺(tái)的設(shè)計(jì)實(shí)現(xiàn),只做前端的設(shè)計(jì)實(shí)現(xiàn),此時(shí)數(shù)據(jù)結(jié)構(gòu)我們簡(jiǎn)化為[商品ID、廣告詞]。另外有朋友可能看到了,可以直接把 Tomcat 部分干掉,通過 Lua 直接讀取Mysql 進(jìn)行回源實(shí)現(xiàn)。為了完整性此處我們還是做回源到 Tomcat 的設(shè)計(jì),因?yàn)槿绻壿嫳容^復(fù)雜的話或一些限制(比如使用 Java 特有協(xié)議的 RPC)還是通過 Java 去實(shí)現(xiàn)更方便一些。

項(xiàng)目搭建

項(xiàng)目部署目錄結(jié)構(gòu)。

Java 代碼

/usr/chapter6  
  redis_6660.conf  
  redis_6661.conf  
  nginx_chapter6.conf  
  nutcracker.yml  
  nutcracker.init  
  webapp  
WEB-INF  
   lib  
   classes  
   web.xml   

Redis+Twemproxy 配置

此處根據(jù)實(shí)際情況來決定 Redis 大小,此處我們已兩個(gè) Redis 實(shí)例(6660、6661),在Twemproxy 上通過一致性 Hash 做分片邏輯。

安裝

之前已經(jīng)介紹過 Redis 和 Twemproxy 的安裝了。

Redis配置redis_6660.conf和redis_6661.conf

Java 代碼

\#分別為6660 6661  
port 6660  
\#進(jìn)程ID 分別改為redis_6660.pid redis_6661.pid  
pidfile "/var/run/redis_6660.pid"  
\#設(shè)置內(nèi)存大小,根據(jù)實(shí)際情況設(shè)置,此處測(cè)試僅設(shè)置20mb  
maxmemory 20mb  
\#內(nèi)存不足時(shí),按照過期時(shí)間進(jìn)行LRU刪除  
maxmemory-policy volatile-lru  
\#Redis的過期算法不是精確的而是通過采樣來算的,默認(rèn)采樣為3個(gè),此處我們改成10  
maxmemory-samples 10  
\#不進(jìn)行RDB持久化  
save “”  
\#不進(jìn)行AOF持久化  
appendonly no   

將如上配置放到 redis_6660.conf 和 redis_6661.conf 配置文件最后即可,后邊的配置會(huì)覆蓋前邊的。

Twemproxy配置nutcracker.yml

Java 代碼

server1:  
  listen: 127.0.0.1:1111  
  hash: fnv1a_64  
  distribution: ketama  
  redis: true  
  timeout: 1000  
  servers:  
   - 127.0.0.1:6660:1 server1  
   - 127.0.0.1:6661:1 server2   

復(fù)制 nutcracker.init 到 /usr/chapter6 下,并修改配置文件為 /usr/chapter6/nutcracker.yml。

啟動(dòng)

Java 代碼

nohup /usr/servers/redis-2.8.19/src/redis-server  /usr/chapter6/redis_6660.conf &  
nohup /usr/servers/redis-2.8.19/src/redis-server  /usr/chapter6/redis_6661.conf &  
/usr/chapter6/nutcracker.init start  
ps -aux | grep -e redis  -e nutcracker  

Mysql+Atlas 配置

Atlas 類似于 Twemproxy,是 Qihoo 360 基于 Mysql Proxy 開發(fā)的一個(gè) Mysql 中間件,據(jù)稱每天承載讀寫請(qǐng)求數(shù)達(dá)幾十億,可以實(shí)現(xiàn)分表、讀寫分離、數(shù)據(jù)庫連接池等功能,缺點(diǎn)是沒有實(shí)現(xiàn)跨庫分表(分庫)功能,需要在客戶端使用分庫邏輯。另一個(gè)選擇是使用如阿里的 TDDL,它是在客戶端完成之前說的功能。到底選擇是在客戶端還是在中間件根據(jù)實(shí)際情況選擇。

此處我們不做 Mysql 的主從復(fù)制(讀寫分離),只做分庫分表實(shí)現(xiàn)。

Mysql 初始化

為了測(cè)試我們此處分兩個(gè)表。

Java 代碼

CREATE DATABASE chapter6 DEFAULT CHARACTER SET utf8;  
use chapter6;  
CREATE TABLE  chapter6.ad_0(  
      sku_id BIGINT,  
      content VARCHAR(4000)  
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;  
CREATE TABLE  chapter6.ad_1  
      sku_id BIGINT,  
      content VARCHAR(4000)  
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;  

Atlas 安裝

Java 代碼

cd /usr/servers/  
wget https://github.com/Qihoo360/Atlas/archive/2.2.1.tar.gz -O Atlas-2.2.1.tar.gz  
tar -xvf Atlas-2.2.1.tar.gz  
cd Atlas-2.2.1/  
\#Atlas依賴mysql_config,如果沒有可以通過如下方式安裝  
apt-get install libmysqlclient-dev  
\#安裝Lua依賴  
wget http://www.lua.org/ftp/lua-5.1.5.tar.gz  
tar -xvf lua-5.1.5.tar.gz  
cd lua-5.1.5/  
make linux && make install  
\#安裝glib依賴  
apt-get install libglib2.0-dev  
\#安裝libevent依賴  
apt-get install libevent    
\#安裝flex依賴  
apt-get install flex  
\#安裝jemalloc依賴  
apt-get install libjemalloc-dev  
\#安裝OpenSSL依賴  
apt-get install openssl  
apt-get install libssl-dev  
 apt-get install libssl0.9.8  

./configure --with-mysql=/usr/bin/mysql_config  
./bootstrap.sh  
make && make install  

Atlas 配置

Java 代碼

vim /usr/local/mysql-proxy/conf/chapter6.cnf  
Java代碼  收藏代碼
[mysql-proxy]  
\#Atlas代理的主庫,多個(gè)之間逗號(hào)分隔  
proxy-backend-addresses = 127.0.0.1:3306  
\#Atlas代理的從庫,多個(gè)之間逗號(hào)分隔,格式ip:port@weight,權(quán)重默認(rèn)1  
\#proxy-read-only-backend-addresses = 127.0.0.1:3306,127.0.0.1:3306  
\#用戶名/密碼,密碼使用/usr/servers/Atlas-2.2.1/script/encrypt 123456加密  
pwds = root:/iZxz+0GRoA=  
\#后端進(jìn)程運(yùn)行  
daemon = true  
\#開啟monitor進(jìn)程,當(dāng)worker進(jìn)程掛了自動(dòng)重啟  
keepalive = true  
\#工作線程數(shù),對(duì)Atlas的性能有很大影響,可根據(jù)情況適當(dāng)設(shè)置  
event-threads = 64  
\#日志級(jí)別  
log-level = message  
\#日志存放的路徑  
log-path = /usr/chapter6/  
\#實(shí)例名稱,用于同一臺(tái)機(jī)器上多個(gè)Atlas實(shí)例間的區(qū)分  
instance = test  
\#監(jiān)聽的ip和port  
proxy-address = 0.0.0.0:1112  
\#監(jiān)聽的管理接口的ip和port  
admin-address = 0.0.0.0:1113  
\#管理接口的用戶名  
admin-username = admin  
\#管理接口的密碼  
admin-password = 123456  
\#分表邏輯  
tables = chapter6.ad.sku_id.2  
\#默認(rèn)字符集  
charset = utf8     

因?yàn)楸纠龥]有做讀寫分離,所以讀庫 proxy-read-only-backend-addresses 沒有配置。分表邏輯即:數(shù)據(jù)庫名.表名.分表鍵.表的個(gè)數(shù),分表的表名格式是 table_N,N 從 0 開始。

Atlas 啟動(dòng)/重啟/停止

Java 代碼

/usr/local/mysql-proxy/bin/mysql-proxyd chapter6 start  
/usr/local/mysql-proxy/bin/mysql-proxyd chapter6 restart  
/usr/local/mysql-proxy/bin/mysql-proxyd chapter6 stop     

如上命令會(huì)自動(dòng)到 /usr/local/mysql-proxy/conf 目錄下查找 chapter6.cnf 配置文件。

Atlas 管理

通過如下命令進(jìn)入管理接口

Java 代碼

mysql -h127.0.0.1 -P1113  -uadmin -p123456  

通過執(zhí)行 SELECT * FROM help 查看幫助。還可以通過一些 SQL 進(jìn)行服務(wù)器的動(dòng)態(tài)添加/移除。

Atlas 客戶端

通過如下命令進(jìn)入客戶端接口

Java 代碼

mysql -h127.0.0.1 -P1112  -uroot -p123456    

Java 代碼

use chapter6;  
insert into ad values(1 '測(cè)試1);      
insert into ad values(2, '測(cè)試2');      
insert into ad values(3 '測(cè)試3);      
select * from ad where sku_id=1;  
select * from ad where sku_id=2;  
#通過如下sql可以看到實(shí)際的分表結(jié)果  
select * from ad_0;  
select * from ad_1;   

此時(shí)無法執(zhí)行 select from ad,需要使用如 “select from ad where sku_id=1” 這種 SQL 進(jìn)行查詢;即需要帶上 sku_id 且必須是相等比較;如果是范圍或模糊是不可以的;如果想全部查詢,只能挨著遍歷所有表進(jìn)行查詢。即在客戶端做查詢-聚合。

此處實(shí)際的分表邏輯是按照商家進(jìn)行分表,而不是按照商品編號(hào),因?yàn)槲覀兒笈_(tái)查詢時(shí)是按照商家維度的,此處是為了測(cè)試才使用商品編號(hào)的。

到此基本的 Atlas 就介紹完了,更多內(nèi)容請(qǐng)參考如下資料: Mysql 主從復(fù)制 http://369369.blog.51cto.com/319630/790921/ Mysql中間件介紹 http://www.guokr.com/blog/475765/ Atlas使用 http://www.0550go.com/database/mysql/mysql-atlas.html Atlas文檔 https://github.com/Qihoo360/Atlas/blob/master/README_ZH.md

Java+Tomcat 安裝

Java 安裝

Java 代碼

cd /usr/servers/  
\#首先到如下網(wǎng)站下載JDK  
\#http://www.oracle.com/technetwork/cn/java/javase/downloads/jdk7-downloads-1880260.html  
\#本文下載的是 jdk-7u75-linux-x64.tar.gz。  
tar -xvf jdk-7u75-linux-x64.tar.gz  
vim ~/.bashrc  
在文件最后添加如下環(huán)境變量  
export JAVA_HOME=/usr/servers/jdk1.7.0_75/  
export PATH=$JAVA_HOME/bin:$JAVA_HOME/jre/bin:$PATH  
export CLASSPATH=$CLASSPATH:.:$JAVA_HOME/lib:$JAVA_HOME/jre/lib  

\#使環(huán)境變量生效  
source ~/.bashrc  

Tomcat 安裝

Java 代碼

cd /usr/servers/  
wget http://ftp.cuhk.edu.hk/pub/packages/apache.org/tomcat/tomcat-7/v7.0.59/bin/apache-tomcat-7.0.59.tar.gz  
tar -xvf apache-tomcat-7.0.59.tar.gz  
cd apache-tomcat-7.0.59/  
\#啟動(dòng)   
/usr/servers/apache-tomcat-7.0.59/bin/startup.sh   
\#停止  
/usr/servers/apache-tomcat-7.0.59/bin/shutdown.sh  
\#刪除tomcat默認(rèn)的webapp  
rm -r apache-tomcat-7.0.59/webapps/*  
\#通過Catalina目錄發(fā)布web應(yīng)用  
cd apache-tomcat-7.0.59/conf/Catalina/localhost/  
vim ROOT.xml   

ROOT.xml

Java 代碼

<!-- 訪問路徑是根,web應(yīng)用所屬目錄為/usr/chapter6/webapp -->  
<Context path="" docBase="/usr/chapter6/webapp"></Context>    

Java 代碼

\#創(chuàng)建一個(gè)靜態(tài)文件隨便添加點(diǎn)內(nèi)容  
vim /usr/chapter6/webapp/index.html  
\#啟動(dòng)  
/usr/servers/apache-tomcat-7.0.59/bin/startup.sh    

訪問如 http://192.168.1.2:8080/index.html 能處理內(nèi)容說明配置成功。

Java 代碼

\#變更目錄結(jié)構(gòu)  
cd /usr/servers/  
mv apache-tomcat-7.0.59 tomcat-server1  
\#此處我們創(chuàng)建兩個(gè)tomcat實(shí)例  
cp –r tomcat-server1 tomcat-server2  
vim tomcat-server2/conf/server.xml     

Java 代碼

\#如下端口進(jìn)行變更  
8080--->8090  
8005--->8006  

啟動(dòng)兩個(gè) Tomcat

Java 代碼

/usr/servers/tomcat-server1/bin/startup.sh   
/usr/servers/tomcat-server2/bin/startup.sh  

分別訪問,如果能正常訪問說明配置正常。

  • http://192.168.1.2:8080/index.html
  • http://192.168.1.2:8090/index.html

如上步驟使我們?cè)谝粋€(gè)服務(wù)器上能啟動(dòng)兩個(gè) tomcat 實(shí)例,這樣的好處是我們可以做本機(jī)的 Tomcat 負(fù)載均衡,假設(shè)一個(gè) tomcat 重啟時(shí)另一個(gè)是可以工作的,從而不至于不給用戶返回響應(yīng)。

Java+Tomcat 邏輯開發(fā)

搭建項(xiàng)目

我們使用 Maven 搭建 Web 項(xiàng)目,Maven 知識(shí)請(qǐng)自行學(xué)習(xí)。

項(xiàng)目依賴

本文將最小化依賴,即僅依賴我們需要的 servlet、mysql、druid、jedis。

Java 代碼

<dependencies>  
  <dependency>  
    <groupId>javax.servlet</groupId>  
    <artifactId>javax.servlet-api</artifactId>  
    <version>3.0.1</version>  
    <scope>provided</scope>  
  </dependency>  
  <dependency>  
    <groupId>mysql</groupId>  
    <artifactId>mysql-connector-java</artifactId>  
    <version>5.1.27</version>  
  </dependency>  
  <dependency>  
    <groupId>com.alibaba</groupId>  
    <artifactId>druid</artifactId>  
    <version>1.0.5</version>  
  </dependency>  
  <dependency>  
    <groupId>redis.clients</groupId>  
    <artifactId>jedis</artifactId>  
    <version>2.5.2</version>  
  </dependency>  
</dependencies>  

核心代碼

com.github.zhangkaitao.chapter6.servlet.AdServlet

Java 代碼

public class AdServlet extends HttpServlet {  
    @Override  
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
        String idStr = req.getParameter("id");  
        Long id = Long.valueOf(idStr);  
        //1、讀取Mysql獲取數(shù)據(jù)  
        String content = null;  
        try {  
            content = queryDB(id);  
        } catch (Exception e) {  
            e.printStackTrace();  
            resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);  
            return;  
        }  
        if(content != null) {  
            //2.1、如果獲取到,異步寫Redis  
            asyncSetToRedis(idStr, content);  
            //2.2、如果獲取到,把響應(yīng)內(nèi)容返回  
            resp.setCharacterEncoding("UTF-8");  
            resp.getWriter().write(content);  
        } else {  
            //2.3、如果獲取不到,返回404狀態(tài)碼  
            resp.setStatus(HttpServletResponse.SC_NOT_FOUND);  
        }  
    }  

    private DruidDataSource datasource = null;  
    private JedisPool jedisPool = null;  

    {  
        datasource = new DruidDataSource();  
        datasource.setUrl("jdbc:mysql://127.0.0.1:1112/chapter6?useUnicode=true&characterEncoding=utf-8&autoReconnect=true");  
        datasource.setUsername("root");  
        datasource.setPassword("123456");  
        datasource.setMaxActive(100);  

        GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();  
        poolConfig.setMaxTotal(100);  
        jedisPool = new JedisPool(poolConfig, "127.0.0.1", 1111);  
    }  

    private String queryDB(Long id) throws Exception {  
        Connection conn = null;  
        try {  
            conn = datasource.getConnection();  
            String sql = "select content from ad where sku_id = ?";  
            PreparedStatement psst = conn.prepareStatement(sql);  
            psst.setLong(1, id);  
            ResultSet rs = psst.executeQuery();  
            String content = null;  
            if(rs.next()) {  
                content = rs.getString("content");  
            }  
            rs.close();  
            psst.close();  
            return content;  
        } catch (Exception e) {  
            throw e;  
        } finally {  
            if(conn != null) {  
                conn.close();  
            }  
        }  
    }  

    private ExecutorService executorService = Executors.newFixedThreadPool(10);  
    private void asyncSetToRedis(final String id, final String content) {  
        executorService.submit(new Runnable() {  
            @Override  
            public void run() {  
                Jedis jedis = null;  
                try {  
                    jedis = jedisPool.getResource();  
                    jedis.setex(id, 5 * 60, content);//5分鐘  
                } catch (Exception e) {  
                    e.printStackTrace();  
                    jedisPool.returnBrokenResource(jedis);  
                } finally {  
                    jedisPool.returnResource(jedis);  
                }  

            }  
        });  
    }  
}    

整個(gè)邏輯比較簡(jiǎn)單,此處更新緩存一般使用異步方式去更新,這樣不會(huì)阻塞主線程;另外此處可以考慮走 Servlet 異步化來提示吞吐量。

web.xml 配置

Java 代碼

<servlet>  
    <servlet-name>adServlet</servlet-name>  
    <servlet-class>com.github.zhangkaitao.chapter6.servlet.AdServlet</servlet-class>  
</servlet>  
<servlet-mapping>  
    <servlet-name>adServlet</servlet-name>  
    <url-pattern>/ad</url-pattern>  
</servlet-mapping>  

打 WAR 包

Java 代碼

cd D:\workspace\chapter6  
mvn clean package  
此處使用maven命令打包,比如本例將得到chapter6.war,然后將其上傳到服務(wù)器的/usr/chapter6/webapp,然后通過unzip chapter6.war解壓。

測(cè)試

啟動(dòng) Tomcat 實(shí)例,分別訪問如下地址將看到廣告內(nèi)容:

Java 代碼

http://192.168.1.2:8080/ad?id=1  
http://192.168.1.2:8090/ad?id=1  

nginx 配置

vim /usr/chapter6/nginx_chapter6.conf

Java 代碼

upstream backend {  
    server 127.0.0.1:8080 max_fails=5 fail_timeout=10s weight=1 backup=false;  
    server 127.0.0.1:8090 max_fails=5 fail_timeout=10s weight=1 backup=false;  
    check interval=3000 rise=1 fall=2 timeout=5000 type=tcp default_down=false;  
    keepalive 100;  
}  
server {  
    listen       80;  
    server_name  _;  

    location ~ /backend/(.*) {  
        keepalive_timeout   30s;  
        keepalive_requests  100;  

        rewrite /backend(/.*) $1 break;  
        #之后該服務(wù)將只有內(nèi)部使用,ngx.location.capture  
        proxy_pass_request_headers off;  
        #more_clear_input_headers Accept-Encoding;  
        proxy_next_upstream error timeout;  
        proxy_pass http://backend;  
    }  
}   

upstream 配置:http://nginx.org/cn/docs/http/ngx_http_upstream_module.html。
server:指定上游到的服務(wù)器, weight:權(quán)重,權(quán)重可以認(rèn)為負(fù)載均衡的比例; fail_timeout+max_fails:在指定時(shí)間內(nèi)失敗多少次認(rèn)為服務(wù)器不可用,通過proxy_next_upstream來判斷是否失敗。
check:ngx_http_upstream_check_module模塊,上游服務(wù)器的健康檢查,interval:發(fā)送心跳包的時(shí)間間隔,rise:連續(xù)成功rise次數(shù)則認(rèn)為服務(wù)器up,fall:連續(xù)失敗fall次則認(rèn)為服務(wù)器down,timeout:上游服務(wù)器請(qǐng)求超時(shí)時(shí)間,type:心跳檢測(cè)類型(比如此處使用 tcp )更多配置請(qǐng)參考 [https://github.com/yaoweibin/ nginx_upstream_check_module](https://github.com/yaoweibin/ nginx_upstream_check_module) 和 http://tengine.taobao.org/document_cn/http_upstream_check_cn.html。
keepalive:用來支持 upstream server http keepalive 特性(需要上游服務(wù)器支持,比如 tomcat )。默認(rèn)的負(fù)載均衡算法是 round-robin,還可以根據(jù) ip、url 等做 hash 來做負(fù)載均衡。更多資料請(qǐng)參考官方文檔。

tomcat keepalive 配置: http://tomcat.apache.org/tomcat-7.0-doc/config/http.html。 maxKeepAliveRequests:默認(rèn)100; keepAliveTimeout:默認(rèn)等于 connectionTimeout,默認(rèn) 60 秒;

location proxy 配置:http://nginx.org/cn/docs/http/ngx_http_proxy_module.html。 rewrite:將當(dāng)前請(qǐng)求的 url 重寫,如我們請(qǐng)求時(shí)是 /backend/ad,則重寫后是 /ad。 proxy_pass:將整個(gè)請(qǐng)求轉(zhuǎn)發(fā)到上游服務(wù)器。 proxy_next_upstream:什么情況認(rèn)為當(dāng)前 upstream server 失敗,需要 next upstream,默認(rèn)是連接失敗/超時(shí),負(fù)載均衡參數(shù)。 proxy_pass_request_headers:之前已經(jīng)介紹過了,兩個(gè)原因:1、假設(shè)上游服務(wù)器不需要請(qǐng)求頭則沒必要傳輸請(qǐng)求頭;2、ngx.location.capture時(shí)防止 gzip 亂碼(也可以使用more_clear_input_headers 配置)。 keepalive:keepalive_timeout:keepalive 超時(shí)設(shè)置,keepalive_requests:長(zhǎng)連接數(shù)量。此處的 keepalive(別人訪問該 location 時(shí)的長(zhǎng)連接)和 upstream keepalive(nginx 與上游服務(wù)器的長(zhǎng)連接)是不一樣的;此處注意,如果您的服務(wù)是面向客戶的,而且是單個(gè)動(dòng)態(tài)內(nèi)容就沒必要使用長(zhǎng)連接了。

vim /usr/servers/nginx/conf/nginx.conf

Java 代碼

include /usr/chapter6/nginx_chapter6.conf;  
\#為了方便測(cè)試,注釋掉example.conf  
\#include /usr/example/example.conf;  

重啟 nginx

/usr/servers/nginx/sbin/nginx -s reload

訪問如 192.168.1.2/backend/ad?id=1 即看到結(jié)果??梢?kill 掉一個(gè) tomcat,可以看到服務(wù)還是正常的。

vim /usr/chapter6/nginx_chapter6.conf

Java 代碼

location ~ /backend/(.*) {  
    internal;  
    keepalive_timeout   30s;  
    keepalive_requests  1000;  
    #支持keep-alive  
    proxy_http_version 1.1;  
    proxy_set_header Connection "";  

    rewrite /backend(/.*) $1 break;  
    proxy_pass_request_headers off;  
    #more_clear_input_headers Accept-Encoding;  
    proxy_next_upstream error timeout;  
    proxy_pass http://backend;  
}   

加上 internal,表示只有內(nèi)部使用該服務(wù)。

Nginx+Lua 邏輯開發(fā)

核心代碼

/usr/chapter6/ad.lua

Java 代碼

local redis = require("resty.redis")  
local cjson = require("cjson")  
local cjson_encode = cjson.encode  
local ngx_log = ngx.log  
local ngx_ERR = ngx.ERR  
local ngx_exit = ngx.exit  
local ngx_print = ngx.print  
local ngx_re_match = ngx.re.match  
local ngx_var = ngx.var  

local function close_redis(red)  
    if not red then  
        return  
    end  
    --釋放連接(連接池實(shí)現(xiàn))  
    local pool_max_idle_time = 10000 --毫秒  
    local pool_size = 100 --連接池大小  
    local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)  

    if not ok then  
        ngx_log(ngx_ERR, "set redis keepalive error : ", err)  
    end  
end  
local function read_redis(id)  
    local red = redis:new()  
    red:set_timeout(1000)  
    local ip = "127.0.0.1"  
    local port = 1111  
    local ok, err = red:connect(ip, port)  
    if not ok then  
        ngx_log(ngx_ERR, "connect to redis error : ", err)  
        return close_redis(red)  
    end  

    local resp, err = red:get(id)  
    if not resp then  
        ngx_log(ngx_ERR, "get redis content error : ", err)  
        return close_redis(red)  
    end  
        --得到的數(shù)據(jù)為空處理  
    if resp == ngx.null then  
        resp = nil  
    end  
    close_redis(red)  

    return resp  
end  

local function read_http(id)  
    local resp = ngx.location.capture("/backend/ad", {  
        method = ngx.HTTP_GET,  
        args = {id = id}  
    })  

    if not resp then  
        ngx_log(ngx_ERR, "request error :", err)  
        return  
    end  

    if resp.status ~= 200 then  
        ngx_log(ngx_ERR, "request error, status :", resp.status)  
        return  
    end  

    return resp.body  
end  

--獲取id  
local id = ngx_var.id  

--從redis獲取  
local content = read_redis(id)  

--如果redis沒有,回源到tomcat  
if not content then  
   ngx_log(ngx_ERR, "redis not found content, back to http, id : ", id)  
    content = read_http(id)  
end  

--如果還沒有返回404  
if not content then  
   ngx_log(ngx_ERR, "http not found content, id : ", id)  
   return ngx_exit(404)  
end  

--輸出內(nèi)容  
ngx.print("show_ad(")  
ngx_print(cjson_encode({content = content}))  
ngx.print(")")   

將可能經(jīng)常用的變量做成局部變量,如 local ngx_print = ngx.print;使用 jsonp 方式輸出,此處我們可以將請(qǐng)求 url 限定為 /ad/id 方式,這樣的好處是1、可以盡可能早的識(shí)別無效請(qǐng)求;2、可以走 nginx 緩存 /CDN 緩存,緩存的 key 就是 URL,而不帶任何參數(shù),防止那些通過加隨機(jī)數(shù)穿透緩存;3、jsonp 使用固定的回調(diào)函數(shù) show_ad(),或者限定幾個(gè)固定的回調(diào)來減少緩存的版本。

vim /usr/chapter6/nginx_chapter6.conf

Java 代碼

location ~ ^/ad/(\d+)$ {  
    default_type 'text/html';  
    charset utf-8;  
    lua_code_cache on;  
    set $id $1;  
    content_by_lua_file /usr/chapter6/ad.lua;  
}  

重啟 nginx

Java 代碼

/usr/servers/nginx/sbin/nginx -s reload  

訪問如 http://192.168.1.2/ad/1 即可得到結(jié)果。而且注意觀察日志,第一次訪問時(shí)不命中Redis,回源到 Tomcat;第二次請(qǐng)求時(shí)就會(huì)命中 Redis 了。

第一次訪問時(shí)將看到 /usr/servers/nginx/logs/error.log 輸出類似如下的內(nèi)容,而第二次請(qǐng)求相同的 url 不再有如下內(nèi)容:

Java 代碼

redis not found content, back to http, id : 2  

到此整個(gè)架構(gòu)就介紹完了,此處可以直接不使用 Tomcat,而是 Lua 直連 Mysql 做回源處理;另外本文只是介紹了大體架構(gòu),還有更多業(yè)務(wù)及運(yùn)維上的細(xì)節(jié)需要在實(shí)際應(yīng)用中根據(jù)自己的場(chǎng)景自己摸索。后續(xù)如使用 LVS/HAProxy 做負(fù)載均衡、使用 CDN 等可以查找資料學(xué)習(xí)。