开启多线程异步执行踩得坑

背景

我想实现一个点赞收藏功能,这个数据是存到redis中,然后通过定时任务给刷到数据库里面去的一个数据,在这个过程,我想通过异步的方式来实现一个操作日志的记录,所以就踩了坑

代码

java 复制代码
@Service
@Slf4j
public class VideoOperateServiceImpl implements VideoOperateService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private VideoOperateMapper videoOperateMapper;

    private BlockingQueue<VideoOperate> videoTasks = new ArrayBlockingQueue<>(1024 * 1024);
    private static final ExecutorService VIDEO_OPERATE_EXECUTOR = Executors.newSingleThreadExecutor();


/**
     * 这个方法的作用是保证在这个类初始化的后,就可以开始执行子线程的调用
     * 因为要一开始就要判断队列中是否有信息可用,可用就执行
     */
    @PostConstruct
    private void init() {
        VIDEO_OPERATE_EXECUTOR.submit(new VideoOperateHandler());
    }
    private VideoOperateService proxy;
    @Override
    public Integer like(Long videoId) {
        Long userId = UserHolder.getUser().getId();
        String isLikeKey = RedisConstants.VIDEO_LIKE + videoId;
        String likeCountKey = RedisConstants.VIDEO_LIKE_COUNT+videoId;
        Boolean isLike = stringRedisTemplate.opsForSet().isMember(isLikeKey, String.valueOf(userId));
        //获取点赞数
        String s = stringRedisTemplate.opsForValue().get(likeCountKey);
        Integer likeCount = Integer.valueOf(s);
        //生成对象
        VideoOperate videoOperate = createVideoOperate(videoId,userId,0);
        //已经点赞
        if(isLike){
            //总的点赞数-1
            stringRedisTemplate.opsForValue().set(likeCountKey,String.valueOf(likeCount-1));
            //挪出set列表
            stringRedisTemplate.opsForSet().remove(isLikeKey, String.valueOf(userId));
            videoOperate.setIsLike(String.valueOf(0));
            videoTasks.add(videoOperate);
            //获取代理对象,由于异步的线程代理对象不是当前线程的对象,所以需要将现在的代理对象传进去
            proxy = (VideoOperateService) AopContext.currentProxy();
            //返回0表示 要取消点赞
            return Integer.valueOf(0);
        }
        //没有点赞
        //总的点赞数+1
        stringRedisTemplate.opsForValue().set(likeCountKey,String.valueOf(likeCount+1));
        //加入set列表
        stringRedisTemplate.opsForSet().add(isLikeKey,String.valueOf(userId));
        videoOperate.setIsLike(String.valueOf(1));
        //返回1表示点赞成功
        //异步执行对于数据库的操作
        videoTasks.add(videoOperate);
        return Integer.valueOf(1);
    }

    private VideoOperate createVideoOperate(Long videoId, Long userId, int type) {
        VideoOperate videoOperate = new VideoOperate();
        videoOperate.setVideoId(videoId);
        videoOperate.setUserId(userId);
        videoOperate.setOperateType(String.valueOf(type));
        return videoOperate;
    }

    @Override
    public Integer collect(Long videoId) {
        Long userId1 = UserHolder.getUser().getId();
        String userId = String.valueOf(userId1);
        String isCollectKey = RedisConstants.VIDEO_COLLECT + videoId;
        String collectCountKey = RedisConstants.VIDEO_COLLECT_COUNT+videoId;
        Boolean isCollect = stringRedisTemplate.opsForSet().isMember(isCollectKey, userId);
        //获取收藏数
        String s = stringRedisTemplate.opsForValue().get(collectCountKey);
        Integer collectCount = Integer.valueOf(s);
        //创建对象
        VideoOperate videoOperate = createVideoOperate(videoId,userId1,1);
        //已经收藏
        if(isCollect){
            //总的收藏数-1
            stringRedisTemplate.opsForValue().set(collectCountKey,String.valueOf(collectCount-1));
            //挪出set列表
            stringRedisTemplate.opsForSet().remove(isCollectKey,userId);
            videoOperate.setIsCollect(String.valueOf(0));
            videoTasks.add(videoOperate);
            //返回0表示 要取消收藏
            return Integer.valueOf(0);
        }
        //没有收藏
        //总的收藏数+1
        stringRedisTemplate.opsForValue().set(collectCountKey,String.valueOf(collectCount+1));
        //加入set列表
        stringRedisTemplate.opsForSet().add(isCollectKey,userId);
        videoOperate.setIsCollect(String.valueOf(1));
        videoTasks.add(videoOperate);
        //返回1表示收藏成功
        return Integer.valueOf(1);

    }
    private class VideoOperateHandler implements Runnable{
        @Override
        public void run() {
            //子线程,开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能
            while (true) {
                //1.获取队列中的订单信息
                try {
                    //从阻塞队列中获取
                    VideoOperate v = videoTasks.take();
                    //2.操作数据库
                    proxy.handlerData(v);
                } catch (Exception e) {
                    log.error("数据异常", e);
                }
            }
        }
    }
    @Override
    @Transactional
    public void handlerData(VideoOperate v) {
        //如果是已经点赞,或者已经收藏 那就将原来数据删掉
        if(("0".equals(String.valueOf(v.getIsLike())))||("0".equals(String.valueOf(v.getIsCollect())))){
            videoOperateMapper.delete(v.getVideoId(), v.getOperateType());
        }
        videoOperateMapper.add(v);
    }
}

