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 > 缓存 > 数据库