1.设置缓存的时候,要考虑多容器加锁的场景。
(1)场景,短信回执场景,会有二次回执的情况,但是我们只处理一次回执的消息体,如何不处理二次回执呢?
// 队列中有数据且容量未达到100,可继续放入队列数据
if (viberReceiptQueue.size() > 0 && (dlvViberReceiptMap.size() + fldViberReceiptMap.size()) <= 100) {
while (viberReceiptQueue.size() > 0
&& (dlvViberReceiptMap.size() + fldViberReceiptMap.size()) <= 100) {
ViberReceipt viberReceipt = viberReceiptQueue.take();
logger.info("HandlerRunnable viberReceipt take:{}", JSONObject.toJSONString(viberReceipt));
if (Objects.isNull(viberReceipt.getPartId())) {
MccContact mccContact = mccContactMapper.selectByPrimaryKey(viberReceipt.getMessageId());
viberReceipt.setPartId(Long.parseLong(mccContact.getPartId().toString()));
// 这里做一个二次回执业务过滤,判断contact表的状态是否已经被修改,如果已经被修改,则不处理
if (!CmsStateDef.CONTACT_STATE_CSD.equals(mccContact.getContactState())) {
continue;
}
}
//failed 发送失败 pending mmgw发送成功 success 发送成功 ceg一次成功回执pending 但一次失败回执failed 去重
if ("failed".equals(viberReceipt.getResult())) {
String key = viberReceipt.getMessageId() + "-" + viberReceipt.getResult();
String value = RedisUtil.getValue(RedisUtil.FUNTYPE1, key);
logger.info("qry FLD cache key:{}, is repeat flag:{}", key, value);
if (StringUtils.isEmpty(value)) {
fldViberReceiptMap.put(viberReceipt.getMessageId(), viberReceipt);
RedisUtil.setValue(RedisUtil.FUNTYPE1, key, "2", 3600);
}
else {
RedisUtil.delKey(RedisUtil.FUNTYPE1, key);
}
}
else if ("success".equals(viberReceipt.getResult())) {
dlvViberReceiptMap.put(viberReceipt.getMessageId(), viberReceipt);
}
}
这里我们通过RedisUtil.setValue(RedisUtil.FUNTYPE1, key, “2”, 3600);方法。将一次回执之后的key设置到缓存中去,这样二次回执过来的时候,会去查看缓存中是否有值,有值,则一次回执已经处理,该消息体为二次回执。不处理。
(2)上面这种解决方法看起来好像没有什么问题,但是Redis一般都是分布在多容器的,如果两个容器的两个消息同时getValue,这样既取不到数据,并且两条消息都进行了处理,显然与我们要求的不符合。
解决方法:使用RedisUtil的单线程处理方法incr
// 队列中有数据且容量未达到100,可继续放入队列数据
if (viberReceiptQueue.size() > 0 && (dlvViberReceiptMap.size() + fldViberReceiptMap.size()) <= 100) {
while (viberReceiptQueue.size() > 0
&& (dlvViberReceiptMap.size() + fldViberReceiptMap.size()) <= 100) {
ViberReceipt viberReceipt = viberReceiptQueue.take();
logger.info("HandlerRunnable viberReceipt take:{}", JSONObject.toJSONString(viberReceipt));
MccContact mccContact = mccContactMapper.selectByPrimaryKey(viberReceipt.getMessageId());
viberReceipt.setPartId(Long.parseLong(mccContact.getPartId().toString()));
// 这里做一个二次回执业务过滤,判断contact表的状态是否已经被修改,如果已经被修改,则不处理
if (!CmsStateDef.CONTACT_STATE_CSD.equals(mccContact.getContactState())) {
continue;
}
//failed 发送失败 pending mmgw发送成功 success 发送成功 ceg一次成功回执pending 但一次失败回执failed 去重
if ("failed".equals(viberReceipt.getResult())) {
String key = viberReceipt.getMessageId() + "-" + viberReceipt.getResult();
int incr = RedisUtil.incr(RedisUtil.FUNTYPE1, key, 1, 3600l);
logger.info("qry FLD cache key:{}, is repeat flag:{}", key, incr);
if (incrValue != incr) {
fldViberReceiptMap.put(viberReceipt.getMessageId(), viberReceipt);
}
}
else if ("success".equals(viberReceipt.getResult())) {
dlvViberReceiptMap.put(viberReceipt.getMessageId(), viberReceipt);
}
}
(3)为什么使用incr:(自增计数器)
1.Redis自增key 的好处
- 原子性(atomicity):一个事务是一个不可分割的最小工作单位,事务中包括的诸操作要么都做,要么都不做。
- Redis所有单个命令的执行都是原子性的,这与它的单线程机制有关;
- Redis命令的原子性使得我们不用考虑并发问题,可以方便的利用原子性自增操作
(4)为什么不使用加锁的方式(不选择Redis锁的原因:)
- 它获取锁的方式简单粗暴,获取不到锁直接不断尝试获取锁,比较消耗性能。
- 另外来说的话,Redis 的设计定位决定了它的数据并不是强一致性的,在某些极端情况下,可能会出现问题。锁的模型不够健壮。
- 即便使用 Redlock 算法来实现,在某些复杂场景下,也无法保证其实现 100% 没有问题,关于 Redlock 的讨论可以看 How to do distributed locking。
Redis 分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。
(5)其他使用场景:。
限流可以使用redis原子计数器incr.但是除了限流以外,很多系统会在一些节日的时候搞一些活动,当然,这些活动是有奖品的,并且奖品的数量也是有限的。为了防止在高并发的时候,出现多个人中奖的情况,那么可以使用分布式锁,比如redis的分布式锁,zookeeper的分布式锁。当然,我们也可以采用简单一点的方案,就是使用redis原子计数器incr来统计。