文章目录
- 前言
- 一、Redisson是什么?
- 二、各种锁的实现原理
- 1.可重入锁
- 2.公平锁(Fair Lock)
- 3.联锁(MultiLock)
- 4. 红锁(RedLock)
- 5. 读写锁(ReadWriteLock)
- 总结
前言
例如:在前面两篇文章之中我们自己使用了lua脚本实现了锁的应用,但是也面临种种问题,性能不够好,注重实现等等,显然很麻烦,有没有一套成熟的redis的分布式锁的实现呢?
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid),下方提供了官网链接可以详细查看
一、Redisson是什么?
Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
二、各种锁的实现原理
1.可重入锁
代码如下(示例):
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setDatabase(0)
.setConnectionMinimumIdleSize(10)
.setConnectionPoolSize(60)
.setIdleConnectionTimeout(60000)//50个线程超过60秒就被销毁
.setTimeout(20000)//响应超时时间
.setConnectTimeout(10000);//连接超时时间
return Redisson.create(config);
}
}
//简单使用
@SneakyThrows
@GetMapping("/lock")
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
private void redissonLock() {
RLock lock = redissonClient.getLock("lock");
lock.lock(30,TimeUnit.SECONDS);
try {
lock.lock();
String stock = stringRedisTemplate.opsForValue().get("stock");
if (!StringUtils.isEmpty(stock) && Integer.valueOf(stock) > 0) {
Integer st = new Integer(stock);
stringRedisTemplate.opsForValue().set("stock", String.valueOf(--st));
}
} finally {
lock.unlock();
}
}
这和我们的实现和java的Lock锁非常相似我们来梳理一下底层原理
我们发现他也实现了lock锁
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
return this.evalWriteAsync(this.getRawName(), LongCodec.INSTANCE, command, "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);", Collections.singletonList(this.getRawName()), new Object[]{unit.toMillis(leaseTime), this.getLockName(threadId)});
}
果不其然底层和我们一样也实现了lua脚本,我们来读一下大致相似,并且用了更精细的时间格式
private void renewExpiration() {
ExpirationEntry ee = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
if (ee != null) {
Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
public void run(Timeout timeout) throws Exception {
ExpirationEntry ent = (ExpirationEntry)RedissonBaseLock.EXPIRATION_RENEWAL_MAP.get(RedissonBaseLock.this.getEntryName());
if (ent != null) {
Long threadId = ent.getFirstThreadId();
if (threadId != null) {
CompletionStage<Boolean> future = RedissonBaseLock.this.renewExpirationAsync(threadId);
future.whenComplete((res, e) -> {
if (e != null) {
RedissonBaseLock.log.error("Can't update lock " + RedissonBaseLock.this.getRawName() + " expiration", e);
RedissonBaseLock.EXPIRATION_RENEWAL_MAP.remove(RedissonBaseLock.this.getEntryName());
} else {
if (res) {
RedissonBaseLock.this.renewExpiration();
} else {
RedissonBaseLock.this.cancelExpirationRenewal((Long)null);
}
}
});
}
}
}
}, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
}
果不其然他的renew方法也用了定时任务去做而且处理的更加完善并结合异步,我们来看看他的实现
protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
return this.evalWriteAsync(this.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(this.getRawName()), this.internalLockLeaseTime, this.getLockName(threadId));
}
这个lua是不是也很熟悉返回的是一个异步布尔类型的参数也是不断询问是否存在锁 如果存在继续续期,没存在RedissonBaseLock.this.cancelExpirationRenewal((Long)null);
关掉他的续期操作,我们这里没有做后续处理,CompletionStage大家如果有点陌生的话,public class CompletableFuture implements Future, CompletionStage ,这个大家应该不陌生吧,已经用过很多次了,这是他的上锁方法,我们接着看看他的解锁
public RFuture<Void> unlockAsync(long threadId) {
RFuture<Boolean> future = this.unlockInnerAsync(threadId);
CompletionStage<Void> f = future.handle((opStatus, e) -> {
this.cancelExpirationRenewal(threadId);
if (e != null) {
throw new CompletionException(e);
} else if (opStatus == null) {
IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: " + this.id + " thread-id: " + threadId);
throw new CompletionException(cause);
} else {
return null;
}
});
return new CompletableFutureWrapper(f);
}
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return this.evalWriteAsync(this.getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end; local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0; else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil;", Arrays.asList(this.getRawName(), this.getChannelName()), new Object[]{LockPubSub.UNLOCK_MESSAGE, this.internalLockLeaseTime, this.getLockName(threadId)});
}
也是很像,看看是否,他的在没有重入次数结束的时候还主动续期了一下,直到解锁完成并发布给其他服务
2.公平锁(Fair Lock)
代码如下(示例):
@SneakyThrows
@GetMapping("/fairlock/{id}")
public void fairLock(@PathVariable String id){
RLock fairLock = redissonClient.getFairLock("fairlock");
fairLock.lock();
System.out.println(id);
TimeUnit.SECONDS.sleep(10);
fairLock.unlock();
}
简单的使用一下并且在浏览器从1到5访问几次
我们可以看到它实现了公平操作我们来看看底层原理
对于这个锁他有一个队列记录这先后顺序每次解完锁后删除当前锁并自动获取下一个队列中的线程并让他去执行
3.联锁(MultiLock)
基于Redis的Redisson分布式联锁RedissonMultiLock对象可以将多个RLock对象关联为一个联锁,每个RLock对象实例可以来自于不同的Redisson实例。
RLock lock1 = redissonInstance1.getLock(“lock1”);
RLock lock2 = redissonInstance2.getLock(“lock2”);
RLock lock3 = redissonInstance3.getLock(“lock3”);RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 所有的锁都上锁成功才算成功。
lock.lock();
…
lock.unlock();
对于这种例子大家还是少用,但凡有一个redis实例挂了,这个锁在生产就有很大的问题
4. 红锁(RedLock)
基于Redis的Redisson红锁RedissonRedLock对象实现了Redlock介绍的加锁算法。该对象也可以用来将多个RLock对象关联为一个红锁,每个RLock对象实例可以来自于不同的Redisson实例。
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();
5. 读写锁(ReadWriteLock)
基于Redis的Redisson分布式可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。其中读锁和写锁都继承了RLock接口。
分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。
@SneakyThrows
@GetMapping("/readlock")
public void readlock(){
RReadWriteLock rwlock = redissonClient.getReadWriteLock("rwlock");
rwlock.readLock().lock();
TimeUnit.SECONDS.sleep(10);
rwlock.readLock().unlock();
}
@SneakyThrows
@GetMapping("/writelock")
public void writelock(){
RReadWriteLock rwlock = redissonClient.getReadWriteLock("rwlock");
rwlock.writeLock().lock();
TimeUnit.SECONDS.sleep(10);
rwlock.writeLock().unlock();
}
当我们一直去访问读的接口发现大家都可以一起去读,此时我们写的接口一直处于阻塞状态
当我们访问写的时候同时只有一个线程能被响应,并且此时读也无法进行访问
此时我们总结出
线程1 | 线程2 | 可并发 |
写 | 写 | x |
读 | 写 | x |
读 | 读 | √ |
为什么读和写不能并发,如果并发就会出现2个问题
幻读:事务A 按照一定条件进行数据读取, 期间事务B 插入了相同搜索条件的新数据,事务A再次按照原先条件进行读取时,发现了事务B 新插入的数据 称为幻读
脏读:事务A将某一值修改,然后事务B读取该值,此后A因为某种原因撤销对该值的修改,这就导致了B所读取到的数据是无效的,值得注意的是,脏读一般是针对于update操作的。
总结
由于篇幅有限我们大概讲一下redisson的常用操作