【转】Memcached Java Client API详解

转自 http://blog.csdn.net/qqiabc521/article/details/6438429 ,蓝色字是我加的加重或是注解

Memcached Java Client API详解

针对Memcached官方网站提供的java_memcached-release_2.0.1版本进行阅读分析,Memcached Java客户端lib库主要提供的调用类是SockIOPool和MemCachedClient?,关键类及方法整理说明如下。

SockIOPool

这个类用来创建管理客户端和服务器通讯连接池,客户端主要的工作包括数据通讯、服务器定位、hash码生成等都是由这个类完成的。

  • public static SockIOPool getInstance()
    • 获得连接池的单态方法。这个方法有一个重载方法getInstance( String poolName ),每个poolName只构造一个SockIOPool实例。缺省构造的poolName是default。
    • 如果在客户端配置多个memcached服务,一定要显式声明poolName。
  • public void setServers( String[] servers )
    • 设置连接池可用的cache服务器列表,server的构成形式是IP:PORT(如:127.0.0.1:11211)
  • public void setWeights( Integer[] weights )
    • 设置连接池可用cache服务器的权重,和server数组的位置一一对应
    • 其实现方法是通过根据每个权重在连接池的bucket中放置同样数目的server(如下代码所示),因此所有权重的最大公约数应该是1,不然会引起bucket资源的浪费。
