`

memcached并发CAS模式

cas 
阅读更多

http://hudeyong926.iteye.com/blog/1463992

应用场景分析:http://hudeyong926.iteye.com/blog/1172189

如原来MEMCACHED中的KES的内容为A,客户端C1和客户端C2都把A取了出来,C1往准备往其中加B,C2准备往其中加C,这就会造成C1和C2执行后的CACHE KEYS要么是AB要么是AC,而不会出现我们期望的ABC。这种情况,如果不是在集群环境中,而只是单机服务器,可以通过在写CACHE KEYS时增加同步锁,就可以解决问题,可是在集群环境中,同步锁是显然解决不了问题的。

memcached是原子的吗?宏观
所有的被发送到memcached的单个命令是完全原子的。如果您针对同一份数据同时发送了一个set命令和一个get命令,它们不会影响对方。它们将被串行化、先后执行。即使在多线程模式,所有的命令都是原子的;命令序列不是原子的。如果您通过get命令获取了一个item,修改了它,然后想把它set回memcached,我们不保证这个item没有被其他进程(process,未必是操作系统中的进程)操作过。在并发的情况下,您也可能覆写了一个被其他进程set的item。
memcached 1.2.5以及更高版本,提供了gets和cas命令,它们可以解决上面的问题。如果您使用gets命令查询某个key的item,memcached会 给您返回该item当前值的唯一标识。如果您覆写了这个item并想把它写回到memcached中,您可以通过cas命令把那个唯一标识一起发送给 memcached。如果该item存放在memcached中的唯一标识与您提供的一致,您的写操作将会成功。如果另一个进程在这期间也修改了这个 item,那么该item存放在memcached中的唯一标识将会改变,您的写操作就会失败。

 

memcached保存的key value都有一个唯一标识casUnique,在进行incr decr操作时,首先获取casUnique,执行incr,检验返回值是否casUnique+1,如果是,则更新,否则,失败不更新!
尽管这种设计在处理并发时还存在缺陷,但可以通过简单的重试来解决问题!

最初的解决方案:
计划利用memcached的add操作的原子性来控制并发,具体方式如下:
1.申请锁:在校验是否创建过活动前,执行add操作key为memberId,如果add操作失败,则表示有另外的进程在并发的为该memberId创建活动,返回创建失败。否则表示无并发
2.执行创建活动
3.释放锁:创建活动完成后,执行delete操作,删除该memberId。

Java代码 复制代码 收藏代码
  1. if (memcache.get(key) == null) {  
  2.     // 3 min timeout to avoid mutex holder crash  
  3.     if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {  
  4.         value = db.get(key);  
  5.         memcache.set(key, value);  
  6.         memcache.delete(key_mutex);  
  7.     } else {  
  8.         sleep(50);  
  9.         retry();  
  10.     }  
  11. }  
if (memcache.get(key) == null) {
    // 3 min timeout to avoid mutex holder crash
    if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
        value = db.get(key);
        memcache.set(key, value);
        memcache.delete(key_mutex);
    } else {
        sleep(50);
        retry();
    }
}

问题:
如此实现存在一些问题:
1.memcached中存放的值有有效期,即过期后自动失效,如add过M1后,M1失效,可以在此add成功
2.即使通过配置,可以使memcached永久有效,即不设有效期,memcached有容量限制,当容量不够后会进行自动替换,即有可能add过M1后,M1被其他key值置换掉,则再次add可以成功。
3.此外,memcached是基于内存的,掉电后数据会全部丢失,导致重启后所有memberId均可重新add。

应对问题:
针对上述的几个问题,根本原因是add操作有时效性,过期,被替换,重启,都会是原来的add操作失效。解决该问题有方法
1.减轻时效性的影响,使用memcached CAS(check and set)方式。

CAS的基本原理
基本原理非常简单,一言以蔽之,就是“版本号”。每个存储的数据对象,多有一个版本号。我们可以从下面的例子来理解:

如果不采用CAS,则有如下的情景:
第一步,A取出数据对象X;
第二步,B取出数据对象X;
第三步,B修改数据对象X,并将其放入缓存;
第四步,A修改数据对象X,并将其放入缓存。
我们可以发现,第四步中会产生数据写入冲突。

如果采用CAS协议,则是如下的情景。
第一步,A取出数据对象X,并获取到CAS-ID1;
第二步,B取出数据对象X,并获取到CAS-ID2;
第三步,B修改数据对象X,在写入缓存前,检查CAS-ID与缓存空间中该数据的CAS-ID是否一致。结果是“一致”,就将修改后的带有CAS-ID2的X写入到缓存。
第四步,A修改数据对象Y,在写入缓存前,检查CAS-ID与缓存空间中该数据的CAS-ID是否一致。结果是“不一致”,则拒绝写入,返回存储失败。

 

memcached中除了add操作是原子的,还有另外两个操作也是原子的:incr和decr ,使用CAS模式即:
 1.预先在memcached中设置一个key值,假设为CREATKEY=1
 2.每次创建活动时,在规则校验前先get出CREATEKEY=x;
 3.进行规则校验
 4.执行incr CREATEKEY操作,检验返回值是否为所期望的x+1,如果不是,则说明在此期间有另外的进程执行了incr操作,即存在并发,放弃更新。否则
 5.执行创建活动
 这种方法可能存在误判,即本来不存在并发,却被判为并发,如缓存重启,或key值失效后,incr值可能不同于期望值,导致误判。
 而使用memcached的CAS方式,可以以几乎0成本的方式解决时效性问题,尽管存在一点小缺陷,但这种缺陷可以通过简单的重试即可解决。考虑实际的产出比,采用memcached的CAS方式更适合实际情况。

具体实现:

1.由于需要使用cas方法,php的memcache客户端不支持该方法,所以改用php的memcached客户端 。这个东西还是很强大的,它所使用的libmemcached库是各种语言都支持的。(需要额外安装编译,可以使用PECL装,或手动编译)

2. 去除session.save_handler 的设置,然后使用:

Java代码 复制代码 收藏代码
  1. session_set_save_handler(array("SessionMemd","open"),array("SessionMemd","close"),array("SessionMemd","read"),array("SessionMemd","write"),array("SessionMemd","destroy"),array("SessionMemd","gc"));  
session_set_save_handler(array("SessionMemd","open"),array("SessionMemd","close"),array("SessionMemd","read"),array("SessionMemd","write"),array("SessionMemd","destroy"),array("SessionMemd","gc"));

SessionMemd的类如下:

Java代码 复制代码 收藏代码
  1. <?php  
  2. /** 
  3. * 使用Memcached处理session,纯静态类 
  4. * @author yuyii 2010/3/12 
  5. */  
  6. class SessionMemd {  
  7.     private static $_sess;  
  8.     private static $cas;  
  9.     const RETRY = 3;    //重试次数  
  10.     public static $maxlife = 3600;  
  11.   
  12.     private function __construct() {}  
  13.   
  14.     public static function open($save_path) {  
  15.      self::$_sess = new Memcached();  
  16.      $servers = explode(",",$save_path);  
  17.      foreach($servers as &$v) {  
  18.          if (preg_match('!tcp://([^:]*):(\d{4,5})!',$v,$match)) {  
  19.              self::$_sess->addServer($match[1],$match[2]);  
  20.          }  
  21.      }  
  22.      self::$_sess->setOption(Memcached::OPT_DISTRIBUTION, Memcached::DISTRIBUTION_CONSISTENT);  
  23.      self::$_sess->setOption(Memcached::OPT_HASH, Memcached::HASH_CRC);  
  24.      return true;  
  25.     }  
  26.   
  27.     public static function close() {  
  28.        return true;  
  29.     }  
  30.   
  31.     public static function read($id) {  
  32.      $ret = self::$_sess->get($id,null,$cas);  
  33.      self::$cas = $cas;  
  34.      return $ret;  
  35.     }  
  36.   
  37.     public static function write($id, $sess_data) {  
  38.      if (empty(self::$cas)) {  
  39.          $ret = self::$_sess->set($id,$sess_data,self::$maxlife);  
  40.      }  
  41.      else {  
  42.          $ret = self::$_sess->cas(self::$cas,$id,$sess_data,self::$maxlife); //该方法对于空值会写失败  
  43.      }  
  44.   
  45.      if ($ret === false) {  
  46.          /** 
  47.           * 处理验证码失败的情况,让其重试 
  48.           */  
  49.          if ($_SERVER['SCRIPT_NAME'] == "/captcha.php") {  
  50.              $i = 0;  
  51.              while($i++ < self::RETRY) {  
  52.                  $ret = self::$_sess->get($id,null,$cas);  
  53.                  if (self::$_sess->cas($cas,$id,$sess_data,self::$maxlife)) return true;  
  54.                  usleep(1000);  
  55.              }  
  56.          }  
  57.          return false;  
  58.      }  
  59.      else {  
  60.          return true;  
  61.      }  
  62.     }  
  63.   
  64.     public static function destroy($id) {  
  65.      return self::$_sess->delete($id);  
  66.     }  
  67.   
  68.     public static function gc() {  
  69.        return true;  
  70.     }  
  71. }  
  72.   
  73. //获取设置的生命周期值  
  74. SessionMemd::$maxlife = ini_get("session.gc_maxlifetime");    
  75. ?>   
<?php
/**
* 使用Memcached处理session,纯静态类
* @author yuyii 2010/3/12
*/
class SessionMemd {
	private static $_sess;
	private static $cas;
	const RETRY = 3;    //重试次数
	public static $maxlife = 3600;

	private function __construct() {}

	public static function open($save_path) {
	 self::$_sess = new Memcached();
	 $servers = explode(",",$save_path);
	 foreach($servers as &$v) {
		 if (preg_match('!tcp://([^:]*):(\d{4,5})!',$v,$match)) {
			 self::$_sess->addServer($match[1],$match[2]);
		 }
	 }
	 self::$_sess->setOption(Memcached::OPT_DISTRIBUTION, Memcached::DISTRIBUTION_CONSISTENT);
	 self::$_sess->setOption(Memcached::OPT_HASH, Memcached::HASH_CRC);
	 return true;
	}

	public static function close() {
	   return true;
	}

	public static function read($id) {
	 $ret = self::$_sess->get($id,null,$cas);
	 self::$cas = $cas;
	 return $ret;
	}

	public static function write($id, $sess_data) {
	 if (empty(self::$cas)) {
		 $ret = self::$_sess->set($id,$sess_data,self::$maxlife);
	 }
	 else {
		 $ret = self::$_sess->cas(self::$cas,$id,$sess_data,self::$maxlife); //该方法对于空值会写失败
	 }

	 if ($ret === false) {
		 /**
		  * 处理验证码失败的情况,让其重试
		  */
		 if ($_SERVER['SCRIPT_NAME'] == "/captcha.php") {
			 $i = 0;
			 while($i++ < self::RETRY) {
				 $ret = self::$_sess->get($id,null,$cas);
				 if (self::$_sess->cas($cas,$id,$sess_data,self::$maxlife)) return true;
				 usleep(1000);
			 }
		 }
		 return false;
	 }
	 else {
		 return true;
	 }
	}

	public static function destroy($id) {
	 return self::$_sess->delete($id);
	}

	public static function gc() {
	   return true;
	}
}

//获取设置的生命周期值
SessionMemd::$maxlife = ini_get("session.gc_maxlifetime");  
?> 

其中captcha.php 是我这边验证码的文件名,这边可以稍作调整。用回调函数来加载,这样比较灵活。遇到是验证码,程序会去自动重试。

总结:存数据库也会遇到同样的情况,但是数据库锁起来会更方便

分享到:
评论

相关推荐

    Memcached CAS 命令

    Memcached CAS(Check-And-Set 或 Compare-And-Swap)命令是分布式内存缓存系统Memcached中的一个重要特性,主要用于确保并发环境下的数据一致性。在多客户端共享同一个数据存储时,CAS命令允许客户端以原子的方式...

    php实现的memcached队列类

    在PHP编程环境中,Memcached是一...此外,对于并发控制,还需要了解如锁机制(如Memcached本身的CAS操作)来确保数据的一致性和完整性。总之,这个PHP Memcached队列类为构建高性能、高并发的Web应用提供了有力的支持。

    memcached源代码下载.rar

    - **命令处理**:`memcached`支持GET、SET、DELETE等基本操作,以及CAS(Check and Set)操作,确保数据一致性。 **4. EnyimMemcached** `EnyimMemcached-11226.zip`是一个.NET客户端库,用于与`memcached`服务器...

    Memcached Redis MongoDB对比

    Memcached在并发场景下使用CAS(Compare And Swap)操作保证数据一致性。Redis的事务支持较弱,其仅保证事务中的每个操作连续执行。MongoDB不支持事务,但可以利用单个文档的原子操作保证数据的一致性。 数据分析:...

    Memcached源码剖析笔记

    - `gets`:获取键对应的值及其CAS值(用于处理并发更新问题)。 ##### 删除命令 - `delete`:删除指定的键值对。 ##### 高级命令 - `incr`/`decr`:对指定键对应的数值型值进行增加或减少操作。 - `flush_all`:...

    Memcached内存分析、调优、集群

    Memcached作为一款高性能的分布式内存缓存服务器,其在内存管理、分布式策略和跨语言支持方面表现出色,是构建高并发、低延迟应用的理想选择。通过对Memcached内部机制的深入理解和合理调优,可以充分发挥其在大规模...

    memcached缓存很全的文档及说明

    此外,还支持多键操作如`gets`(获取键的同时校验CAS值,用于防止并发问题)。 **4. 分布式存储与一致性哈希** Memcached的分布式特性源于客户端的分发策略,其中最常见的是一致性哈希算法。该算法通过计算哈希值...

    memcached-1.2.1-win32.rar

    - Memcached基于键值对存储,客户端将数据以键值对的形式发送到服务器,服务器负责存储并返回一个唯一的标识(称为CAS,Check and Set),用于乐观锁操作。 3. **分布式特性** - 数据分布是基于一致性哈希,使得...

    java_memcached-release_2.6.6.rar

    Java Memcached客户端通常支持CAS(Compare and Swap)操作,这是一种无锁算法,可以保证在多线程环境下的数据一致性。 7. **性能优化**:为了提高性能,Java Memcached客户端可能会采用批量操作,如批量设置、获取...

    java连接memcached示例代码

    Java连接Memcached是一种常见的缓存操作,特别是在处理大数据量或者需要快速响应的Web应用程序中。Memcached是一款高性能、分布式内存对象...结合合理的缓存策略和分布式部署,Memcached能在高并发场景下发挥重要作用。

    缓存服务器memcached代码及使用文档

    - **数据分片**:根据业务需求,可以将数据分散到多个Memcached实例上,以提高并发性能和整体容量。 - **缓存一致性**:在分布式环境中,需要考虑缓存与数据库的一致性问题,如使用缓存失效策略或更新时的CAS...

    memcached-jar

    4. **缓存操作**: 使用客户端API进行数据的set、get、delete等操作,还可以使用cas操作保证并发环境下的数据一致性。 5. **过期策略**: 可以为每个缓存项设置生命周期,到期后自动从缓存中移除。 6. **异常处理**: ...

    MemcachedDemo----分布式缓存系统C#应用实例

    此外,Memcached支持多种操作,如删除缓存项(`Delete`方法)、检查是否存在(`GetCas`方法配合` Cas`操作)以及批量操作(`AddBulk`、`SetBulk`等)。你还可以设置自定义的过期时间,或者使用`Slabs`机制来管理内存...

    Memcached Study

    基于libevent的事件驱动模型,使得Memcached能够处理大量并发连接。服务器内部维护一个哈希表,用于快速定位和检索缓存数据。各服务器之间相互独立,不了解彼此的存在,这简化了架构并提高了系统的灵活性。 客户端...

Global site tag (gtag.js) - Google Analytics