Memcached的CAS机制解决的问题及其原理:
1. 实现了Check-and-Set原子操作功能;
2. 其使用方式为:首先使用gets指令一个key-value及key对应value的版本号;其次操作产生新的value值;最后使用cas指令重新提交key-value,并附带刚刚获得到的版本号;
3. 当服务端判断cas操作中的版本号不是最新的时,则认为改key的值已经被修改,本次cas操作失败。程序设计人员通过CAS机制可实现自增和自减的原子操作;
Memcached的CAS机制的实现:
1. Memcached CAS的核心是一个64-bit唯一的版本。服务端会为每个key生成一个64-bit唯一的整数值作为版本号,并保存在item结构体中,具体的存储结构见结构体item的定义:
typedef struct _stritem {
struct _stritem *next;
struct _stritem *prev;
struct _stritem *h_next; /* hash chain next */
rel_time_t time; /* least recent access */
rel_time_t exptime; /* expire time */
int nbytes; /* size of data */
unsigned short refcount;
uint8_t nsuffix; /* length of flags-and-length string */
uint8_t it_flags; /* ITEM_* above */
uint8_t slabs_clsid;/* which slab class we're in */
uint8_t nkey; /* key length, w/terminating null and padding */
/* this odd type prevents type-punning issues when we do
* the little shuffle to save space when not using CAS. */
union {
uint64_t cas;
char end;
} data[];
/* if it_flags & ITEM_CAS we have 8 bytes CAS */
/* then null-terminated key */
/* then " flags length\r\n" (no terminating null) */
/* then data with terminating \r\n (no terminating null; it's binary!) */
} item;
cas的版本号的值存储在item中key之前的位置处。
2. Memcached CAS的开启和关闭由settings.use_cas选项来控制。结构体struct settings存储了Memcached服务器的各种选项存储,其定义的代码如下:
struct settings {
size_t maxbytes;
int maxconns;
int port;
int udpport;
char *inter;
int verbose;
rel_time_t oldest_live; /* ignore existing items older than this */
int evict_to_free;
char *socketpath; /* path to unix socket if using local socket */
int access; /* access mask (a la chmod) for unix domain socket */
double factor; /* chunk size growth factor */
int chunk_size;
int num_threads; /* number of worker (without dispatcher) libevent threads to run */
int num_threads_per_udp; /* number of worker threads serving each udp socket */
char prefix_delimiter; /* character that marks a key prefix (for stats) */
int detail_enabled; /* nonzero if we're collecting detailed stats */
int reqs_per_event; /* Maximum number of io to process on each
io-event. */
bool use_cas;
enum protocol binding_protocol;
int backlog;
int item_size_max; /* Maximum item size, and upper end for slabs */
bool sasl; /* SASL on/off */
bool maxconns_fast; /* Whether or not to early close connections */
bool slab_reassign; /* Whether or not slab reassignment is allowed */
bool slab_automove; /* Whether or not to automatically move slabs */
int hashpower_init; /* Starting hash power level */
};
默认情况下settings.use_cas的值为true,在settings_init调用的时候进行初始化设置:
static void settings_init(void) {
settings.use_cas = true;
... ...
}
当启动Memcached服务时,如果指定了-C选项,则关闭cas机制,改部分代码在main函数中:
int main (int argc, char **argv) {
... ...
case 'C' :
settings.use_cas = false;
break;
... ...
}
3. Memcached在分配item的内存时会根据是否开启settings.use_cas选项来分配内存和设置item->flags:
item *do_item_alloc(char *key, const size_t nkey, const int flags, const rel_time_t exptime, const int nbytes) {
... ...
if (settings.use_cas) {
ntotal += sizeof(uint64_t);
}
... ...
it->it_flags = settings.use_cas ? ITEM_CAS : 0;
... ...
}
4. Memcached CAS机制的相关实现:
在Memcached中,所有的命令处理逻辑在process_command中实现,该函数的实现根据不同的命令又细分为不同的处理函数。
gets指令调用process_get_command,该函数当最后的参数为true时,代表处理带cas的get操作,会在response中包含cas的值。
cas指令调用process_update_command,该函数当最后的参数true代表,处理带cas的update操作,最终会调用do_store_item函数来处理update操作。相关的cas的逻辑代码有:
/* validate cas operation */
if(old_it == NULL) {
// LRU expired
stored = NOT_FOUND;
pthread_mutex_lock(&c->thread->stats.mutex);
c->thread->stats.cas_misses++;
pthread_mutex_unlock(&c->thread->stats.mutex);
}
else if (ITEM_get_cas(it) == ITEM_get_cas(old_it)) {
// cas validates
// it and old_it may belong to different classes.
// I'm updating the stats for the one that's getting pushed out
pthread_mutex_lock(&c->thread->stats.mutex);
c->thread->stats.slab_stats[old_it->slabs_clsid].cas_hits++;
pthread_mutex_unlock(&c->thread->stats.mutex);
item_replace(old_it, it, hv);
stored = STORED;
} else {
pthread_mutex_lock(&c->thread->stats.mutex);
c->thread->stats.slab_stats[old_it->slabs_clsid].cas_badval++;
pthread_mutex_unlock(&c->thread->stats.mutex);
if(settings.verbose > 1) {
fprintf(stderr, "CAS: failure: expected %llu, got %llu\n",
(unsigned long long)ITEM_get_cas(old_it),
(unsigned long long)ITEM_get_cas(it));
}
stored = EXISTS;
}
cas指令的cas版本值的递增在do_item_link函数中完成:
int do_item_link(item *it, const uint32_t hv) {
... ...
/* Allocate a new CAS ID on link. */
ITEM_set_cas(it, (settings.use_cas) ? get_cas_id() : 0);
... ...
}
5. 其它:
除了显式的gets和cas操作为,incr/decr操作也会使用cas机制;
append/prepend操作也会涉及到cas的相关操作;
最近笔者自己的项目中,遇到了乐观锁的需求。memcache天然的支持这种并发原语,即:GETS和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是否一致。结果是“不一致”,则拒绝写入,返回存储失败。
我们可以通过重试,或者其他业务逻辑解决第四步设置失败的问题。
没有CAS的方案
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();
}
}
最初的解决方案:
利用memcached的add操作的原子性来控制并发,具体方式如下:
1.申请锁:在校验是否创建过活动前,执行add操作key为key,如果add操作失败,则表示有另外的进程在并发的为该key创建活动,返回创建失败。否则表示无并发
2.执行创建活动
3.释放锁:创建活动完成后,执行delete操作,删除该key。
问题:
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的基本原理
基本原理非常简单,一言以蔽之,就是“版本号”。每个存储的数据对象,多有一个版本号。我们可以从下面的例子来理解:
package com.home.phl;
import net.rubyeye.xmemcached.CASOperation;
import net.rubyeye.xmemcached.GetsResponse;
import net.rubyeye.xmemcached.MemcachedClient;
import net.rubyeye.xmemcached.MemcachedClientBuilder;
import net.rubyeye.xmemcached.XMemcachedClientBuilder;
import net.rubyeye.xmemcached.command.BinaryCommandFactory;
import net.rubyeye.xmemcached.utils.AddrUtil;
/**
* 参考文章
* @author piaohailin
* @date 2014-6-28
*/
public class TestCAS {
public static void main(String[] args) throws Exception {
MemcachedClientBuilder builder = new XMemcachedClientBuilder(AddrUtil.getAddresses("192.168.56.3:11211"));
builder.setCommandFactory(new BinaryCommandFactory());
MemcachedClient memcachedClient = builder.build();
memcachedClient.set("key", 0, value);
final GetsResponse<Object> response = memcachedClient.gets("key");
System.out.println(response.getValue());
System.out.println(response.getCas());
boolean flag = memcachedClient.cas("key", new CASOperation<Object>() {
@Override
public int getMaxTries() {
return 3;
}
@Override
public Object getNewValue(long currentCAS,
Object currentValue) {
if (currentCAS != response.getCas()) {
throw new RuntimeException("CAS不对");
}
value = db.get(key);
return value;
}
});
System.out.println(flag);
memcachedClient.shutdown();
}
}