for ( int i = 0; i < servers.length; i+/+ ) { if ( this.weights /!= null &&this.weights.length > i ) { for ( int k = 0; k < this.weights[i].intValue(); k+/+ ) { this.buckets.add( servers[i] ); if ( log.isDebugEnabled() ) log.debug("++++ added " + servers[i] + " to server bucket" ); } }

这个文档上举例:

Lets say you have 3 servers.  Server 1 and server 2 have 3GB of space

and server 3 has 2GB of space for cache.  Here is how I would set up

my client.

如果有三个server,分别有3GB,3GB,2GB的缓存空间,那么权重对应应该是3:3:2

 

  • public void setInitConn( int initConn )
    • 设置开始时每个cache服务器的可用连接数
  • public void setMinConn( int minConn )
    • 设置每个服务器最少可用连接数
  • public void setMaxConn( int maxConn )
    • 设置每个服务器最大可用连接数
  • public void setMaxIdle( long maxIdle )
    • 设置可用连接池的最长等待时间
  • public void setMaintSleep( long maintSleep )
    • 设置连接池维护线程的睡眠时间
    • 设置为0,维护线程不启动
    • 维护线程主要通过log输出socket的运行状况,监测连接数目及空闲等待时间等参数以控制连接创建和关闭。
  • public void setNagle( boolean nagle )
    • 设置是否使用Nagle算法,因为我们的通讯数据量通常都比较大(相对TCP控制数据)而且要求响应及时,因此该值需要设置为false(默认是true)

Nagle算法是用于对缓冲区内的一定数量的消息进行自动连接。该处理过程(称为Nagling),通过减少必须发送的封包的数量,提高了网络应用程序系统的效率。这个该不该禁用还需的具体分析,

参见http://www.blogjava.net/killme2008/archive/2011/10/25/353441.html 这个也是与memcached相关的经验。

  • ublic void setSocketTO( int socketTO )
    • 设置socket的读取等待超时值
  • public void setSocketConnectTO( int socketConnectTO )
    • 设置socket的连接等待超时值
  • public void setAliveCheck( boolean aliveCheck )
    • 设置连接心跳监测开关。
    • 设为true则每次通信都要进行连接是否有效的监测,造成通信次数倍增,加大网络负载,因此该参数应该在对HA要求比较高的场合设为TRUE,默认状态是false。
  • public void setFailback( boolean failback )
    • 设置连接失败恢复开关
    • 设置为TRUE,当宕机的服务器启动或中断的网络连接后,这个socket连接还可继续使用,否则将不再使用,默认状态是true,建议保持默认。
  • public void setFailover( boolean failover )
    • 设置容错开关
    • 设置为TRUE,当当前socket不可用时,程序会自动查找可用连接并返回,否则返回NULL,默认状态是true,建议保持默认。

      这里我补充一下,当这个设置为true时,写操作会对此server池中所有的server都进行,例如set(‘1’,’abc’);则每个server都存储了1=abc,当设置为false时,则把所有server作为一个整体,那么上一个set操作可能就放置在server1,而下一个set操作可能就放置在server2.

  • public void setHashingAlg( int alg )
    • 设置hash算法
      • alg=0 使用String.hashCode()获得hash code,该方法依赖JDK,可能和其他客户端不兼容,建议不使用
      • alg=1 使用original 兼容hash算法,兼容其他客户端
      • alg=2 使用CRC32兼容hash算法,兼容其他客户端,性能优于original算法
      • alg=3 使用MD5 hash算法
    • 采用前三种hash算法的时候,查找cache服务器使用余数方法。采用最后一种hash算法查找cache服务时使用consistent方法。
  • public void initialize()
    • 设置完pool参数后最后调用该方法,启动pool。

MemCachedClient?

  • public void setCompressEnable( boolean compressEnable )
    • 设定是否压缩放入cache中的数据
    • 默认值是ture
    • 如果设定该值为true,需要设定CompressThreshold?
  • public void setCompressThreshold( long compressThreshold )
    • 设定需要压缩的cache数据的阈值
    • 默认值是30k
  • public void setPrimitiveAsString( boolean primitiveAsString )
    • 设置cache数据的原始类型是String
    • 默认值是false
    • 只有在确定cache的数据类型是string的情况下才设为true,这样可以加快处理速度。
  • public void setDefaultEncoding( String defaultEncoding )
    • 当primitiveAsString为true时使用的编码转化格式
    • 默认值是utf-8
    • 如果确认主要写入数据是中文等非ASCII编码字符,建议采用GBK等更短的编码格式
  • cache数据写入操作方法
    • set方法
      • 将数据保存到cache服务器,如果保存成功则返回true
      • 如果cache服务器存在同样的key,则替换之
      • set有5个重载方法,key和value是必须的参数,还有过期时间,hash码,value是否字符串三个可选参数
    • add方法
      • 将数据添加到cache服务器,如果保存成功则返回true
      • 如果cache服务器存在同样key,则返回false
      • add有4个重载方法,key和value是必须的参数,还有过期时间,hash码两个可选参数
    • replace方法
      • 将数据替换cache服务器中相同的key,如果保存成功则返回true
      • 如果cache服务器不存在同样key,则返回false
      • replace有4个重载方法,key和value是必须的参数,还有过期时间,hash码两个可选参数
    • 建议分析key的规律,如果呈现某种规律有序,则自己构造hash码,提高存储效率
  • cache数据读取操作方法
    • 使用get方法从cache服务器获取一个数据
      • 如果写入时是压缩的或序列化的,则get的返回会自动解压缩及反序列化
      • get方法有3个重载方法,key是必须的参数,hash码和value是否字符串是可选参数
    • 使用getMulti方法从cache服务器获取一组数据
      • get方法的数组实现,输入参数keys是一个key数组
      • 返回是一个map
  • 通过cache使用计数器
    • 使用storeCounter方法初始化一个计数器
    • 使用incr方法对计数器增量操作
    • 使用decr对计数器减量操作

Memcached Client API 优化(草)

实现memcached的遍历操作

有些应用情况下,需要遍历memcached服务器中所有被cache的数据,目前memcached client API不支持遍历操作,需要进行扩展。

实现get时刷新数据过期时间(应用于session,可能需要修改服务器端程序)

当memcached被用作session服务器的时候,需要支持session的access方法,根据最近访问时间刷新过期时间,目前memcached也不支持该操作,需要进行扩展。

Continue reading 【转】Memcached Java Client API详解

Memcached-Java-Client使用

官网是:https://github.com/gwhalin/Memcached-Java-Client/

Memcached-Java-Client是memcahed的java客户端库,依据其源码文档doc/howto.txt 介绍,是meetup网站目前使用的客户端。

可参见 http://developer.51cto.com/art/201106/271749.htm

从各方面参考,这个是目前最靠谱的java client了.参见

http://blog.ureshika.com/archives/752.html

http://www.infoq.com/cn/articles/memcached-java

http://lveyo.iteye.com/blog/240146

 http://www.iteye.com/topic/154767

2.5版之后较之前有较大性能提升,所以网上许多怀疑其性能问题的一般都是在2011年之前的文章。

它的doc很简单,在源码目录doc/howto.txt 中,这位仁兄整理了较新的中文版:

http://blog.ureshika.com/archives/754.html

其实也可以理解,协议就那几个。

其源码中MemcachedBench是压力测试的例子。

如下是我测试的结果(客户端和服务器不是一台机子,服务端10m缓存空间,其他默认):

100000 sets: 73047ms
100000 gets: 49594ms
100000 getMulti: 30485ms
100000 deletes: 43281ms

10000 sets: 7579ms
10000 gets: 7328ms
10000 getMulti: 4015ms
10000 deletes: 4172ms

1000 sets: 906ms
1000 gets: 781ms
1000 getMulti: 375ms
1000 deletes: 437ms

100 sets: 109ms
100 gets: 109ms
100 getMulti: 47ms
100 deletes: 47ms

这说明不了什么,仅供参考。

Continue reading Memcached-Java-Client使用

【转】JAVA客户端调用memcached比较

style="width: 902px; height: 88px" />

转自http://blog.chinaunix.net/space.php?uid=20787846&do=blog&id=1841943 ,

有趣的是这篇文章测试结果显示上篇转载的ali client性能有问题,那篇文章还说是优化了whalin,结果反而不如它,不知怎么回事!

蓝色字是博主加的加重标记或是注解。

 

1.memcached client for java客户端API:memcached client for java 

网址:http://www.whalin.com/memcached 

最新版本:java_memcached-release_2.0.1 

操作示例: 

Java代码
  1. import com.danga.MemCached.*;  
  2. import org.apache.log4j.*;  
  3. public class TestMemcached {  
  4.     public static void main(String[] args) {  
  5.         /*初始化SockIOPool,管理memcached的连接池*/ 
  6.         String[] servers = { "192.168.1.20:12111" };  
  7.         SockIOPool pool = SockIOPool.getInstance();  
  8.         pool.setServers(servers);  
  9.         pool.setFailover(true);  
  10.         pool.setInitConn(10);  
  11.         pool.setMinConn(5);  
  12.         pool.setMaxConn(250);  
  13.         pool.setMaintSleep(30);  
  14.         pool.setNagle(false);  
  15.         pool.setSocketTO(3000);  
  16.         pool.setAliveCheck(true);  
  17.         pool.initialize();  
  18.         /*建立MemcachedClient实例*/ 
  19.         MemCachedClient memCachedClient = new MemCachedClient();  
  20.         for (int i = 0; i < 10; i++) {  
  21.             /*将对象加入到memcached缓存*/ 
  22.             boolean success = memCachedClient.set("" + i, "Hello!");  
  23.             /*从memcached缓存中按key值取对象*/ 
  24.             String result = (String) memCachedClient.get("" + i);  
  25.             System.out.println(String.format("set( %d ): %s", i, success));  
  26.             System.out.println(String.format("get( %d ): %s", i, result));  
  27.         }  
  28.     }  

2.spymemcached客户端API:spymemcached client 

网址:http://code.google.com/p/spymemcached/ 

最新版本:memcached-2.1.jar 

操作示例: 

用spymemcached将对象存入缓存 

Java代码
  1. import java.net.InetSocketAddress;  
  2. import java.util.concurrent.Future;  
  3.  
  4. import net.spy.memcached.MemcachedClient;  
  5.  
  6. public class MClient {  
  7.       
  8.     public static void main(String[] args){  
  9.         try{  
  10.             /*建立MemcachedClient 实例,并指定memcached服务的IP地址和端口号*/ 
  11.             MemcachedClient mc = new MemcachedClient(new InetSocketAddress("192.168.1.20", 12111));  
  12.             Future<Boolean> b = null;  
  13.             /*将key值,过期时间(秒)和要缓存的对象set到memcached中*/ 
  14.             b = mc.set("neea:testDaF:ksIdno", 900, "someObject");  
  15.             if(b.get().booleanValue()==true){  
  16.                 mc.shutdown();  
  17.             }  
  18.         }  
  19.         catch(Exception ex){  
  20.             ex.printStackTrace();  
  21.         }  
  22.     }  

用spymemcached从缓存中取得对象 

Java代码
  1. import java.net.InetSocketAddress;  
  2. import java.util.concurrent.Future;  
  3.  
  4. import net.spy.memcached.MemcachedClient;  
  5.  
  6. public class MClient {  
  7.       
  8.     public static void main(String[] args){  
  9.         try{  
  10.             /*建立MemcachedClient 实例,并指定memcached服务的IP地址和端口号*/ 
  11.             MemcachedClient mc = new MemcachedClient(new InetSocketAddress("192.168.1.20", 12111));  
  12.             /*按照key值从memcached中查找缓存,不存在则返回null */ 
  13. Object b = mc.get("neea:testDaF:ksIdno ");  
  14.             mc.shutdown();  
  15.         }  
  16.         catch(Exception ex){  
  17.             ex.printStackTrace();  
  18.         }  
  19.     }  
 
3.alisoft-xplatform-asf-cache-2.4
这是国产的memcached client.我花时间测试过,配置简单,使用也简单,有很详细的中文文档和英文文档.

网址:http://code.google.com/p/memcache-client-forjava/

最新版本: alisoft-xplatform-asf-cache-2.5.2.jar

使用例子
memcached.xml
=========================
<?xml version="1.0" encoding="UTF-8"?>

<memcached>

    

    <client name="mclient0" compressEnable="true" defaultEncoding="UTF-8" socketpool="pool0">

        <errorHandler>com.moit.xplatform.asf.cache.memcached.MemcachedErrorHandler</errorHandler>

    </client>

    

    <client name="mclient1" compressEnable="true" defaultEncoding="UTF-8" socketpool="pool1">

        <errorHandler>com.moit.xplatform.asf.cache.memcached.MemcachedErrorHandler</errorHandler>

    </client>

    

    <client name="mclient2" compressEnable="true" defaultEncoding="UTF-8" socketpool="pool2">

        <errorHandler>com.moit.xplatform.asf.cache.memcached.MemcachedErrorHandler</errorHandler>

    </client>

    

    <socketpool name="pool0" failover="true" initConn="5" minConn="5" maxConn="250" maintSleep="0"

        nagle="false" socketTO="3000" aliveCheck="true">

        <servers>192.168.3.251:11211,192.168.3.251:11211</servers>

    </socketpool>

    

    <socketpool name="pool1" failover="true" initConn="5" minConn="5" maxConn="250" maintSleep="0"

        nagle="false" socketTO="3000" aliveCheck="true">

        <servers>192.168.3.251:11211,192.168.3.251:11211</servers>

    </socketpool>

    

    <socketpool name="pool2" failover="true" initConn="5" minConn="5" maxConn="250" maintSleep="0"

        nagle="false" socketTO="3000" aliveCheck="true">

        <servers>192.168.3.251:11211,192.168.3.251:11211</servers>

    </socketpool>

    <cluster name="cluster1">

        <memCachedClients>mclient1,mclient2</memCachedClients>

    </cluster>

</memcached>
 
  //用containsKey方法判断cache服务器按指定的key值是否存在。

  System.out.println("是否包含了key的数据="+cache.containsKey("key"));

  if(cache.containsKey("key"))

  {

    System.out.println("包含了key的数据");

    System.out.println("从cache服务器获得key值");

  }

  else

  {

    System.out.println("没有包含了key的数据");

    System.out.println("cache服务器,没有数据,则去取数据库数据!");

  }

  

  例子:

  static ICacheManager<IMemcachedCache> manager;

 
  /**

   * 测试MemCached

   * @return

   */

  public String memcache()

  { 
    manager = CacheUtil.getCacheManager(IMemcachedCache.class,

    MemcachedCacheManager.class.getName());

    manager.start();

    try

    {

      IMemcachedCache cache = manager.getCache("mclient0");

      //根据key得到缓存数据

      String a =(String)cache.get("key");

      //用containsKey方法判断cache服务器按指定的key值是否存在。

      System.out.println("是否包含了key的数据="+cache.containsKey("key"));

      if(cache.containsKey("key"))

      {

        System.out.println("包含了key的数据");

        System.out.println("从cache服务器获得key值");

      }

      else

      {

        System.out.println("没有包含了key的数据");

        System.out.println("cache服务器,没有数据,则去取数据库数据!");

      }

      //根据key删除服务器上的对应的缓存数据

      cache.remove("key");

      //根据key保存数据到服务器上

      cache.put("key", "你好!"); 
      //设置带有过期时间的例子

      //过30分钟

      Calendar calendar = Calendar.getInstance();//当前日期

        calendar.setTime(new Date());

        calendar.add(Calendar.MINUTE, 30);//

        cache.remove("keytime");

        cache.put("keytime", "30分钟后过期",calendar.getTime());

        System.out.println("30分钟后过期=keytime="+cache.get("keytime"));

        System.out.println("cache服务器getTime="+calendar.getTime());  
  
  }finally{ manager.stop();}

     //jsp 使用请参考test.jsp文件

     return "testmempage";

  }

总结:三种API比较 

memcached client for java:较早推出的memcached JAVA客户端API,应用广泛,运行比较稳定。 [注:这个是meetup正在使用的]

spymemcached:A simple, asynchronous, single-threaded memcached client written in java. 支持异步,单线程的memcached客户端,用到了java1.5版本的concurrent和nio,存取速度会高于前者,但是稳定性不好,测试中常报timeOut等相关异常。

alisoft-xplatform-asf-cache-2.4 我开始采用,后来发现性能很差,尤其是调用频繁时特别容易出问题.我后来还是采用第一个.现在还没发现什么问题.

由于memcached client for java发布了新版本,性能上有所提高,并且运行稳定,所以建议使用memcached client for java。

Continue reading 【转】JAVA客户端调用memcached比较

【转】Memcached深度分析

此文写得好不得不收藏, 转自 http://kb.cnblogs.com/page/42776/ ,蓝色字是我加的评注或是对原文的加重标记。

Memcached是danga.com(运营LiveJournal的技术团队)开发的一套分布式内存对象缓存系统,用于在动态系统中减少数据库负载, 提升性能。关于这个东西,相信很多人都用过,本文意在通过对memcached的实现及代码分析,获得对这个出色的开源软件更深入的了解,并可以根据我们 的需要对其进行更进一步的优化。末了将通过对BSM_Memcache扩展的分析,加深对memcached的使用方式理解。


本文的部分内容可能需要比较好的数学基础作为辅助。



◎Memcached是什么



在 阐述这个问题之前,我们首先要清楚它“不是什么”。很多人把它当作和SharedMemory那种形式的存储载体来使用,虽然memcached使用了同 样的“Key=>Value”方式组织数据,但是它和共享内存、APC等本地缓存有非常大的区别。Memcached是分布式的,也就是说它不是本地的。它基于网络连接[PS:这个说法容易产生误解,我澄清一下:memcache主要是提供服务端和api,需要客户端配合运行,那么一客一服可以说是分布式了吧,客户服同在一机器也算是分布式,其实分布是最初的意思就是客户机服务器,然后才是扩展:我爱钻牛角尖!](当然它也可以使用localhost)方式完成服务,本身它是一个独立于应用的程序或守护进程(Daemon方式)。



Memcached 使用libevent库实现网络连接服务,理论上可以处理无限多的连接,但是它和Apache不同,它更多的时候是面向稳定的持续连接的,所以它实际的并发能力是有限制的。在保守情况下memcached的最大同时连接数为200,这和Linux线程能力有关系,这个数值是可以调整的。关于 libevent可以参考相关文档。 Memcached内存使用方式也和APC不同。APC是基于共享内存和MMAP的,memcachd有自己的内存分配算法和管理方式,它和共享内存没有 关系,也没有共享内存的限制,通常情况下,每个memcached进程可以管理2GB的内存空间,如果需要更多的空间,可以增加进程数。 



◎Memcached适合什么场合



在很多时候,memcached都被滥用了,这当然少不了对它的抱怨。我经常在论坛上看见有人发贴,类似于“如何提高效率”,回复是“用memcached”,至于怎么用,用在哪里,用来干什么一句没有。memcached不是万能的,它也不是适用在所有场合。



Memcached 是“分布式”的内存对象缓存系统,那么就是说,那些不需要“分布”的,不需要共享的,或者干脆规模小到只有一台服务器的应用,memcached不会带来 任何好处,相反还会拖慢系统效率,因为网络连接同样需要资源,即使是UNIX本地连接也一样。 在我之前的测试数据中显示,memcached本地读写速度要比直接PHP内存数组慢几十倍,而APC、共享内存方式都和直接数组差不多。可见,如果只是 本地级缓存,使用memcached是非常不划算的。



Memcached在很多时候都是作为数据库前端cache使用的。因为它比数据库 少了很多SQL解析、磁盘操作等开销,而且它是使用内存来管理数据的,所以它可以提供比直接读取数据库更好的性能,在大型系统中,访问同样的数据是很频繁的,memcached可以大大降低数据库压力,使系统执行效率提升。另外,memcached也经常作为服务器之间数据共享的存储媒介,例如在SSO系统中保存系统单点登陆状态的数据就可以保存在memcached中,被多个应用共享。



需要注意的是,memcached使用内存管理数 据,所以它是易失的,当服务器重启,或者memcached进程中止,数据便会丢失,所以memcached不能用来持久保存数据。很多人的错误理 解,memcached的性能非常好,好到了内存和硬盘的对比程度,其实memcached使用内存并不会得到成百上千的读写速度提高,它的实际瓶颈在于网络连接,它和使用磁盘的数据库系统相比,好处在于它本身非常“轻”,因为没有过多的开销和直接的读写方式,它可以轻松应付非常大的数据交换量,所以经常 会出现两条千兆网络带宽都满负荷了,memcached进程本身并不占用多少CPU资源的情况。



◎Memcached的工作方式 [这部分还是看Memcached 原理和使用详解(PPT/PDF)较好理解]


以下的部分中,读者最好能准备一份memcached的源代码。



Memcached是传统的网络服务程序,如果启动的时候使用了-d参数,它会以守护进程的方式执行。创建守护进程由daemon.c完成,这个程序只有一个daemon函数,这个函数很简单(如无特殊说明,代码以1.2.1为准):

CODE:
#include <fcntl.h>

#include <stdlib.h>


#include <unistd.h>



int


daemon(nochdir, noclose)


    int nochdir, noclose;


{


    int fd; 



    switch (fork()) {


    case -1:


        return (-1);


    case 0: 


        break;  
    default:


        _exit(0);


    }



    if (setsid() == -1)


        return (-1);



    if (!nochdir)


        (void)chdir("/");



    if (!noclose && (fd = open("/dev/null", O_RDWR, 0)) != -1) {


        (void)dup2(fd, STDIN_FILENO);


        (void)dup2(fd, STDOUT_FILENO);


        (void)dup2(fd, STDERR_FILENO);


        if (fd > STDERR_FILENO)


            (void)close(fd);


    }


    return (0);


}

这个函数 fork 了整个进程之后,父进程就退出,接着重新定位 STDIN 、 STDOUT 、 STDERR 到空设备, daemon 就建立成功了。 



Memcached 本身的启动过程,在 memcached.c 的 main 函数中顺序如下: 



1 、调用 settings_init() 设定初始化参数


2 、从启动命令中读取参数来设置 setting 值


3 、设定 LIMIT 参数


4 、开始网络 socket 监听(如果非 socketpath 存在)( 1.2 之后支持 UDP 方式)


5 、检查用户身份( Memcached 不允许 root 身份启动)


6 、如果有 socketpath 存在,开启 UNIX 本地连接(Sock 管道)


7 、如果以 -d 方式启动,创建守护进程(如上调用 daemon 函数)


8 、初始化 item 、 event 、状态信息、 hash 、连接、 slab


9 、如设置中 managed 生效,创建 bucket 数组


10 、检查是否需要锁定内存页


11 、初始化信号、连接、删除队列


12 、如果 daemon 方式,处理进程 ID


13 、event 开始,启动过程结束, main 函数进入循环。 



在 daemon 方式中,因为 stderr 已经被定向到黑洞,所以不会反馈执行中的可见错误信息。 



memcached.c 的主循环函数是 drive_machine ,传入参数是指向当前的连接的结构指针,根据 state 成员的状态来决定动作。 



Memcached 使用一套自定义的协议完成数据交换,它的 protocol 文档可以参考: http://code.sixapart.com/svn/memcached/trunk/server/doc/protocol.txt



在API中,换行符号统一为\r\n



◎Memcached的内存管理方式



Memcached有一个很有特色的内存管理方式,为了提高效率,它使用预申请和分组的方式管理内存空间,而并不是每次需要写入数据的时候去malloc,删除数据的时候free一个指针。Memcached使用slab->chunk的组织方式管理内存。



1.1和1.2的slabs.c中的slab空间划分算法有一些不同,后面会分别介绍。



Slab 可以理解为一个内存块,一个slab是memcached一次申请内存的最小单位,在memcached中,一个slab的大小默认为1048576字节 (1MB),所以memcached都是整MB的使用内存。每一个slab被划分为若干个chunk,每个chunk里保存一个item,每个item同 时包含了item结构体、key和value(注意在memcached中的value是只有字符串的)。slab按照自己的id分别组成链表,这些链表 又按id挂在一个slabclass数组上,整个结构看起来有点像二维数组。slabclass的长度在1.1中是21,在1.2中是200。



slab有一个初始chunk大小,1.1中是1字节,1.2中是80字节,1.2中有一个factor值,默认为1.25



在 1.1中,chunk大小表示为初始大小*2^n,n为classid,即:id为0的slab,每chunk大小1字节,id为1的slab,每 chunk大小2字节,id为2的slab,每chunk大小4字节……id为20的slab,每chunk大小为1MB,就是说id为20的slab里 只有一个chunk:

CODE:
void slabs_init(size_t limit) {

    int i;


    int size=1;



    mem_limit = limit;


    for(i=0; i<=POWER_LARGEST; i++, size*=2) {


        slabclass[i].size = size;


        slabclass[i].perslab = POWER_BLOCK / size;


        slabclass[i].slots = 0;


        slabclass[i].sl_curr = slabclass[i].sl_total = slabclass[i].slabs = 0;


        slabclass[i].end_page_ptr = 0;


        slabclass[i].end_page_free = 0;


        slabclass[i].slab_list = 0;


        slabclass[i].list_size = 0;


        slabclass[i].killing = 0;


    }



    /* for the test suite:  faking of how much we've already malloc'd */


    {


        char *t_initial_malloc = getenv("T_MEMD_INITIAL_MALLOC");


        if (t_initial_malloc) {


            mem_malloced = atol(getenv("T_MEMD_INITIAL_MALLOC"));


        }


    }



    /* pre-allocate slabs by default, unless the environment variable


       for testing is set to something non-zero */


    {


        char *pre_alloc = getenv("T_MEMD_SLABS_ALLOC");


        if (!pre_alloc || atoi(pre_alloc)) {


            slabs_preallocate(limit / POWER_BLOCK);


        }


    }


}

在1.2中,chunk大小表示为初始大小*f^n,f为factor,在memcached.c中定义,n为classid,同时,201个头不是全部 都要初始化的,因为factor可变,初始化只循环到计算出的大小达到slab大小的一半为止,而且它是从id1开始的,即:id为1的slab,每 chunk大小80字节,id为2的slab,每chunk大小80*f,id为3的slab,每chunk大小80*f^2,初始化大小有一个修正值 CHUNK_ALIGN_BYTES,用来保证n-byte排列 (保证结果是CHUNK_ALIGN_BYTES的整倍数)。这样,在标准情况下,memcached1.2会初始化到id40,这个slab中每个 chunk大小为504692,每个slab中有两个chunk。最后,slab_init函数会在最后补足一个id41,它是整块的,也就是这个 slab中只有一个1MB大的chunk:

CODE:
void slabs_init(size_t limit, double factor) {

    int i = POWER_SMALLEST - 1;


    unsigned int size = sizeof(item) + settings.chunk_size;



    /* Factor of 2.0 means use the default memcached behavior */


    if (factor == 2.0 && size < 128)


        size = 128;



    mem_limit = limit;


    memset(slabclass, 0, sizeof(slabclass));



    while (++i < POWER_LARGEST && size <= POWER_BLOCK / 2) {


        /* Make sure items are always n-byte aligned */


        if (size % CHUNK_ALIGN_BYTES)


            size += CHUNK_ALIGN_BYTES - (size % CHUNK_ALIGN_BYTES);



        slabclass[i].size = size; 


        slabclass[i].perslab = POWER_BLOCK / slabclass[i].size;


        size *= factor; 


        if (settings.verbose > 1) {


            fprintf(stderr, "slab class %3d: chunk size %6d perslab %5d\n",


                    i, slabclass[i].size, slabclass[i].perslab);


        }       


    }



    power_largest = i;


    slabclass[power_largest].size = POWER_BLOCK;


    slabclass[power_largest].perslab = 1;



    /* for the test suite:  faking of how much we've already malloc'd */


    {


        char *t_initial_malloc = getenv("T_MEMD_INITIAL_MALLOC");


        if (t_initial_malloc) {


            mem_malloced = atol(getenv("T_MEMD_INITIAL_MALLOC"));


        }       



    }



#ifndef DONT_PREALLOC_SLABS


    {


        char *pre_alloc = getenv("T_MEMD_SLABS_ALLOC");


        if (!pre_alloc || atoi(pre_alloc)) {


            slabs_preallocate(limit / POWER_BLOCK);


        }


    }


#endif


}

由上可以看出,memcached的内存分配是有冗余的,当一个slab不能被它所拥有的chunk大小整除时,slab尾部剩余的空间就被丢弃了,如id40中,两个chunk占用了1009384字节,这个slab一共有1MB,那么就有39192字节被浪费了。


Memcached 使用这种方式来分配内存,是为了可以快速的通过item长度定位出slab的classid,有一点类似hash,因为item的长度是可以计算的,比如 一个item的长度是300字节,在1.2中就可以得到它应该保存在id7的slab中,因为按照上面的计算方法,id6的chunk大小是252字 节,id7的chunk大小是316字节,id8的chunk大小是396字节,表示所有252到316字节的item都应该保存在id7中。同理,在 1.1中,也可以计算得到它出于256和512之间,应该放在chunk_size为512的id9中(32位系统)。



Memcached 初始化的时候,会初始化slab(前面可以看到,在main函数中调用了slabs_init())。它会在slabs_init()中检查一个常量 DONT_PREALLOC_SLABS,如果这个没有被定义,说明使用预分配内存方式初始化slab,这样在所有已经定义过的slabclass中,每 一个id创建一个slab。这样就表示,1.2在默认的环境中启动进程后要分配41MB的slab空间,在这个过程里,memcached的第二个内存冗 余发生了,因为有可能一个id根本没有被使用过,但是它也默认申请了一个slab,每个slab会用掉1MB内存



当一个slab用光后,又有新的item要插入这个id,那么它就会重新申请新的slab,申请新的slab时,对应id的slab链表就要增长,这个链表是成倍增长的,在函数grow_slab_list函数中,这个链的长度从1变成2,从2变成4,从4变成8……:

CODE:
static int grow_slab_list (unsigned int id) {

    slabclass_t *p = &slabclass[id];


    if (p->slabs == p->list_size) {


        size_t new_size =  p->list_size ? p->list_size * 2 : 16; 


        void *new_list = realloc(p->slab_list, new_size*sizeof(void*));


        if (new_list == 0) return 0;


        p->list_size = new_size;


        p->slab_list = new_list;


    }


    return 1;


}

在定位item时,都是使用slabs_clsid函数,传入参数为item大小,返回值为classid,由这个过程可以看出,memcached的第 三个内存冗余发生在保存item的过程中,item总是小于或等于chunk大小的,当item小于chunk大小时,就又发生了空间浪费。


◎Memcached的NewHash算法



Memcached 的item保存基于一个大的hash表,它的实际地址就是slab中的chunk偏移,但是它的定位是依靠对key做hash的结果,在 primary_hashtable中找到的。在assoc.c和items.c中定义了所有的hash和item操作。



Memcached使用了一个叫做NewHash的算法,它的效果很好,效率也很高。1.1和1.2的NewHash有一些不同,主要的实现方式还是一样的,1.2的hash函数是经过整理优化的,适应性更好一些。



NewHash的原型参考:http://burtleburtle.net/bob/hash/evahash.html。数学家总是有点奇怪,呵呵~



为了变换方便,定义了u4和u1两种数据类型,u4就是无符号的长整形,u1就是无符号char(0-255)。



具体代码可以参考1.1和1.2源码包。



注 意这里的hashtable长度,1.1和1.2也是有区别的,1.1中定义了HASHPOWER常量为20,hashtable表长为 hashsize(HASHPOWER),就是4MB(hashsize是一个宏,表示1右移n位),1.2中是变量16,即hashtable表长 65536:

CODE:
typedef  unsigned long  int  ub4;   /* unsigned 4-byte quantities */

typedef  unsigned       char ub1;   /* unsigned 1-byte quantities */



#define hashsize(n) ((ub4)1<<(n))


#define hashmask(n) (hashsize(n)-1)

在assoc_init()中,会对primary_hashtable做初始化,对应的hash操作包括:assoc_find()、 assoc_expand()、assoc_move_next_bucket()、assoc_insert()、assoc_delete(),对应 于item的读写操作。其中assoc_find()是根据key和key长寻找对应的item地址的函数(注意在C中,很多时候都是同时直接传入字符串 和字符串长度,而不是在函数内部做strlen),返回的是item结构指针,它的数据地址在slab中的某个chunk上。


items.c是数据项的操作程序,每一个完整的item包括几个部分,在item_make_header()中定义为:



key:键


nkey:键长


flags:用户定义的flag(其实这个flag在memcached中没有启用)


nbytes:值长(包括换行符号\r\n)


suffix:后缀Buffer


nsuffix:后缀长



一个完整的item长度是键长+值长+后缀长+item结构大小(32字节),item操作就是根据这个长度来计算slab的classid的。



hashtable 中的每一个桶上挂着一个双链表,item_init()的时候已经初始化了heads、tails、sizes三个数组为0,这三个数组的大小都为常量 LARGEST_ID(默认为255,这个值需要配合factor来修改),在每次item_assoc()的时候,它会首先尝试从slab中获取一块空 闲的chunk,如果没有可用的chunk,会在链表中扫描50次,以得到一个被LRU踢掉的item,将它unlink,然后将需要插入的item插入 链表中。



注意item的refcount成员。item被unlink之后只是从链表上摘掉,不是立刻就被free的,只是将它放到删除队列中(item_unlink_q()函数)。



item对应一些读写操作,包括remove、update、replace,当然最重要的就是alloc操作。



item 还有一个特性就是它有过期时间,这是memcached的一个很有用的特性,很多应用都是依赖于memcached的item过期,比如session存 储、操作锁等。item_flush_expired()函数就是扫描表中的item,对过期的item执行unlink操作,当然这只是一个回收动作, 实际上在get的时候还要进行时间判断:

CODE:
/* expires items that are more recent than the oldest_live setting. */

void item_flush_expired() {


    int i;  
    item *iter, *next;


    if (! settings.oldest_live)


        return; 


    for (i = 0; i < LARGEST_ID; i++) {


        /* The LRU is sorted in decreasing time order, and an item's timestamp


         * is never newer than its last access time, so we only need to walk


         * back until we hit an item older than the oldest_live time.


         * The oldest_live checking will auto-expire the remaining items.


         */


        for (iter = heads[i]; iter != NULL; iter = next) { 


            if (iter->time >= settings.oldest_live) {


                next = iter->next;


                if ((iter->it_flags & ITEM_SLABBED) == 0) { 


                    item_unlink(iter);


                }       


            } else {


                /* We've hit the first old item. Continue to the next queue. */


                break;  
            }       


        }       


    }


}

 

CODE:
/* wrapper around assoc_find which does the lazy expiration/deletion logic */

item *get_item_notedeleted(char *key, size_t nkey, int *delete_locked) {


    item *it = assoc_find(key, nkey);


    if (delete_locked) *delete_locked = 0;


    if (it && (it->it_flags & ITEM_DELETED)) {


        /* it's flagged as delete-locked.  let's see if that condition


           is past due, and the 5-second delete_timer just hasn't


           gotten to it yet... */


        if (! item_delete_lock_over(it)) {


            if (delete_locked) *delete_locked = 1;


            it = 0; 


        }       


    }


    if (it && settings.oldest_live && settings.oldest_live <= current_time &&


        it->time <= settings.oldest_live) {


        item_unlink(it);


        it = 0; 


    }


    if (it && it->exptime && it->exptime <= current_time) {


        item_unlink(it);


        it = 0; 


    }


    return it;


}

Memcached的内存管理方式是非常精巧和高效的,它很大程度上减少了直接alloc系统内存的次数,降低函数开销和内存碎片产生几率,虽然这种方式会造成一些冗余浪费,但是这种浪费在大型系统应用中是微不足道的。



◎Memcached的理论参数计算方式



影响 memcached 工作的几个参数有: 



常量REALTIME_MAXDELTA 60*60*24*30


最大30天的过期时间



conn_init()中的freetotal(=200)


最大同时连接数



常量KEY_MAX_LENGTH 250


最大键长



settings.factor(=1.25)


factor将影响chunk的步进大小



settings.maxconns(=1024)


最大软连接



settings.chunk_size(=48)


一个保守估计的key+value长度,用来生成id1中的chunk长度(1.2)。id1的chunk长度等于这个数值加上item结构体的长度(32),即默认的80字节。



常量POWER_SMALLEST 1


最小classid(1.2)



常量POWER_LARGEST 200


最大classid(1.2)



常量POWER_BLOCK 1048576


默认slab大小



常量CHUNK_ALIGN_BYTES (sizeof(void *))


保证chunk大小是这个数值的整数倍,防止越界(void *的长度在不同系统上不一样,在标准32位系统上是4)



常量ITEM_UPDATE_INTERVAL 60


队列刷新间隔



常量LARGEST_ID 255


最大item链表数(这个值不能比最大的classid小)



变量hashpower(在1.1中是常量HASHPOWER)


决定hashtable的大小



根据上面介绍的内容及参数设定,可以计算出的一些结果:



1、在memcached中可以保存的item个数是没有软件上限的,之前我的100万的说法是错误的。

2、假设NewHash算法碰撞均匀,查找item的循环次数是item总数除以hashtable大小(由hashpower决定),是线性的。


3、Memcached限制了可以接受的最大item是1MB,大于1MB的数据不予理会。


4、Memcached的空间利用率和数据特性有很大的关系,又与DONT_PREALLOC_SLABS常量有关。 在最差情况下,有198个slab会被浪费(所有item都集中在一个slab中,199个id全部分配满)。
 



◎Memcached的定长优化



根据上面几节的描述,多少对memcached有了一个比较深入的认识。在深入认识的基础上才好对它进行优化。



Memcached 本身是为变长数据设计的,根据数据特性,可以说它是“面向大众”的设计,但是很多时候,我们的数据并不是这样的“普遍”,典型的情况中,一种是非均匀分 布,即数据长度集中在几个区域内(如保存用户 Session);另一种更极端的状态是等长数据(如定长键值,定长数据,多见于访问、在线统计或执行锁)。



这里主要研究一下定长数据的优化方案(1.2),集中分布的变长数据仅供参考,实现起来也很容易。



解决定长数据,首先需要解决的是slab的分配问题,第一个需要确认的是我们不需要那么多不同chunk长度的slab,为了最大限度地利用资源,最好chunk和item等长,所以首先要计算item长度。



在之前已经有了计算item长度的算法,需要注意的是,除了字符串长度外,还要加上item结构的长度32字节。



假设我们已经计算出需要保存200字节的等长数据。



接下来是要修改slab的classid和chunk长度的关系。在原始版本中,chunk长度和classid是有对应关系的,现在如果把所有的 chunk都定为200个字节,那么这个关系就不存在了,我们需要重新确定这二者的关系。一种方法是,整个存储结构只使用一个固定的id,即只使用199 个槽中的1个,在这种条件下,就一定要定义DONT_PREALLOC_SLABS来避免另外的预分配浪费。另一种方法是建立一个hash关系,来从 item确定classid,不能使用长度来做键,可以使用key的NewHash结果等不定数据,或者直接根据key来做hash(定长数据的key也 一定等长)。这里简单起见,选择第一种方法,这种方法的不足之处在于只使用一个id,在数据量非常大的情况下,slab链会很长(因为所有数据都挤在一条 链上了),遍历起来的代价比较高。



前面介绍了三种空间冗余,设置chunk长度等于item长度,解决了第一种空间浪费问题,不预申请空 间解决了第二种空间浪费问题,那么对于第一种问题(slab内剩余)如何解决呢,这就需要修改POWER_BLOCK常量,使得每一个slab大小正好等 于chunk长度的整数倍,这样一个slab就可以正好划分成n个chunk。这个数值应该比较接近1MB,过大的话同样会造成冗余,过小的话会造成次数 过多的alloc,根据chunk长度为200,选择1000000作为POWER_BLOCK的值,这样一个slab就是100万字节,不是 1048576。三个冗余问题都解决了,空间利用率会大大提升。



修改 slabs_clsid 函数,让它直接返回一个定值(比如 1 ):

CODE:
unsigned int slabs_clsid(size_t size) {

        return 1;


}

修改slabs_init函数,去掉循环创建所有classid属性的部分,直接添加slabclass[1]:

CODE:
slabclass[1].size = 200;                //每chunk200字节

slabclass[1].perslab = 5000;        //1000000/200

◎Memcached客户端



Memcached是一个服务程序,使用的时候可以根据它的协议,连接到 memcached服务器上,发送命令给服务进程,就可以操作上面的数据。为了方便使用,memcached有很多个客户端程序可以使用,对应于各种语 言,有各种语言的客户端。基于C语言的有libmemcache、APR_Memcache;基于Perl的有Cache::Memcached;另外还 有Python、Ruby、Java、C#等语言的支持。PHP的客户端是最多的,不光有mcache和PECL memcache两个扩展,还有大把的由PHP编写的封装类,下面介绍一下在PHP中使用memcached的方法:



mcache扩展是 基于libmemcache再封装的。libmemcache一直没有发布stable版本,目前版本是1.4.0-rc2,可以在这里找到。 libmemcache有一个很不好的特性,就是会向stderr写很多错误信息,一般的,作为lib使用的时候,stderr一般都会被定向到其它地 方,比如Apache的错误日志,而且libmemcache会自杀,可能会导致异常,不过它的性能还是很好的。



mcache扩展最后更 新到1.2.0-beta10,作者大概是离职了,不光停止更新,连网站也打不开了(~_~),只能到其它地方去获取这个不负责的扩展了。解压后安装方法 如常:phpize & configure & make & make install,一定要先安装libmemcache。使用这个扩展很简单:

CODE:
<?php

$mc
= memcache();    // 创建一个memcache连接对象,注意这里不是用new!

$mc->add_server('localhost', 11211);    // 添加一个服务进程

$mc->add_server('localhost', 11212);    // 添加第二个服务进程

$mc->set('key1', 'Hello');    // 写入key1 => Hello

$mc->set('key2', 'World', 10);    // 写入key2 => World,10秒过期

$mc->set('arr1', array('Hello', 'World'));    // 写入一个数组

$key1 = $mc->get('key1');    // 获取'key1'的值,赋给$key1

$key2 = $mc->get('key2');    // 获取'key2'的值,赋给$key2,如果超过10秒,就取不到了

$arr1 = $mc->get('arr1');    // 获取'arr1'数组

$mc->delete('arr1');    // 删除'arr1'

$mc->flush_all();    // 删掉所有数据

$stats = $mc->stats();    // 获取服务器信息

var_dump($stats);    // 服务器信息是一个数组

?>

这个扩展的好处是可以很方便地实现分布式存储和负载均衡,因为它可以添加多个服务地址,数据在保存的时候是会根据hash结果定位到某台服务器上的,这也 是libmemcache的特性。libmemcache支持集中hash方式,包括CRC32、ELF和Perl hash。


PECL memcache是PECL发布的扩展,目前最新版本是2.1.0,可以在pecl网站得到。memcache扩展的使用方法可以在新一些的PHP手册中找到,它和mcache很像,真的很像:

CODE:
<?php


$memcache = new Memcache;

$memcache->connect('localhost', 11211) or die ("Could not connect");


$version = $memcache->getVersion();

echo
"Server's version: ".$version."n";


$tmp_object = new stdClass;

$tmp_object->str_attr = 'test';

$tmp_object->int_attr = 123;


$memcache->set('key', $tmp_object, false, 10) or die ("Failed to save data at the server");

echo
"Store data in the cache (data will expire in 10 seconds)n";


$get_result = $memcache->get('key');

echo
"Data from the cache:n";


var_dump($get_result);


?>

这个扩展是使用php的stream直接连接memcached服务器并通过socket发送命令的。它不像libmemcache那样完善,也不支持 add_server这种分布操作,但是因为它不依赖其它的外界程序,兼容性要好一些,也比较稳定。至于效率,差别不是很大。


另外,有很多的PHP class可以使用,比如MemcacheClient.inc.php,phpclasses.org上可以找到很多,一般都是对perl client API的再封装,使用方式很像。



◎BSM_Memcache



从 C client来说,APR_Memcache是一个很成熟很稳定的client程序,支持线程锁和原子级操作,保证运行的稳定性。不过它是基于APR的 (APR将在最后一节介绍),没有libmemcache的应用范围广,目前也没有很多基于它开发的程序,现有的多是一些Apache Module,因为它不能脱离APR环境运行。但是APR倒是可以脱离Apache单独安装的,在APR网站上可以下载APR和APR-util,不需要 有Apache,可以直接安装,而且它是跨平台的。



BSM_Memcache是我在BS.Magic项目中开发的一个基于APR_Memcache的PHP扩展,说起来有点拗口,至少它把APR扯进了PHP扩展中。这个程序很简单,也没做太多的功能,只是一种形式的尝试,它支持服务器分组。



和mcache扩展支持多服务器分布存储不同,BSM_Memcache支持多组服务器,每一组内的服务器还是按照hash方式来分布保存数据,但是两个组中保存的数据是一样的,也就是实现了热备,它不会因为一台服务器发生单点故障导致数据无法获取,除非所有的服务器组都损坏(例如机房停电)。当然实现这个功能的代价就是性能上的牺牲,在每次添加删除数据的时候都要扫描所有的组,在get数据的时候会随机选择一组服务器开始轮询,一直到找到数据为止,正常情 况下一次就可以获取得到。



BSM_Memcache只支持这几个函数:

CODE:
zend_function_entry bsm_memcache_functions[] =

{


    PHP_FE(mc_get,          NULL)


    PHP_FE(mc_set,          NULL)


    PHP_FE(mc_del,          NULL)


    PHP_FE(mc_add_group,    NULL)


    PHP_FE(mc_add_server,   NULL)


    PHP_FE(mc_shutdown,     NULL)


    {NULL, NULL, NULL}


};

mc_add_group函数返回一个整形(其实应该是一个object,我偷懒了~_~)作为组ID,mc_add_server的时候要提供两个参数,一个是组ID,一个是服务器地址(ADDRORT)。

CODE:
/**

* Add a server group


*/


PHP_FUNCTION(mc_add_group)


{


    apr_int32_t group_id;


    apr_status_t rv;



    if (0 != ZEND_NUM_ARGS())


    {


        WRONG_PARAM_COUNT;


        RETURN_NULL();


    }



    group_id = free_group_id();


    if (-1 == group_id)


    {


        RETURN_FALSE;


    }



    apr_memcache_t *mc;


    rv = apr_memcache_create(p, MAX_G_SERVER, 0, &mc);



    add_group(group_id, mc);



    RETURN_DOUBLE(group_id);


}

 

CODE:
/**

* Add a server into group


*/


PHP_FUNCTION(mc_add_server)


{


    apr_status_t rv;


    apr_int32_t group_id;


    double g;


    char *srv_str;


    int srv_str_l;



    if (2 != ZEND_NUM_ARGS())


    {


        WRONG_PARAM_COUNT;


    }



    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ds", &g, &srv_str, &srv_str_l) == FAILURE)


    {


        RETURN_FALSE;


    }



    group_id = (apr_int32_t) g;



    if (-1 == is_validate_group(group_id))


    {


        RETURN_FALSE;


    }



    char *host, *scope;


    apr_port_t port;



    rv = apr_parse_addr_port(&host, &scope, &port, srv_str, p);


    if (APR_SUCCESS == rv)


    {


        // Create this server object


        apr_memcache_server_t *st;


        rv = apr_memcache_server_create(p, host, port, 0, 64, 1024, 600, &st);


        if (APR_SUCCESS == rv)


        {


            if (NULL == mc_groups[group_id])


            {


                RETURN_FALSE;


            }



            // Add server


            rv = apr_memcache_add_server(mc_groups[group_id], st);



            if (APR_SUCCESS == rv)


            {


                RETURN_TRUE;


            }


        }


    }



    RETURN_FALSE;


}

在set和del数据的时候,要循环所有的组:

CODE:
/**

* Store item into all groups


*/


PHP_FUNCTION(mc_set)


{


    char *key, *value;


    int key_l, value_l;


    double ttl = 0;


    double set_ct = 0;



    if (2 != ZEND_NUM_ARGS())


    {


        WRONG_PARAM_COUNT;


    }



    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss|d", &key, &key_l, &value, &value_l, ttl) == FAILURE)


    {


        RETURN_FALSE;


    }



    // Write data into every object


    apr_int32_t i = 0;


    if (ttl < 0)


    {


        ttl = 0;


    }



    apr_status_t rv;



    for (i = 0; i < MAX_GROUP; i++)


    {


        if (0 == is_validate_group(i))


        {


            // Write it!


            rv = apr_memcache_add(mc_groups[i], key, value, value_l, (apr_uint32_t) ttl, 0);


            if (APR_SUCCESS == rv)


            {


                set_ct++;


            }


        }


    }



    RETURN_DOUBLE(set_ct);


}

在mc_get中,首先要随机选择一个组,然后从这个组开始轮询:

CODE:
/**

* Fetch a item from a random group


*/


PHP_FUNCTION(mc_get)


{               
    char *key, *value = NULL;


    int key_l;


    apr_size_t value_l;



    if (1 != ZEND_NUM_ARGS())


    {


        WRONG_PARAM_COUNT;


    }



    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &key, &key_l) == FAILURE)


    {


        RETURN_MULL();


    }


    


    // I will try ...


    // Random read


    apr_int32_t curr_group_id = random_group();


    apr_int32_t i = 0;


    apr_int32_t try = 0;


    apr_uint32_t flag;


    apr_memcache_t *oper;


    apr_status_t rv;



    for (i = 0; i < MAX_GROUP; i++)


    {


        try = i + curr_group_id;


        try = try % MAX_GROUP;


        if (0 == is_validate_group(try))


        {


            // Get a value


            oper = mc_groups[try];


            rv = apr_memcache_getp(mc_groups[try], p, (const char *) key, &value, &value_l, 0);


            if (APR_SUCCESS == rv)


            {


                RETURN_STRING(value, 1);


            }


        }


    }



    RETURN_FALSE;


}

 

CODE:
/**

* Random group id


* For mc_get()


*/


apr_int32_t random_group()


{


    struct timeval tv;


    struct timezone tz;


    int usec;



    gettimeofday(&tv, &tz);



    usec = tv.tv_usec;



    int curr = usec % count_group();



    return (apr_int32_t) curr;


}

BSM_Memcache的使用方式和其它的client类似:

CODE:
<?php

$g1
= mc_add_group();    // 添加第一个组

$g2 = mc_add_group();    // 添加第二个组

mc_add_server($g1, 'localhost:11211');    // 在第一个组中添加第一台服务器

mc_add_server($g1, 'localhost:11212');    // 在第一个组中添加第二台服务器

mc_add_server($g2, '10.0.0.16:11211');    // 在第二个组中添加第一台服务器

mc_add_server($g2, '10.0.0.17:11211');    // 在第二个组中添加第二台服务器


mc_set('key', 'Hello');    // 写入数据

$key = mc_get('key');    // 读出数据

mc_del('key');    // 删除数据

mc_shutdown();    // 关闭所有组

?>

APR_Memcache的相关资料可以在这里找到,BSM_Memcache可以在本站下载。


◎APR环境介绍



APR 的全称:Apache Portable Runtime。它是Apache软件基金会创建并维持的一套跨平台的C语言库。它从Apache httpd1.x中抽取出来并独立于httpd之外,Apache httpd2.x就是建立在APR上。APR提供了很多方便的API接口可供使用,包括如内存池、字符串操作、网络、数组、hash表等实用的功能。开发 Apache2 Module要接触很多APR函数,当然APR可以独立安装独立使用,可以用来写自己的应用程序,不一定是Apache httpd的相关开发。



◎后记



这是我在农历丙戌年(我的本命年)的最后一篇文章,由于Memcached的内涵很多,仓促整理一定有很多遗漏和错误。感谢新浪网提供的研究机会,感谢部门同事的帮助。



NP博士 02-13-2007

Continue reading 【转】Memcached深度分析

Memcache 调研

官网:http://memcached.org/

文档:http://code.google.com/p/memcached/wiki/NewStart?tm=6

这位仁兄收集了些文章:

分布式缓存系统Memcached简介与实践

Memcached深度分析 [好文,必读]

自己实现memcached客户端库

memcached server LRU 深入分析[这个针对实际问题阐述memcached内存存储的调整,回头再看Memcached深度分析 ]

Memcached 原理和使用详解(PPT/PDF) [这个写的很好,必读]

Memcached使用点滴 [这个需要对memcache有一定了解之后,打算自己实现client可做借鉴]

memcached全面剖析–PDF总结篇 [very good,日本mixi网站工程师写的,国人翻译的]

 

api client 列表 http://code.google.com/p/memcached/wiki/Clients

参见http://blog.ureshika.com/archives/753.html 

memcached 安装见http://www.ccvita.com/257.html

以下是我的安装命令[需要root权限]:

------------------------------------------------------------------------------------------------------------

#安装libevent

cd /tmp

wget http://memcached.googlecode.com/files/memcached-1.4.13.tar.gz

wget --no-check-certificate https://github.com/downloads/libevent/libevent/libevent-2.0.17-stable.tar.gz

tar zxvf libevent-2.0.17-stable.tar.gz

cd libevent-2.0.17-stable

./configure --prefix=/usr

make

make install

#检查是否安装

ls -al /usr/lib | grep libevent

#安装memcached

tar zxvf memcached-1.4.13.tar.gz

cd memcached-1.4.13

./configure --with-libevent=/usr

make

make install

#测试是否成功安装memcached:

ls -al /usr/local/bin/mem*

#查看12000端口是否被占用

netstat -tl | grep 12000

#启动并将进程号存入memcached.pid

/usr/local/bin/memcached -d -m 10 -u root -l 192.168.0.200 -p 12000 -c 256 -P memcached.pid

#查看配置状态

echo "stats settings" | nc localhost 12000

#查看内存分块状态

echo "stats slabs" | nc localhost 12000

#查看健康状况

echo "stats" | nc localhost 12000

详细参数列表:

-p <num>      TCP port number to listen on (default: 11211)
-U <num>      UDP port number to listen on (default: 11211, 0 is off)
-s <file>     UNIX socket path to listen on (disables network support)
-a <mask>     access mask for UNIX socket, in octal (default: 0700)
-l <addr>     interface to listen on (default: INADDR_ANY, all addresses)
              <addr> may be specified as host:port. If you don't specify
              a port number, the value you specified with -p or -U is
              used. You may specify multiple addresses separated by comma
              or by using -l multiple times
-d            run as a daemon
-r            maximize core file limit
-u <username> assume identity of <username> (only when run as root)
-m <num>      max memory to use for items in megabytes (default: 64 MB)
-M            return error on memory exhausted (rather than removing items)
-c <num>      max simultaneous connections (default: 1024)
-k            lock down all paged memory.  Note that there is a
              limit on how much memory you may lock.  Trying to
              allocate more than that would fail, so be sure you
              set the limit correctly for the user you started
              the daemon with (not for -u <username> user;
              under sh this is done with 'ulimit -S -l NUM_KB').
-v            verbose (print errors/warnings while in event loop)
-vv           very verbose (also print client commands/reponses)
-vvv          extremely verbose (also print internal state transitions)
-h            print this help and exit
-i            print memcached and libevent license
-P <file>     save PID in <file>, only used with -d option
-f <factor>   chunk size growth factor (default: 1.25)
-n <bytes>    minimum space allocated for key+value+flags (default: 48)
-L            Try to use large memory pages (if available). Increasing
              the memory page size could reduce the number of TLB misses
              and improve the performance. In order to get large pages
              from the OS, memcached will allocate the total item-cache
              in one large chunk.
-D <char>     Use <char> as the delimiter between key prefixes and IDs.
              This is used for per-prefix stats reporting. The default is
              ":" (colon). If this option is specified, stats collection
              is turned on automatically; if not, then it may be turned on
              by sending the "stats detail on" command to the server.
-t <num>      number of threads to use (default: 4)
-R            Maximum number of requests per event, limits the number of
              requests process for a given connection to prevent 
              starvation (default: 20)
-C            Disable use of CAS
-b            Set the backlog queue limit (default: 1024)
-B            Binding protocol - one of ascii, binary, or auto (default)
-I            Override the size of each slab page. Adjusts max item size
              (default: 1mb, min: 1k, max: 128m)
-o            Comma separated list of extended or experimental options
              - (EXPERIMENTAL) maxconns_fast: immediately close new
                connections if over maxconns limit
              - hashpower: An integer multiplier for how large the hash
                table should be. Can be grown at runtime if not big enough.
                Set this based on "STAT hash_power_level" before a 
                restart.

注意-f  增量 -n chunck块初始大小[默认1024b] -I slab page大小[不能超过slab大小,1k—128M,默认1M]

------------------------------------------------------------------------------------------------------------

以下摘自: Memcached 原理和使用详解(PPT/PDF) 

数据存储方式:Slab Allocation

Slab Alloction 构造图:

image

Slab Allocator的基本原理是按照预先规定的大小,将分配的内存分割成特定长度的块,以完全解决内存碎片问题。

Slab Allocation的原理相当简单。 将分配的内存分割成各种尺寸的块(chunk),并把尺寸相同的块分成组(chunk的集合)

Slab Classes 分配图

image

Page:分配给Slab的内存空间,默认是1MB。分配给Slab之后根据slab的大小切分成chunk。

Chunk:用于缓存记录的内存空间。

注意:摘自http://y.jmeye.com/?aid=185

Slab是一个内存块,它是memcached一次申请内存的最小单位。 在启动memcached的时候一般会使用参数-m指定其可用内存,但是并不是在启动的那一刻所有的内存就全部分配出去了,只有在需要的时候才会去申请, 而且每次申请一定是一个slab。Slab的大小固定为1M(1048576 Byte),一个slab由若干个大小相等的chunk组成。每个chunk中都保存了一个item结构体、一对key和value。

注意:

chunk中不仅保持了缓存对象的value,而且保存了缓存对象的key,expire time, flag等详细信息

参见: http://tank.blogs.tkiicpp.com/category/programming/memcache/

Slab Class:特定大小的chunk的组。

memcached根据收到的数据的大小,选择最适合数据大小的slab。

memcached中保存着slab内空闲chunk的列表,根据该列表选择chunk,然后将数据缓存于其中。

Slab Alloction 缺点

image

这个问题就是,由于分配的是特定长度的内存,因此无法有效利用分配的内存。例如,将100字节的数据缓存到128字节的chunk中,剩余的28字节就浪费了。

 

以下摘自: http://blog.ureshika.com/archives/750.html

在1.2中,chunk大小表示为初始大小*f^n,f为factor,在memcached.c中定义,n为classid,同时,201个头不是全部 都要初始化的,因为factor可变,初始化只循环到计算出的大小达到slab大小的一半为止,而且它是从id1开始的,即:id为1的slab,每 chunk大小80字节,id为2的slab,每chunk大小80*f,id为3的slab,每chunk大小80*f^2,初始化大小有一个修正值 CHUNK_ALIGN_BYTES,用来保证n-byte排列 (保证结果是CHUNK_ALIGN_BYTES的整倍数)。这样,在标准情况下,memcached1.2会初始化到id40,这个slab中每个 chunk大小为504692,每个slab中有两个chunk。最后,slab_init函数会在最后补足一个id41,它是整块的,也就是这个 slab中只有一个1MB大的chunk
 

以下摘自:

http://blog.csdn.net/jarfield/article/details/4322953

http://blog.csdn.net/jarfield/article/details/4336035

http://blog.csdn.net/jarfield/article/details/4341819

所有的被发送到memcached的单个命令是完全原子的。如果您针对同一份数据同时发送了一个set命令和一个get命令,它们不会影响对方。它们将被串行化、先后执行。

通常,基于memcached中item的值来修改item,是一件棘手的事情。除非您很清楚自己在做什么,否则请不要做这样的事情。

 
以下摘自: http://www.iteye.com/topic/225692
memcached中新的value过来存放的地址是该value的大小决定的,value总是会被选择存放到chunk与其最接近的一个slab中,比如上面的例子,如果我的value是80b,那么我这所有的value总是会被存放到1号slab中,而1号slab中的free_chunks已经是0了,怎么办呢,如果你在启动memcached的时候没有追加-M(禁止LRU,这种情况下内存不够时会out of memory),那么memcached会把这个slab中最近最少被使用的chunk中的数据清掉,然后放上最新的数据。这就解释了为什么我的内存还有40%的时候LRU就执行了,因为我的其他slab中的chunk_size都远大于我的value,所以我的value根本不会放到那几个slab中,而只会放到和我的value最接近的chunk所在的slab中(而这些slab早就满了,郁闷了)。这就导致了我的数据被不停的覆盖,后者覆盖前者。
 
我总结一下:
逻辑上的item 存于chunk中,相同大小chunk组成slab(chunk初始值由-n配置), 各个slab大小是一致的(-I配置),但是slab1和slab2其内部的chunk大小是不同的,slab2内部的chunk大小是slab1内部chunk大小的 f增量子倍(-f配置),另外slab page是将slab划分的内存块,一个slab可划分为多个page,每个page下存储多个chunk
逻辑元素: slab,item
内存元素:page,chunk
这些可通过memcached的状态打印输出看出来

STAT 1:chunk_size 80

STAT 1:chunks_per_page 13107

STAT 1:total_pages 1

STAT 1:total_chunks 13107

STAT 1:used_chunks 13107

STAT 1:free_chunks 0

STAT 1:free_chunks_end 13107


•在 Memcached 中可以保存的item数据量是没有限制的,只有内存足够

• 最大键长为250字节,大于该长度无法存储,常量代码中KEY_MAX_LENGTH 250 控制

• 单个item最大数据默认是1MB,超过1MB数据不予存储,常量代码POWER_BLOCK 1048576 进行控制,参数-I控制的是slab page大小,因此它不能超过slab大小,slab大小只能通过编译源码来改。

32位系统下Memcached单进程最大使用内存为2G,要使用更多内存,可以分多个端口开启多个Memcached进程或改为64位系统。

-----------------------------------------------------------------------------

它的接口文档见http://code.google.com/p/memcached/wiki/NewCommands

其中有些还没被Memcached-Java-Client实现,比如cas,可能是新加的接口。

memcached提供的接口目标就是简单,其用起来可以说简单,但是不简单的地方是什么呢,当然他的内部机理比较复杂,最主要的是实际应用场景的相关问题。

你做数据缓存,那么哪些数据应该缓存?怎么样缓存?过期策略?

做session集群共享,要不要保证可靠性?

很多时候不该memcached做的事情也让它来做了,这时可考虑下nosql地盘的东东了。

可参考转载的这篇文章:http://blog.ureshika.com/archives/757.html

Continue reading Memcache 调研

ThreadLocal 使用搞明白

我最初使用ThreadLocal是copy Myfaces中的源码,为了保存Httprequest对象,现在想一想,总是听说ThreadLocal是解决并发安全性的好方法,这其中有个道理还是没想清楚:

ThreadLocal只是维护资源副本,因此对本线程资源的改变是不会对其他线程起作用的。

那么我们为什么用它呢,最常用的情况是维护Singleton对象,即一个线程内所有对此对象的访问都可以做到是一个对象,这与类静态变量维护的Sington对象是不同的。

可参考:Hibernate的SessionFactory,IBatis的SqlClient

参见文章:

ThreadLocal如何解决并发安全性 http://www.java3z.com/cwbwebhome/article/article8/81086.html

    前面我们介绍了Java当中多个线程抢占一个共享资源的问题。但不论是同步还是重入锁,都不能实实在在的解决资源紧缺的情况,这些方案只是靠制定规则来约束线程的行为,让它们不再拼命的争抢,而不是真正从实质上解决他们对资源的需求。
在JDK 1.2当中,引入了java.lang.ThreadLocal。它为我们提供了一种全新的思路来解决线程并发的问题。但是他的名字难免让我们望文生义:本地线程?
什么是本地线程?
本地线程开玩笑的说:不要迷恋哥,哥只是个传说。
   其实ThreadLocal并非Thread at Local,而是LocalVariable in a Thread。
根据WikiPedia上的介绍,ThreadLocal其实是源于一项多线程技术,叫做Thread Local Storage,即线程本地存储技术。不仅仅是Java,在C++、C#、.NET、Python、Ruby、Perl等开发平台上,该技术都已经得以实现。

   当使用ThreadLocal维护变量时,它会为每个使用该变量的线程提供独立的变量副本。也就是说,他从根本上解决的是资源数量的问题,从而使得每个线程持有相对独立的资源。这样,当多个线程进行工作的时候,它们不需要纠结于同步的问题,于是性能便大大提升。但资源的扩张带来的是更多的空间消耗,ThreadLocal就是这样一种利用空间来换取时间的解决方案。
说了这么多,来看看如何正确使用ThreadLocal。
通过研究JDK文档,我们知道,ThreadLocal中有几个重要的方法:get()、set()、remove()、initailValue(),对应的含义分别是:
返回此线程局部变量的当前线程副本中的值、将此线程局部变量的当前线程副本中的值设置为指定值、移除此线程局部变量当前线程的值、返回此线程局部变量的当前线程的“初始值”。
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
还记得我们在第三篇的上半节引出的那个例子么?几个线程修改同一个Student对象中的age属性。为了保证这几个线程能够工作正常,我们需要对Student的对象进行同步。
下面我们对这个程序进行一点小小的改造,我们通过继承Thread来实现多线程:

/**
*
* @author x-spirit
*/
public class ThreadDemo3 extends Thread{
private ThreadLocal<Student> stuLocal = new ThreadLocal<Student>();
public ThreadDemo3(Student stu){
        stuLocal.set(stu);
    }
public static void main(String[] args) {
        Student stu = new Student();
        ThreadDemo3 td31 = new ThreadDemo3(stu);
        ThreadDemo3 td32 = new ThreadDemo3(stu);
        ThreadDemo3 td33 = new ThreadDemo3(stu);
        td31.start();
        td32.start();
        td33.start();
    }
    @Override
public void run() {
        accessStudent();
    }
public void accessStudent() {
        String currentThreadName = Thread.currentThread().getName();
        System.out.println(currentThreadName + " is running!");
        Random random = new Random();
int age = random.nextInt(100);
        System.out.println("thread " + currentThreadName + " set age to:" + age);
        Student student = stuLocal.get();
        student.setAge(age);
        System.out.println("thread " + currentThreadName + " first  read age is:" + student.getAge());
try {
            Thread.sleep(5000);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        System.out.println("thread " + currentThreadName + " second read age is:" + student.getAge());
    }
}

转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
貌似这个程序没什么问题。但是运行结果却显示:这个程序中的3个线程会抛出3个空指针异常。读者一定感到很困惑。我明明在构造器当中把Student对象set进了ThreadLocal里面阿,为什么run起来之后居然在调用stuLocal.get()方法的时候得到的是NULL呢?
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
带着这个疑问,让我们深入到JDK的代码当中,去一看究竟。
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
原来,在ThreadLocal中,有一个内部类叫做ThreadLocalMap。这个ThreadLocalMap并非java.util.Map的一个实现,而是利用java.lang.ref.WeakReference实现的一个键-值对应的数据结构其中,key是ThreadLocal类型,而value是Object类型,我们可以简单的视为HashMap<ThreadLocal,Object>。
而在每一个Thread对象中,都有一个ThreadLocalMap的引用,即Thread.threadLocals。而ThreadLocal的set方法就是首先尝试从当前线程中取得ThreadLocalMap(以下简称Map)对象。如果取到的不为null,则以ThreadLocal对象自身为key,来取Map中的value。如果取不到Map对象,则首先为当前线程创建一个ThreadLocalMap,然后以ThreadLocal对象自身为key,将传入的value放入该Map中。

    ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
    }   
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
if (map != null)
            map.set(this, value);
else
            createMap(t, value);
    }

而get方法则是首先得到当前线程的ThreadLocalMap对象,然后,根据ThreadLocal对象自身,取出相应的value。当然,如果在当前线程中取不到ThreadLocalMap对象,则尝试为当前线程创建ThreadLocalMap对象,并以ThreadLocal对象自身为key,把initialValue()方法产生的对象作为value放入新创建的ThreadLocalMap中。

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
        }
