场景
生成唯一的准考证号,为日期+六位字符(大写字母+数字),如170808ABC123,并希望生成的准考证号是不连续的
问题
这里简化为返回一个唯一的数字,之前的解决方案是:
- 第一次请求,返回一个固定的开始数字 + rand(1, 30),并将数字存入memcache
- 之后的请求,每次先用memcached->get()读取memcache中上一次的数字,然后加上rand(1, 30),作为本次生成的数字返回,并更新memcache
#伪代码#
$conn = new Memcached();
$conn->addServer("localhost", 11211);
$key = '_lock_test_long_';
$num = $conn->get($key);
if($conn->getResultCode() == Memcached::RES_NOTFOUND){
$num = NUM_START;
}
$num += rand(1, 30);
$conn->set($key, $num);
return $num;
这个做法的问题是,并发场景下,两个并发请求从 memcached->get() 中获取了同一个数,然后rand(1,30)也碰巧相同了,则此时两个请求返回了相同的数字,造成生成准考证号重复。
解决
至少有三种解决方法:
A. 使用Memcached::cas方法,get()的同时获取key的cas_token,然后累加随机数,最后更新memcache时使用cas(),如果更新失败,说明这个key已经在当前请求的get()后发生了更改,生成失败。
#伪代码#
$conn = new Memcached();
$conn->addServer('localhost', 11211);
$key = '_lock_test_long_';
do {
$num = $conn->get($key, null, $cas);
/* 如果key不存在, 创建并进行一个原子添加*/
if ($conn->getResultCode() == Memcached::RES_NOTFOUND) {
$num = NUM_START;
$conn->add($key, $num);
} else {
$num += rand(1, 30);
$conn->cas($cas, $key, $num);
}
} while ($conn->getResultCode() != Memcached::RES_SUCCESS);
return $num;
B. 使用Memcached::increment方法,return increment($key, rand(1, 30)),用increment函数的原子性保证并发请求得到顺序处理
#伪代码#
$conn = new Memcached();
$conn->addServer("localhost", 11211);
$key = '_lock_test_long_';
$num = $conn->increment($key, rand(1, 30), NUM_START);
return $num;
C. 使用Redis::incr,原理同 2,并且没有key存在时,值从0开始。
最后,方法1的缺点是需要额外处理失败的情况,方法2的缺点是CI对memcached的组件封装没有increment方法(汗。。。),需要手工改框架。最后选了方法3。