注意:本節代碼基於《第七章 企業項目開發--本地緩存guava cache》
1、本地緩存的問題
- 本地緩存速度一開始高於分布式緩存,但是隨着其緩存數量的增加,所占內存越來越大,系統運行內存越來越小,最后系統會被拖慢(這一點與第二點聯系起來)
- 本地緩存存於本機,其緩存數量與大小受本機內存大小限制
- 本地緩存存於本機,其他機器的訪問不到這樣的緩存
解決方案:分布式緩存
- Jboss cache:緩存還存於本機,但是會同步更新到其他機器(解決了第三個問題,解決不了第一和第二個問題),如果緩存機器數量很多,同步更新很耗時
- memcached:緩存存於其他機器,理論上緩存數量與大小無限(因為集群可伸縮),且不需要同步,所以即使緩存機器數量很多,也無所謂,但是這樣就會造成單點故障問題,最簡單易行的解決方案是緩存備份,即緩存至少存兩份。
2、memcached Java客戶端的選用
當下常用的三種memcached Java客戶端:
- Memcached Client for Java:memcached官方提供,基於Java BIO實現
- SpyMemcached:基於Java NIO
- XMemcached:基於Java NIO,並發性能優於XMemcached,實際上SpyMemcached性能也很高
三者的實驗比較結果:
http://xmemcached.googlecode.com/svn/trunk/benchmark/benchmark.html
所以,我們選用XMemcached來實現客戶端的編寫。
3、代碼
在原來的代碼結構上,我增加了一個ssmm0-cache模塊,專門用於放置分布式緩存相關(memcached、redis、spring cache)的代碼。
項目整體結構:
說明:怎樣新建maven項目,並加入原來項目,最后引入eclipse,見第一章《第一章 企業項目開發--maven+springmvc+spring+mybatis+velocity整合》
3.1、ssmm0
pom.xml

