在上面睡了一晚之后,我想出了几个办法。哪一个都不是特别优雅,但让我把它们放在这里作为思考:
MemCache 确实提供(至少)4 个命令,它们以某种方式以原子方式修改条目并返回有关其先前状态的一些信息(@Moshe Shaham 也指出了其中两个):
-
delete: 删除一个条目并返回它是否存在
-
increment: 增加一个条目并返回它的新值
-
put: 放置一个条目并返回它是否存在(如果与正确的 policy 一起使用)
-
putIfUntouched: 如果条目仍然匹配预期状态,则放置条目(返回是否匹配)
让我为它们中的每一个提供一个可能的实现。前 3 个将特定于整数键,并要求实际条目具有正键(因为它们将使用相应的负键作为标记条目)。
注意:下面给出的示例本质上是概念性的。其中一些可能仍然允许竞争条件,而我实际上还没有测试过它们中的任何一个。所以,对它们持保留态度。
这里是:
1.删除
当第一次存储条目时,将一个标记连同它一起存储(使用派生的对应键)。根据尝试删除此标记的成功,门控获取和删除。
public void putForAtomicGnD(MemcacheService memcache, int key, Object value) {
assert key>=0;
memcache.put(key, value); // Put entry
memcache.put(-key-1, null); // Put a marker
}
public Object atomicGetAndDelete(MemcacheService memcache, int key) {
assert key>=0;
if (!memcache.delete(-key-1)) return null; // Are we first to request it?
Object result = memcache.get(key); // Grab content
memcache.delete(key); // Delete entry
return result;
}
可能的竞争条件:putForAtomicGnD 可能会覆盖正在读取的值。可以通过使用 ADD_ONLY_IF_NOT_PRESENT 策略进行放置和 millisNoReAdd 删除策略来避免。
可能的优化:使用putAll。
2。递增
有一个与每个条目相关联的读取计数器,用于控制获取和删除。
public Object atomicGetAndDelete(MemcacheService memcache, int key) {
assert key>=0;
if (memcache.increment(-key-1, 1L, 0L) != 1L) return null; // Are we 1st?
Object result = memcache.get(key); // Grab content
memcache.delete(key); // Delete entry
return result;
}
注意:如此处所示,此代码只允许每个键使用一次。要重复使用,需要删除相应的标记,这会导致竞争条件(再次可以通过 millisNoReAdd 解决)。
3.放
具有与门获取和删除的每个条目相关联的读取标志(不存在标记条目)。本质上与方法“1.删除”相反。
public Object atomicGetAndDelete(MemcacheService memcache, int key) {
assert key>=0;
if (!memcache.put(-key-1, null, null, SetPolicy.ADD_ONLY_IF_NOT_PRESENT))
return null; // Are we 1st?
Object result = memcache.get(key); // Grab content
memcache.delete(key); // Delete entry
memcache.delete(-key-1, 10000); // Delete marker with millisNoReAdd
return result;
}
可能的竞争条件:另一个 put 可能会覆盖正在读取的值。同样,可以使用millisNoReAdd 解决。
可能的优化:使用deleteAll。
4. putIfUntouched
我目前最喜欢的:尝试在模拟交易中获取一个条目并将其设置为 null,然后使用该条目的成功来门控删除。
public Object atomicGetAndDelete(MemcacheService memcache, Object key) {
IdentifiableValue result = memcache.getIdentifiable(key);
if (result==null) return null; // Entry didn't exist
if (result.getValue()==null) return null; // Someone else got it
if (!memcache.putIfUntouched(key, result, null)) return null; // Still 1st?
memcache.delete(key); // Delete entry
return result.getValue();
}
注意:这种方法几乎是完全通用的(对密钥类型没有限制),因为它不需要能够从给定的标记对象派生密钥。它的唯一限制是它不支持实际的 null-条目,因为该值被保留以表示“条目已被采用”。
可能的竞争条件?我目前没有看到任何内容,但也许我遗漏了什么?
可能的优化:putIfUntouched 立即使用Expiration (find out how here!) 而不是删除。
结论
似乎有很多方法可以做到这一点,但到目前为止我想出的方法都没有一个看起来特别优雅。请使用此处给出的示例主要作为思考的食粮,让我们看看我们是否可以提出更清洁的解决方案,或者至少说服自己上面的一个(putIfUntouched?)确实可以可靠地工作。