在大量请求并发访问和更新Redis中储存的共享资源时,必须有一种精准高效的并发控制机制来防止逻辑异常和数据错误,乐观锁就是这样一种机制。比起原生Redis,云数据库Redis版性能增强型实例集成的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数据转变为具有原子性的变量。您可以使用Redis企业版性能增强型实例的TairString数据结构,构建高性能的乐观锁来达成这个效果。

使用TairString实现乐观锁

TairString,又称为exString(extended string),是一种带版本号的string类型数据结构。原生Redis String仅由key和value组成,而TairString不仅包含key和value,还携带了版本(version),极为适合乐观锁等场景。详细介绍及命令解析请参见TairString命令

说明 TairString与Redis原生String是两种不同的数据结构,相关命令不可混用。

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