1 <?xml version="1.0" encoding="UTF-8"?> 2 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 3 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4 5 <modelVersion>4.0.0</modelVersion> 6 7 <groupId>com.xxx</groupId> 8 <artifactId>ssmm0</artifactId> 9 <version>1.0-SNAPSHOT</version> 10 11 <name>ssmm0</name> 12 <packaging>pom</packaging><!-- 父模塊 --> 13 14 <!-- 管理子模塊 --> 15 <modules> 16 <module>userManagement</module><!-- 具體業務1-人員管理系統 --> 17 <module>data</module><!-- 封裝數據操作 --> 18 <module>cache</module><!-- 緩存模塊 --> 19 </modules> 20 21 <properties> 22 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 23 <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> 24 </properties> 25 26 <!-- dependencyManagement不會引入實際的依賴,只是作為一個依賴池,供其和其子類使用 --> 27 <dependencyManagement> 28 <dependencies> 29 <!-- json --> 30 <dependency> 31 <groupId>com.alibaba</groupId> 32 <artifactId>fastjson</artifactId> 33 <version>1.1.39</version> 34 </dependency> 35 <!-- servlet --> 36 <dependency> 37 <groupId>javax.servlet</groupId> 38 <artifactId>javax.servlet-api</artifactId> 39 <version>3.0.1</version> 40 <scope>provided</scope> 41 </dependency> 42 <!-- spring --> 43 <dependency> 44 <groupId>org.springframework</groupId> 45 <artifactId>spring-core</artifactId> 46 <version>3.2.6.RELEASE</version> 47 </dependency> 48 <dependency> 49 <groupId>org.springframework</groupId> 50 <artifactId>spring-beans</artifactId> 51 <version>3.2.6.RELEASE</version> 52 </dependency> 53 <dependency> 54 <groupId>org.springframework</groupId> 55 <artifactId>spring-context</artifactId> 56 <version>3.2.6.RELEASE</version> 57 </dependency> 58 <dependency> 59 <groupId>org.springframework</groupId> 60 <artifactId>spring-web</artifactId> 61 <version>3.2.6.RELEASE</version> 62 </dependency> 63 <dependency> 64 <groupId>org.springframework</groupId> 65 <artifactId>spring-webmvc</artifactId> 66 <version>3.2.6.RELEASE</version> 67 </dependency> 68 <!-- 這個是使用velocity的必備包 --> 69 <dependency> 70 <groupId>org.springframework</groupId> 71 <artifactId>spring-context-support</artifactId> 72 <version>3.2.6.RELEASE</version> 73 </dependency> 74 <!-- mysql --> 75 <dependency> 76 <groupId>mysql</groupId> 77 <artifactId>mysql-connector-java</artifactId> 78 <version>5.1.27</version> 79 <scope>runtime</scope> 80 </dependency> 81 <!-- 數據源 --> 82 <dependency> 83 <groupId>org.apache.tomcat</groupId> 84 <artifactId>tomcat-jdbc</artifactId> 85 <version>7.0.47</version> 86 </dependency> 87 <!-- mybatis --> 88 <dependency> 89 <groupId>org.mybatis</groupId> 90 <artifactId>mybatis</artifactId> 91 <version>3.1.1</version> 92 </dependency> 93 <dependency> 94 <groupId>org.mybatis</groupId> 95 <artifactId>mybatis-spring</artifactId> 96 <version>1.1.1</version> 97 </dependency> 98 <!-- velocity --> 99 <dependency> 100 <groupId>org.apache.velocity</groupId> 101 <artifactId>velocity</artifactId> 102 <version>1.5</version> 103 </dependency> 104 <dependency> 105 <groupId>velocity-tools</groupId> 106 <artifactId>velocity-tools-generic</artifactId> 107 <version>1.2</version> 108 </dependency> 109 <!-- 用於加解密 --> 110 <dependency> 111 <groupId>commons-codec</groupId> 112 <artifactId>commons-codec</artifactId> 113 <version>1.7</version> 114 </dependency> 115 <dependency> 116 <groupId>org.bouncycastle</groupId> 117 <artifactId>bcprov-jdk15on</artifactId> 118 <version>1.47</version> 119 </dependency> 120 <!-- 集合工具類 --> 121 <dependency> 122 <groupId>org.apache.commons</groupId> 123 <artifactId>commons-collections4</artifactId> 124 <version>4.0</version> 125 </dependency> 126 <!-- 字符串處理類 --> 127 <dependency> 128 <groupId>org.apache.commons</groupId> 129 <artifactId>commons-lang3</artifactId> 130 <version>3.4</version> 131 </dependency> 132 <!-- http --> 133 <dependency> 134 <groupId>org.apache.httpcomponents</groupId> 135 <artifactId>httpclient</artifactId> 136 <version>4.2.6</version> 137 </dependency> 138 </dependencies> 139 </dependencyManagement> 140 141 <!-- 引入實際依賴 --> 142 <dependencies> 143 <!-- json --> 144 <dependency> 145 <groupId>com.alibaba</groupId> 146 <artifactId>fastjson</artifactId> 147 </dependency> 148 <!-- spring --> 149 <dependency> 150 <groupId>org.springframework</groupId> 151 <artifactId>spring-core</artifactId> 152 </dependency> 153 <dependency> 154 <groupId>org.springframework</groupId> 155 <artifactId>spring-beans</artifactId> 156 </dependency> 157 <dependency> 158 <groupId>org.springframework</groupId> 159 <artifactId>spring-context</artifactId> 160 </dependency> 161 <!-- 集合工具類 --> 162 <dependency> 163 <groupId>org.apache.commons</groupId> 164 <artifactId>commons-collections4</artifactId> 165 </dependency> 166 <!-- 字符串處理類 --> 167 <dependency> 168 <groupId>org.apache.commons</groupId> 169 <artifactId>commons-lang3</artifactId> 170 </dependency> 171 </dependencies> 172 173 <build> 174 <resources> 175 <!-- 這里配置了這一塊兒true,才可以讓指定文件(這里是src/main/resources/spring-data.xml)讀到pom.xml中的配置信息 176 , 值得注意的是,如果src/main/resources下還有其他文件,而你不想讓其讀pom.xml, 你還必須得把src/main/resources下的其余文件再配置一遍,配置為false(不可讀pom.xml), 177 如下邊的注釋那樣,否則,會報這些文件找不到的錯誤 178 --> 179 <resource> 180 <directory>src/main/resources</directory> 181 <filtering>true</filtering> 182 <includes> 183 <include>*.xml</include> 184 <include>*.properties</include> 185 </includes> 186 </resource> 187 <!-- 188 <resource> 189 <directory>src/main/resources</directory> 190 <filtering>false</filtering> 191 <includes> 192 <include>*.properties</include> 193 </includes> 194 </resource> 195 --> 196 <resource> 197 <directory>src/main/resources</directory> 198 <filtering>false</filtering> 199 <includes> 200 <!-- 這里如果不加這一條,那么在spring-data.xml中配置的xml將找不到classpath:mapper/admin/AdminMapper.xml --> 201 <include>mapper/**/*.xml</include> 202 </includes> 203 </resource> 204 </resources> 205 </build> 206 207 <!-- 208 profiles可以定義多個profile,然后每個profile對應不同的激活條件和配置信息,從而達到不同環境使用不同配置信息的效果 209 注意兩點: 210 1)<activeByDefault>true</activeByDefault>這種情況表示服務器啟動的時候就采用這一套env(在這里,就是prod) 211 2)當我們啟動服務器后,想采用開發模式,需切換maven的env為dev,如果env的配置本身就是dev,需要將env換成rc或prod,點擊apply,然后再將env切換成dev,點擊apply才行 212 --> 213 <profiles> 214 <!-- 開發env --> 215 <profile> 216 <id>dev</id> 217 <activation> 218 <!-- 這里為了測試方便,改為了true,在上線的時候一定要改成false,否則線上使用的就是這一套dev的環境了 --> 219 <activeByDefault>true</activeByDefault> 220 <property> 221 <name>env</name> 222 <value>dev</value> 223 </property> 224 </activation> 225 <properties> 226 <env>dev</env> 227 228 <jdbc.driverClassName>com.mysql.jdbc.Driver</jdbc.driverClassName> 229 <!-- 230 對於jdbc.url中內容的配置,如果需要配置 &時,有兩種方法: 231 1)如下邊這樣,使用<![CDATA[XXX]]>包起來 232 2)使用jdbc.properties文件來讀取此pom.xml,然后spring.xml再讀取jdbc.properties文件 顯然,前者更方便,而且還省了一個jdbc.properties的文件,但是,有的時候,還是會用后者的; 233 在使用后者的時候,注意三點: 234 1)需要修改上邊的build中的內容 235 2)需要在spring.xml中配置<context:property-placeholder location="classpath:jdbc.properties"/> 236 3)將jdbc.properties放在ssmm0-data項目中,之后需要將ssmm0-data項目的env配置為dev 237 --> 238 <jdbc.url><![CDATA[jdbc:mysql://127.0.0.1:3306/blog?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8]]></jdbc.url> 239 <jdbc.username>root</jdbc.username> 240 <jdbc.password>123456</jdbc.password> 241 242 <!-- memcache,多台服務器之間需要使用空格隔開,而不要使用英文逗號隔開,因為Xmemcached的AddrUtil源碼是根據空格隔開的 --> 243 <memcached.servers><![CDATA[127.0.0.1:11211]]></memcached.servers> 244 <memcached.max.client>10</memcached.max.client><!-- 最多的客戶端數 --> 245 <memcached.expiretime>900</memcached.expiretime><!-- 過期時間900s --> 246 <memcached.hash.consistent>true</memcached.hash.consistent><!-- 是否使用一致性hash算法 --> 247 <memcached.connection.poolsize>1</memcached.connection.poolsize><!-- 每個客戶端池子的連接數 --> 248 <memcached.op.timeout>2000</memcached.op.timeout><!-- 操作超時時間 --> 249 </properties> 250 </profile> 251 <!-- 預上線env --> 252 <profile> 253 <id>rc</id> 254 <activation> 255 <activeByDefault>false</activeByDefault> 256 <property> 257 <name>env</name> 258 <value>rc</value> 259 </property> 260 </activation> 261 <properties> 262 <env>rc</env> 263 264 <jdbc.driverClassName>com.mysql.jdbc.Driver</jdbc.driverClassName> 265 <!-- 假設的一個地址 --> 266 <jdbc.url><![CDATA[jdbc:mysql://10.10.10.100:3306/blog?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8]]></jdbc.url> 267 <jdbc.username>root2</jdbc.username> 268 <jdbc.password>1234562</jdbc.password> 269 </properties> 270 </profile> 271 <!-- 線上env --> 272 <profile> 273 <id>prod</id> 274 <activation> 275 <!-- 這里為了測試方便,改為了false,在上線的時候一定要改成true,否則線上使用的就不是這一套環境了 --> 276 <activeByDefault>false</activeByDefault> 277 <property> 278 <name>env</name> 279 <value>prod</value> 280 </property> 281 </activation> 282 <properties> 283 <env>prod</env> 284 285 <jdbc.driverClassName>com.mysql.jdbc.Driver</jdbc.driverClassName> 286 <!-- 假設的一個地址 --> 287 <jdbc.url><![CDATA[jdbc:mysql://99.99.99.999:3307/blog?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8]]></jdbc.url> 288 <jdbc.username>sadhijhqwui</jdbc.username> 289 <jdbc.password>zxczkchwihcznk=</jdbc.password> 290 </properties> 291 </profile> 292 </profiles> 293 </project>
說明:這里給出了完整版,實際上只做了以下5點改動:
- 新增cache子module
- 引入了commons-lang3的jar包,該jar封裝了一些對於字符串的處理方法,eg.isBlank()
- <resources>部分因為要將該pom.xml中的配置讀取到ssmm0-cache模塊的properties文件中去,所以添加了過濾目錄
- 在dev環境下配置了與memcached相關的服務器列表及參數
- 最后一點,只是為了測試方便,將dev設為默認選用的環境,而prod不是,在實際上線之前,一定要改回來
注意:
- 在pom.xml中配置memcached服務器列表時,要以"ip1:port1 ip2:port2..."這樣的形式,即每台服務器之間用空格隔開,這與Xmemcached的AddrUtil讀取服務器列表的方式有關。
3.2、ssmm0-cache
模塊結構:
3.2.1、pom.xml

1 <?xml version="1.0" encoding="UTF-8"?> 2 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 3 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4 5 <modelVersion>4.0.0</modelVersion> 6 7 <!-- 指定父模塊 --> 8 <parent> 9 <groupId>com.xxx</groupId> 10 <artifactId>ssmm0</artifactId> 11 <version>1.0-SNAPSHOT</version> 12 </parent> 13 14 <groupId>com.xxx.ssmm0</groupId> 15 <artifactId>ssmm0-cache</artifactId> 16 17 <name>ssmm0-cache</name> 18 <packaging>jar</packaging> 19 20 <!-- 引入實際依賴 --> 21 <dependencies> 22 <!-- memcached --> 23 <dependency> 24 <groupId>com.googlecode.xmemcached</groupId> 25 <artifactId>xmemcached</artifactId> 26 <version>1.4.3</version> 27 </dependency> 28 </dependencies> 29 </project>
說明:在該pom.xml中引入了xmemcached的jar包,注意該jar依賴於slf4j.jar,如果不是用maven的話,需要手動導入slf4j.jar,但是用maven的話,maven自己會導入相關的依賴包。
3.2.2、cache_config.properties

1 #memcached配置# 2 #memcached服務器集群 3 memcached.servers = ${memcached.servers} 4 #緩存過期時間 5 memcached.expiretime = ${memcached.expiretime} 6 #是否使用一致性hash算法 7 memcached.hash.consistent = ${memcached.hash.consistent} 8 #memcached的最大客戶端數量 9 memcached.max.client = ${memcached.max.client} 10 #每個客戶端池子的連接數 11 memcached.connection.poolsize = ${memcached.connection.poolsize} 12 #操作超時時間 13 memcached.op.timeout = ${memcached.op.timeout}
說明:這里需要從根pom.xml(即ssmm0的pom.xml)中讀取信息,所以在根pom.xml的資源過濾部分有相關的修改
3.2.3、FileUtil.java

1 package com.xxx.cache.util; 2 3 import java.io.IOException; 4 import java.io.InputStream; 5 import java.util.Properties; 6 7 import org.apache.commons.lang3.math.NumberUtils; 8 9 /** 10 * 文件操作工具類 11 */ 12 public class FileUtil { 13 14 /** 15 * 加載屬性文件*.properties 16 * @param fileName 不是屬性全路徑名稱,而是相對於類路徑的名稱 17 */ 18 public static Properties loadProps(String fileName){ 19 Properties props = null; 20 InputStream is = null; 21 22 try { 23 is = Thread.currentThread().getContextClassLoader().getResourceAsStream(fileName);//獲取類路徑下的fileName文件,並且轉化為輸入流 24 if(is != null){ 25 props = new Properties(); 26 props.load(is); //加載屬性文件 27 } 28 } catch (Exception e) { 29 e.printStackTrace(); 30 }finally{ 31 if(is!=null){ 32 try { 33 is.close(); 34 } catch (IOException e) { 35 e.printStackTrace(); 36 } 37 } 38 } 39 40 return props; 41 } 42 43 /* 44 * 從屬性文件中獲取int型數據 45 */ 46 public static int getInt(Properties props, String key, int defaultValue){ 47 int value = defaultValue; 48 if(props.containsKey(key)){ //屬性文件中是否包含給定鍵值 49 value = NumberUtils.toInt(props.getProperty(key), defaultValue);//從屬性文件中取出給定鍵值的value,並且轉換為int型 50 } 51 return value; 52 } 53 54 /* 55 * 從屬性文件中獲取long型數據 56 */ 57 public static long getLong(Properties props, String key, long defaultValue){ 58 long value = defaultValue; 59 if(props.containsKey(key)){ //屬性文件中是否包含給定鍵值 60 value = NumberUtils.toLong(props.getProperty(key), defaultValue);//從屬性文件中取出給定鍵值的value,並且轉換為int型 61 } 62 return value; 63 } 64 65 /* 66 * 從屬性文件中獲取boolean型數據 67 */ 68 public static boolean getBoolean(Properties props, String key, boolean defaultValue){ 69 boolean value = defaultValue; 70 if(props.containsKey(key)){ //屬性文件中是否包含給定鍵值 71 value = toBoolean(props.getProperty(key), defaultValue); 72 } 73 return value; 74 } 75 76 77 public static boolean toBoolean(String str, boolean defaultValue) { 78 if(str == null) { 79 return defaultValue; 80 } 81 return Boolean.parseBoolean(str); 82 } 83 84 /** 85 * 測試 86 */ 87 public static void main(String[] args) { 88 Properties props = FileUtil.loadProps("cache_config.properties"); 89 //System.out.println(props); 90 System.out.println(props.getProperty("memcached.servers", "123"));//從屬性文件中讀取string 91 System.out.println(FileUtil.getInt(props, "httpclient.max.conn.per.route2", 10));//屬性文件中沒有這個key 92 } 93 }
說明:對於該類的介紹與注意點,參看"Java文件相關"系列的《第一章 屬性文件操作工具類》
在該類中,添加了一些方法
- 從屬性文件中將String轉化為long型數據,與String轉int一樣,使用NumberUtil即可
- 從屬性文件中將String轉化為Boolean型數據,直接參考NumberUtil的相關源代碼進行編寫即可
- 從屬性文件中讀取String,props.getProperties(String key, String defaultValue)即可
注意:
- 如果直接運行該類中的main方法來讀取properties文件中由根pom.xml傳來的參數,可能讀取不到;可以通過后邊的MemcachedUtil中的main方法來獲取,或者直接通過最后的瀏覽器訪問來獲取就好
3.2.4、CachePrefix.java

1 package com.xxx.cache.util; 2 3 /** 4 * 在該類中定義一些緩存的前綴 5 * 方式: 6 * 1、定義一個類,在其中添加靜態常量 7 * 2、使用枚舉類(這是最好的方式) 8 * 作用: 9 * 1、防止緩存鍵值重復(通常情況下,每一種業務對應一種前綴) 10 * 2、可以起到根據前綴分類的作用 11 * 后者主要是在memcached中實現類似於redis的實現。 12 */ 13 public enum CachePrefix { 14 USER_MANAGEMENT, //人員管理業務類緩存前綴 15 HOTEL_MANAGEMENT; //酒店管理業務類緩存前綴 16 }
說明:在該類中定義一些緩存的前綴
作用:
- 防止緩存鍵值重復(通常情況下,每一種業務對應一種前綴)
- 以起到根據前綴分類的作用(后者主要是在memcached中實現類似於redis的實現),但是其實memcached可以通過使用命名空間來實現分類,具體的參看我的"Java緩存相關"系列中后續的文章
注意:定義常量類有兩種方式
- 普通類,里邊使用static final常量
- 枚舉類(推薦),這里就是使用了枚舉類
關於枚舉類與普通類做常量類的優缺點對比,查看《effective Java:第二版》第30條,或者參考我"Java高效使用"系列中后續的文章
3.2.5、MemcachedUtil

1 package com.xxx.cache.memcached; 2 3 import java.io.IOException; 4 import java.util.HashMap; 5 import java.util.Map; 6 import java.util.Properties; 7 import java.util.concurrent.TimeoutException; 8 9 import org.apache.commons.lang3.StringUtils; 10 11 import com.xxx.cache.util.CachePrefix; 12 import com.xxx.cache.util.FileUtil; 13 14 15 import net.rubyeye.xmemcached.MemcachedClient; 16 import net.rubyeye.xmemcached.MemcachedClientBuilder; 17 import net.rubyeye.xmemcached.XMemcachedClientBuilder; 18 import net.rubyeye.xmemcached.command.BinaryCommandFactory; 19 import net.rubyeye.xmemcached.exception.MemcachedException; 20 import net.rubyeye.xmemcached.impl.KetamaMemcachedSessionLocator; 21 import net.rubyeye.xmemcached.transcoders.CompressionMode; 22 import net.rubyeye.xmemcached.transcoders.SerializingTranscoder; 23 import net.rubyeye.xmemcached.utils.AddrUtil; 24 25 /** 26 * memcached工具類(基於Xmemcached實現) 27 */ 28 public class MemcachedUtil { 29 private static Map<Integer, MemcachedClient> clientMap 30 = new HashMap<Integer, MemcachedClient>();//client的集合 31 private static int maxClient = 3; 32 private static int expireTime = 900;//900s(默認的緩存過期時間) 33 private static int maxConnectionPoolSize = 1;//每個客戶端池子的連接數 34 private static long op_time = 2000L;//操作超時時間 35 36 private static final String KEY_SPLIT = "-";//用於隔開緩存前綴與緩存鍵值 37 38 /** 39 * 構建MemcachedClient的map集合 40 */ 41 static{ 42 //讀取配置文件 43 Properties props = FileUtil.loadProps("cache_config.properties"); 44 String servers = props.getProperty("memcached.servers", "127.0.0.1:11211");//獲取memcached servers集合 45 maxClient = FileUtil.getInt(props, "", maxClient); 46 expireTime = FileUtil.getInt(props, "memcached.expiretime", expireTime); 47 maxConnectionPoolSize = FileUtil.getInt(props, "memcached.connection.poolsize", maxConnectionPoolSize); 48 op_time = FileUtil.getLong(props, "memcached.op.timeout", op_time); 49 50 if(StringUtils.isNotBlank(servers)){ 51 MemcachedClientBuilder builder = new XMemcachedClientBuilder(AddrUtil.getAddresses(servers)); 52 builder.setConnectionPoolSize(1);//這個默認也是1 53 builder.setSessionLocator(new KetamaMemcachedSessionLocator(true));//使用一致性hash算法 54 55 SerializingTranscoder transcoder = new SerializingTranscoder(1024*1024);//序列化轉換器,指定最大的數據大小1M 56 transcoder.setCharset("UTF-8");//默認為UTF-8,這里可去掉 57 transcoder.setCompressionThreshold(1024*1024);//單位:字節,壓縮邊界值,任何一個大於該邊界值(這里是:1M)的數據都要進行壓縮 58 transcoder.setCompressionMode(CompressionMode.GZIP);//壓縮算法 59 60 builder.setTranscoder(transcoder); 61 builder.setCommandFactory(new BinaryCommandFactory());//命令工廠 62 63 //構建10個MemcachedCient,並放入clientMap 64 for(int i=0;i<maxClient;i++){ 65 try { 66 MemcachedClient client = builder.build(); 67 client.setOpTimeout(op_time);//設置操作超時時間,默認為1s 68 clientMap.put(i, client); 69 } catch (IOException e) { 70 e.printStackTrace(); 71 } 72 } 73 } 74 } 75 76 /** 77 * 從MemcachedClient中取出一個MemcachedClient 78 */ 79 public static MemcachedClient getMemcachedClient(){ 80 /* 81 * Math.random():產生[0,1)之間的小數 82 * Math.random()*maxClient:[0~maxClient)之間的小數 83 * (int)(Math.random()*maxClient):[0~maxClient)之間的整數 84 */ 85 return clientMap.get((int)(Math.random()*maxClient)); 86 } 87 88 /** 89 * 設置緩存 90 * @param keyPrefix 緩存的鍵的前綴 91 * @param key 緩存的鍵 92 * @param value 緩存的值 93 * @param exp 緩存過期時間 94 */ 95 public static void setCacheWithNoReply(CachePrefix keyPrefix, 96 String key, 97 Object value, 98 int exp){ 99 try { 100 MemcachedClient client = getMemcachedClient(); 101 client.addWithNoReply(keyPrefix+KEY_SPLIT+key, exp, value); 102 } catch (InterruptedException e) { 103 e.printStackTrace(); 104 } catch (MemcachedException e) { 105 e.printStackTrace(); 106 } 107 } 108 109 /** 110 * 設置緩存 111 * @param exp 緩存過期時間(默認時間) 112 */ 113 public static void setCacheWithNoReply(CachePrefix keyPrefix, 114 String key, 115 Object value){ 116 setCacheWithNoReply(keyPrefix, key, value, expireTime); 117 } 118 119 /** 120 * 設置緩存,並返回緩存成功與否 121 * 注意: 122 * 1、設置已經設置過的key-value,將會返回false 123 */ 124 public static boolean setCache(CachePrefix keyPrefix, 125 String key, 126 Object value, 127 int exp){ 128 boolean setCacheSuccess = false; 129 try { 130 MemcachedClient client = getMemcachedClient(); 131 setCacheSuccess = client.add(keyPrefix+KEY_SPLIT+key, exp, value); 132 } catch (TimeoutException e) { 133 e.printStackTrace(); 134 } catch (InterruptedException e) { 135 e.printStackTrace(); 136 } catch (MemcachedException e) { 137 e.printStackTrace(); 138 } 139 return setCacheSuccess; 140 } 141 142 /** 143 * 設置緩存,並返回緩存成功與否(緩存超時時間采用默認) 144 * @param key 145 * @param value 146 */ 147 public static boolean setCache(CachePrefix keyPrefix, 148 String key, 149 Object value){ 150 return setCache(keyPrefix, key, value, expireTime); 151 } 152 153 /** 154 * 獲取緩存 155 */ 156 public static Object getCache(CachePrefix keyPrefix, String key){ 157 Object value = null; 158 try { 159 MemcachedClient client = getMemcachedClient(); 160 value = client.get(keyPrefix+KEY_SPLIT+key); 161 } catch (TimeoutException e) { 162 e.printStackTrace(); 163 } catch (InterruptedException e) { 164 e.printStackTrace(); 165 } catch (MemcachedException e) { 166 e.printStackTrace(); 167 } 168 return value; 169 } 170 171 /** 172 * 刪除緩存 173 */ 174 public static void removeCacheWithNoReply(CachePrefix keyPrefix, String key){ 175 try { 176 MemcachedClient client = getMemcachedClient(); 177 client.deleteWithNoReply(keyPrefix+KEY_SPLIT+key); 178 } catch (InterruptedException e) { 179 e.printStackTrace(); 180 } catch (MemcachedException e) { 181 e.printStackTrace(); 182 } 183 } 184 185 /** 186 * 刪除緩存,並返回刪除成功與否 187 */ 188 public static boolean removeCache(CachePrefix keyPrefix, String key){ 189 boolean removeCacheSuccess = false; 190 try { 191 MemcachedClient client = getMemcachedClient(); 192 removeCacheSuccess = client.delete(keyPrefix+KEY_SPLIT+key); 193 } catch (TimeoutException e) { 194 e.printStackTrace(); 195 } catch (InterruptedException e) { 196 e.printStackTrace(); 197 } catch (MemcachedException e) { 198 e.printStackTrace(); 199 } 200 return removeCacheSuccess; 201 } 202 203 /** 204 * 測試 205 * @param args 206 */ 207 public static void main(String[] args) { 208 /*for(int i=0;i<100;i++){ 209 System.out.println(Math.random()); 210 }*/ 211 System.out.println(MemcachedUtil.setCache(CachePrefix.USER_MANAGEMENT,"hello4", "world")); 212 System.out.println(MemcachedUtil.getCache(CachePrefix.USER_MANAGEMENT,"hello4")); 213 /*System.out.println(MemcachedUtil.getCache("hello2")); 214 System.out.println(MemcachedUtil.getCache("hello2")); 215 System.out.println(MemcachedUtil.getCache("hello2")); 216 System.out.println(MemcachedUtil.getCache("hello2")); 217 System.out.println(MemcachedUtil.getCache("hello2"));*/ 218 } 219 }
首先給出該類的一個圖(不知道該怎么稱呼這個圖)
說明:上述的圖可以根據源代碼的追蹤畫出來;該類是基於XMemcached實現了的一個工具類,主要包含以下6個部分
- 屬性默認值的指定
- static靜態塊中讀取屬性文件,並與前邊的默認值一起來指定最終參數值
- 根據服務器列表構建MemcachedClientBuilder,配置builder的相關屬性,包括序列化轉化器、二進制協議工廠等
- 通過上述的MemcachedClientBuilder構建指定個數(這里是3個)的MemcachedClient客戶端,存於clientMap中
- 提供從clientMap獲取MemcachedClient的方法(這里是隨機獲取)
- 提供緩存的基本操作,增刪查操作
注意:
- 我們提供了三個MemcachedClient,那是不是說明同時只能處理三個並發請求呢?不是,Xmemcached基於Java NIO,每一個MemcachedClient都會啟動一個reactor線程和一些工作線程,基於IO多路復用技術,一個reactor線程理論上可以接受極高的並發量,甚至可以說成,該reactor線程可以接過所有到達memcached的請求,然后通過事件機制(類似於觀察者模式)將這些請求派發給工作線程,進行相應的操作。關於Java NIO、reactor線程模型、事件機制等可以參看《netty權威指南(第2版)》。
- 正如上邊所說,每啟動一個MemcachedClient,就必須啟動一個reactor線程和一些工作線程,這其實是一個昂貴的操作,所以構建多個客戶端是比較昂貴的,基於此,XMemcached提供了池化操作,即一個配置參數(setConnectionPoolSize),但是在實際使用中發現,當配置該參數>1的情況下會發生線程死鎖現象,所以還是采用多客戶端的方式吧!在我們的實際使用中,10個客戶端接收百萬級請求絕對是輕輕松松的!
- 這里需要注意的是,服務器列表的配置必須用空格隔開,原理查看AddrUtil的源代碼
- 關於序列化與反序列化發生的時機,請參看《http://blog.csdn.net/tang9140/article/details/43445511》或者我的"Java緩存相關"的后續文章
- Xmemcached提供了很多的緩存操作API,這些API我會在"Java緩存相關"的后續文章介紹,這里想說,如果你需要提供很多的API方法,那么推薦將上述"說明"中的前5部分寫在一個類中(MemcachedFactory),將第6部分(緩存相關操作)寫在一個類中(MemcachedUtil),這樣會很清晰
- 其他屬性的配置,我也會在"Java緩存相關"的后續文章介紹
在這里,需要裝一個memcached服務器了。
我們就簡要的裝一個windows版本的,我發了一個再雲盤上,鏈接:http://pan.baidu.com/s/1dDMlov3,下載后,解壓,
兩種使用方法:
A、雙擊解壓后的"x86"(32位)或"x64"(64位)文件夾中的memcached.exe,跳出窗口即可。
B、在C:\Windows\System32\cmd.exe右擊"以管理員身份運行"-->在命令窗口進入E:\memcached\x86目錄中-->"memcached.exe -d install"-->之后去"本地服務"看看是不是已經有memcached server的服務了,如果已經有了,說明安裝成功-->之后啟動服務,兩種方式:
B1、手工在"本地服務部分"啟動
B2、命令窗口下"memcached.exe -p 11211 -m 32 -c 1024 -d start",該方法可以指定參數啟動memcached服務,-p表示端口,-m表示分配的內存,-c表示最大的並發連接數
當然,我們在實際使用中,會裝在Linux系統上,同時也需要指定一系列參數,例如分配的最大內存、最大並發數等等。
3.3、ssmm0-data
結構:
3.3.1、pom.xml

1 <?xml version="1.0" encoding="UTF-8"?> 2 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 3 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4 5 <modelVersion>4.0.0</modelVersion> 6 7 <!-- 指定父模塊 --> 8 <parent> 9 <groupId>com.xxx</groupId> 10 <artifactId>ssmm0</artifactId> 11 <version>1.0-SNAPSHOT</version> 12 </parent> 13 14 <groupId>com.xxx.ssmm0</groupId> 15 <artifactId>ssmm0-data</artifactId> 16 17 <name>ssmm0-data</name> 18 <packaging>jar</packaging><!-- 只是作為其他模塊使用的工具 --> 19 20 <!-- 引入實際依賴 --> 21 <dependencies> 22 <!-- mysql --> 23 <dependency> 24 <groupId>mysql</groupId> 25 <artifactId>mysql-connector-java</artifactId> 26 </dependency> 27 <!-- 數據源 --> 28 <dependency> 29 <groupId>org.apache.tomcat</groupId> 30 <artifactId>tomcat-jdbc</artifactId> 31 </dependency> 32 <!-- mybatis --> 33 <dependency> 34 <groupId>org.mybatis</groupId> 35 <artifactId>mybatis</artifactId> 36 </dependency> 37 <dependency> 38 <groupId>org.mybatis</groupId> 39 <artifactId>mybatis-spring</artifactId> 40 </dependency> 41 <!-- servlet --><!-- 為了會用cookie --> 42 <dependency> 43 <groupId>javax.servlet</groupId> 44 <artifactId>javax.servlet-api</artifactId> 45 </dependency> 46 <!-- bc-加密 --> 47 <dependency> 48 <groupId>org.bouncycastle</groupId> 49 <artifactId>bcprov-jdk15on</artifactId> 50 </dependency> 51 <!-- cc加密 --> 52 <dependency> 53 <groupId>commons-codec</groupId> 54 <artifactId>commons-codec</artifactId> 55 </dependency> 56 <!-- guava cache --> 57 <dependency> 58 <groupId>com.google.guava</groupId> 59 <artifactId>guava</artifactId> 60 <version>14.0.1</version> 61 </dependency> 62 <!-- 引入自定義cache模塊 --> 63 <dependency> 64 <groupId>com.xxx.ssmm0</groupId> 65 <artifactId>ssmm0-cache</artifactId> 66 <version>1.0-SNAPSHOT</version> 67 </dependency> 68 </dependencies> 69 </project>
說明:只引入了上邊的ssmm0-cache模塊。
3.3.2、Admin

1 package com.xxx.model.userManagement; 2 3 import java.io.Serializable; 4 5 import com.alibaba.fastjson.JSON; 6 7 /** 8 * 管理員 9 * 這里序列化,是為了向xmemcached中存儲,否則會報異常; 10 * 當然除了用序列化之外,還可以將admin對象轉化為json串,然后進行存儲 11 */ 12 public class Admin implements Serializable{ 13 14 private static final long serialVersionUID = 7149009421720474527L; 15 16 private int id; 17 private String username; 18 private String password; 19 20 public int getId() { 21 return id; 22 } 23 24 public void setId(int id) { 25 this.id = id; 26 } 27 28 public String getUsername() { 29 return username; 30 } 31 32 public void setUsername(String username) { 33 this.username = username; 34 } 35 36 public String getPassword() { 37 return password; 38 } 39 40 public void setPassword(String password) { 41 this.password = password; 42 } 43 44 //將json串轉為Admin 45 public static Admin parseJsonToAdmin(String jsonStr){ 46 try { 47 return JSON.parseObject(jsonStr, Admin.class); 48 } catch (Exception e) { 49 e.printStackTrace(); 50 return null; 51 } 52 } 53 54 //將當前實例轉化為json串 55 public String toJson(){ 56 return JSON.toJSONString(this); 57 } 58 }
說明:這里只添加了讓該類實現java.io.Serializable接口,添加了序列號
注意:在實際使用中,把對象存入緩存有兩種方式
- 序列化:使用上述的方式,或者使用其他序列化方式
- 將對象轉化為json串,在該類中,有兩個方法:一個將Admin-->Json,一個將Json-->Admin
這兩種方式都可以,只是Java默認的序列化效率低且生成的碼流大,但是使用方便,當然第二種方式使用也相當簡單。
關於各種序列化的方式以及優缺點對比,查看《netty權威指南(第2版)》,或者查看我的"Java高效使用"系列的后續文章
3.3.3、AdminMapper

1 /**************memcached**************/ 2 3 @Select("SELECT * FROM userinfo WHERE id = #{id}") 4 @Results(value = { 5 @Result(id = true, column = "id", property = "id"), 6 @Result(column = "username", property = "username"), 7 @Result(column = "password", property = "password") }) 8 public Admin selectById(@Param("id") int id);
說明:添加了上述按照ID查找用戶的方法。
3.3.4、AdminDao

1 /******************memcached********************/ 2 public Admin getUserById(int id){ 3 return adminMapper.selectById(id); 4 }
說明:添加了上述方法。
3.3.5、AdminService

1 /*********************memcached********************/ 2 public Admin findAdminById(int id) { 3 //從緩存中獲取數據 4 Admin admin = (Admin)MemcachedUtil.getCache(CachePrefix.USER_MANAGEMENT, String.valueOf(id)); 5 //若緩存中有,直接返回 6 if(admin != null){ 7 return admin; 8 } 9 //若緩存中沒有,從數據庫查詢 10 admin = adminDao.getUserById(id); 11 //若查詢出的數據不為null 12 if(admin!=null){ 13 //將數據存入緩存 14 MemcachedUtil.setCacheWithNoReply(CachePrefix.USER_MANAGEMENT, String.valueOf(id), admin); 15 } 16 //返回從數據庫查詢的admin(當然也可能數據庫中也沒有,就是null) 17 return admin; 18 }
說明:添加了上述方法。
注意:
上述方法是緩存使用中最常見的模式,即"從緩存獲取-->若沒有,從數據庫查詢,存入緩存-->返回數據",這就是guava cache的get(Object key)使用一個方法完成的原子操作。
3.4、ssmm0-userManagement
在該模塊中,只在一個類中添加了一個方法。
AdminController.java

1 /** 2 * 根據id查找Admin 3 */ 4 @ResponseBody 5 @RequestMapping("/findAdminById") 6 public Admin findAdminById(@RequestParam(value="id") int id){ 7 8 return adminService.findAdminById(id); 9 }
說明:下邊這個方法就是該模塊中唯一添加的一個方法。
4、測試
在瀏覽器輸入"localhost:8080/ssmm0-userManagement/admin/findAdminById?id=1",這樣就可以測試緩存,具體測試方式看《第七章 企業項目開發--本地緩存guava cache》
這里要說明的是兩點:
- 由於在根pom.xml文件中將dev改成了服務器啟動后默認使用的環境,所以在之后的測試中,不需要再修改環境了,但是實際上線時,一定要將prod改成默認環境才行
- 我想在上述URL中不輸入項目名ssmm0-userManagement也可以訪問相關資源,使用如下方式:run as-->run configurations..-->Context參數改為"/"即可
關於memcached的相關內容和Xmemcached的相關內容,請參看下邊鏈接或者我的"Java緩存相關"的后續文章: