最近发现最代码会有重复的event生成,比如微信扫描关注+5牛币,下载源码等操作,很多牛牛之前反映过此类问题没有在意,最近有时间来深入研究下此类问题。
线上出错案例:
于是在Service层的方法做了如下同步操作:
private Event checkProjectEvent(User user, Project project, int type) { Event event = null; Page<Event> events = findAllByTypeAndUserIdAndSourceId( type, user.getId(), project.getId(), 1, 1); if (events.getTotalElements() == 0) { event = initEvent(user, type); event.setSourceId(project.getId()); event.setSource(project); event.setSourceUserId(project.getUserId()); event.setSourceUser(project.getUser()); System.out.println(currentThread().getName() + " new event " + user.getId() + "_" + project.getId() + "_" + type); } else { event = events.getContent().get(0); System.out.println(currentThread().getName() + " get event " + user.getId() + "_" + project.getId() + "_" + type); } return event; } //同步检测同一用户,同一个project,同一type的event并发的情况 @Transactional(readOnly = false, propagation = Propagation.REQUIRED) public void synchronizedCollectCreateProjectEvent(User user, Project project) { int type = ModuleConstants.EVENT_TYPE_RULE_PROJECT_COLLECT; String lock = getUserSourceTypeLock(user.getId(), project.getId(), type); System.out.println(currentThread().getName() + " " + lock); synchronized (lock.intern()) { Event event = checkProjectEvent(user, project, type); if (event.getId() != null && event.getStatus() >= ModuleConstants.MODULE_STATUS_NORMAL) {//已收藏则直接返回该event即可,无须再更新收藏数 return; } int status = ModuleConstants.MODULE_STATUS_NORMAL; if (ModuleConstants.PUBLIC_EVENTS.contains(type)) { status = ModuleConstants.EVENT_STATUS_PUBLIC; } event.setStatus(status); save(event); } JSONObject extendJson = project.getExtend(); int collectCount = extendJson .getInt(ModuleConstants.PROJECT_EXTEND_JSON_COLLECT_COUNT); collectCount++; extendJson.put(ModuleConstants.PROJECT_EXTEND_JSON_COLLECT_COUNT, collectCount); project.setExtendJson(extendJson.toString()); project.setExtend(extendJson); projectService.save(project); }
但是发现连续点击收藏按钮并发时还是出现多次new event的log
http-apr-80-exec-3 1_6183_304
http-apr-80-exec-3 new event 1_6183_304
http-apr-80-exec-5 1_6183_304
http-apr-80-exec-5 new event 1_6183_304
http-apr-80-exec-4 get event 1_6183_303
http-apr-80-exec-7 1_6183_304
http-apr-80-exec-7 get event 1_6183_304
http-apr-80-exec-9 get event 1_6183_303
http-apr-80-exec-6 get event 1_6183_303
通过以下单元测试方法确认java线程并发同步时加锁的机制是确认正常生效的
private static final HashMap EVENTMAP = new HashMap(); public static String synchronizedCollectRemoveProjectEvent(String userId, String projectId, String type, Thread thread) { String lock = userId + "_" + projectId + "_" + type; String id = null; synchronized (lock.intern()) { if (EVENTMAP.containsKey(lock)) { id = EVENTMAP.remove(lock); System.out.println(thread.getName() + " remove " + id); } else { System.out.println(thread.getName() + " not find " + id); } } return id; } public static void main(String[] args) throws InterruptedException { final String userId = "1"; final String projectId = "1"; final String type = "1"; for (int i = 0; i < 20; i++) { new Thread("create" + i) { public void run() { Thread currentThread = currentThread(); String id = synchronizedCollectCreateProjectEvent(userId, projectId, type, currentThread); } }.start(); } }
之后将synchronized关键词加到方法上测试也可以实现并发加锁机制,真是百思不得其解呀。
后来想到应该不是java线程语法的问题,或许是和spring有关,于是百度"spring synchronized intern"得到一篇文章:
才恍然大悟,synchronized同步肯定有效果,但是因为有spring事务配置,同步代码如下:
synchronized (lock.intern()) { Event event = checkProjectEvent(user, project, type); if (event.getId() != null && event.getStatus() >= ModuleConstants.MODULE_STATUS_NORMAL) {//已收藏则直接返回该event即可,无须再更新收藏数 return; } int status = ModuleConstants.MODULE_STATUS_NORMAL; if (ModuleConstants.PUBLIC_EVENTS.contains(type)) { status = ModuleConstants.EVENT_STATUS_PUBLIC; } event.setStatus(status); save(event); }
多个线程执行该block时,虽然调用了save event,但是因为该方法尚未执行结束,事务也尚未提交,所以多线程并发执行时,肯定没save event,每次返回的都是new event,除非某个线程中该方法的代码全部执行完毕提交了事务。
果断改写代码
测试后log打印输出正常
enjoy it all 牛牛!