return setInitialValue();
    }
private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
if (map != null)
            map.set(this, value);
else
            createMap(t, value);
return value;
    }
protected T initialValue() {
return null;
    }

这样,我们就明白上面的问题出在哪里:我们在main方法执行期间,试图在调用ThreadDemo3的构造器时向ThreadLocal置入Student对象,而此时,以ThreadLocal对象为key,Student对象为value的Map是被放入当前的活动线程内的。也就是Main线程。而当我们的3个ThreadDemo3线程运行起来以后,调用get()方法,都是试图从当前的活动线程中取得ThreadLocalMap对象,但当前的活动线程显然已经不是Main线程了,于是,程序最终执行了ThreadLocal原生的initialValue()方法,返回了null。
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
讲到这里,我想不少朋友一定已经看出来了:ThreadLocal的initialValue()方法是需要被覆盖的。
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
于是,ThreadLocal的正确使用方法是:将ThreadLocal以内部类的形式进行继承,并覆盖原来的initialValue()方法,在这里产生可供线程拥有的本地变量值。
这样,我们就有了下面的正确例程:

/**
*
* @author x-spirit
*/
public class ThreadDemo3 extends Thread{
private ThreadLocal<Student> stuLocal = new ThreadLocal<Student>(){
        @Override
protected Student initialValue() {
return new Student();
        }
    };
public ThreadDemo3(){
    }
public static void main(String[] args) {
        ThreadDemo3 td31 = new ThreadDemo3();
        ThreadDemo3 td32 = new ThreadDemo3();
        ThreadDemo3 td33 = new ThreadDemo3();
        td31.start();
        td32.start();
        td33.start();
    }
    @Override
public void run() {
        accessStudent();
    }
public void accessStudent() {
        String currentThreadName = Thread.currentThread().getName();
        System.out.println(currentThreadName + " is running!");
        Random random = new Random();
int age = random.nextInt(100);
        System.out.println("thread " + currentThreadName + " set age to:" + age);
        Student student = stuLocal.get();
        student.setAge(age);
        System.out.println("thread " + currentThreadName + " first  read age is:" + student.getAge());
try {
            Thread.sleep(5000);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        System.out.println("thread " + currentThreadName + " second read age is:" + student.getAge());
    }
}

可见,要正确使用ThreadLocal,必须注意以下几点:
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
1. 总是对ThreadLocal中的initialValue()方法进行覆盖。
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
2. 当使用set()或get()方法时牢记这两个方法是对当前活动线程中的ThreadLocalMap进行操作,一定要认清哪个是当前活动线程!
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
3. 适当的使用泛型,可以减少不必要的类型转换以及可能由此产生的问题。
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
运行该程序,我们发现:程序的执行过程只需要5秒,而如果采用同步的方法,程序的执行结果相同,但执行时间需要15秒。以前是多个线程为了争取一个资源,不得不在同步规则的制约下互相谦让,浪费了一些时间。
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
现在,采用ThreadLocal机制以后,可用的资源多了,你有我有全都有,所以,每个线程都可以毫无顾忌的工作,自然就提高了并发性,线程安全也得以保证。
当今很多流行的开源框架也采用ThreadLocal机制来解决线程的并发问题。比如大名鼎鼎的 Struts 2.x 和 Spring 等。
把ThreadLocal这样的话题放在我们的同步机制探讨中似乎显得不是很合适。但是ThreadLocal的确为我们解决多线程的并发问题带来了全新的思路。它为每个线程创建一个独立的资源副本,从而将多个线程中的数据隔离开来,避免了同步所产生的性能问题,是一种“以空间换时间”的解决方案。
但这并不是说ThreadLocal就是包治百病的万能药了。如果实际的情况不允许我们为每个线程分配一个本地资源副本的话,同步还是非常有意义的。

Continue reading ThreadLocal 使用搞明白

什么时候锻炼好?

最近国内流行锻炼的说法:下午4点到7点最好。

这不不是和我们一贯以来早上锻炼的习惯相反吗?

处于对国内砖家叫兽的了解,我特地查了一下国外的资料,有如下结论:

科学研究表明,下午4点到7点确实是人体较活跃的时间段,但是并不是说这个时间段锻炼就是最适合你的,实际上,你觉得什么时候锻炼适合你,那就是你锻炼的最好时间,贵在坚持!

国外研究表明,经常早上锻炼得人早上肌肉有较好的状态,晚上锻炼得人,晚上肌肉有较好的状态。

锻炼一个小时以内,可吃可不吃,但是一定要喝点水。

一个小时以上者,则一定要吃点食物,喝些水。

 

以下是我个人观点:

从我对中医的领悟来讲,冬天还是太阳出来一小时以后,太阳落山半小时之前这段时间锻炼较好,冬天主藏,不要将自己的身体暴露在肃杀寒气之中。也对应古人“日出而作日入而息”的生活规律。

参见:

http://www.naturallyintense.net/blog/personal-trainer-nyc-articles/when-is-the-best-time-to-exercise/

http://goaskalice.columbia.edu/best-time-day-exercise

Continue reading 什么时候锻炼好?

【转】Apache Rewrite 规则详解(很不错)

转自:http://www.iocblog.net/blog/site/apache-rewrite-all.html

1、Rewrite规则简介:

Rewirte主要的功能就是实现URL的跳转,它的正则表达式是基于Perl语言。可基于服务器级的(httpd.conf)和目录级的 (.htaccess)两种方式。如果要想用到rewrite模块,必须先安装或加载rewrite模块。方法有两种一种是编译apache的时候就直接安装rewrite模块,别一种是编译apache时以DSO模式安装apache,然后再利用源码和apxs来安装rewrite模块。

基于服务器级的(httpd.conf)有两种方法,一种是在httpd.conf的全局下直接利用RewriteEngine on来打开rewrite功能;另一种是在局部里利用RewriteEngine on来打开rewrite功能,下面将会举例说明,需要注意的是,必须在每个virtualhost里用RewriteEngine on来打开rewrite功能。否则virtualhost里没有RewriteEngine on它里面的规则也不会生效。

基于目录级的(.htaccess),要注意一点那就是必须打开此目录的FollowSymLinks属性且在.htaccess里要声明RewriteEngine on。

细讲每一条规则:

RewriteRule

Syntax: RewriteRule Pattern Substitution [flags]

