前言
最近开发任务需要在多个时间点执行,时间点一部分由定时任务设定,一部分由mq监听来执行,但必须保证同一时间点只能有一个任务来执行。我们知道jdk自身提供的锁只能适用用单机应用,但如今分布式架构应用越来越多,多个应用部署集群时,我们可能需要某个时刻只有一个应用执行,这时就需要用到分布式锁。
分布锁基本应用场景及设计
###案例
有2个计算基金涨跌幅数据服务,一个是定时计算,一个是在每天基金净值更新后发送到mq,然后由多个监听服务去计算,净值更新时间是不固定的,因此计算时间也不固定。所以2个计算涨跌幅服务的服务会发生冲突,如何保证在同一时间只有一个任务执行,那就需要用到分布式锁。在这个场景中我用的是redis作为分布式锁来实现的。
上图基本为实现这个案例的流程,设计服务时我们需要保证以下几点:
1、分布锁必须保证多个服务请求同一个锁时绝对互斥。
2、需要设计超时间,防止任务停止或服务阻塞时锁无法释放,从而引发下一次任务一直无法获取锁。
Redis具体实现分布式锁
redis实现分布锁是通过SETNX
命令来实现的,具体格式
SETNX key value
命令的作用是如果key存在,则什么都不做,并且返回0,如果key不存在则将key的值设置成value,并且返回1,该命令是原子性的。我们可以利用该命令来实现分布式锁。
主要代码:
//这里设置锁过期时间,防止在获得锁的服务阻塞或者崩溃引起的锁无法释放
boolean getlock = lockManager.aquireLock("redisKey");
//判断获取锁成功
if(getlock){
try{
//do some thing
}finally {
//释放锁
lockManager.releaseLock("redisKey");
}
}
//lockManager
@Service
public class LockServiceImpl implements LockManager {
@Resource(name = "redisServiceImpl")
private RedisManager redisManager;
//设置锁时间为10分钟,避免停服务 时计算涨跌幅,锁没释放,可根据其它具体业务需求修改过期时间
private static final Long LOCK_EXPIRE = 10*60L;
@Override
public boolean aquireLock(String key) {
boolean isSet = redisManager.put(key, "1", LOCK_EXPIRE, true);
return isSet;
}
@Override
public void releaseLock(String key){
redisManager.delete(key);
}
}
redisManager.put方法
//redis2.1之前,spring-data-redis不支持直接setnx 调过期时间,可用以下方法实现
String lockKey = event.getModel() + ":" + event.getAction() + ":" + event.getId() + ":" + event.getMessage_id();
log.info("lockKey : {}" , lockKey);
// 使用sessionCallBack处理
SessionCallback<Boolean> sessionCallback = new SessionCallback<Boolean>() {
List<Object> exec = null;
@Override
@SuppressWarnings("unchecked")
public Boolean execute(RedisOperations operations) throws DataAccessException {
operations.multi();
stringRedisTemplate.opsForValue().setIfAbsent(lockKey,JSON.toJSONString(event));
stringRedisTemplate.expire(lockKey,Constants.REDIS_KEY_EXPIRE_SECOND_1_HOUR, TimeUnit.SECONDS);
exec = operations.exec();
if(exec.size() > 0) {
return (Boolean) exec.get(0);
}
return false;
}
};
return stringRedisTemplate.execute(sessionCallback);
升级到2.1又后,可以直接用
setIfAbsent(k key,V value,long timeout,TimeUnit unit);
后续
以上为我所用到的reids作为分布式锁的一个应用场景,或许你已经看出上面应用案列中有可能的问题。因为本业务对互斥要求不高,所以即使重复计算也不会有多大问题,但对于互斥要求高的服务或许需要考虑的更多。
比如redis宕机了,该如何保证redis的可用性。即使是用redis的主从集群复制,主挂了,从可以接替上,但是依然会出现问题,因为redis主从复制是异步的,没办法保证主挂了,从节点上一定有锁数据。