踩的坑

这个多线程异步实现的思路,首先就是去操作redis,然后将要操作的数据丢到阻塞队列中,当这个类初始化的时候(init())就会执行该线程池,然后就从阻塞队列中获取要的数据,去对数据库进行修改,但我的redis的数据会进行修改,但是Mysql数据库的一直都不会进行修改,也不会报错

一、多线程修改数据库的问题

使用多线程修改数据库的时候要注意事务的问题,Spring不会默认为我们开启事务,我们可以通过在方法上或者类上加上@Transactional 来代表要介入Spring的事务

二、事务失效的问题

我们在调用被@Transactional注解的方法的时候要注意事务失效的问题,如果直接调用,那么默认会以this.method()的方式进行调用(this对象,在编译的时候就已经被加载到栈中的局部变量表中了)。而我们在调用bean的时候要注意是要用交给Spring进行管理的对象,所以,才会使用代理对象来生成一个proxy对象,来调用方法,这样就防止了Spring事务的失效

小结

Spring框架对于多线程操作数据,为了保证数据的原子性和隔离性,在一定的情况下,Spring会阻止我们的多线程操作数据库,所以只有在保证事务的情况下,才会进行数据库的修改操作。

相关推荐
leobertlan7 小时前
2025年终总结
前端·后端·程序员
面向Google编程7 小时前
从零学习Kafka:数据存储
后端·kafka
易安说AI8 小时前
Claude Opus 4.6 凌晨发布,我体验了一整晚,说说真实感受。
后端
易安说AI8 小时前
Ralph Loop 让Claude无止尽干活的牛马...
前端·后端
易安说AI8 小时前
用 Claude Code 远程分析生产日志,追踪 Claude Max 账户被封原因
后端
JH30738 小时前
SpringBoot 优雅处理金额格式化:拦截器+自定义注解方案
java·spring boot·spring
颜酱9 小时前
图结构完全解析:从基础概念到遍历实现
javascript·后端·算法
Coder_Boy_9 小时前
技术让开发更轻松的底层矛盾
java·大数据·数据库·人工智能·深度学习
invicinble10 小时前
对tomcat的提供的功能与底层拓扑结构与实现机制的理解
java·tomcat
较真的菜鸟10 小时前
使用ASM和agent监控属性变化
java