利用 redis incr 做超高并发延迟累加器(一亿数量级)
前景提要:
公司有一个字段,需要支持 qps 为 1000万。 但是可以将结果作为延迟结果发送过来,比如 20秒发一次结果。但是不能直接 incr 1000万次,redis即使是集群 也扛不住。
提示: redis只有在网络读取请求利用了多线程,在磁盘io等处理数据方面依然是单线程。
思路: 多应用示例部署,利用多应用本地jvm缓存,来进行分流,各应用本地之间近行多线程本地累加。利用时间段来提交多应用的累加结果,此时再利用incr原子特性进行多应用结果汇合,得到总结果。
我要做什么?
- 多应用部署不在讨论范围
- 本机缓存,多线程访问保证高并发的数据正确性!
- 利用时间间隔去请求incr
- 是否存在内存泄漏问题?
- 是否存在遗留问题?
代码实现
总体分析有两个临界资源:
- redis针对特定key的value进行累加
- 本地缓存,针对特定key的value进行累加
如何解决临界资源并发问题?
- redis针对key的value进行累加,用incr或者incrBy即可。
- 针对本地缓存,可以考虑使用longadder数据类型来保证。
解释为啥用 longAdder?
- longadder数据类型可以保证并发安全(CAS),由于我们对临界资源的操作远小于线程上下文切换的时间,所以用CAS收益更好。
- 它比普通的AtmoicLong要更快,比long更快。利用了禁用cpu cache缓存,用空间换时间方式防止伪共享,详情请看longAdder详解
ok,交代完背景以后,我们来整理下代码思路,应该分以下几点:
- 声明本地临界资源
- 声明计时器,间隔一段时间把累加结果同步到redis
- 将长时间未用到的临界资源释放掉,为了避免内存泄漏
开始编码,先给出分步代码,然后再给出整体代码
声明临界资源:
static final Map<String, LongAdder> countMap = new ConcurrentHashMap<>();
声明计时器:
private static ScheduledExecutorService executor;
public void init() {
executor = Executors.newScheduledThreadPool(1);
// 30秒执行一次 可以将30秒改为动态的
executor.scheduleAtFixedRate(new LocalSum(), 0,30, TimeUnit.SECONDS);
}
本地计算
public static void incr(String key) {
try {
LongAdder longAdder = countMap.get( taskId);
if (longAdder == null) {
synchronized (this) {
longAdder = countMap.get(taskId);
if (longAdder == null) {
longAdder = new LongAdder();
countMap.put(taskId, longAdder);
}
}
}
longAdder.increment();
} catch (Exception e) {
log.error("incr 异常了?, e);
}
}
计算完成后,对redis进行incr
// 利用多线程处理
public static class LocalSum implements Runnable {
@Override
public void run() {
for (Map.Entry<String, LongAdder> entry : countMap.entrySet()) {
try {
String key = entry.getKey();
LongAdder longAdder = entry.getValue();
long count = longAdder.sum();
jedis.incrBy(key, count);
longAdder.add(-count);
ttlMap.remove(key);
} catch (Exception e) {
log.error("异常了, e);
}
}
}
}
这样有一个问题 countMap 会随着项目代码的增加变的无限大,造成内存泄漏
解决方案: 如果n次执行中,该key没有被用到,则被移除!
修改下对redis进行incr代码
// 重试次数
private static Integer ttlCount = 60;
// 保存key的
private static Map<String, Integer> ttlCountMap = new ConcurrentHashMap<>();
// 利用多线程处理
public static class LocalSum implements Runnable {
@Override
public void run() {
for (Map.Entry<String, LongAdder> entry : countMap.entrySet()) {
try {
String key = entry.getKey();
LongAdder longAdder = entry.getValue();
long count = longAdder.sum();
if (count == 0) {
Integer tempTTL= ttlMap.get(key);
if (tempTTL== null) {
tempTTL= ttlCount ;
}
ttlMap.put(key, --tempTTL);
if (currentTTL == 0) {
countMap.remove(key);
ttlCountMap .remove(key);
}
continue;
}
jedis.incrBy(key, count);
longAdder.add(-count);
ttlMap.remove(key);
} catch (Exception e) {
log.error("异常了, e);
}
}
}
}
结束!
代价是 一段时间后才提交记录
收获是避免了redis的频繁访问,redis虽然快,但是也是需要走网络和io的。