  一条RewriteRule指令,定义一条重写规则,规则间的顺序非常重要。对Apache1.2及以后的版本,模板(pattern)是一个POSIX正则式,用以匹配当前的URL。当前的URL不一定是用记最初提交的URL,因为可能用一些规则在此规则前已经对URL进行了处理。

  对mod_rewrite来说,!是个合法的模板前缀,表示“非”的意思,这对描述“不满足某种匹配条件”的情况非常方便,或用作最后一条默认规则。当使用!时,不能在模板中有分组的通配符,也不能做后向引用。

  当匹配成功后,Substitution会被用来替换相应的匹配,它除了可以是普通的字符串以外,还可以包括:

$N,引用RewriteRule模板中匹配的相关字串,N表示序号,N=0..9

%N,引用最后一个RewriteCond模板中匹配的数据,N表示序号

%{VARNAME},服务器变量

${mapname:key|default},映射函数调用

这些特殊内容的扩展,按上述顺序进行。

  一个URL的全部相关部分都会被Substitution替换,而且这个替换过程会一直持续到所有的规则都被执行完,除非明确地用L标志中断处理过程。

  当susbstitution有”-”前缀时,表示不进行替换,只做匹配检查。

  利用RewriteRule,可定义含有请求串(Query String)的URL,此时只需在Sustitution中加入一个?,表示此后的内容放入QUERY_STRING变量中。如果要清空一个QUERY_STRING变量,只需要以?结束Substitution串即可。

