高性能计数器
场景
计数场景很多,如点赞,收藏等等,那解决方案也很多,主要讨论分布式场景:
- 数据库计数
- redis 计数
无论是其中的哪一种,在高并发的情况,可能都会遇到瓶颈
方案
如果我们不考虑强一致性的话,那么可以试试这里的方案
- 使用map进行分桶在内存计数,如按每秒分桶
- 使用定时任务将3秒前的分桶计数累加到redis
- 减少对redis写的操作
- 后面还可以通过定时对redis数据持久化到db
实现
BucketManager
@Slf4j
public class BucketManager {
private static final String KEY_CNT = "count";
private static final Integer INTERVAL = 3;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public static Map<Long, AtomicLong> counter = new ConcurrentHashMap<>();
private ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
public void incr() {
//秒级桶
long bucket = System.currentTimeMillis() / 1000;
AtomicLong cnt = counter.computeIfAbsent(bucket, k -> new AtomicLong(0));
long tmp = cnt.incrementAndGet();
log.info("cnt bucket:{},cnt: {}",bucket, tmp);
}
public Long getCount(){
Object o = redisTemplate.opsForValue().get(KEY_CNT);
return o !=null ? Long.valueOf(o.toString()):null;
}
//每隔2秒刷新到redis
@PostConstruct
public void countJob() {
scheduledExecutorService.scheduleAtFixedRate(() -> {
log.info("count job schedule at: {}", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
//取出3秒前的数据
Instant instant = Instant.now().minusSeconds(INTERVAL);
long lastBucketId = instant.toEpochMilli() / 1000;
Set<Long> keySet = counter.keySet();
if (CollectionUtils.isEmpty(keySet)) {
log.info("count job no count {}", lastBucketId);
return;
}
Set<AtomicLong> removeList = new HashSet<>();
for (Long tmp : keySet) {
if (tmp.compareTo(lastBucketId) <= 0) {
log.info("record bucket: {}", tmp);
removeList.add(counter.remove(tmp));
}
}
log.info("removeList cnt: {}", removeList);
//累加
Long total = removeList.stream().mapToLong(AtomicLong::get).sum();
if (total>0){
//同步到 redis
redisTemplate.opsForValue().increment(KEY_CNT, total);
}
}, 5, 2, TimeUnit.SECONDS);
}
}
测试
@RestController
public class UserCounterController {
@Autowired
private BucketManager bucketManager;
/**
* 计数
* @return
*/
@GetMapping("/cnt/incr")
public String incr() {
bucketManager.incr();
return "success";
}
/**
* 获取计数
* @return
*/
@GetMapping("/cnt/get")
public Long get() {
return bucketManager.getCount();
}
}
good luck!