缓存是用来避免频繁到服务器端获取数据而建立的一个存取更快的临时存储器。缓存的容量相对较小,但执行速度非常快,其主要作用为:
- 存储系统经常访问的数据。
- 存储耗时较长的计算结果。
合理地缓存数据,可以提高系统的性能。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配置文件里。