1、分布式锁目前可能存在的问题(基于redis客户端jedis)
加锁: set key value [expiration EX seconds|PX milliseconds] [NX|XX]
该加锁方式是从Redis2.8之后便支持这种原子性加锁方式,之前设置setnx和设置过期时间不是原子性的。
String ret = jedis.set(getKey(key), new CacheEntity<T>(serializable, version, System.currentTimeMillis()).getBytes(), "NX".getBytes(), "EX".getBytes(), expireSecond); return StringUtils.equals("OK", ret);
参数解释如下:
- EX second :设置key的过期时间为 second 秒
- PX millisecond :设置key的过期时间为 millisecond 毫秒
- NX :只在key不存在时,才对key进行设置操作
- XX :只在key已经存在时,才对key进行设置操作
解锁
ret = jedis.del(getKey(key));
使用
if (!lock.tryLockXxx(key)) { return ; } try { // TODO } finally { lock.unlockXxx(key); }
基于jedis实现的分布式锁缺点
- 不支持重入
- 加锁时设置的value无意义
- 锁超时时间不能自动续期,所以不好取值
- 锁超时时间 > 业务执行时间,业务正常执行完成释放锁,没问题,业务节点奔溃,尚未释放锁,会导致其他业务系统线程最多等待整个超时时间
- 锁超时时间 < 业务执行时间,锁超时自动释放,业务执行完成,再执行unlock,可能会错误的解锁
- 加锁key设置在Master节点上,尚未同步到slave就宕机了,salve提升为新的master,没有上一个锁的信息,其他线程依然可以获取同一个key的锁
2、Redisson客户端优势
- 支持 SSL
- 线程安全的实现
- Reactive Streams API
- 异步 API
- 异步连接池
- Lua 脚本
- 分布式Java对象
- Object holder, Binary stream holder, Geospatial holder, BitSet, AtomicLong, AtomicDouble, PublishSubscribe, Bloom filter, HyperLogLog
- 分布式Java集合
- Map, Multimap, Set, List, SortedSet, ScoredSortedSet, LexSortedSet, Queue, Deque, Blocking Queue, Bounded Blocking Queue, Blocking Deque, Delayed Queue, Priority Queue, Priority Deque
- 分布式锁和同步器
- Lock, FairLock, MultiLock, RedLock, ReadWriteLock, Semaphore, PermitExpirableSemaphore, CountDownLatch
Redisson的基本用法参考这篇博客《基于redission的分布式锁》
自动续期源码:
// tryAcquireAsync -> scheduleExpirationRenewal -> renewExpiration // 自动续期 private void renewExpiration() { ... Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() { @Override public void run(Timeout timeout) throws Exception { ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName()); if (ent == null) { return; } Long threadId = ent.getFirstThreadId(); if (threadId == null) { return; } RFuture<Boolean> future = renewExpirationAsync(threadId); future.onComplete((res, e) -> { if (e != null) { log.error("Can't update lock " + getRawName() + " expiration", e); EXPIRATION_RENEWAL_MAP.remove(getEntryName()); return; } if (res) { // reschedule itself renewExpiration(); } }); } }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); ... }
真正执行续期的lua脚本
protected RFuture<Boolean> renewExpirationAsync(long threadId) { return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return 1; " + "end; " + "return 0;", Collections.singletonList(getRawName()), internalLockLeaseTime, getLockName(threadId)); }
Netty中的时间轮算法 HashedWheelTimer