我的Rest API工作正常。但是,我关注并发问题,虽然我已经通过脚本进行了测试,但还没有看到任何问题。在我的研究中,我遇到了一些关于利用concurrentHasMap使用原子值的材料,以避免相当于脏读。我的问题是双重的。首先,考虑到我的实施,我应该担心吗?第二,如果我应该,那么实施原子价值最谨慎的方法是什么呢?我已经考虑为RestTemplate删除包装类并简单地将String传递回Angular 4组件作为速度的催化剂,但考虑到我可能在其他地方使用值对象,我犹豫不决。参见下面的实现。
@Service
@EnableScheduling
public class TickerService implements IQuoteService {
@Autowired
private ApplicationConstants Constants;
private ConcurrentHashMap<String,Quote> quotes = new ConcurrentHashMap<String, Quote>();
private ConcurrentHashMap<String,LocalDateTime> quoteExpirationQueue = new ConcurrentHashMap<String, LocalDateTime>();
private final RestTemplate restTemplate;
public TickerService(RestTemplateBuilder restTemplateBuilder) {
this.restTemplate = restTemplateBuilder.build();
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public Quote getQuote(String symbol) {
if (this.quotes.containsKey(symbol)){
Quote q = (Quote)this.quotes.get(symbol);
//Update Expiration
LocalDateTime ldt = LocalDateTime.now();
this.quoteExpirationQueue.put(symbol, ldt.plus(Constants.getQuoteExpirationMins(),ChronoUnit.MINUTES));
return q;
} else {
QuoteResponseWrapper qRes = this.restTemplate.getForObject( Constants.getRestURL(symbol), QuoteResponseWrapper.class, symbol);
ArrayList<Quote> res = new ArrayList<Quote>();
res = qRes.getQuoteResponse().getResult();
//Add to Cache
quotes.put(symbol, res.get(0));
//Set Expiration
LocalDateTime ldt = LocalDateTime.now();
this.quoteExpirationQueue.put(symbol, ldt.plus(Constants.getQuoteExpirationMins(),ChronoUnit.MINUTES));
return res.get(0);
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public ConcurrentHashMap<String,Quote> getQuotes(){
return this.quotes;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@Scheduled(fixedDelayString = "${application.quoteRefreshFrequency}")
public void refreshQuotes(){
if (quoteExpirationQueue.isEmpty()) {
return;
}
LocalDateTime ldt = LocalDateTime.now();
//Purge Expired Quotes
String expiredQuotes = quoteExpirationQueue.entrySet().stream().filter(x -> x.getValue().isBefore(ldt)).map(p -> p.getKey()).collect(Collectors.joining(","));
if (!expiredQuotes.equals("")) {
this.purgeQuotes(expiredQuotes.split(","));
}
String allQuotes = quoteExpirationQueue.entrySet().stream().filter(x -> x.getValue().isAfter(ldt)).map(p -> p.getKey()).collect(Collectors.joining(","));
List<String> qList = Arrays.asList(allQuotes.split(","));
Stack<String> stack = new Stack<String>();
stack.addAll(qList);
// Break Requests Into Manageable Chunks using property file settings
while (stack.size() > Constants.getMaxQuoteRequest()) {
String qSegment = "";
int i = 0;
while (i < Constants.getMaxQuoteRequest() && !stack.isEmpty()) {
qSegment = qSegment.concat(stack.pop() + ",");
i++;
}
logger.debug(qSegment.substring(0, qSegment.lastIndexOf(",")));
this.updateQuotes(qSegment);
}
// Handle Remaining Request Delta
if (stack.size() < Constants.getMaxQuoteRequest() && !stack.isEmpty()) {
String rSegment = "";
while (!stack.isEmpty()){
rSegment = rSegment.concat(stack.pop() + ",");
}
logger.debug(rSegment);
this.updateQuotes(rSegment.substring(0, rSegment.lastIndexOf(",")));
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private void updateQuotes(String symbols) {
if (symbols.equals("")) {
return;
}
System.out.println("refreshing -> " + symbols);
QuoteResponseWrapper qRes = this.restTemplate.getForObject( Constants.getRestURL(symbols), QuoteResponseWrapper.class, symbols);
for (Quote q : qRes.getQuoteResponse().getResult()) {
this.quotes.put(q.getSymbol(), q);
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private void purgeQuotes(String[] symbols) {
for (String q : symbols) {
System.out.println("purging -> " + q);
this.quotes.remove(q);
this.quoteExpirationQueue.remove(q);
}
}
}
答案 0 :(得分:0)
更改了IQuoteService和实现TickerService的实现,以将concurrenHashMap与原子参考一起使用:
@Autowired
private ApplicationConstants Constants;
private ConcurrentHashMap<AtomicReference<String>,AtomicReference<Quote>>
quotes = new ConcurrentHashMap<AtomicReference<String>,AtomicReference<Quote>> ();
private ConcurrentHashMap<AtomicReference<String>,AtomicReference<LocalDateTime>> quoteExpirationQueue = new ConcurrentHashMap<AtomicReference<String>,AtomicReference<LocalDateTime>>();
private final RestTemplate restTemplate;
代码的工作原理与之前的实现完全相同,新实现是“应该”确保在完全写入之前不会部分读取值的更新,并且获得的值应该是一致的。鉴于,我找不到合理的例子并且没有得到关于这个主题的答案,我将对此进行测试并发布我发现的任何问题。
答案 1 :(得分:0)
如果要同时调用refreshQuotes(),则出现此代码的主要并发风险。如果存在这种风险,则只需将refreshQuotes标记为已同步。
在这样的前提下,一次只能调用一次refreshQuotes(),并且Quote / LocalDateTime都是不可变的。那么问题似乎是在ConcurrentHashMap中更新不可变值是否存在脏读/写风险。答案是否定的,值是不可变的,并且ConcurrentHashMap处理并发更新引用。
有关更多信息,我强烈建议阅读JSR133 (The Java Memory Model)。它详细介绍了线程之间何时将出现数据以及何时将看不到数据。道格·李的JSR133 Cookbook几乎可以肯定会为您提供比您想知道的更多的信息。