多个消费者,其实就是开启了多线程同时执行多个消费者(这个可以通过日志打印每个线程名称可以看到)
最初的场景:为了加快消息的消费,所以开启了多线程的模式对消息进行消费,并且一开始我是在自己本地(单服务部署)的情况下调试。
由于这个需求是把工业领域的监控视频RTSP流逐帧拆成图片传输给AI模型分析里面的信息,帧与帧之间的时间差距很小,如果全部插入数据库的话,数据量几分钟就有几万条,运行一天数据很庞大,需要把数据重复的,数据产出时间间隔很小的冗余数据丢弃掉,只插入代表性的数据。
遇到的问题:涉及数据库的数据插入,所以整一块都添加了@Transactional这个事务注解,并且由于是多线程,为了排除冗余数据,我采取的数据是取出数据库中 “同类型上一条的数据插入的数据”,只有产出时间time与当前插入的数据的时间相差5分钟才被认为是有代表性数据并进行数据库的插入。执行一次,去数据库检查数据的时候发现由于数据库的IO执行需要时间,某些数据在相差0.1毫秒0.2毫秒的情况下同时插入了,即使我修改了用redis作缓存(与数据库类似,只不过读取的是redis中的同类型数据key-value),每个操作都基于上一个缓存—-即是否命中redis的key,命中则跳过,否则则进行后续的插入,但是依然有重复插入的问题.
@Transactional(rollbackFor = Exception.class)
public void comsumeMessage(){
....redis操作(IO内存需要时间);
....数据库MySql操作(IO磁盘需要时间);
}
原因很明显就是不管是redis还是Mysql,IO都需要时间,由于消费速度快,两个线程基本是同时操作,在A线程查询完缓存后,还未进行插入的时候,B线程刚好查询缓存,在A完成插入之前,B已经查询并且判断了缓存不存在同类型数据所以进行了插入操作,导致AB两线程的数据重复了.
最初解决:后面给整一块的redis缓存操作都加入了同步锁,使时候类型的数据在redis操作中变为串行运行,至此单机问题结束。
public void comsumeMessage(){
synchronized (this){
....redis操作(IO内存需要时间);
}
updateDB();
}
@Transactional(rollbackFor = Exception.class)
public void updateDB(){
....redis操作(IO内存需要时间)
....数据库MySql操作(IO磁盘需要时间)
}
至于为啥这么写,主要是考略到@Transactional与synchronized之间的一些冲突,因为synchronized 锁的是这个方法,@Transactional的事物是Spring的AOP开启的。进入这个方法前,AOP会先开启事物,然后进入方法后, 此时会加锁,当方法结束后,锁释放,然后才会提交事物。在释放锁和提交事物之间有其他线程请求,读取到的是A线程还未提交事务的数据 , 这种情况会导致程序不安全。所以应该在开启事物前加锁(我们的redis也加了事务机制)。
后面的问题:后面更新上了开发环境(多个服务部署),查看了日志,发现有好几条报错是在我那块的,仔细一看是重复主键的报错,我当时想会不会是多服务的缘故,因为在redis中用redission新增分布式锁。过了一天,发现还是有原来的重复主键问题,继续排查,我们的服务用的是雪花算法,雪花算法会有取上一个时间戳的这么一个变量,仔细查看,发现是时间戳的问题,可是我看到生存算法是加了同步锁的,按道理不应该呀。
public static int lastTimeStamp = -1;
问题排查:我用过thread.sleep调用该方法,发现锁失效了,仔细一看,静态变量时间戳emmmm,那就尝试吧static去掉,虽然时间戳维持着默认值-1,但是锁生效了!!
猜想:经过查找了一些资料,说是锁的变化,导致了加锁失效(仍待验证)
public static void synchronized snowflack(){
.....
}
解决方法:那既然变量锁不住,那我直接对调用这个雪花算法的对象进行加锁,虽然消费速度确实会出现明显的下降,但是解决线程安全问题还是更重要的.