  如果给一个Substitution增加一个http://thishost[:port]的前缀,则mod_rewrite会自动将此前缀去掉。因此,利用http://thisthost做一个无条件的重定向到自己,将难以奏效。要实现这种效果,必须使用R标志。

  Flags是可选参数,当有多个标志同时出现时,彼此间以逗号分隔。

'redirect|R [=code]' (强制重定向)

  给当前的URI增加前缀http://thishost[:thisport]/, 从而生成一个新的URL,强制生成一个外部重定向(external redirection,指生的URL发送到客户端,由客户端再次以新的URL发出请求,虽然新URL仍指向当前的服务器). 如果没有指定的code值,则HTTP应答以状态值302 (MOVED TEMPORARILY),如果想使用300-400(不含400)间的其它值可以通过在code的位置以相应的数字指定,也可以用标志名指定: temp (默认值), permanent, seeother.

  注意,当使用这个标志时,要确实substitution是个合法的URL,这个标志只是在URL前增加http://thishost[:thisport]/前缀而已,重写操作会继续进行。如果要立即将新URL重定向,用L标志来中重写流程。

'forbidden|F' (强制禁止访问URL所指的资源)

  立即返回状态值403 (FORBIDDEN)的应答包。将这个标志与合适的RewriteConds 联合使用,可以阻断访问某些URL。

'gone|G' (强制返回URL所指资源为不存在(gone))

