加锁处理重复插入问题,包含多线程的单元测试

先说明业务场景吧:

做的一个课程学习模块,要求同一个人同一个课程章节的学习记录只有一条

之前的处理流程伪代码如下:

if(!isExist()){  //第一步
      insert();  //第二步
    }

即每次插入前都做判断,在大部分情况下,都不会出问题,但是并发情况下,就极有可能出现重复的数据。因为上述第二步操作依赖于第一步操作,这两步操作并不是原子性的。

先看段单元测试,模拟并发:

package com.inesa.coursecenter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.inesa.coursecenter.domain.entity.LearnLog;
import com.inesa.coursecenter.web.LearnLogController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(SpringRunner.class)
@SpringBootTest
public class LearnLogControllerTest {
    @Autowired
    @InjectMocks
    private LearnLogController learnLogController;

    private MockMvc mockMvc;

    @Before
    public void setUp() {
        mockMvc = MockMvcBuilders.standaloneSetup(learnLogController).alwaysDo(print()).build();;
    }
    @Test
    public void batchAddLearnLog() throws InterruptedException {
        final CountDownLatch countDownLatch = new CountDownLatch(5);   // 1
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        addLearnLog();
                        countDownLatch.countDown();  // 2
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        countDownLatch.await();   // 3
        executorService.shutdown();
    }

    private void addLearnLog() throws Exception {
        try {
            LearnLog learnLog = new LearnLog();
            learnLog.setAuth(true);
            learnLog.setUserId("a83f6f8d972b434eb8b1b690e9215b5c");
            learnLog.setCourseId("559");
            learnLog.setChapterId("7fefe1dd9b484becadaac1e036fc7132");
            learnLog.setOrgId("32");
            learnLog.setLearnHour(Double.valueOf(20));
            ObjectMapper mapper = new ObjectMapper();
            String requestJson = mapper.writeValueAsString(learnLog);
            System.out.println(requestJson);
            mockMvc.perform(post("/learn")
                    .accept(MediaType.APPLICATION_JSON).contentType(MediaType.APPLICATION_JSON)
                    .content(requestJson))
                    .andExpect(status().isOk())
                    .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                    .andExpect(jsonPath("$.result").value("Success"))
                    .andReturn();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

测试代码中,标注的三行非常重要。如果去掉这三行,线程池中的代码还来不及运行,主程序就将线程池shutdown了。对于CountDownLatch的用法,如果不清楚的请参考我的另一篇博文线程安全性--atomic里面有详细的说明。 运行完上述代码,并发导致的重复插入问题就很好复现了,如图所示,同一个人,同一个章节的学习记录插入了四条数据,影响后续做统计或计算。

学习记录

处理方案:

 private ReentrantLock lock = new ReentrantLock();
 @Override
    @Transactional(rollbackFor = Exception.class)
    public RestfulResult add(LearnLog learnLog, Chapter chapter, Course course) {
        lock.lock();
        try {
            String existId = exist(learnLog);
            if (!StringUtils.isBlank(existId)) {
                learnLog.setId(existId);
            } else {
                if (DoubleUtil.isEqual(chapter.getAllHour(), learnLog.getLearnHour())) {
                    learnLog.setStatus(String.valueOf(Enums.ChapterStatus.LEARNED.getValue()));
                } else {
                    learnLog.setStatus(String.valueOf(Enums.ChapterStatus.LEARNING.getValue()));
                }
                saveBase(learnLog);
                learnLog.setUserId(learnLog.getCreateBy());
                if (learnLog.getPer() == null) {
                    getChapterPer(learnLog, chapter.getAllHour());
                } else {
                    if (DoubleUtil.isEqual(learnLog.getPer(), Double.valueOf(100))) {
                        learnLog.setStatus(String.valueOf(Enums.ChapterStatus.LEARNED.getValue()));
                    }
                }
                learnLogMapper.insertSelective(learnLog);
                //异步更新课程学习进度
                executorCourse.executorUpdateCourseStatus(course, learnLog.getUserId());
                return RestfulResult.success(learnLog);
            }
        } finally {
            lock.unlock();
        }

        //更新  课程章节学习完之后状态不再修改
        LearnLog tmp = getById(learnLog.getId());
        if (!String.valueOf(Enums.ChapterStatus.LEARNED.getValue()).equals(tmp.getStatus())) {
            if (DoubleUtil.isEqual(chapter.getAllHour(), learnLog.getLearnHour())) {
                tmp.setStatus(String.valueOf(Enums.ChapterStatus.LEARNED.getValue()));
            } else {
                tmp.setStatus(String.valueOf(Enums.ChapterStatus.LEARNING.getValue()));
            }
        }
        tmp.setLearnHour(learnLog.getLearnHour());
        beforeUpdate(tmp);
        if (learnLog.getPer() == null) {
            getChapterPer(tmp, chapter.getAllHour());
        } else {
            tmp.setPer(learnLog.getPer());
            if (DoubleUtil.isEqual(learnLog.getPer(), Double.valueOf(100))) {
                learnLog.setStatus(String.valueOf(Enums.ChapterStatus.LEARNED.getValue()));
            }
        }
        learnLogMapper.updateByPrimaryKeySelective(tmp);
        //异步更新课程学习进度
        executorCourse.executorUpdateCourseStatus(course, learnLog.getUserId());
        return RestfulResult.success(learnLog);
    }

先暂时这么处理,本地3个人50个并发没有问题,等待线上的反应

转载于:https://my.oschina.net/wuyiyi/blog/3040412

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值