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