  立即返回状态值410 (GONE)的应答包。用这个标志来标记URL所指的资源永久消失了.

# 'proxy|P' (强制将当前URL送往代理模块(proxy module))

  这个标志,强制将substitution当作一个发向代理模块的请求,并立即将共送往代理模块。因此,必须确保substitution串是一个合法的URI (如, 典型的情况是以http://hostname开头),否则会从代理模块得到一个错误. 这个标志,是ProxyPass指令的一个更强劲的实现,将远程请求(remote stuff)映射到本地服务器的名字空间(namespace)中来。

  注意,使用这个功能必须确保代理模块已经编译到Apache 服务器程序中了. 可以用“httpd -l ”命令,来检查输出中是否含有mod_proxy.c来确认一下。如果没有,而又需要使用这个功能,则需要重新编译``httpd''程序并使用mod_proxy有效。

'last|L' (最后一条规则)

  中止重写流程,不再对当前URL施加更多的重写规则。这相当于perl的last命令或C的break命令。

'next|N' (下一轮)

  重新从第一条重写规则开始执行重写过程,新开的过程中的URL不应当与最初的URL相同。 这相当于Perl的next命令或C的continue命令. 千万小心不要产生死循环。

# 'chain|C' (将当前的规则与其后续规则捆绑(chained))

  当规则匹配时,处理过程与没有捆绑一样;如果规则不匹配,则捆绑在一起的后续规则也不在检查和执行。

'type|T=MIME-type' (强制MIME类型)

  强制将目标文件的MIME-type为某MIME类型。例如,这可用来模仿mod_alias模块对某目录的ScriptAlias指定,通过强制将该目录下的所有文件的类型改为 “application/x-httpd-cgi”.

'nosubreq|NS' (used only if no internal sub-request )

  这个标志强制重写引擎跳过为内部sub-request的重写规则.例如,当mod_include试图找到某一目录下的默认文件时 (index.xxx),sub-requests 会在Apache内部发生. Sub-requests并非总是有用的,在某些情况下如果整个规则集施加到它上面,会产生错误。利用这个标志可排除执行一些规则。

'nocase|NC' (模板不区分大小写)

  这个标志会使得模板匹配当前URL时忽略大小写的差别。

'qsappend|QSA' (追加请求串(query string))

  这个标志,强制重写引擎为Substitution的请求串追加一部分串,则不是替换掉原来的。借助这个标志,可以使用一个重写规则给请求串增加更多的数据。

'noescape|NE' (不对输出结果中的特殊字符进行转义处理)

  通常情况下,mod_write的输出结果中,特殊字符(如'%', '$', ';', 等)会转义为它们的16进制形式(如分别为'%25', '%24', and '%3B')。这个标志会禁止mod_rewrite对输出结果进行此类操作。 这个标志只能在 Apache 1.3.20及以后的版本中使用。

'passthrough|PT' (通过下一个处理器)

  这个标志强制重写引擎用filename字段的值来替换内部request_rec数据结构中uri字段的值。. 使用这个标志,可以使后续的其它URI-to-filename转换器的Alias、ScriptAlias、Redirect等指令,也能正常处理RewriteRule指令的输出结果。用一个小例子来说明它的语义:如果要用mod_rewrite的重写引擎将/abc转换为/def,然后用mod_alas将/def重写为ghi,则要:

RewriteRule ^/abc(.*) /def$1 [PT]

Alias /def /ghi

如果PT标志被忽略,则mod_rewrite也能很好完成工作,如果., 将 uri=/abc/... 转换为filename=/def/... ,完全符合一个URI-to-filename转换器的动作。接下来 mod_alias 试图做 URI-to-filename 转换时就会出问题。

注意:如果要混合都含有URL-to-filename转换器的不同的模块的指令,必须用这个标志。最典型的例子是mod_alias和mod_rewrite的使用。

'skip|S=num' (跳过后面的num个规则)

  当前规则匹配时,强制重写引擎跳过后续的num个规则。用这个可以来模仿if-then-else结构:then子句的最后一条rule的标志是skip=N,而N是else子句的规则条数。

'env|E=VAR:VAL' (设置环境变量)

  设置名为VAR的环境变量的值为VAL,其中VAL中可以含有正则式的后向引用($N或%N)。这个标志可以使用多次,以设置多个环境变量。这儿设置的变量,可以在多种情况下被引用,如在XSSI或CGI中。另外,也可以在RewriteCond模板中以%{ENV:VAR}的形式被引用。

注意:一定不要忘记,在服务器范围内的配置文件中,模板(pattern)用以匹配整个URL;而在目录范围内的配置文件中,目录前缀总是被自动去掉后再进行模板匹配的,且在替换完成后自动再加上这个前缀。这个功能对很多种类的重写是非常重要的,因为如果没有去前缀,则要进行父目录的匹配,而父目录的信息并不是总能得到的。一个例外是,当substitution中有http://打头时,则不再自动增加前缀了,如果P标志出现,则会强制转向代理。

注意:如果要在某个目录范围内启动重写引擎,则需要在相应的目录配置文件中设置“RewriteEngine on”,且目录的“Options FollowSymLinks”必须设置。如果管理员由于安全原因没有打开FollowSymLinks,则不能使用重写引擎。

2、举例说明:

下面是在一个虚拟主机里定义的规则。功能是把client请求的主机前缀不是www.colorme.com和203.81.23.202都跳转到主机前缀为http://www.colorme.com.cn,避免当用户在地址栏写入http://colorme.com.cn时不能以会员方式登录网站。

NameVirtualHost 192.168.100.8:80

ServerAdmin webmaster@colorme.com.cn
DocumentRoot "/web/webapp"
ServerName www.colorme.com.cn
ServerName colorme.com.cn
RewriteEngine on #打开rewirte功能
RewriteCond %{HTTP_HOST} !^www.colorme.com.cn [NC] #声明Client请求的主机中前缀不是www.colorme.com.cn,[NC]的意思是忽略大小写
RewriteCond %{HTTP_HOST} !^203.81.23.202 [NC] #声明Client请求的主机中前缀不是203.81.23.202,[NC]的意思是忽略大小写
RewriteCond %{HTTP_HOST} !^$ #声明Client请求的主机中前缀不为空,[NC]的意思是忽略大小写
RewriteRule ^/(.*) http://www.colorme.com.cn/ [L]
#含义是如果Client请求的主机中的前缀符合上述条件,则直接进行跳转到http://www.colorme.com.cn/,[L]意味着立即停止重写操作,并不再应用其他重写规则。这里的.*是指匹配所有URL中不包含换行字符,()括号的功能是把所有的字符做一个标记,以便于后面的应用.就是引用前面里的(.*)字符。

例二.将输入 folio.test.com 的域名时跳转到profile.test.com

listen 8080
NameVirtualHost 10.122.89.106:8080
ServerAdmin webmaster@colorme.com.cn
DocumentRoot "/usr/local/www/apache22/data1/"
ServerName profile.test.com
RewriteEngine on
RewriteCond %{HTTP_HOST} ^folio.test.com [NC]
RewriteRule ^/(.*) http://profile.test.com/ [L]

3.Apache mod_rewrite规则重写的标志一览

1) R[=code](force redirect) 强制外部重定向
强制在替代字符串加上http://thishost[:thisport]/前缀重定向到外部的URL.如果code不指定,将用缺省的302 HTTP状态码。
2) F(force URL to be forbidden)禁用URL,返回403HTTP状态码。
3) G(force URL to be gone) 强制URL为GONE,返回410HTTP状态码。
4) P(force proxy) 强制使用代理转发。
5) L(last rule) 表明当前规则是最后一条规则,停止分析以后规则的重写。
6) N(next round) 重新从第一条规则开始运行重写过程。
7) C(chained with next rule) 与下一条规则关联

