前言
老规矩,整体开始之前来唠唠嗑,我感觉在正式内容开始之前跟大家唠嗑感觉就像和多年老朋友未见面一样和大家唠嗑,这种感觉心里很是舒坦;(刚刚准备写,因为查生产数据给打断了、、、继续唠嗑);和去年一样,今天我这是2023年最后一天(腊月二十八,明天回家,旁边同事走的也可多了)了,今年本来打算搞搞我的云服务器的,因为前段时间服务器被挖矿了,内存一直给我打满,还请不掉;但是来后晕晕乎乎的,然后没啥心情,就打开了csdn,看看文章,发现今年一篇文章竟然没写,2023前期在找工作,后期在疯狂卷,压力山大,还有一方面文章也写了部分,维护在了其它地方,心里看到csdn没写文章,突然感觉这浑身不带劲,那就趁着最后一天上班时间,鱼也别摸了,来整理下正好昨天业务中用到的redisson.lock()和redisson.tryLock()加锁原理和区别;那就整理下分享给大家,我还能再巩固巩固,来吧,废话少说,步入正题。
提前给大家拜个早年:祝大家龙年大吉、身体健康、万事如意、财源滚滚、心想事成;
加锁方式
单节点加锁
在前期刚接触互联网行业时,因为是单体项目,所以在多线程遇到数据安全问题时,则首先想到的是synchronized关键字或者Lock进行加锁,在前期单体项目中,这种问题确实没有问题,因为共享变量加锁了吗,但是目前互联网行业中,几乎全是分布式集群服务,那么就会有多节点,在多节点中如果还用在单体中那种加锁方式的话,数据仍然会是不安全的,会造成脏数据。
分布式加锁
那么上述说了在集群多节点采用单节点的加锁方式,仅仅只能锁住当前节点,但是多节点中的其它节点是无法上锁的,那么就会出现线程安全问题;解决这种多节点数据安全问题,可以从数据库层面在做操作,也可以在代码层采用分布式锁来进行上锁,分布式上锁一般采用两种方式:redis加锁或者zk加锁;常用的是redis加锁;今天就着重来讲redis分布式加锁,但是今天呢不讲加锁如何实现的,因为在前年已经讲过了,请大家移步至Redis和Redission两种方式实现分布锁;今天主要讲的是redisson.lock和redisson.tryLock原理和区别;
redisson.lock()
redisson.lock(),说实话真的没用过它,因为一般在项目中都是使用tryLock(),为了搞清楚它的用法及原理,还特意写了些样例去测试场景。下面来分析分析吧。
redisson.lock()用法:
@RunWith(SpringRunner.class)
@SpringBootTest
public class Test {
// 注意这里要实现自己配置文件(config)
@Autowired
private RedissonClient redissonClient;
@org.junit.Test
public void testRedis() throws InterruptedException {
RLock redissonClientLock = redissonClient.getLock("key_name");
redissonClientLock.lock(10, TimeUnit.SECONDS);
System.out.println("成功获取锁");
Thread.sleep(10000000);
System.out.println("运行结束");
}
}
上述代码用的redisson.lock()加锁,10代表的是加锁成功后,持有锁时长10s;超过10s后,那么锁会自动释放,并且它是一种阻塞式的获取锁,组色式获取锁的含义是:当锁被线程A持有时,那么它将会一直等待线程A释放锁后,成功获取到锁之后它才会向下代码继续执行,否则一直等待。
源码如下所示:
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
long threadId = Thread.currentThread().getId();
// 尝试加锁;-1L代表无需等待;leaseTime代表加锁时间;unit代表单位;
// 返回值ttl代表该锁被其它线程持有时长;如果返回加锁失败,则返回ttl不为null;加锁成功则返回为null
Long ttl = this.tryAcquire(-1L, leaseTime, unit, threadId);
if (ttl != null) {
CompletableFuture<RedissonLockEntry> future = this.subscribe(threadId);
// 判断是否可中断,这里不做讲解
if (interruptibly) {
this.commandExecutor.syncSubscriptionInterrupted(future);
} else {
this.commandExecutor.syncSubscription(future);
}
try {
// 死循环;这里对应上lock是阻塞式加锁;当加锁失败后,那么它将一直循环加锁
while(true) {
// 尝试再次加锁
ttl = this.tryAcquire(-1L, leaseTime, unit, threadId);
// 加锁成功
if (ttl == null) {
return;
}
if (ttl >= 0L) {
try {
/**
* 从订阅锁的 CompletableFuture 中获取到 RedissonLockEntry 对象,
* 然后再从中获取到与锁相关的 CountDownLatch(倒计时门闩),并尝试在指定的时间内获取它。
* 在这段代码中,tryAcquire 方法的参数 ttl 表示等待的时间,以及一个时间单位。
* 如果在指定的时间内成功获取到了倒计时门闩,则说明锁已经被释放,当前线程可以继续执行后续逻辑。
* 如果在指定的时间内无法获取到倒计时门闩,说明在等待时间内锁依然没有被释放,当前线程可能会继续等待或执行相应的处理逻辑。
* 这里就避免了一直循环尝试加锁,降低CPU的开销
*/
((RedissonLockEntry)this.commandExecutor.getNow(future)).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS)