memcached并发CAS方式

memcached并发CAS模式应用场景分析:如原来MEMCACHED中的KES的内容为A,客户端C1和客户端C2都把A取了出来,C1

memcached并发CAS模式

应用场景分析:
如原来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。

<?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 是我这边验证码的文件名,这边可以稍作调整。用回调函数来加载,这样比较灵活。遇到是验证码,程序会去自动重试。

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

1 楼 因为青所以涩 2012-06-07   写的好详细啊,受教了,另外请教一个问题,文章中提到的
第一步,A取出数据对象X,并获取到CAS-ID1;
第二步,B取出数据对象X,并获取到CAS-ID2;
这CAS-ID1是不是等于CAS-ID2?