緩存是用來避免頻繁到服務器端獲取數據而建立的一個存取更快的臨時存儲器。緩存的容量相對較小,但執行速度非常快,其主要作用爲:
- 存儲系統經常訪問的數據。
- 存儲耗時較長的計算結果。
合理地緩存數據,可以提高系統的性能。Play內置了緩存庫,併爲分佈式環境提供了Memcached緩存數據的支持。
Memcached是一套開源的分佈式內存對象緩存系統,它通過在內存中緩存數據和對象來減少讀取數據庫的次數,從而大幅度降低數據庫負載。
如果項目中沒有配置Memcached,Play將使用JVM堆中的獨立緩存進行數據存儲。但是將數據緩存在不同服務器的JVM堆中破壞了Play的share nothing原則:我們不能將應用程序運行在多個服務器上的同時,還期望數據保持一致,這樣做只會導致每個程序實例都擁有各自不同的數據副本。
當我們在使用緩存時,必須明確其自身特性:緩存存在於內存中(不進行持久化),只是用於存放暫時性的數據,時間一到就會過期。因此緩存並不是一個安全的存儲器,不能保證數據可以永久存在。如果發現數據在緩存中已過期,需要重新獲取數據,並再次放入緩存:
public static void allProducts() {
List<Product> products = Cache.get("products", List.class);
if(products == null) {
products = Product.findAll();
Cache.set("products", products, "30mn");
}
render(products);
}
1.1 緩存 API#
play.cache.Cache類提供了一系列訪問緩存的API,包含了完整的設置、替換和獲取數據的方法:
public static void showProduct(String id) {
Product product = Cache.get(id, Product.class);
if(product == null) {
product = Product.findById(id);
Cache.set("product_"+id, product, "30mn");
}
render(product);
}
public static void addProduct(String name, int price) {
Product product = new Product(name, price);
product.save();
showProduct(id);
}
public static void editProduct(String id, String name, int price) {
Product product = Product.findById(id);
product.name = name;
product.price = price;
Cache.set("product_"+id, product, "30mn");
showProduct(id);
}
public static void deleteProduct(String id) {
Product product = Product.findById(id);
product.delete();
Cache.delete("product_"+id);
allProducts();
}
操作緩存的API中有很多方法是以safe作爲前綴的,如safeDelete,safeSet等。帶safe前綴的方法是阻塞的,而標準方法是非阻塞的,這意味當我們執行以下程序時:
Cache.delete("product_"+id);
delete方法會立即返回結果,並沒有等待緩存對象是否被真正地物理刪除。因此,如果程序執行期間發生了錯誤(例如IO錯誤),緩存對象可能仍然存在,並沒有被刪除。
如果操作需要確保緩存對象被刪除,可以使用safeDelete方法:
Cache.safeDelete("product_"+id);
if(!Cache.safeDelete("product_" + id)) {
throw new Exception("Oops, the product has not been removed from the cache");
}
...
帶safe前綴的方法是阻塞式的,會降低應用程序的性能。因此,在實際操作中需要酌情考慮,選擇最佳方案。
Play只允許將少量的數據以字符串形式儲存在HTTP Session中。讀者可能會感到非常不適應,但這樣的設計確實更優雅,因爲Session本來就不應該是緩存數據的地方!
讀者可能已經習慣於以下寫法,將數據緩存在Session中:
httpServletRequest.getSession().put("userProducts", products);
...
// and then in subsequent requests
products = (List<Product>)httpServletRequest.getSession().get("userProducts");
但在Play中實現同樣效果的方式卻截然不同:
Cache.put(session.getId(), products);
...
// and then in subsequent requests
List<Product> products = Cache.get(session.getId(), List.class);
我們可以通過UUID獲取緩存中關聯用戶的信息。
與Session對象不同,緩存中的內容是獨立的,不會綁定任何特定的用戶。
1.3 配置Memcached#
如果項目要啓用Memcached,需要在application.conf中打開Memcached開關,並設置Memcached的守護進程地址:
memcached=enabled
memcached.host=127.0.0.1:11211
我們還可以指定多個守護進程地址,使之連接到同一個分佈式緩存:
memcached=enabled
memcached.1.host=127.0.0.1:11211
memcached.2.host=127.0.0.1:11212
2.1 定義框架 ID#
首先需要給應用指定框架ID。例如,我們使用play id命令將框架ID設置爲production,之後如果需要將Play框架設置爲PROD模式只需在application.conf文件裏進行如下配置:
%production.application.mode=prod
在該模式下啓動應用,Play會預編譯所有的Java文件和模版文件。如果在這一步出現了錯誤,應用是不能被成功啓動的,此時對源文件的修改也不再會被熱編譯與加載。
2.2 設置數據庫#
如果應用還在使用開發數據庫(例如內存數據庫db=mem或者文件數據庫db=fs,),顯然不能滿足產品化的需求,我們必須在產品化的時候選擇更加健壯的數據庫引擎。下例給出通用的JDBC連接方式,並以Mysql爲例:
%production.db.url=jdbc:mysql://localhost/prod
%production.db.driver=com.mysql.jdbc.Driver
%production.db.user=root
%production.db.pass=1515312
2.3 禁用JPA Schema自動更新#
如果應用中使用了Hibernate提供的Schema自動更新特性,我們必須在產品化時將其關閉。在產品服務器中,使用Hibernate來自動更新數據庫與數據並不是可行的方式,因爲這可能會導致諸如數據覆蓋、丟失或是沒有足夠權限操縱數據表等問題的出現。
如果用戶確保應用對產品環境的數據庫有完整的操縱權限,並且只是初次部署(即只做數據初始化工作),也是可以使用這個特性的。針對這種情況,需要在application.conf文件中進行如下配置:
%production.jpa.ddl=create
請確保只在初次發佈時使用該方式,並在之後更新部署時關閉該配置,否則會造成數據覆蓋、丟失等錯誤發生。如果沒有特殊的需求,筆者和Play作者都建議在產品化時將該配置取消。
2.4 配置密鑰#
Play的密鑰具有安全特性,比如在Session簽名中就會使用到,因此在Play應用中請務必保證該密鑰的私有性。我們可以在application.conf配置文件中通過如下配置設定密鑰:
%production.application.secret=c12d1c59af499d20f4955d07255ed8ea333
在Play中可以通過play secret命令生成隨機密鑰。讀者在使用密鑰時需要注意,如果應用需要被部署到分佈式的環境,我們必須要確保所有的應用實例都具有相同的密鑰。
2.5 配置前端HTTP服務器#
如果我們需要將應用部署在Play自帶的服務器,只需在application.conf文件中配置如下信息即可:
%production.http.port=80
直接使用Play內置的服務器將會比使用其他HTTP服務器具有更好的性能。
配置lighttpd
以下這個例子將會展示如何配置lighttpd作爲HTTP服務器。儘管Apache也可以做到這些,但是如果讀者只需要其中的虛擬主機或負載均衡功能,lighttpd會是更輕便、更易於配置的選擇。/etc/lighttpd/lighttpd.conf文件的具體配置如下:
server.modules = (
"mod_access",
"mod_proxy",
"mod_accesslog"
)
…
$HTTP["host"] =~ "www.myapp.com" {
proxy.balance = "round-robin" proxy.server = ( "/" =>
( ( "host" => "127.0.0.1", "port" => 9000 ) ) )
}
$HTTP["host"] =~ "www.loadbalancedapp.com" {
proxy.balance = "round-robin" proxy.server = ( "/" => (
( "host" => "127.0.0.1", "port" => 9000 ),
( "host" => "127.0.0.1", "port" => 9001 ) )
)
}
配置Apache
以下這個例子將展示如何配置Apache httpd server作爲Play應用的HTTP服務器。
Apache服務器的配置文件路徑爲\conf\httpd.conf。Apache配置文件中使用#進行註釋,取消以下這條配置信息前的#註釋(如果存在)。
LoadModule proxy_module modules/mod_proxy.so
<VirtualHost *:80>
ProxyPreserveHost On
ServerName www.loadbalancedapp.com
ProxyPass / http://127.0.0.1:9000/
ProxyPassReverse / http://127.0.0.1:9000/
</VirtualHost>
利用Apache服務器全透明部署應用
如果需要更新Web應用,我們通常會關閉服務器,更新應用,最後重新啓動服務器。但是這個期間造成的中斷服務顯然是對用戶很不友好的表現。理想的情況是更新Web應用而不中斷原有服務,即透明化部署應用的過程。實現該功能的原理是運行同個Play應用的兩個實例,並利用HTTP服務器負載均衡。當其中某個應用實例無法提供服務時,HTTP服務器會將所有的請求切換至仍然能提供服務的另一個實例。
下面將演示如何做到這一點。爲了方便演示,我們會啓動同個Play應用兩次,只是將其中的一個端口設置爲9999,另一個端口設置爲9998。
在實際中,應用很有可能處於兩個不同的服務器。複製一份相同的Play應用並編輯application.conf文件,改變其中的端口配置。然後使用play start命令分別運行這兩個Web應用:
play start mysuperwebapp
接着配置Apache的負載均衡器:
<VirtualHost mysuperwebapp.com:80>
ServerName mysuperwebapp.com
<Location /balancer-manager>
SetHandler balancer-manager
Order Deny,Allow
Deny from all
Allow from .mysuperwebapp.com
</Location>
<Proxy balancer://mycluster>
BalancerMember http://localhost:9999
BalancerMember http://localhost:9998 status=+H
</Proxy>
<Proxy *>
Order Allow,Deny
Allow From All
</Proxy>
ProxyPreserveHost On
ProxyPass /balancer-manager !
ProxyPass / balancer://mycluster/
ProxyPassReverse / http://localhost:9999/
ProxyPassReverse / http://localhost:9998/
</VirtualHost>
在該配置中需要注意的地方是balancer://mycluster,其聲明瞭一個負載均衡器。跟在第二個Play應用後的參數+H表明第二個Play應用處於待用狀態,不過這並不影響其參與負載均衡。其他的配置細節已超出本書的範圍,不再贅述。當我們需要更新應用時,首先停止第一個應用的服務:
play stop mysuperwebapp1
負載均衡器會將所有的請求轉向到mysuperwebapp2。這時便可以對應用mysuperwebapp1進行更新,當我們更新完成之後再次啓動mysuperwebapp1:
play start mysuperwebapp1
Apache也提供了簡單的方式來監視集羣的狀態,即在瀏覽器中轉向/balancer-manager來監視當前集羣。因爲Play是完全無狀態的,所以我們無需管理兩個集羣Session共享的問題。事實上,我們也可以同時運行兩個以上的Play應用集羣。
2.6 高級代理設置#
當Play應用與HTTP服務器運行在不同的機器上時,請求地址會被視爲是來自HTTP服務器的地址。在默認情況下,當Play應用和服務器代理運行在同一臺物理服務器上時,Play應用會將請求地址視爲來自127.0.0.1。
代理服務器可以添加特殊的請求頭來告訴被代理的Web應用當前這個請求是來自哪裏的。大多數Web服務器都會添加X-Forwarded-For來完成這樣的事情,其值通常是運行着Web應用的原始主機IP。如果我們在XForwardedSupport中打開轉發支持,Play會將request.remoteAddress修改爲運行Play的物理服務器IP而不是默認的代理服務器IP,不過爲了使它正常工作我們還必須爲其列出所有的代理服務器。
主機的請求頭仍是不透明的,我們還需要對服務器進行一些配置。以Apache 2.x爲例子,只需要在配置文件中添加如下信息:
ProxyPreserveHost on
Play內置的服務器支持HTTPS協議,在產品化時同樣也適用。內置服務器同時提供了對於證書的管理,包含了對原生的Java keyStore支持,以及對簡單的證書和密鑰文件支持。我們可以通過在application.conf配置文件中配置https.port來爲Play應用打開HTTPS連接器,然後將證書放在conf目錄下:
http.port=9000
https.port=9443
Play支持了X509證書和keystore證書,其中X509證書必須按照如下方式命名:host.cert爲證書,host.key爲密鑰。如果讀者使用的是keystore證書,則默認的命名爲certificate.jks。
使用X509證書的配置實例如下:
# X509 certificates
certificate.key.file=conf/host.key
certificate.file=conf/host.cert
# 如果密鑰文件是用密碼保護
certificate.password=secret
trustmanager.algorithm=JKS
使用keystore證書的配置實例如下:
keystore.algorithm=JKS
keystore.password=secret
keystore.file=conf/certificate.jks
以上例子均採用了默認值。我們還可以通過openssl命令生成自簽名的證書。
openssl genrsa 1024 > host.key
openssl req -new -x509 -nodes -sha1 -days 365 -key host.key > host.cert
對於使用keystore的用戶,可以在application.conf文件中直接配置,生成自簽名證書。配置信息如下:
# Keystore
ssl.KeyManagerFactory.algorithm=SunX509
trustmanager.algorithm=JKS
keystore.password=secret
keystore.file=certificate.jks
2.8 非Python環境下的部署#
Python在大多數Unix系的系統中都被默認安裝了。雖然Play在Windows版本中也包含了嵌入式的Python,但也不排除有無法支持Python運行環境的服務器的存在。針對這個問題,Play附帶了ant配置文件build.xml,提供有限功能的部署方式。
在應用根目錄,使用ant start命令運行服務器:
ant start -Dplay.path=/path/to/playdirectory
如果需要停止服務器,則可以使用ant stop命令:
ant stop -Dplay.path=/path/to/playdirectory
當我們使用Play命令時,輸出會被重定向到System.out。但是使用ant時,標準輸出是無法訪問的。所以使用這種方式部署時,我們必須爲其提供Log4j配置文件。
我們也可以在環境變量中指定Play框架的路徑,或者將路徑直接寫入應用的build.xml配置文件裏。