如果规则匹配则正常处理,该标志无效,如果不匹配,那么下面所有关联的规则都跳过。

8) T=MIME-type(force MIME type) 强制MIME类型
9) NS (used only if no internal sub-request) 只用于不是内部子请求
10) NC(no case) 不区分大小写
11) QSA(query string append) 追加请求字符串
12) NE(no URI escaping of output) 不在输出转义特殊字符
例如:RewriteRule /foo/(.*) /bar?arg=P1\%3d$1 [R,NE] 将能正确的将/foo/zoo转换成/bar?arg=P1=zed
13) PT(pass through to next handler) 传递给下一个处理
例如:
   RewriteRule ^/abc(.*) /def$1 [PT] # 将会交给/def规则处理
   Alias /def /ghi
14) S=num(skip next rule(s)) 跳过num条规则
15) E=VAR:VAL(set environment variable) 设置环境变量

4.Apache rewrite例子集合

   在 httpd 中将一个域名转发到另一个域名虚拟主机世界近期更换了域名,新域名为 www.wbhw.com, 更加简短好记。这时需要将原来的域名webhosting-world.com, 以及论坛所在地址 webhosting-world.com/forums/定向到新的域名,以便用户可以找到,并且使原来的论坛 URL 继续有效而不出现 404 未找到,比如原来的http://www.webhosting-world.com/forums/-f60.html, 让它在新的域名下继续有效,点击后转发到http://bbs.wbhw.com/-f60.html, 这就需要用 apache 的 Mod_rewrite 功能来实现。

