先用setnx
来抢锁,如果抢到之后,再用expire
给锁设置一个过期时间,防止锁忘记了释放。
setnx(key, value)
setnx
的含义就是 SET if Not Exists
,该方法是原子的。如果 key
不存在,则设置当前 key
为 value
成功,返回 1
;如果当前 key
已经存在,则设置当前 key
失败,返回 0
。
expire(key, seconds)
expire
设置过期时间,要注意的是 setnx
命令不能设置 key
的超时时间,只能通过 expire()
来对 key
设置。
1.1 使用Lua脚本(SETNX+EXPIRE)
可以使用Lua脚本来保证原子性(包含setnx和expire两条指令),加解锁代码如下:
/** * 使用Lua脚本,脚本中使用setnex+expire命令进行加锁操作 */ public boolean lock(Jedis jedis, String key, String uniqueId, int seconds) { String luaScript = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" + "redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end"; Object result = jedis.eval(luaScript, Collections.singletonList(key), Arrays.asList(uniqueId, String.valueOf(seconds))); return result.equals(1L); } /** * 使用Lua脚本进行解锁操纵,解锁的时候验证value值 */ public boolean unlock(Jedis jedis, String key, String value) { String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then " + "return redis.call('del',KEYS[1]) else return 0 end"; return jedis.eval(luaScript, Collections.singletonList(key), Collections.singletonList(value)).equals(1L); }
1.2 STW
如果在写文件过程中,发生了 FullGC,并且其时间跨度较长, 超过了锁超时的时间, 那么分布式就自动释放了。在此过程中,client2 抢到锁,写了文件。client1 的FullGC完成后,也继续写文件,注意,此时 client1 的并没有占用锁,此时写入会导致文件数据错乱,发生线程安全问题。这就是STW导致的锁过期问题。STW导致的锁过期问题,如下图所示:
STW导致的锁过期问题,大概的解决方案有:
此方案如果要实现,需要调整业务逻辑,与之配合,所以会入侵代码。
方案二:watch dog自动延期机制
客户端1加锁的锁key默认生存时间才30秒,如果超过了30秒,客户端1还想一直持有这把锁,怎么办呢?简单!只要客户端1一旦加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间。Redission采用的就是这种方案, 此方案不会入侵业务代码。
方案:SET key value [EX seconds] [PX milliseconds] [NX|XX]
EX second
:设置键的过期时间为 second
秒。 SET key value EX second
效果等同于 SETEX key second value
PX millisecond
:设置键的过期时间为 millisecond
毫秒。 SET key value PX millisecond
效果等同于 PSETEX key millisecond value
NX
:只在键不存在时,才对键进行设置操作。 SET key value NX
效果等同于 SETNX key value
XX
:只在键已经存在时,才对键进行设置操作客户端执行以上的命令:
OK
,那么这个客户端获得锁NIL
,那么客户端获取锁失败,可以在稍后再重试2.1 加锁
使用redis命令 set key value NX EX max-lock-time 实现加锁。
Jedis jedis = new Jedis("127.0.0.1", 6379); private static final String SUCCESS = "OK"; /** * 加锁操作 * @param key 锁标识 * @param value 客户端标识 * @param timeOut 过期时间 */ public Boolean lock(String key,String value,Long timeOut){ String var1 = jedis.set(key,value,"NX","EX",timeOut); if(LOCK_SUCCESS.equals(var1)){ return true; } return false; }
jedis.set(key,value,"NX","EX",timeOut)
【保证加锁的原子操作】key
是redis
的key
值作为锁的标识,value
在作为客户端的标识,只有key-value
都比配才有删除锁的权利【保证安全性】timeout
设置过期时间保证不会出现死锁【避免死锁】NX
:只有这个key
不存才的时候才会进行操作,if not exists
EX
:设置key
的过期时间为秒,具体时间由第5
个参数决定,过期时间设置的合理有效期需要根据业务具体决定,总的原则是任务执行time*3
2.2 解锁
使用redis命令 EVAL 实现解锁。
Jedis jedis = new Jedis("127.0.0.1", 6379); private static final String SUCCESS = "OK"; /** * 加锁操作 * @param key 锁标识 * @param value 客户端标识 * @param timeOut 过期时间 */ public Boolean lock(String key,String value,Long timeOut){ String var1 = jedis.set(key,value,"NX","EX",timeOut); if(LOCK_SUCCESS.equals(var1)){ return true; } return false; }
2.3 重试
如果在业务中去拿锁如果没有拿到是应该阻塞着一直等待还是直接返回,这个问题其实可以写一个重试机制,根据重试次数和重试时间做一个循环去拿锁,当然这个重试的次数和时间设多少合适,是需要根据自身业务去衡量的。
/** * 重试机制 * @param key 锁标识 * @param value 客户端标识 * @param timeOut 过期时间 * @param retry 重试次数 * @param sleepTime 重试间隔时间 * @return */ public Boolean lockRetry(String key,String value,Long timeOut,Integer retry,Long sleepTime){ Boolean flag = false; try { for (int i=0;i<retry;i++){ flag = lock(key,value,timeOut); if(flag){ break; } Thread.sleep(sleepTime); } }catch (Exception e){ e.printStackTrace(); } return flag; }
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还实现了可重入锁(Reentrant Lock)、公平锁(Fair Lock、联锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)等,还提供了许多分布式服务。
3.1 特性功能
3.2 Watch dog
总体的Redisson框架的分布式锁类型大致如下:
3.3 实现方案
添加依赖
<!-- 方式一:redisson-java --> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.11.4</version> </dependency> <!-- 方式二:redisson-springboot --> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.11.4</version> </dependency>
定义接口
import org.redisson.api.RLock; import java.util.concurrent.TimeUnit; public interface DistributedLocker { RLock lock(String lockKey); RLock lock(String lockKey, int timeout); RLock lock(String lockKey, TimeUnit unit, int timeout); boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int leaseTime); void unlock(String lockKey); void unlock(RLock lock); }
实现分布式锁
import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import java.util.concurrent.TimeUnit; public class RedissonDistributedLocker implements DistributedLocker{ private RedissonClient redissonClient; @Override public RLock lock(String lockKey) { RLock lock = redissonClient.getLock(lockKey); lock.lock(); return lock; } @Override public RLock lock(String lockKey, int leaseTime) { RLock lock = redissonClient.getLock(lockKey); lock.lock(leaseTime, TimeUnit.SECONDS); return lock; } @Override public RLock lock(String lockKey, TimeUnit unit ,int timeout) { RLock lock = redissonClient.getLock(lockKey); lock.lock(timeout, unit); return lock; } @Override public boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int leaseTime) { RLock lock = redissonClient.getLock(lockKey); try { return lock.tryLock(waitTime, leaseTime, unit); } catch (InterruptedException e) { return false; } } @Override public void unlock(String lockKey) { RLock lock = redissonClient.getLock(lockKey); lock.unlock(); } @Override public void unlock(RLock lock) { lock.unlock(); } public void setRedissonClient(RedissonClient redissonClient) { this.redissonClient = redissonClient; } }
3.4 高可用的RedLock(红锁)原理
RedLock算法思想是不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁,n / 2 + 1,必须在大多数redis节点上都成功创建锁,才能算这个整体的RedLock加锁成功,避免说仅仅在一个redis实例上加锁而带来的问题。
更多JAVA、高并发、微服务、架构、解决方案、中间件的总结在:https://github.com/yu120/lemon-guide