企业项目开发--分布式缓存memcached(2)

叁叁肆2018-12-18 10:52

此文已由作者赵计刚授权网易云社区发布。

欢迎访问网易云社区,了解更多网易技术产品运营经验。


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系统上,同时也需要指定一系列参数,例如分配的最大内存、最大并发数等等。


免费领取验证码、内容安全、短信发送、直播点播体验包及云服务器等套餐

更多网易技术、产品、运营经验分享请点击