文章结构:
1. Memcached 简介
2.Memcached 常用命令
3.Memcached 原理
4.Memcached 客户端操作Memcached
Memcached 简介
memcached 是以 LiveJournal 旗下 Danga Interactive 公司的 Brad Fitzpatric 为首开发的一款高性能的分布式内存缓存服务器。一般应用的数据都存放在关系数据库中,如果数据量很大,访问很频繁,就出现数据库的负担过重,影响速度慢等问题,所以通过缓存,可以减少数据库的访问次数,提高响应的速度和用户的体验度。使用缓存后,结构如下所示(图来源于网络):
Memcached 具有如下的特点:
1.协议简单,是基于简单的文本行的协议
2.基于 libevent 的事件处理,
3.内存存储方式
4.各个 Memcached 实例不相互通信
libevent 是个程序库,它将 Linux 的 epoll、BSD 类操作系统的 kqueue 等事件处理功能封装成统一的接口。即使对服务器的连接数增加,也能发挥 O(1)的性能。memcached 使用这个 libevent 库,因此能在 Linux、BSD、Solaris 等操作系统上发挥其高性能
Memcached 常用命令
要想学习一项技术或知识,动手是必不可少的,接下来看看 Memcached 的常用命令的一个用法,可以把常用的命令分为三类,存储命令,查找命令和统计命令;Memcached 的安装和配置不再赘述,下面直接看看命令的用法:
首先使用 telnet 连接 Memcached : telnet 127.0.0.1 11211
存储命令
Memcached 存储命令
命令 | 命令格式 | 参数 | 例子 |
---|---|---|---|
set 命令 将value存储在指定的key 中,如果key存在,则更新旧值 | set key flags exptime bytes [noreply] value | key : 键,用于查找对于的值 flags : 整型参数,客户端使用它来存储 关于键值对的额外信息 exptime : 过期时间,以秒为单位,0 表示永不过期 bytes : 存储的字节数 noreply : 可选,告知服务器不需要返回数据 value : key 对应的值 | |
add 命令 将value存储在指定的key 中,如果key存在, 不会更新旧值 | add ... [同set] | 同 set 命令 | key不存在: key 已存在: |
replace命令 替换旧值,如果key不存在 ,则失败 | replace ... [同set] | 同 set 命令 | key存在 key不存在 |
append 命令 向已存在的key后面追加value | append ...[同set] | 同 set 命令 | 注意:一个中文三个字节 |
prepend命令 在已存在的key前面追加value | prepend...[同set] | 同 set 命令 | ![]() |
CAS 命令 线程安全的设置key 的值 | cas key flags exptime bytes unique_cas_token [noreply] | 比其他命令多了个 unique_cas_token 参数, unique_cas_token : 通过 gets 命令 获取的key-value的版本号,为64位 其他参数同 set 命令 | ![]() |
查找命令
Memcached 查找命令
命令 | 命令格式 | 参数 | 例子 |
---|---|---|---|
get 命令 查找对应key的value, 如果key不存在,返回空 | get key get key1 key2 key3... | key : 要查找value对应的key | 查找单个key 查找多个key |
gets 命令 获取带有版本号的value, 如果key不存在,返回空 | gets key get key1 key2 key3... | key : 要查找value对应的key | 获取单个key, 417 为版本号 获取多个key |
delete 命令 删除对应的key | delete key [norepyl] | key : 要删除的key norepyl : 可选,该参数告知服务 器不需要返回数据 | ![]() |
incr 与 decr 命令 用于对已存在的 key 的数字值 进行自增或自减操作 | incr key increment_value decr key decrement_value | key : 要自增值对应的key increment_value:自增的值 decrement_value:自减的值 | ![]() |
统计命令
Memcached 统计命令
命令 | 命令格式 | 参数 | 例子 |
---|---|---|---|
stats 命令 用于返回统计信息 例如 PID(进程号)、版本号、连接数等 | stats | 无 | ![]() |
stats items 命令 用于显示各个 slab 中 item 的数目、 和存储时长(最后一次访问距离现在的秒数) | stats items | 无 | ![]() |
stats slabs 命令 用于显示各个slab的信息, 包括chunk的大小、数目、使用情况 | stats slabs | 无 | ![]() |
stats sizes 命令 用于显示所有item的大小和个数
| stats sizes | 无 | 第一列为 item 大小,第二列为 item 个数 |
flush_all 命令 清理缓存中的所有key-value | flush_all [time] [noreply] | time:在指定的时间后执行清理操作 norepyl : 可选,该参数告知服务 器不需要返回数据 | 之前存储的key不再存在 |
获取 Memcached 中的列表数据
1. 使用 stats items 获取所有的item
2. 使用 stats cachedump id 0 获取, 0表示全部列出
Memcached 原理
memcached 的内存存储机制
memcached 采用了名为 Slab Allocator 的机制来分配、管理内存,可以有效的减少内存的碎片问题。
Slab Allocator的原理:按照预先规定的大小,将分配的内存分割成各种尺寸的块(chunk),并把尺寸相同的块分成组(chunk 的集合),而且,slab allocator 还有重复使用已分配的内存的目的。也就是说,分配到的内存不会释放,而是重复利用;
如下图所示(图片来源于网络)
Slab Allocator 相关概念:
Page : 分配给 Slab 的内存空间,默认为 1M,分配给 Slab 之后,根据 Slab 的大小切分为 chunk
chunk : 用于缓存记录的内存空间
Slab Class : 特定大小的 chunk 组
可以通过 stats slabs 命令来查看:
可以看到,每个chunk的大小为96byte,共有 10922 个chunk,共有1个page...............
Slab Allocator的缺点:无法有效的利用内存,因为内存是按照特定长度进行分配的,如果把一个 100 byte的数据存放到 128byte的 chunk中,则会有 28 byte的空间浪费。如果预先知道客户端发送的数据的公用大小,或者仅缓存大小相同的数据的情况下,只要使用适合数据大小的组的列表,就可以减少浪费,如可以调节 slab class的大小的差别
memcached 的删除
memcached 不会释放已分配的内存,如果缓存的记录失效的话,客户端是不能看到该值,但是数据还在,存储空间可以重复使用;在memcached内部不会去监视记录是否过期,只是在get的时候,查看记录的时间戳是否过期,好处就是,不会在过期监视上面浪费时间。
当向缓存中添加记录的时候,会优先那些已经过期记录的空间,但是如果添加时,空间还是不够的话,则会根据LRU原则删除相关的记录,把这部分空间分配给新记录。
LRU:Least Recently Used,即最近最少使用原则,也就是最长时间未被使用。
memcached 分布式
memcached 虽然称为“分布式”缓存服务器,但服务器端并没有“分布式”功能,完全是由客户端进行控制;
客户端的分布式算法主要有两种
1. 求余,即根据 key的hash值,对memcached 服务器节点数进行求余即可得到该key应该存放在哪个节点上,缺点就是,如果服务器增加或减少,则缓存的命中率会大大降低。
2. 一致性哈希算法,后面在详细介绍。
Memcached 客户端操作Memcached
使用 XMemcached 客户端来操作 memcached ,关于 XMemcached 的相关信息可以参考官网文档:XMemcached
首先导入相关jar包:
<dependency> <groupId>com.googlecode.xmemcached</groupId> <artifactId>xmemcached</artifactId> <version>2.0.0</version> </dependency>
之后创建客户端实例对象:
MemcachedClient memcache;
@Before
public void init() {
try {
memcache = new XMemcachedClientBuilder(AddrUtil.getAddresses("127.0.0.1:11211")).build();
} catch (IOException e) {
System.out.println("初始化失败...");
e.printStackTrace();
}
}
@After
public void close() throws IOException {
memcache.shutdown();
}
set 命令相关方法:
@Test
public void testSet() throws InterruptedException, MemcachedException, TimeoutException {
// key, 过期时间:默认为30天, 对应的值
memcache.set("bj", 0, "北京");
System.out.println("bj = " + memcache.get("bj"));
// key, 过期时间, 对应的值, 操作时超时时间:默认为5s, 超过则抛出 TimeoutException
memcache.set("cd",0, "成都", 2);
System.out.println("cd = " + memcache.get("cd"));
}
bj = 北京
cd = 成都
add 命令相关方法
@Test
public void testAdd() throws InterruptedException, MemcachedException, TimeoutException {
// 如果 key 已经存在,则不会更新对应的值
memcache.add("bj", 0, "北京2");
System.out.println("bj = " + memcache.get("bj"));
memcache.add("gy", 0, "贵阳");
System.out.println("gy = " + memcache.get("gy"));
}
bj = 北京
gy = 贵阳
replace 命令相关方法
@Test
public void testReplace() throws InterruptedException, MemcachedException, TimeoutException {
memcache.replace("cd", 0, "成都2");
System.out.println("cd = " + memcache.get("cd"));
memcache.replace("no", 0, "noKey");
System.out.println("no = " + memcache.get("no"));
}
cd = 成都2
no = null
append 命令相关方法:
@Test
public void testAppend() throws InterruptedException, MemcachedException, TimeoutException {
memcache.set("sc", 0, "四川");
System.out.println("sc = " + memcache.get("sc"));
memcache.append("sc", "成都");
System.out.println("sc = " + memcache.get("sc"));
memcache.append("sc", "高新");
System.out.println("sc = " + memcache.get("sc"));
// 不存在,则为null
memcache.append("a", "A");
System.out.println("a = " + memcache.get("a"));
}
sc = 四川
sc = 四川成都
sc = 四川成都高新
a = null
prepend 命令相关方法
@Test
public void testPrepend() throws InterruptedException, MemcachedException, TimeoutException {
memcache.set("gy", 0, "贵阳");
System.out.println("gy = " + memcache.get("gy"));
memcache.prepend("gy", "中国");
System.out.println("gy = " + memcache.get("gy"));
}
gy = 贵阳
gy = 中国贵阳
CAS 命令相关方法
@Test
public void testCas() throws InterruptedException, MemcachedException, TimeoutException {
memcache.set("sh", 0, "上海");
long cas = memcache.gets("sh").getCas();
memcache.cas("sh", 0, "上海摊", cas);
System.out.println("sh = " + memcache.get("sh") + ", cas = " + cas);
memcache.cas("sh", 0, new CASOperation<String>() {
@Override
public int getMaxTries() {
return 1; // 重试次数
}
@Override
public String getNewValue(long currentCAS, String currentValue) {
return "上海摊2";
}
});
System.out.println("sh = " + memcache.get("sh"));
}
sh = 上海摊, cas = 437
sh = 上海摊2
get 命令相关方法
@Test
public void testGet() throws InterruptedException, MemcachedException, TimeoutException {
String val = memcache.get("sh");
System.out.println(val);
List<String> keys = Arrays.asList("sh", "bj", "sc");
Map<String, String> valsMap = memcache.get(keys);
for (Map.Entry<String, String> entry : valsMap.entrySet()) {
System.out.println("key = " + entry.getKey() + ", value = " + entry.getValue());
}
GetsResponse getsResponse = memcache.gets("bj");
long cas = getsResponse.getCas();
String val2 = (String) getsResponse.getValue();
System.out.println("cas = " + cas + ", Val = " + val2);
}
上海摊2
key = bj, value = 北京
key = sc, value = 四川成都高新
key = sh, value = 上海摊2
cas = 428, Val = 北京
delete命令相关方法
@Test
public void testDelete() throws InterruptedException, MemcachedException, TimeoutException {
memcache.set("t", 0, "tttt");
String t = memcache.get("t");
System.out.println("t = " + t);
memcache.delete("t");
t = memcache.get("t");
System.out.println(t);
}
t = tttt
null
incr, decr 命令相关方法
@Test
public void testIncrDecr() throws InterruptedException, MemcachedException, TimeoutException {
memcache.set("i", 0, "100");
long ret = memcache.incr("i", 5);
System.out.println(ret);
ret = memcache.decr("i", 10);
System.out.println(ret);
}
105
95
XMemcached 的分布式
XMemcached支持客户端的分布策略,默认分布的策略是按照key的哈希值模以连接数得到的余数,对应的连接就是将要存储的节点。如果使用默认的分布策略,不需要做任何配置或者编程。
如要是使用一致性哈希策略,则需要进行以下设置
MemcachedClientBuilder builder = new XMemcachedClientBuilder(...)
builder.setSessionLocator(new KetamaMemcachedSessionLocator());
MemcachedClient client=builder.build();
接下来看下两种策略的源码实现:
默认求余策略:
public class XMemcachedClientBuilder implements MemcachedClientBuilder {
private MemcachedSessionLocator sessionLocator = new ArrayMemcachedSessionLocator();
.........................
}
public class ArrayMemcachedSessionLocator extends AbstractMemcachedSessionLocator {
public ArrayMemcachedSessionLocator() {
this.hashAlgorighm = HashAlgorithm.NATIVE_HASH;
}
public final long getHash(int size, String key) {
long hash = this.hashAlgorighm.hash(key);
return hash % size;
}
.........................
}
一致性哈希策略:
builder.setSessionLocator(new KetamaMemcachedSessionLocator());
public class KetamaMemcachedSessionLocator extends AbstractMemcachedSessionLocator {
private transient volatile TreeMap<Long, List<Session>> ketamaSessions = new TreeMap<Long, List<Session>>();
public KetamaMemcachedSessionLocator() {
this.hashAlg = HashAlgorithm.KETAMA_HASH; // 一致性哈希
this.cwNginxUpstreamConsistent = false;
}
.........................
private final void buildMap(Collection<Session> list, HashAlgorithm alg) {
TreeMap<Long, List<Session>> sessionMap = new TreeMap<Long, List<Session>>();
for (Session session : list) {
String sockStr = null;
.........................
int numReps = NUM_REPS;
if (alg == HashAlgorithm.KETAMA_HASH) {
for (int i = 0; i < numReps / 4; i++) {
byte[] digest = HashAlgorithm.computeMd5(sockStr + "-" + i);
for (int h = 0; h < 4; h++) {
long k = (long) (digest[3 + h * 4] & 0xFF) << 24
| (long) (digest[2 + h * 4] & 0xFF) << 16
| (long) (digest[1 + h * 4] & 0xFF) << 8
| digest[h * 4] & 0xFF;
this.getSessionList(sessionMap, k).add(session);
}
}
} else {
for (int i = 0; i < numReps; i++) {
long key = alg.hash(sockStr + "-" + i);
this.getSessionList(sessionMap, key).add(session);
}
}
}
this.ketamaSessions = sessionMap;
this.maxTries = list.size();
}
......................................
}
上述就是memcached的简单操作,memcached 虽然简单,但是动手和看还是不一样的,动手之后,理解会更加深刻,印象会更深。