When multiple clients concurrently read and update the same key, the last write silently overwrites all preceding writes — a race condition known as last-write-wins. TairString, the extended string (exString) data type in Tair (Enterprise Edition), has a built-in version number that makes optimistic locking a native operation, letting you detect and retry conflicting writes without external locks or transactions.
How version-number optimistic locking works
Optimistic locking works by attaching a version number to each key. When you read a key, you also read its version. When you write, you supply the version you read. If another client has written to the key in the meantime, the server version has incremented and no longer matches yours — the write is rejected rather than silently applied. You then retry with the updated value and version.
This is the problem that TairString solves natively: native Redis strings consist of only keys and values, with no mechanism to detect whether a key changed between a read and a write.
The race condition: why last-write-wins causes data loss
The following diagram illustrates a typical race condition.

Consider a counter that two clients increment concurrently. Both read the current value (10), independently compute 11, and write back. The result is 11 instead of the expected 12 — one increment is silently lost. The same pattern applies to any shared key:
key_1starts with the valuehello.Application 1 reads
hello.Application 2 reads
hello.Application 1 writes
worldtokey_1.Application 2 writes
universetokey_1.
Application 1 expects key_1 to be world, but the actual value is universe. To prevent this, the read-modify-write sequence must be atomic — which is exactly what TairString's version-number mechanism provides.
TairString commands for optimistic locking
TairStrings consist of keys, values, and version numbers. TairString integrates all Redis string features except bit operations.
TairString commands and native Redis string commands are not interchangeable.
| Command | Purpose | Behavior |
|---|---|---|
| EXSET | Create a TairString key | Default version number is 1 |
| EXGET | Query a TairString key | Returns both value and version |
| EXSET with version | Update with version check | Succeeds and increments version by 1; fails with ERR update version is stale if the version does not match |
| EXCAS | Compare-And-Swap in a single call | Verifies version and applies the write atomically; on failure, returns the error, current value, and current version in one response |
For the full command reference, see exString.
Implement optimistic locking with EXGET and EXSET
The basic optimistic locking pattern uses a retry loop: read the current value and version with EXGET, compute the new value, then write with EXSET, passing the version you read. If a competing write occurred between your read and write, the version check fails and the loop retries.
while(true){
{value, version} = EXGET(key); // Retrieve the value and version number of the key.
value2 = update(...); // Save the new value as value 2.
ret = EXSET(key, value2, version); // Update the key and assign the return value to the ret variable.
if(ret == OK)
break; // If the return value is OK, the update is successful and the while loop exits.
else if (ret.contains("version is stale"))
continue; // If the return value contains the "version is stale" error message, the update fails and the while loop is repeated.
}This guarantees that if any other client updated the key after your EXGET, your EXSET fails rather than silently overwriting their change. Each iteration of this loop requires two round trips to Tair: one EXGET to read the current value and version, and one EXSET to attempt the write.
When a TairString key is deleted and re-created with the same name, the new key starts at version
1. It does not inherit the previous version.To skip version verification and force-overwrite a value, use the ABS option with EXSET. See EXSET for details.
Reduce I/O with EXCAS
The EXGET-then-EXSET loop requires two I/O operations per retry cycle. The EXCAS (Compare-And-Swap) command reduces this to one I/O per cycle — a significant improvement under high concurrency.
EXCAS atomically checks the version and applies the write in a single round trip. On failure, it returns all three of the following in one response:
"ERR update version is stale"The current value
The current version number
Because the current state is included in the failure response, the next retry cycle can start immediately — no extra EXGET call is needed.
while(true){
{ret, value, version} = excas(key, new_value, old_version) // Use the CAS command to replace the original value with a new value.
if(ret == OK)
break; // If the return value is OK, the update is successful and the while loop exits.
else (if ret.contains("update version is stale")) // If the return value contains the "update version is stale" error message, the update fails. The values of the value and old_version variables are updated.
update(value);
old_version = version;
}