redis实现分布式锁
一 场景
分布式环境,一共三台机器,跑批时,为了保证跑批触发时间点只有一个机器进行job跑批,故增加分布式锁来控制防重跑。
二 redis实现分布式锁
/**
* 对于分布式加锁本例中使用 setnx()含义是SET if Not Exists,其主要有两个参数 setnx(key, value)。该方法是原子的,如果key不存在,则设置当前key成功,返回1;如果当前key已经存在,则设置当前key失败,返回0。
* 另外使用 getset()也可以:这个命令主要有两个参数 getset(key, newValue)。该方法是原子的,对key设置newValue这个值,并且返回key原来的旧值。
* ttl命令:当 key 不存在时,返回 -2 。 当 key 存在但没有设置剩余生存时间时,返回 -1 。 否则,以毫秒为单位,返回 key 的剩余生存时间。(在 Redis 2.8 以前,当 key 不存在,或者 key 没有设置剩余生存时间时,命令都返回 -1 )。
*/
@Component
public class RedisLock {
private static final String PREFIX_LOCK_KEY = "RULE:LOCK:"; //缓存 key前辍
private static final Integer EXPIRE_SECOND = 300; //缓存时间
@Resource
private RedisClusterClient redisClusterClient;
public boolean tryLock(String key) {
String redisKey = PREFIX_LOCK_KEY + key;
Long val = redisClusterClient.tryLock(redisKey);
LOGGER.info("获取锁,key:{},val:{}", redisKey, val);
if (val > 0) {
redisClusterClient.expire(redisKey, EXPIRE_SECOND);
return true;
} else {
if (redisClusterClient.ttl(redisKey) < 0) {
LOGGER.info("delete lock,key:{}", redisKey);
redisClusterClient.expire(redisKey, EXPIRE_SECOND);
return true;
}
}
return false;
}
public void unLock(String key) {
redisClusterClient.del(PREFIX_LOCK_KEY + key);
LOGGER.info("释放锁 unLock,key:{}", key);
}
}
应用代码如下,
public void xx() {
if (redisLock.tryLock(key)) {
try {
LOGGER.info("拿到redis分布式锁!");
.....
} catch (Exception e) {
LOGGER.error(e);
} finally {
redisLock.unLock(key);
}
} else {
LOGGER.info("请稍后再试!");
}
缺点:
-
getSet与expire不是一个原子操作,可能执行完setnx该进程就挂了。
- 当锁过期后,该进程还没执行完,可能造成同时多个进程取得锁。(貌似这个问题目前还没有很优雅的解决方案)
三 3种分布式锁方案对比:
数据库锁:
优点:直接使用数据库,使用简单。
缺点:分布式系统大多数瓶颈都在数据库,使用数据库锁会增加数据库负担。
缓存锁:
优点:性能高,实现起来较为方便,在允许偶发的锁失效情况,不影响系统正常使用,建议采用缓存锁。
缺点:通过锁超时机制不是十分可靠,当线程获得锁后,处理时间过长导致锁超时,就失效了锁的作用。
zookeeper锁:
优点:不依靠超时时间释放锁;可靠性高;系统要求高可靠性时,建议采用zookeeper锁。
缺点:性能比不上缓存锁,因为要频繁的创建节点删除节点。
上面几种方式,哪种方式都无法做到完美。就像CAP一样,在复杂性、可靠性、性能等方面无法同时满足,所以,根据不同的应用场景选择最适合自己的才是王道。
从理解的难易程度角度(从低到高)
数据库 > 缓存 > Zookeeper
从实现的复杂性角度(从低到高)
Zookeeper >= 缓存 > 数据库
从性能角度(从高到低)
缓存 > Zookeeper >= 数据库
从可靠性角度(从高到低)
Zookeeper > 缓存 > 数据库

浙公网安备 33010602011771号