Memcached的CAS机制的实现

来源:互联网 发布:淘宝虚拟物品怎么收货 编辑:程序博客网 时间:2024/06/06 14:02

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();  }}

 

0 0
原创粉丝点击