楔子
很多情况下可能需要在用户首次访问进行一些触发,触发一些初始化的动作,比如数据的初始化、加载缓存等。但无论是数据初始化还是缓存的初始化的触发,究其本质都是利用在请求发生时进行一定的行为触发;但是,在实际情况下会出现多个请求,请求时会多个请求会到同一个触发点;这种情况形同于多个线程同时对一个代码块进行访问,那么这种情况必须得保证多个请求访问的情况下必须只允许一个请求进行触发,就类似于单例模式以保证避免重复创建对象的动作。
实际应用
接下来我就说说我在实际开发中、在什么样的业务中又该如何应用
我们先来看看业务流程,由于涉及到详细的实际业务功能,所以这里我就比较笼统的使用“计算项目费用”表示
流程
1、用户访问页面携带项目ID
2、判断当前的用户请求携带的项目ID是否计算过费用
3、如果没有计算过话那么就会触发计算项目费用的逻辑
4、计算完毕后将项目费用存入到数据库
5、如果已经计算过的话直接读取计算后的项目费用值
在以上流程中如果同时只是一个请求的话那么并不会出现任何问题,但是,在实际的生产环境中不可能同时只有一个请求访问而是会有多个请求同时访问,那么在这种情况下如何保证多个请求同时访问时只保证只有一个请求触发计算项目费用呢?
在这里我使用了分段锁,实现了类似于单例模式下使用同步锁来锁住对象实例化的代码块;在这里为何不使用同步锁或者重复锁呢?
那么我们来看下直接使用同步锁,锁住的情况,看看是否符合实际的业务要求
因为,在锁住代码逻辑块这里必要要考虑到访问不同的项目ID时的情况,只有在访问在同一个项目ID时才会锁住;如果使用的是同步锁时锁住则是所有项目这样一来由于锁的细粒度不够就会导致锁住所有的项目,而不是根据不同的项目ID来锁住,实际的需求应该需要将锁下沉一层,分划成更小的细粒度
如下图所示
这里就需要使用分段锁来针对不同的项目ID来进行加锁,如果,请求访问的是同一个项目ID才会进入锁,如果,访问的不是同一个项目ID的话还是按照正常的代码逻辑执行
在实际开发中的应用分段锁解决重复“计算项目费用”
代码
ConcurrentReentrantLock lock = new ConcurrentReentrantLock();
public void ifNecessaryCalculateSiteAmount(Long projectId){
final String signal = "1";
Assert.notNull(projectId, "项目ID不能为空!");
Long bulletinProjectId = projectId;
// Redis储存标志位,Redis的key
String signalKey = CACHE_KEY_SITE_COST_SIGNAL + ":" + bulletinProjectId;
// 标记位
Supplier<Boolean> hasCalculateSignal = ()->{
// 判断项目费用是否已经计算
String signalVal = jedisUtil.getString(signalKey);
return StringUtils.equals(signalVal, signal);
};
//初次检查
if(hasCalculateSignal.get()){
return;
}
// 加锁
lock.lock(bulletinProjectId);
// 双检锁
if(hasCalculateSignal.get()){
return;
}
try {
// 判断是否需要计算
// TODO 计算项目费用
// 写入数据库
// 标记当前项目已经计算
}finally {
// 删除锁
lock.unlock(bulletinProjectId);
}
}