分布式基于内存的缓存服务Memcached

一: memcached介绍

memcached是一个分布式的基于内存的缓存服务器,我们一般用memcached来减轻数据库的负载,提高程序的响应速度。

memcahched采用key-value存储数据,将对象序列化成二进制,以便在网络中进行传输。

二: Linux安装

memcached是基于libevent库的,所以要先安装libevent,然后在安装memcached,主要的几步操作如下:

tar -zxvf  memcached-1.4.25.tar.gz

cd

./configure

make

make install

安装好后,启动memcached服务 ./memcached -d -m 1024 -p 10000 -u root -P /tmp/memcached.pid

查看服务是否启动:ps -ef | grep 10000

分布式基于内存的缓存服务Memcached

三:memached的工作机制

3.1 memcached的内存存储

memcached是一个基于内存的高性能的缓存服务器,为了提高性能,memcached中保存的数据都存储在memcached内置的内存存储空间中。由于数据仅存在于内存中,因此重启服务器、重启操作系统等都会导致数据丢失。

memcached目前采用的是Slab Allocator来管理内存,它的原理非常简单,根据我们约定好的块大小,预先将分配的内存分成各个块,并将大小一样的块分成各个组。

当客户端有一个add或者set操作往缓存里存数据的时候,memcached根据添加的数据大小选择合适的某个slab。

分布式基于内存的缓存服务Memcached

1、memcached将内存空间分为一组slab

2、每个page里面包含一组chunk,chunk是真正存放数据的地方,同一个slab里面的chunk的大小是固定的。如果key不和chunk相 匹配,会有一定的内存浪费。但是不会存在内存碎片,我们可以通过在启动服务时-f来减低每个chunk之间的大小差,以便更合理的利用内存

3、相同大小chunk的slab被组织在一起,称为slab_class.

3.2 memcached的缓存过期

在往缓存里面添加数据的时候,可以指定一个expire,表示该数据的过期时间,单位是s,但是由于memecached不会释放已经分配的内存,当我们往缓存里面添加数据的时候,可能会内存空间不足,这个时候memecached就需要择优选择一块可用的内存来存储我们的数据。

memcached使用的是LRU的方式来释放内存,即最近最少使用的内存将优先被用来存新的数据。

memcached采用的是偷懒机制,当某个key过期后,并不会马上释放内存,而是等待下次有get请求到来时,如果发现该key已经过期了才会删除,而且其实memcached内部在很多情况下都会判断某个key是否失效,比如当我们重新set一个新的数据时,这个时候memcached需要重新申请一个item来存储咱们的数据,它会首先判断咱们请求的大小然后选择相应的slab放到里面,在这个地方,它在循环slab里面的item的时候实际上已经对每个item进行了判断是否过期,如果过期了,那么就直接使用这个item了。

3.3 memcahced如何实现分布式

一般的分布式系统,都是在服务器端实现的分布式,但是memcached却不是,由于各个memcached服务器之间不存在主备关系,也没有互相通信,所以memcached的分布式是在客户端实现的。

当我们缓存一个key,value的数据时,客户端首先根据一致性hash算法根据key来决定哪个服务器保存该数据。这个地方的原理很简单,将各个服务器节点的哈希值映射到一个圆上,然后将算出的key的哈希值也映射到圆上,然后顺时针查找第一台服务器,找到了就将该key对应的value存到这台服务器上,如果顺时针查找完还没有找到对应的服务器,则选择第一台服务器保存value值。

当我们下次有get请求来的时候,采用同样算法计算key对应的哈希值,就能找到存储该value的服务器了。

3.4 memcached的二阶段Hash

当我们往缓存里面写入一个数据的时候,memecached会首先根据key算出对应的hash值,找到对应的服务器编号,这是第一个hash,当我们确定好对应的服务器编号之后,memcached通过socket在memcached集群里面找到对应的memcached服务器,将我们的数据写入到服务器chunk中,这是第二个hash。同理,当我们get数据的时候,第一个hash采用相同的算法算出key对应的hash值,自然能找到对应的服务器获取缓存的数据

这个地方需要注意的是memecached采用的是一致性hash算法,而不是传统的余数hash。

一致性Hash算法通过一个叫做一致性Hash环的数据结构实现Key到缓存服务器的Hash映射:下面是一个草图

分布式基于内存的缓存服务Memcached

根据各个服务器节点名称的Hash值将缓存服务器节点放置在这个Hash环上,然后根据需要缓存的数据的Key值计算得到其Hash值,然后在Hash环上顺时针查找这个Key值的Hash值最近的服务器节点,完成Key到服务器的映射,然后保存value即可。

四:JAVA客户端简单操作实现

工具类:

package com.memcached.util;

import java.util.Date;
import java.util.Map;

import com.danga.MemCached.MemCachedClient;
import com.danga.MemCached.SockIOPool;

/**
 * memcached的基本操作类的封装
 *
 * @author tanjie
 *
 */
public class MemcachedUtil {

 private MemcachedUtil() {

 }

 private static MemCachedClient memCachedClient = new MemCachedClient();

 private static MemcachedUtil memcachedUtil = new MemcachedUtil();
 

