Java业务原子性的一种实现(key 独占访问)
开发过程中,有时候为了解决多线程竞争问题需要加锁,通常锁定的对象是class,object,method,但在特定时候我们需要更细粒度的加锁,也就是根据不同输入参数来锁定不同的资源,这样只有调用此方法的不同线程传参一样才会进行竞争。
比如一个简单的例子:假设系统为用户提供借款,每月有个限额。每月的借款记录都记在transaction_detail表中,此时当一个用户需要进行借款的时候,我们需要进行一下操作:
method:borrow_money
1、判断用户是否达到借款限额,本月已借款量:select sum(amount) from transaction_detail where user_id=XX;
2、其他业务处理
3、插入transaction_detail
但是以上三步并非原子的,也就是假设用户限额为3w,已借款量1.5w(还可借1.5w),现在有两个线程a、b,每个线程都需要借款1w。
当线程a执行完第一步判断,此时线程a挂起。
线程2开始执行,由于此时借款1w<可借的1.5w,于是线程2执行下去成功贷款1w,插如新的贷款记录。
a线程此时继续执行2、3步也成功贷款了1w,这样该用户当月贷款量已达3.5w大于总限额了(违背了每月3w的限额)。导致这个情况就是以上3步非原子的,在我们执行借款、插入借款记录的贷款状态很可能与我们判断当月已贷款量已经不同了。
这样我们就需要对上面的三步进行加锁同步,但是如果我们将上诉方法borrow_money使用synchronized,将会引发很严重的效率问题,因为这样整个系统的所有线程在执行borrow_money都是竞争关系的,这样就会造成系统性能瓶颈。
于是我们可以使用一种更细粒度的加锁机制。可以对特定字符串加锁来保证操作的原子性同时减少线程对资源的竞争,这个特定字符串需要与用户一对一的关联,同时确保不会影响系统其它操作,所以可以使用:特殊字符串(确保字符串具有特殊性) + id。
而且我们的字符串需要到jvm的常量池中获取这样确保对于相同的用户获取的字符串是一个对象。
String syncKey = ("borrow_money_" + userId).intern(); synchronized (syncKey) { //1、判断用户本月已借款量:select sum(amount) from transaction_detail where user_id=XX; //2、其他业务处理 //3、插入transaction_detail }
如上便解决对于同一个key 独占访问,但这仅限于同一个jvm,如果服务器部署在集群上便无法达到预期效果。
此时可以将每月总借款信息记录在一张表中,这样在判断与操作的时候可以根据这条记录来判断状态
或者使用分布式的锁,如:ZooKeeper进行管理。