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

背景

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

相关推荐
计算机小白一个20 分钟前
蓝桥杯 Java B 组之岛屿数量、二叉树路径和(区分DFS与回溯)
java·数据结构·算法·蓝桥杯
孤雪心殇22 分钟前
简单易懂,解析Go语言中的Map
开发语言·数据结构·后端·golang·go
菠菠萝宝34 分钟前
【Java八股文】10-数据结构与算法面试篇
java·开发语言·面试·红黑树·跳表·排序·lru
不会Hello World的小苗41 分钟前
Java——链表(LinkedList)
java·开发语言·链表
Allen Bright1 小时前
【Java基础-46.3】Java泛型通配符详解:解锁类型安全的灵活编程
java·开发语言
柃歌1 小时前
【UCB CS 61B SP24】Lecture 7 - Lists 4: Arrays and Lists学习笔记
java·数据结构·笔记·学习·算法
柃歌1 小时前
【UCB CS 61B SP24】Lecture 4 - Lists 2: SLLists学习笔记
java·数据结构·笔记·学习·算法
是姜姜啊!2 小时前
redis的应用,缓存,分布式锁
java·redis·spring
梨落秋溪、2 小时前
输入框元素覆盖冲突
java·服务器·前端
hrrrrb2 小时前
【Java】Java 常用核心类篇 —— 时间-日期API(上)
java·开发语言