1 Redisson功能介紹
基于自定義setnx實現的分布式鎖存在下面的問題:
重入問題:重入問題是指 獲得鎖的線程可以再次進入到相同的鎖的代碼塊中,可重入鎖的意義在于防止死鎖,比如HashTable這樣的代碼中,他的方法都是使用synchronized修飾的,假如他在一個方法內,調用另一個方法,那么此時如果是不可重入的,不就死鎖了嗎?所以可重入鎖他的主要意義是防止死鎖,我們的synchronized和Lock鎖都是可重入的。
不可重試:是指目前的分布式只能嘗試一次,我們認為合理的情況是:當線程在獲得鎖失敗后,他應該能再次嘗試獲得鎖。
**超時釋放:**我們在加鎖時增加了過期時間,這樣的我們可以防止死鎖,但是如果卡頓的時間超長,雖然我們采用了lua表達式防止刪鎖的時候,誤刪別人的鎖,但是畢竟沒有鎖住,有安全隱患
主從一致性: 如果Redis提供了主從集群,當我們向集群寫數據時,主機需要異步的將數據同步給從機,而萬一在同步過去之前,主機宕機了,就會出現死鎖問題。
那么什么是Redisson呢
Redisson是一個在Redis的基礎上實現的Java駐內存數據網格(In-Memory Data Grid)。它不僅提供了一系列的分布式的Java常用對象,還提供了許多分布式服務,其中就包含了各種分布式鎖的實現。
Redission提供了分布式鎖的多種多樣的功能
2 Redisson在Springboot中快速入門(代碼)
2.1 導入依賴
<!-- redisson -->
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.15.3</version>
</dependency>
2.2 Redisson配置
package com.atguigu.gmall.common.config;import lombok.Data;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.config.SingleServerConfig;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;/*** redisson配置信息*/
@Data
@Configuration
@ConfigurationProperties("spring.redis")
public class RedissonConfig {private String host;private String addresses;private String password;private String port;private int timeout = 3000;private int connectionPoolSize = 64;private int connectionMinimumIdleSize=10;private int pingConnectionInterval = 60000;private static String ADDRESS_PREFIX = "redis://";/*** 自動裝配**/@BeanRedissonClient redissonSingle() {Config config = new Config();if(StringUtils.isEmpty(host)){throw new RuntimeException("host is empty");}SingleServerConfig serverConfig = config.useSingleServer()//redis://127.0.0.1:7181.setAddress(ADDRESS_PREFIX + this.host + ":" + port).setTimeout(this.timeout).setPingConnectionInterval(pingConnectionInterval).setConnectionPoolSize(this.connectionPoolSize).setConnectionMinimumIdleSize(this.connectionMinimumIdleSize);if(!StringUtils.isEmpty(this.password)) {serverConfig.setPassword(this.password);}// RedissonClient redisson = Redisson.create(config);return Redisson.create(config);}
}
2.3 將自定義鎖setnx換成Redisson實現(可重入鎖)
實現類詳細看這個章節:https://blog.csdn.net/yu_fu_a_bu/article/details/139408497
3 可重入鎖原理
3.1 自定義分布式鎖setnx為什么不可以重入
數據結構:key-value的形式。String類型
public class DistributedLockExample {private static Jedis jedis = new Jedis("127.0.0.1", 6379);public void acquireLock(String lockKey) {Boolean locked = jedis.setnx(lockKey, "true");if (locked) {System.out.println("Lock acquired successfully.");} else {System.out.println("Lock already acquired by another process.");}}public static void main(String[] args) {DistributedLockExample example = new DistributedLockExample();example.acquireLock("myLock");example.acquireLock("myLock"); // 嘗試重入}
}
在上面的示例中,acquireLock方法通過setnx嘗試獲取鎖。第一次調用acquireLock時成功獲取鎖,因為myLock這個key在Redis中不存在。第二次調用acquireLock嘗試重入時,會返回鎖已被占用的提示,因為Redis的setnx指令無法識別重入情況,每次獲取鎖都需要先檢查是否已被占用。
3.2 redisson為什么可以實現可重入
數據類型:key-value(field-value) Hash類型
不僅存入線程標識(保證不刪除別人的鎖),而且存入可重入的次數(保證可重入)。
3.2.1 獲取鎖的lua腳本
lua腳本可以保證原子性
local key = KEYS[1];--鎖的key
local threadId = ARGV[1]; --線程的唯一標識
local releaseTime = ARGV[2];--鎖的自動釋放時間
--判斷鎖是否存在
if(redis.call('exists',key) == 0) then --不存在,獲取鎖redis.call('hset', key,threadId,'1');--設置有效期redis.call('expire',key,releaseTime);return 1;--返回結果
end;
--如果鎖已經存在,判斷threadId是否是自己的
if(redis.call('hexists',key,threadId) == 1) then--存在,獲取鎖,重入次數加一redis.call('hincrby',key,threadId,'1');-- 設置有效期redis.call('expire',key,releaseTime);return 1;--返回結果
end;
return 0;--獲取鎖失敗
3.2.2 釋放鎖的lua腳本
local key = KEYS[1];--鎖的key
local threadId = ARGV[1]; --鎖的唯一標識
local releaseTime = ARGV[2];--鎖的自動釋放時間
-- 判斷鎖是還是被自己持有
if(redis.call('HEXISTS',key,threadId) == 0) then return nil;--如果已經不是自己,則直接返回
end;
-- 是自己的鎖,則可重入的次數進行-1
local count = redis.call('HINCRVY',key,threadId,-1);
-- 判斷可重入的次數是否為0
if(count > 0){-- 大于0說明不能釋放鎖,重置過期時間然后返回redis.call('EXPIRE',key,releaseTime);return nil;
else --等于0說明可以釋放鎖,直接刪除redis.call('DEL',key);return nil;
end;