一、 前言
最近在做网上法庭的一个比较有意思的小需求,就是通过扫二维码方式允许最多30个人同时进入庭审,但是不限制进入的是是不是庭审人员,也就是说只要扫了这个二维码并且当前案件对应的参与人数不到30那么就可以进入,始终维持一个庭审案件里面最多有30人。
二、 方案研究
扫描二维码会调用后台的一个rpc,而Rpc会调用bo方法进行处理,那么下面就研究下bo里面里面怎么做。
由于需求是要控制一个庭审的人数,而扫码人肯定是并发的访问这个bo方法,首先会有两种思路使用数据库的锁或者在业务层面进行控制。
2.1 使用乐观锁来控制
case_id | count |
---|---|
1 | 0 |
如表每条记录case_id唯一,并且对应一个count字段用来维持进入庭审人员个数。
进入庭审
prviate final int COUNT = 30public boolean boEnterMethod(String enCaseid){
第一步解密enCaseid获取真正caseId;
第二步根据caseId获取记录(里面包含count)nowCount = count ; if( nowCount == COUNT){ return false;
}
update 表 set count=nowCount+1 where count =nowCount and id = #id;
Long rows = 更新语句返回行数; if(rows == 1){
处理业务 return true;
} return false;
}
退出庭审
public boolean boExitMethod(String enCaseid){
第一步解密enCaseid获取真正caseId;
第二步根据caseId获取记录(里面包含count)nowCount = count; if( nowCount == 0){ return false;
}
update 表 set count=nowCount-1 where count =nowCount and id = #id;
Long rows = 更新语句返回行数; if(rows == 1){
处理业务 return true;
} return false}
这种方式好处是在执行时候才进行校验不需要提前对记录进行加锁,坏处是,假如两个人同时扫描二维码,获取的nowCount=1那么只有一个能真正进入庭审,另外一个会失败,结果是他进入不了庭审。
乐观锁下有咩有办法解决那?答案是肯定的,还记得AQS里面的trylock?第一次cas失败,那好吧,我再循环一次再试试。所以改进在于可以加个循环,如下:
进入庭审
public boolean boEnterMethod(String enCaseid){
第一步解密enCaseid获取真正caseId; for(;;){
第二步根据caseId获取记录(里面包含count)nowCount = count; if( nowCount == COUNT){ return false;
}
update 表 set count=nowCount+1 where count =nowCount and id = #id;
Long rows = 更新语句返回行数; if(rows == 1){
处理业务 return true;
}
}
}
退出庭审
public boolean boExitMethod(String enCaseid){
第一步解密enCaseid获取真正caseId; for(;;){
第二步根据caseId获取记录(里面包含count)nowCount = count; if( nowCount == 0){ return false;
}
update 表 set count=nowCount-1 where count =nowCount and id = #id;
Long rows = 更新语句返回行数; if(rows == 1){
处理业务 return true;
}
}
}
加个循环目前是为了避免当访问量不足30时候由于乐观锁竞争导致的失败,这里当当前访问量为30的时候直接返回是为了避免大量请求线程空轮造成tomcat线程池满。但是问题是可能查询数据库的频率比较高。
2.2 使用悲观锁来控制
乐观锁
public boolean boEnterMethod(String enCaseid){ 第一步解密enCaseid获取真正caseId; 第二步根据caseId获取记录(里面包含count)使用select * from 表 where .. for update 对本记录加锁 nowCount = count; rowId = id; if( nowCount == COUNT){ return false; } update 表 set count=nowCount+1 where id = rowId; Long rows = 更新语句返回行数; if(rows == 1){ 处理业务 return true; } return false; }
退出庭审
public boolean boExitMethod(String enCaseid){
第二步根据caseId获取记录(里面包含count)使用select * from 表 where .. for update 对本记录加锁
nowCount = count;
rowId = id; if( nowCount == 0){ return false;
}
update 表 set count=nowCount-1 where id = rowId;
Long rows = 更新语句返回行数; if(rows == 1){
处理业务 return true;
} return false}
使用悲观锁方式是事先对记录加锁,其他事务访问时候需要等待,直到当前事务提交。
2.3 使用业务锁来控制
public class TestLock { private final int COUNT_NUM = 30; private final SafeIntegerCount count = new SafeIntegerCount(COUNT_NUM); private final static ConcurrentHashMap<String, SafeIntegerCount> caseLockMap = new ConcurrentHashMap<>(); //初始化缓存
public void init() { //select所有caseid到list
for (String caseId:list) {
SafeIntegerCount count = new SafeIntegerCount(COUNT_NUM);
caseLockMap.put(caseId, count);
}
} public void boEnterMethod(String enCaseid) {
对enCaseid进行解密得到caseId
SafeIntegerCount count = caseLockMap.get(caseId); // 进入
if (null == count) { return;
} try { if (count.inc()) { // 处理业务
}
} catch (Exception e) {
count.desc();
}
} public void boExitMethod(String enCaseid) {
对enCaseid进行解密得到caseId
SafeIntegerCount count = caseLockMap.get(caseId); // 进入
if (null == count) { return;
} try { if (count.desc()) { // 处理业务
}
} catch (Exception e) {
count.inc();
}
}
}
使用ReentrantLock实现了一个可以判断上下限的计数器。眨眼看可以解决问题,但是仅仅单台机器可以正常,多台机器下会有问题,另外案件量特别大时候缓存可能占用大量内存。
2.4 总结
推荐使用悲观锁方式。