在中添加下面的重定向规则:

RewriteEngine On
# Redirect webhosting-world.com/forums to bbs.wbhw.com
RewriteCond %{REQUEST_URI} ^/forums/
RewriteRule /forums/(.*) http://bbs.wbhw.com/$1 [R=permanent,L]
# Redirect webhosting-world.com to wbhw.com
RewriteCond %{REQUEST_URI} !^/forums/
RewriteRule /(.*) http://www.wbhw.com/$1 [R=permanent,L]

添加了上面的规则以后, 里的全部内容如下:

ServerAlias webhosting-world.com
ServerAdmin admin@webhosting-world.com
DocumentRoot /path/to/webhosting-world/root
ServerName www.webhosting-world.com
RewriteEngine On
# Redirect webhosting-world.com/forums to bbs.wbhw.com
RewriteCond %{REQUEST_URI} ^/forums/
RewriteRule /forums/(.*) http://bbs.wbhw.com/$1 [R=permanent,L]
# Redirect webhosting-world.com to wbhw.com
RewriteCond %{REQUEST_URI} !^/forums/
RewriteRule /(.*) http://www.wbhw.com/$1 [R=permanent,L]

URL重定向

例子一:

1.http://www.zzz.com/xxx.php-> http://www.zzz.com/xxx/
2.http://yyy.zzz.com-> http://www.zzz.com/user.php?username=yyy 的功能
RewriteEngine On
RewriteCond %{HTTP_HOST} ^www.zzz.com
RewriteCond %{REQUEST_URI} !^user\.php$
RewriteCond %{REQUEST_URI} \.php$
RewriteRule (.*)\.php$ http://www.zzz.com/$1/ [R]
RewriteCond %{HTTP_HOST} !^www.zzz.com
RewriteRule ^(.+) %{HTTP_HOST} [C]
RewriteRule ^([^\.]+)\.zzz\.com http://www.zzz.com/user.php?username=$1

例子二:

/type.php?typeid=*   --> /type*.html
/type.php?typeid=*&page=*   --> /type*page*.html
RewriteRule ^/type([0-9]+).html$ /type.php?typeid=$1 [PT]
RewriteRule ^/type([0-9]+)page([0-9]+).html$ /type.php?typeid=$1&page=$2 [PT]

5.使用Apache的URL Rewrite配置多用户虚拟服务器

   要实现这个功能,首先要在DNS服务器上打开域名的泛域名解析(自己做或者找域名服务商做)。比如,我就把 *.semcase.com和 *.semcase.cn全部解析到了我的这台Linux Server上。

然后,看一下我的Apache中关于*.semcase.com的虚拟主机的设定。

#*.com,*.osall.net

ServerAdmin webmaster@semcase.com
DocumentRoot /home/www/www.semcase.com
ServerName dns.semcase.com
ServerAlias dns.semcase.com semcase.com semcase.net *.semcase.com *.semcase.net
CustomLog /var/log/httpd/osa/access_log.log" common
    ErrorLog /var/log/httpd/osa/error_log.log"
AllowOverride None
Order deny,allow
#AddDefaultCharset GB2312   
RewriteEngine on      
RewriteCond %{HTTP_HOST}        ^[^.]+\.osall\.(com|net)$      
RewriteRule ^(.+)     %{HTTP_HOST}$1   [C]      
RewriteRule ^([^.]+)\.osall\.(com|net)(.*)$
/home/www/www.semcase.com/sylvan$3?un=$1&%{QUERY_STRING} [L]

在这段设定中,我把*.semcase.net和*.semcase.com 的Document Root都设定到了 /home/www/www.semcase.com

但是,继续看下去,看到...配置了吗?在这里我就配置了URL Rewrite规则。
RewriteEngine on #打开URL Rewrite功能
RewriteCond %{HTTP_HOST} ^[^.]+.osall.(com|net)$ #匹配条件,如果用户输入的URL中主机名是类似 xxxx.semcase.com 或者 xxxx.semcase.cn 就执行下面一句
RewriteRule ^(.+) %{HTTP_HOST}$1 [C] #把用户输入完整的地址(GET方式的参数除外)作为参数传给下一个规则,[C]是Chain串联下一个规则的意思
RewriteRule ^([^.]+).osall.(com|net)(.*)$ /home/www/dev.semcase.com/sylvan$3?un=$1&%{QUERY_STRING} [L]
# 最关键的是这一句,使用证则表达式解析用户输入的URL地址,把主机名中的用户名信息作为名为un的参数传给/home/www /dev.semcase.com目录下的脚本,并在后面跟上用户输入的GET方式的传入参数。并指明这是最后一条规则([L]规则)。注意,在这一句中指明的重写后的地址用的是服务器上的绝对路径,这是内部跳转。如果使用http://xxxx这样的URL格式,则被称为外部跳转。使用外部跳转的话,浏览着的浏览器中的URL地址会改变成新的地址,而使用内部跳转则浏览器中的地址不发生改变,看上去更像实际的二级域名虚拟服务器。

这样设置后,重启Apache服务器,测试一下,就大功告成了!

Continue reading 【转】Apache Rewrite 规则详解(很不错)

Pagination


Total views.

© 2013 - 2024. All rights reserved.

Powered by Hydejack v6.6.1