1.为什么使用分布式锁?
分布式锁多数用在分布式场景中,如果是单机的话用jvm的锁就行了。分布式锁的原理就是利用redis的set nx多线程的互斥特性,在多线程场景中锁住对共享资源的访问。并且redis是基于内存存储的中间件,加锁解锁的性能都非常快。
2.实现
1.设置set NX如果键不存在就是存储,EX是10秒钟后过期也可以使用PX毫秒
SET mykey myvalue NX EX 10
2.java实现
redisTemplate.opsForValue().setIfAbsent(key,"uid",10,TimeUnit.SECONDS);
//底层是execute直接执行,也可以保证操作的原子性
return (Boolean)this.execute((connection) -> {
return connection.set(rawKey, rawValue, expiration, SetOption.ifAbsent());
}, true);
3.看门狗机制
引入开门狗机制主要是防止在线程持有锁的时候,代码没有执行完毕就因为锁到期释放了锁,导致共享资源被修改。
RLock rLock = redissonClient.getLock(name);
try {
/**
* waitTime 获取锁的最长等待时间
* leaseTime 持有锁的时间
* unit 单位
* */
//TODO 开启看门狗lease -1 自我理解看门狗的功能就是在超时后任务没有执行完成就续期,
// 如果没有看门狗并且设置了leaseTime 就是当前锁的失效时间了
Boolean bo = rLock.tryLock(5,-1,TimeUnit.MINUTES);
if (!bo){
System.out.println("没有拿到锁,遗憾离场:"+name);
continue;
}else {
System.out.println("拿到了锁,并开始耗时:"+name);
Thread.sleep(600);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (rLock.isHeldByCurrentThread()){
rLock.unlock();
System.out.println("释放了锁:"+name);
} else {
System.out.println("遗憾离场,并来到了finally:"+name);
}
}
//底层逻辑 开一个监听线程看执行完毕删除锁,未执行完毕就加过期时间
Long ttl = this.tryAcquire(leaseTime, unit, threadId);
//判断是否开启了看门狗机制
private RFuture<Boolean> tryAcquireOnceAsync(long leaseTime, TimeUnit unit, final long threadId) {
if (leaseTime != -1L) {
return this.tryLockInnerAsync(leaseTime, unit, threadId,
RedisCommands.EVAL_NULL_BOOLEAN);
} else {
//延期时间默认30毫秒 this.lockWatchdogTimeout = 30000L;
RFuture<Boolean> ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.
getConnectionManager().getCfg().getLockWatchdogTimeout(),
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
//监听这个线程时间
ttlRemainingFuture.addListener(new FutureListener<Boolean>() {
public void operationComplete(Future<Boolean> future) throws Exception {
if (future.isSuccess()) {
Boolean ttlRemaining = (Boolean)future.getNow();
if (ttlRemaining) {
RedissonLock.this.scheduleExpirationRenewal(threadId);
}
}
}
});
return ttlRemainingFuture;
}
}
//线程的具体监听方法 锁不存在了就取消监听
private void scheduleExpirationRenewal(final long threadId) {
if (!expirationRenewalMap.containsKey(this.getEntryName())) {
Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
public void run(Timeout timeout) throws Exception {
RFuture<Boolean> future = RedissonLock.this.commandExecutor.evalWriteAsync(RedissonLock.this.getName(), 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(RedissonLock.this.getName()), new Object[]{RedissonLock.this.internalLockLeaseTime, RedissonLock.this.getLockName(threadId)});
future.addListener(new FutureListener<Boolean>() {
public void operationComplete(Future<Boolean> future) throws Exception {
RedissonLock.expirationRenewalMap.remove(RedissonLock.this.getEntryName());
if (!future.isSuccess()) {
RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", future.cause());
} else {
if ((Boolean)future.getNow()) {
RedissonLock.this.scheduleExpirationRenewal(threadId);
}
}
}
});
}
}, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
if (expirationRenewalMap.putIfAbsent(this.getEntryName(), task) != null) {
task.cancel();
}
}
}
4.redLock
redLock主要是为了防止在redis集群中,在一个节点加了锁,但在加锁后节点就挂掉了,导致之前加的锁失效,线程重复加锁的问题。
RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 红锁在大部分节点上加锁成功就算成功。
lock.lock();
...
lock.unlock();
但红锁也有很多问题
-
资源竞争问题:当多个客户端竞争同一资源时,如果向多个Redis实例请求获取锁,容易出现没有获胜者的情况。这种情况下,没有获得过半数锁的客户端应及时释放锁,以避免长时间占用资源。
-
安全性问题:在某个主节点宕机时,可能会出现锁安全性问题。例如,当Redis的持久化策略为AOF使用appendfsync=everysec即每秒fsync一次时,故障时会丢失1秒的数据,这可能导致锁的丢失。
-
实现复杂性:虽然Redlock算法提供了一种实现分布式锁的方法,但在实际应用中,需要确保所有Redis实例的时间是同步的,这增加了实现的复杂性。
-
性能开销:由于Redlock需要在多个Redis实例上同时进行操作,这可能会增加额外的性能开销,尤其是在高并发场景下2。
-
可靠性问题:虽然Redlock算法设计用于避免死锁和确保最终一致性,但在实践中,如果锁定资源的服务崩溃或分区,仍然可能存在释放锁的可靠性问。
5.读写锁
读写锁是一种并发控制机制,用于控制对共享资源的访问。它允许多个读操作同时进行,但写操作是互斥的。这样可以在保证数据一致性的同时,提高系统的并发性能。
在Redis缓存中,我们可以将数据分为热点数据和非热点数据。热点数据是指访问频率较高的数据,而非热点数据访问频率较低。对于热点数据,我们可以采用读写锁机制,以提高并发性能。
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(mobile);
readWriteLock.readLock();
readWriteLock.writeLock();