服务必须缓存在内存数据中并将数据保存在数据库中。如果之前未调用getAmount(id)
方法,addAmount()
将检索当前余额或零
指定的id。如果第一次调用方法,addAmount(id, amount)
会增加平衡或设置。服务必须是线程安全的。线程安全是我的实现吗?可以做些什么改进?
public class AccountServiceImpl implements AccountService {
private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class);
private LoadingCache cache;
private AccountDAO accountDAO = new AccountDAOImpl();
public AccountServiceImpl() {
cache = CacheBuilder.newBuilder()
.expireAfterAccess(1, TimeUnit.HOURS)
.concurrencyLevel(4)
.maximumSize(10000)
.recordStats()
.build(new CacheLoader<Integer, Account>() {
@Override
public Account load(Integer id) throws Exception {
return new Account(id, accountDAO.getAmountById(id));
}
});
}
public Long getAmount(Integer id) throws Exception {
synchronized (cache.get(id)) {
return cache.get(id).getAmount();
}
}
public void addAmount(Integer id, Long value) throws Exception {
Account account = cache.get(id);
synchronized (account) {
accountDAO.addAmount(id, value);
account.setAmount(accountDAO.getAmountById(id));
cache.put(id, account);
}
}
}
答案 0 :(得分:2)
如果帐户被从缓存中逐出,并且正在对该帐户进行多次更新,则可能会出现竞争情况。驱逐导致多个Account实例,因此同步不提供原子性,并且可以将过时值插入缓存中。
如果您更改设置,竞争会更加明显,例如maximumSize(0)
。在当前设置中,竞赛的可能性可能很少,但即使在访问之后仍可能发生驱逐。这是因为可能会选择条目进行驱逐但尚未删除,因此即使从策略的角度忽略访问,后续读取也会成功。
在番石榴中执行此操作的正确方法是Cache.invalidate()
条目。 DAO在事务上更新记录系统,因此它确保了操作的原子性。 LoadingCache确保正在计算的条目的原子性,因此在加载新值时将阻止读取。这导致额外的数据库查找,这似乎是不必要的,但在实践中可以忽略不计。不幸的是,即使仍然存在微小的潜在竞争,因为Guava不会使加载条目无效。
Guava不支持您尝试实现的直写缓存行为。它的继任者Caffeine通过公开Java 8的compute
映射方法和很快的CacheWriter
抽象来实现。也就是说,Guava期望的加载方法简单,优雅,并且比手动更新更不容易出错。
答案 1 :(得分:1)
这里有两个问题需要处理:
如果您已声明:
class Account { long amount; }
在32位系统上更改字段值不是原子的。它在64位系统上是原子的。请参阅:Are 64 bit assignments in Java atomic on a 32 bit machine?
所以,最好的方法是将声明更改为“volatile long amout;”然后,值的更新始终是原子的,而且,volatile确保其他线程/ CPU看到更改的值。
这意味着更新单个值,您不需要同步块。
使用同步语句,您只需解决第一个问题。但是你的代码中有多个种族。
请参阅此代码:
synchronized (cache.get(id)) {
return cache.get(id).getAmount();
}
您显然假设cache.get(id)在调用相同的id时返回相同的对象实例。事实并非如此,因为缓存基本上不能保证这一点。
Guava Cache会阻塞,直到加载完成。其他缓存可能阻止也可能不阻塞,这意味着如果请求并行进入,将调用多个加载,导致存储的缓存值发生多次更改。
然而,Guava Cache是一个缓存,因此该项可能随时从缓存中逐出,因此对于下一次获取另一个实例将被返回。
这里有同样的问题:
public void addAmount(Integer id, Long value) throws Exception {
Account account = cache.get(id);
/* what happens if lots of requests come in and another
threads evict the account object from the cache? */
synchronized (account) {
. . .
通常:永远不要在生命周期不在您控制范围内的对象上进行同步。 BTW:其他缓存实现可能只存储序列化对象值并在每个请求上返回另一个实例。
由于修改后你有一个cache.put,你的解决方案可能会有效。但是,同步只是为了实现刷新内存的目的,它可能会或可能不会真正锁定。
在数据库中更改值后,将发生缓存更新。这意味着即使应用程序已在数据库中更改,应用程序也可以读取以前的值。这可能会导致不一致。
解决方案1
拥有您通过键值选择的一组静态锁定对象,例如通过锁[id%locks.length]。请在此处查看我的回答:Guava Cache, how to block access while doing removal
解决方案2
使用数据库事务,并使用模式进行更新:
Transaction.begin();
cache.remove(id);
accountDAO.addAmount(id, value);
Transaction.commit();
不要直接在缓存中更新值。这将导致更新比赛并再次需要锁定。
如果交易仅在DAO中处理,则这意味着您的软件架构应该在DAO中而不是在外部实现缓存。
解决方案3
为什么不将金额值存储在缓存中?如果在更新时允许缓存结果可能与数据库内容不一致,则最简单的解决方案是:
public AccountServiceImpl() {
cache = CacheBuilder.newBuilder()
.expireAfterAccess(1, TimeUnit.HOURS)
.concurrencyLevel(4)
.maximumSize(10000)
.recordStats()
.build(new CacheLoader<Integer, Account>() {
@Override
public Account load(Integer id) throws Exception {
return accountDAO.getAmountById(id);
}
});
}
Long getAmount(Integer id) {
return cache.get(id);
}
void addAmount(Integer id, Long value) {
accountDAO.addAmount(id, value);
cache.remove(id);
}
答案 2 :(得分:0)
不,
private LoadingCache cache;
必须是最终的。
cache.get(id)
必须同步。您是否正在使用图书馆?
答案 3 :(得分:0)
必须同步缓存。否则两个线程同时更新金额,你永远不会确定最终结果。检查使用库的`put'方法的实现