在大量请求并发访问和更新实例中储存的共享资源时,必须有一种精准高效的并发控制机制来防止逻辑异常和数据错误,乐观锁就是这样一种机制。比起原生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数据转变为具有原子性的变量。您可以使用exString数据结构,构建高性能的乐观锁来达成这个效果。
使用TairString实现乐观锁
TairString,又称为exString(extended string),是一种带版本号的String类型数据结构。原生Redis String仅由Key和Value组成,而TairString不仅包含Key和Value,还携带了版本(Version),非常适合乐观锁等场景。更多介绍及命令详情信息请参见exString。
TairString与Redis原生String是两种不同的数据结构,相关命令不可混用。
TairString有以下特性:
每个Key都带有Version,用于说明当前Value的版本。使用EXSET命令新建Key时,默认Version为1。
使用EXGET命令查询Key时,可以获取到Value和Version两个字段。
更新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.contains("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,如果校验失败则返回三个字段:
"ERR 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.contains("update version is stale")) // 如果返回值包含"update version is stale"则更新失败,更新两个变量
update(value);
old_version = version;
}