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

背景

我想实现一个点赞收藏功能,这个数据是存到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会阻止我们的多线程操作数据库,所以只有在保证事务的情况下,才会进行数据库的修改操作。

相关推荐
码上一元2 小时前
SpringBoot自动装配原理解析
java·spring boot·后端
计算机-秋大田2 小时前
基于微信小程序的养老院管理系统的设计与实现,LW+源码+讲解
java·spring boot·微信小程序·小程序·vue
魔道不误砍柴功4 小时前
简单叙述 Spring Boot 启动过程
java·数据库·spring boot
失落的香蕉4 小时前
C语言串讲-2之指针和结构体
java·c语言·开发语言
枫叶_v4 小时前
【SpringBoot】22 Txt、Csv文件的读取和写入
java·spring boot·后端
wclass-zhengge4 小时前
SpringCloud篇(配置中心 - Nacos)
java·spring·spring cloud
路在脚下@4 小时前
Springboot 的Servlet Web 应用、响应式 Web 应用(Reactive)以及非 Web 应用(None)的特点和适用场景
java·spring boot·servlet
黑马师兄4 小时前
SpringBoot
java·spring
数据小小爬虫4 小时前
如何用Java爬虫“偷窥”淘宝商品类目API的返回值
java·爬虫·php
暮春二十四4 小时前
关于用postman调用接口成功但是使用Java代码调用却失败的问题
java·测试工具·postman