基于TairString实现高性能乐观锁

在大量请求并发访问和更新实例中储存的共享资源时,必须有一种精准高效的并发控制机制来防止逻辑异常和数据错误,乐观锁就是这样一种机制。比起原生Redis,Tair(企业版)的TairString模块能帮助您实现性能更高、成本更低的乐观锁。

并发与Last-Writer-Win

下图展示了一个典型的并发导致资源竞争的场景:

  1. 初始状态,string类型数据key_1的值为hello

  2. t1时刻,App1读取到key_1的值hello

  3. t2时刻,App2读取到key_1的值hello

  4. t3时刻,App1将key_1的值修改为world

  5. 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;
 }