Memcached的CAS机制的实现

本文介绍了Memcached的CAS机制,包括其实现的原子性Check-and-Set操作、工作流程和内部实现细节,如版本号存储、选项控制、内存分配等。通过CAS,Memcached能解决分布式环境下的并发冲突,防止数据的脏写。文章还讨论了不采用CAS可能产生的问题,并对比分析了使用CAS的解决方案。
摘要由CSDN通过智能技术生成

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

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值