在大量请求并发访问和更新Tair中储存的共享资源时,必须有一种精准高效的并发控制机制来防止逻辑异常和数据错误,乐观锁就是这样一种机制。比起原生Redis,云原生内存数据库Tair的TairString模块能帮助您实现性能更高、成本更低的乐观锁。
并发与Last-Writer-Win
下图展示了一个典型的并发导致资源竞争的场景:

- 初始状态,string类型数据key_1的值为
hello
。 - t1时刻,App1读取到key_1的值
hello
。 - t2时刻,App2读取到key_1的值
hello
。 - t3时刻,App1将key_1的值修改为
world
。 - t4时刻,App2将key_1的值修改为
universe
。
key_1的值是由最后一次写入决定的,到了t4时刻,App1对key_1的认知已经出现了明显的误差,后续操作很可能出现问题,这就是所谓的Last-Writer-Win。要解决Last-Writer-Win问题,就需要保证访问并更新string数据这个操作的原子性,或者说,将作为共享资源的string数据转变为具有原子性的变量。您可以使用TairString数据结构,构建高性能的乐观锁来达成这个效果。
使用TairString实现乐观锁
TairString,又称为exString(extended string),是一种带版本号的string类型数据结构。原生Redis String仅由key和value组成,而TairString不仅包含key和value,还携带了版本(version),极为适合乐观锁等场景。详细介绍及命令解析请参见TairString。
TairString有以下特性:
- 每个key都有对应的version,用于说明key当前的版本。使用EXSET命令创建一个key时,默认其version为1。
- 对某个key使用EXGET时,可以获取到value和version两个字段。
- 更新TairString的value时,需要校验version,如果校验失败会返回异常信息
ERR update version is stale
。 - value更新后version自动加1。
- 除了比特位(bit)相关操作外,TairString可以覆盖原生Redis String的所有其它功能。
因为这些特性,TairString类型的数据本身就具有锁的机制,使用TairString实现乐观锁就非常方便了,示例如下:
while(true){
{value, version} = EXGET(key); // 获取key的value和version
value2 = update(...); // 先将新value保存到value2
ret = EXSET(key, value2, version); // 尝试更新key并将返回值赋予变量ret
if(ret == OK)
break; // 如果返回值为OK则更新成功,跳出循环
else if (ret.contanis("version is stale"))
continue; // 如果返回值包含"version is stale"则更新失败,重复循环
}
- 删除TairString后,即便以相同的key重新设置一条TairString,其version也会是1,而不会继承原TairString的version。
- 使用ABS选项可以跳过version校验强行覆盖version并更新TairString,详情参见EXSET。
降低乐观锁的性能消耗
前文的示例代码中,如果在执行EXGET后该共享资源被其它客户端更新了,当前客户端会获取到更新失败的异常信息,然后重复循环,再次执行EXGET获取共享资源的当前value和version,直到更新成功,这样每次循环都有两次访问Redis的IO操作。如果使用TairString的EXCAS命令,可以将两次访问减少为一次,极大地节约系统资源消耗,提升高并发场景下的服务性能。
EXCAS命令可以在调用时携带一个用于校验的version值,如果校验成功则直接更新TairString的value,如果校验失败则返回三个字段:
update version is stale
- value
- version
更新失败后可以直接得到TairString当前的版本,无需再次查询,将原本每个循环需要进行两次的访问减少到一次。示例如下:
while(true){
{ret, value, version} = excas(key, new_value, old_version) // 直接尝试用CAS命令置换value
if(ret == OK)
break; // 如果返回值为OK则更新成功,跳出循环
else (if ret.contanis("update version is stale")) // 如果返回值包含"update version is stale"则更新失败,更新两个变量
update(value);
old_version = version;
}