最近在编写公司APP产品的商品砍价功能,其中有一个接口涉及并发访问。自测时通过ApiFox接口管理工具进行压测,落地数据时出现了"锁失效"的情景。十分感谢后端小伙伴的帮助排查,解决了这个问题。
并发接口中,先对主表数据进行读取,进行业务判断后,新增、修改它表的数据。在理应串行执行的情况下发生了多个请求线程读取到了相同的主表数据,导致数据处理异常。也正是前言中所说的"锁失效"了。(实际情况加锁操作是有效的)
@RequestMapping("/test") @Transactional(rollbackFor = Exception.class) public String test() { DistributedLock.lock("ct_lock"); try { Map<String, Object> resultMap = jdbcTemplate.queryForMap("select * from concurrent_read_uncommit"); int num = Integer.parseInt(resultMap.get("num").toString()); num++; jdbcTemplate.update("update concurrent_read_uncommit set num = " + num); } finally { DistributedLock.unlock("ct_lock"); } return "success"; }
ApiFox中通过创建100个请求线程进行压测,最终concurrent_read_uncommit表中的num字段值为94,而非100。
新编写了两个简单接口,第一个接口加锁,并线程休眠30秒后释放锁。另一个接口加同样的锁,打印一条语句后直接返回。先调用第一个接口,在调用第二个接口。Debug中发现锁是有效的,在redis中存有锁Key。并且访问第二个接口时,线程被阻塞在了加锁行代码。
查询数据库事务默认隔离级别:
select @@tx_isolation;
结果
REPEATABLE-READ
就是默认的RR级别,那么说明同个事务内多次读取数据都会是一样的,不会读取到脏数据。
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
与这个并没有关系,八竿子打不着。当时的想法时是多个并发请求在进入到了同个事务内,并一起读取到了没有被修改前的数据。细想想:
在锁代码块中调用事务方法,而不是在事务方法中进行加锁。
原因为:并发情境下,执行速度过快,很有可能发生:请求线程在释放锁后没有来得及提交事务,另一个请求线程在加锁处被唤醒,继而读取到了事务未提交的数据。即读取到了脏数据,产生了"锁失效"的效果。
修正代码:
@RequestMapping("/test2") public String test2() { ConcurrentTransactionalController proxyBean = SpringContextUtils.getBean(this.getClass()); proxyBean.doTest2(); return "success"; } @Transactional(rollbackFor = Exception.class) public void doTest2() { DistributedLock.lock("ct_lock"); try { Map<String, Object> resultMap = jdbcTemplate.queryForMap("select * from concurrent_read_uncommit"); int num = Integer.parseInt(resultMap.get("num").toString()); num++; jdbcTemplate.update("update concurrent_read_uncommit set num = " + num); Thread.sleep(500); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { DistributedLock.unlock("ct_lock"); } }
这样就保证了不会读取到事务未提交的数据,同时又具有锁的排他性。
其实锁一直都是有效的,本质原因就在于Spring的事务代理Bean屏蔽了事务代码。我们不能手动的进行控制,也就是说你变更了不了事务代码的顺序。如果能将提交事务的行代码写到释放锁之前,就不会存在这个问题了。所以,也可以通过编程式事务解决这个问题,关于编程式事务,Spring也有做代码封装。如果不通过编程式事务,那么就只能通过上述代码变相的来实现。