 /**
  * 设置与缓存服务器的连接池
  */
 static {

  String[] servers = { "192.168.8.88:10000" };// Ip地址和端口号
  // 权重
  Integer[] weights = { 3 };

  // 获取socket连接池的实例对象
  SockIOPool pool = SockIOPool.getInstance();

  pool.setServers(servers);
 
  //设置连接池可用cache服务器的权重,和server数组的位置一一对应
  pool.setWeights(weights);

  // 设置初始连接数、最小、最大连接数以及最大处理时间
  pool.setInitConn(5);
  pool.setMinConn(5);
  pool.setMaxConn(250);
  pool.setMaxIdle(1000 * 60 * 60 * 6);

  // 设置主线程的睡眠时间
  pool.setMaintSleep(30);

  // 设置TCP的参数,连接超时等
  pool.setNagle(false);
  pool.setSocketTO(3000);
  pool.setSocketConnectTO(0);

  // 初始化连接池
  pool.initialize();

  // 设置序列化,因为java的基本类型不支持序列化,在确定cache的数据类型是string的情况下设为true,可以加快处理速度
 
  memCachedClient.setPrimitiveAsString(true);

 }
 
 public Object get(String key) {

  return memCachedClient.get(key);
 }

 public static MemcachedUtil getInstance() {
  return memcachedUtil;
 }

 //如果key不存在,就增加,如果存在,就覆盖
 public boolean add(String key, Object value) {
  return memCachedClient.add(key, value);
 }

 public boolean add(String key, Object value, Date expiryDate) {
  return memCachedClient.add(key, value, expiryDate);
 }

 //如果key不存在,报错
 public boolean replace(String key, Object value) {
  return memCachedClient.replace(key, value);
 }

 public boolean replace(String key, Object value, Date expiry) {

  return memCachedClient.replace(key, value, expiry);
 }

 public boolean delete(String key){
  return memCachedClient.delete(key);
 }
 
 public boolean delete(String key, Date expiry){
  return memCachedClient.delete(key, expiry);
 }
 
 public boolean delete (String key, Integer hashCode, Date expiry){
  return memCachedClient.delete(key, hashCode, expiry);
 }
 
 //如果key存在,则失败
 public  boolean set(String key,Object value){
    return  memCachedClient.set(key, value);
 }
 
 public boolean set(String key,Object value,Integer hashCode){
  return memCachedClient.set(key, value, hashCode);
 }
 
 //cache计数
 public boolean storeCounter(String key,long value){
  return  memCachedClient.storeCounter(key, value);
 }
 
 public long incr(String key,long value){
  return memCachedClient.incr(key, value);
 }
 
 //根据多个key获取对象
 public Map<String,Object> getObjects(String[] keys){
  return memCachedClient.getMulti(keys);
 }
 
 //清空对象
 public boolean flush(){
  return memCachedClient.flushAll();
 }
 
 //清空缓存对象,servers表示批量清空哪些机器的缓存
 public boolean flushBaseTime(String[] servers){
   return memCachedClient.flushAll(servers);
 }
 
 //获取服务器状态
 public Map<String,Map<String,String>> getStats(){
  return memCachedClient.stats();
 }
 
 //获取各个slab里item的数量
 public Map<String,Map<String,String>> getStatsItem(){
  return memCachedClient.statsItems();
 }
 
}

实体类:

package com.memcached.vo;

import java.io.Serializable;

public class User implements Serializable {

 private static final long serialVersionUID = 1L;

 private String userId;
 
 private String userName;
 
 private String adder;

 public String getUserId() {
  return userId;
 }

 public void setUserId(String userId) {
  this.userId = userId;
 }

 public String getUserName() {
  return userName;
 }

 public void setUserName(String userName) {
  this.userName = userName;
 }

 public String getAdder() {
  return adder;
 }

 public void setAdder(String adder) {
  this.adder = adder;
 }
 
}

分别运行后结果如下: 
 test():

从缓存中获取数据:memcached 
123 
张三 
重庆 

testDelete():

从缓存中获取数据:memcached
再次从缓存中获取数据:null

testReplace():

从缓存中获取数据:memcached 
第一次从缓存里面获取user:123 
再次从缓存中获取数据:replaceId 

testGetStats():

ip:10000
key:items:4:evicted_nonzero,value:0
key:items:1:evicted_unfetched,value:0
key:items:1:evicted_time,value:0
key:items:1:expired_unfetched,value:0
key:items:4:tailrepairs,value:0
key:items:1:number,value:1
key:items:4:crawler_items_checked,value:0
key:items:1:lrutail_reflocked,value:0
key:items:1:reclaimed,value:0
key:items:4:crawler_reclaimed,value:0
key:items:1:evicted_nonzero,value:0
key:items:4:outofmemory,value:0
key:items:4:age,value:0
key:items:4:evicted,value:0
key:items:1:evicted,value:0
key:items:4:lrutail_reflocked,value:0
key:items:1:tailrepairs,value:0
key:items:4:evicted_time,value:0
key:items:1:age,value:0
key:items:1:crawler_reclaimed,value:0
key:items:4:reclaimed,value:0
key:items:1:outofmemory,value:0
key:items:1:crawler_items_checked,value:0
key:items:4:expired_unfetched,value:0
key:items:4:evicted_unfetched,value:0
key:items:4:number,value:1

  其实memcached客户端的方法都大同小异,只是各个客户端所特有的特性不一样罢了,这就看大家在实际生产中的需求了。

五:memcached的缺点
    经过上面的分析,相信大家对memcached已经有了初步的认识,memcached很快,性能足以满足大多数应用,但是它也有很多需要改进的地方:
  1:memcache没有身份认证机制,所以任何机器都可以通过 telnet 等方式连接到缓存服务器。
  2:没有合理的日志功能,一旦服务器遇到错误而崩溃,将难以找到错误原因。
  3:内存中的数据不够安全,服务器停电或者其他因素将会造成数据的丢失。
  4.slab 处理对象时,会先对其归类,比如 100KB 的对象会放到 120KB 的空间内,会浪费较多的内存空间。

Memcached 的详细介绍:请点这里
Memcached 的下载地址:请点这里

相关推荐