Skip to content

Memcached - CAS

CAS (Check-And-Set,检查并设置) 是管理并发的关键命令。它允许您仅在键自上次您获取以来未被其他客户端修改的情况下,才为其设置新值。这种机制也称为“乐观锁”,对于防止竞态条件导致的数据损坏至关重要。

想象两个进程试图更新一个计数器:1) 它们都读取当前值 ‘5’。2) 它们都将其增加到 ‘6’。3) 它们都将 ‘6’ 写回。最终值是 ‘6’,而不是 ‘7’。CAS 解决了这个问题。工作流程是:1) 使用 gets 命令获取值和一个唯一的 ‘CAS 令牌’。2) 执行您的修改。3) 使用 cas 命令写入新值,并提供该令牌。只有当服务器上的令牌与您提供的令牌仍然匹配时,写入才会成功。

# 首先,获取值和令牌
gets key
# 然后,尝试更新它
cas key flags exptime bytes unique_cas_token [noreply]
value

unique_cas_token 是一个 64 位整数,用于唯一标识项目的版本。您必须从之前的 gets 命令中获取此令牌。

cas 命令可能返回以下结果:

  • STORED(已存储)表示由于 CAS 令牌匹配,新值已存储。
  • EXISTS(已存在)表示自您获取该项以来,它已被其他客户端修改(您的 CAS 令牌已过期)。
  • NOT_FOUND(未找到)表示该键不再存在。
  • ERROR(错误)表示语法错误。
# 1. 为键 'page_views' 设置一个初始值
set page_views 0 900 1
0
STORED
# 2. 使用 'gets' 检索值和其唯一的 CAS 令牌。
gets page_views
VALUE page_views 0 1 1 <-- 末尾的 '1' 是 CAS 令牌。
0
END
# 3. 尝试使用不正确的 CAS 令牌(例如 '99')进行更新。
# 这模拟了另一个客户端已经更新了该键的情况。
cas page_views 0 900 1 99
1
EXISTS <-- 'EXISTS' 意味着键存在,但 CAS 令牌不正确。
# 4. 现在,使用我们从 'gets' 获取的正确 CAS 令牌('1')。
cas page_views 0 900 1 1
1
STORED <-- 成功!值已更新。
# 5. 让我们验证新值并获取新的 CAS 令牌。
gets page_views
VALUE page_views 0 1 2 <-- 请注意,CAS 令牌现在是 '2'。
1
END
# 6. 如果我们再次尝试使用旧令牌('1')执行 'cas',它将失败。
cas page_views 0 900 1 1
2
EXISTS

现代应用:在 Python 中实现并发计数器

Section titled “现代应用:在 Python 中实现并发计数器”

让我们使用 Python 和 pymemcache 实现一个安全的并发计数器,pymemcache 内置支持 gets 和 cas 操作。

import time
from pymemcache.client.base import Client
MEMCACHED_SERVER = ('127.0.0.1', 11211)
def safe_increment(client, key, max_retries=5):
"""使用 CAS 重试循环安全地增加计数器。"""
for i in range(max_retries):
# 1. 获取值和 CAS 令牌
result = client.gets(key)
if result is None:
# 键不存在,我们无法递增。或许需要初始化它?
print(f"尝试 {i+1}: 键 '{key}' 未找到。初始化为 1。")
# 'add' 是一种如果键不存在则原子性设置它的方式。
if client.add(key, '1', expire=300):
return 1 # 成功初始化
else:
# 竞态条件:其他人已创建了它。重试循环。
time.sleep(0.01)
continue
current_value, cas_token = result
new_value = str(int(current_value) + 1)
# 2. 尝试使用 CAS 令牌设置新值
# 如果成功,cas 方法返回 True;如果令牌不匹配,则返回 False。
if client.cas(key, new_value, cas_token, expire=300):
print(f"尝试 {i+1}: 成功将 '{key}' 递增到 {new_value}。")
return int(new_value)
else:
# CAS 失败,另一个客户端修改了键。重试。
print(f"尝试 {i+1}: 键 '{key}' 的 CAS 失败。正在重试...")
time.sleep(0.01) # 添加一个小延迟以防止紧密循环空转
raise Exception(f"在 {max_retries} 次重试后,键 '{key}' 仍未能成功递增。")
def run_cas_example():
client = Client(MEMCACHED_SERVER)
key = 'concurrent_likes'
print(f"--- 为键: {key} 运行 CAS 演示 ---")
client.delete(key) # 从一个干净的状态开始
# 模拟多个并发递增
final_value = 0
for _ in range(5):
final_value = safe_increment(client, key)
print(f"\n最终计数器值为: {final_value}")
print(f"Memcached 中的值: {client.get(key).decode()}")
client.close()
if __name__ == "__main__":
run_cas_example()
--- 为键: concurrent_likes 运行 CAS 演示 ---
尝试 1: 键 'concurrent_likes' 未找到。初始化为 1。
尝试 1: 成功将 'concurrent_likes' 递增到 2。
尝试 1: 成功将 'concurrent_likes' 递增到 3。
尝试 1: 成功将 'concurrent_likes' 递增到 4。
尝试 1: 成功将 'concurrent_likes' 递增到 5。
最终计数器值为: 5
Memcached 中的值: 5
  • 实现重试逻辑: 在并发系统中,CAS 失败是预期结果。您的应用程序代码必须通过重试整个 gets -> 修改 -> cas 序列来处理这种情况。
  • 实际场景: 社交媒体的“点赞”按钮是一个经典示例。为了避免因竞态条件导致“点赞”丢失,您会使用这种模式来增加帖子的点赞计数器。
  • 配置开关: 更新缓存中存储的功能开关或配置值应通过 CAS 完成,以确保两个管理员不会互相覆盖更改。
  • 惊群问题(Thundering Herd Problem): CAS 也可用于缓解“惊群问题”。当一个热门缓存项过期时,许多进程可能同时尝试重新生成它。使用 add 或 CAS,只有一个进程能够“赢得”重新生成数据的权利,而其他进程则等待。