并发情况下的重复记录分析
直播数据中心观众统计不准确,直播数据存在同一直播间同一用户两条记录
录的情况
- 抓取异常请求日志观察
- 第一次 2022-07-06 10:40:48,586|INFO
- 第二次 2022-07-06 10:40:48,606|INFO
- 两次请求上报的时间戳来看,间隔不到30毫秒
- 抓取接口平均响应时效
- 接口平均耗时在30毫秒-50毫秒作用
- 在第一次和第二次通过直播间ID 和用户ID 查询时 很大可能获取不到数据,同时进行了两次insert操作,也就造成了重复数据
解决方案
- 前端修改上报频率
- 前端上报的时间间隔, 最起码应该是秒以上的单位,不能在毫秒内上报两次,用户的观看时长应该是秒级别的
- 服务端对改接口进行并发加锁
基于redis的分布式锁
一个相对安全的分布式锁,一般需要具备以下特征:
- 互斥性。互斥是锁的基本特征,同一时刻锁只能被一个线程持有,执行临界区操作。
- 超时释放。通过超时释放,可以避免死锁,防止不必要的线程等待和资源浪费,类似于 MySQL 的 InnoDB 引擎中的 innodblockwait_timeout 参数配置。
- 可重入性。一个线程在持有锁的情况可以对其再次请求加锁,防止锁在线程执行完临界区操作之前释放。
- 高性能和高可用。加锁和释放锁的过程性能开销要尽可能的低,同时也要保证高可用,防止分布式锁意外失效。
实现思路:
- redis保存lockKey,在获取不到锁的情况下进行一次重试
- 利用RocketMQ延时队列,进行异常和获取不到的锁的情况下,进行重试
- 数据库新增唯一索引,在丢失一条数据情况下,不影响后续数据的累加(可能会丢失并发时长数据)
public Boolean lock(String key, long time) {
try {
String id = String.valueOf(Thread.currentThread().getId());
String redisKey = LOCK + key;
BoundValueOperations<String, String> ops = stringRedisTemplate.boundValueOps(redisKey);
Boolean ans = ops.setIfAbsent(id, time, TimeUnit.MILLISECONDS);
//可能有活锁问题
if (Objects.equals(ans, Boolean.FALSE)) {
Thread.sleep(time);
return ops.setIfAbsent(id, time, TimeUnit.MILLISECONDS);
}
return true;
} catch (Exception e) {
log.error("redislock error#", e);
}
return false;
}
当前锁缺陷:
- 没有进行锁的重入性判断,当加锁代码超过本身锁失效时间,可能会造成锁的失效,进而影响数据的正确性
- 可能会产生活锁问题,在重试过程中,如果一直获取不到锁,那么就有可能反复重试,没有任何一个线程能持有锁,造成活锁问题
- 当前代码不是有效的分布式锁,当redis本身出现异常,例如:主从同步延时较高,主库挂掉,从库升级master节点过程中,没有同步数据,可能会造成多个线程获取到锁,也无法保证数据的正确性
思考
- 在分布式系统中,不能过多的依赖数据接口的执行时间,在临界操作的代码,一定要考虑一些数据正确性的保证,除了加锁操作,也可以通过消息队列做一些重试操作