今日清明節:萬物生長此時,皆清潔而明淨
說到redis就不得不提到jedis和redisson,這兩個對於redis的操作各有優劣,具體的分析可以百度搜索,本文通過redisson來實現分散式鎖。
1、引入依賴
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.17.0</version>
</dependency>
具體使用的版本可以去maven中心搜尋,我這邊使用的是3.17.0,使用的redis版本是3.2.100,由於是自己下載的windows版本,所以是單例項的redis,後續的文章會涉及到主從、哨兵和叢集模式
2、配置Redisson
Config config = new Config();
RedissonClient redisClient = Redisson.create(config);
RLock lock = redissonClient.getLock("this is lock");
大致的用法如上,先進行配置,然後建立RedissonClient,再獲取鎖,但是這樣會有一個問題,我們一般不需要每次都去生成一個RedissonClient,所以我們可以將其注入到Spring的容器中去管理,配置如下。
這裡是將叢集、哨兵和單機的配置放到了一塊。
package com.example.moonlight.common.config.redis; import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.ClusterServersConfig; import org.redisson.config.Config; import org.redisson.config.SentinelServersConfig; import org.redisson.config.SingleServerConfig; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.util.StringUtils; @Configuration @EnableConfigurationProperties({RedissonProperties.class}) public class RedissonConfig { /** * 主機名 */ @Value("${spring.redis.host:}") private String host; /** * 密碼 */ @Value("${spring.redis.password:}") private String password; /** * 埠 */ @Value("${spring.redis.port:}") private String port; /** * 叢集節點 */ @Value("${spring.redis.cluster.nodes:}") private String clusterNodes; /** * 哨兵節點 */ @Value("${spring.redis.sentinel.nodes:}}") private String sentinelNodes; @Value("${spring.redis.sentinel.master:}") private String masterName; private static final String redisAddressPrefix = "redis://"; /** * 針對每次都要獲取RedissonClient的問題,這裡注入一個bean,實現單例 */ @Bean @ConditionalOnMissingBean//該註解保證當前bean只能被注入一次,實現單例 public RedissonClient getRedissonClient(RedissonProperties redissonProperties) { //建立redisson的配置 Config config = new Config(); //這裡需要區分redis是單機部署、主從模式、哨兵模式還是叢集模式, //對應Config中的,SingleServerConfig、MasterSlaveServersConfig、SentinelServersConfig、ClusterServersConfig if (!StringUtils.isEmpty(clusterNodes)) {//叢集模式 ClusterServersConfig clusterServersConfig = config.useClusterServers(); //設定叢集節點 String[] nodes = clusterNodes.split(","); for (String node : nodes) { //若是配置的節點包含了redis://,則不用拼接 if (node.contains(redisAddressPrefix)) { clusterServersConfig.addNodeAddress(node); } else { clusterServersConfig.addNodeAddress(redisAddressPrefix + node); } } clusterServersConfig.setScanInterval(2000); clusterServersConfig.setPassword(password); //設定密碼 clusterServersConfig.setPassword(password); //如果當前連線池裡的連線數量超過了最小空閒連線數,而同時有連線空閒時間超過了該數值,那麼這些連線將會自動被關閉, // 並從連線池裡去掉。時間單位是毫秒。 clusterServersConfig.setIdleConnectionTimeout(redissonProperties.getIdleConnectionTimeout()); //同任何節點建立連線時的等待超時。時間單位是毫秒。 clusterServersConfig.setConnectTimeout(redissonProperties.getConnectTimeout()); //等待節點回覆命令的時間。該時間從命令傳送成功時開始計時。 clusterServersConfig.setTimeout(redissonProperties.getTimeout()); clusterServersConfig.setPingConnectionInterval(redissonProperties.getPingTimeout()); //當與某個節點的連線斷開時,等待與其重新建立連線的時間間隔。時間單位是毫秒。 clusterServersConfig.setFailedSlaveReconnectionInterval(redissonProperties.getReconnectionTimeout()); } else if (StringUtils.isEmpty(sentinelNodes)) {//哨兵模式 SentinelServersConfig sentinelServersConfig = config.useSentinelServers(); //哨兵模式本質還是主從模式,所有的資料存在一個redis例項上,從伺服器上只是主服務的備份 sentinelServersConfig.setDatabase(0); sentinelServersConfig.setMasterName(masterName); sentinelServersConfig.setScanInterval(2000); sentinelServersConfig.setPassword(password); String[] nodes = sentinelNodes.split(","); for (String node : nodes) { sentinelServersConfig.addSentinelAddress(node); } } else {//單機模式 //指定使用單節點部署方式 SingleServerConfig singleServerConfig = config.useSingleServer(); singleServerConfig.setAddress("redis://" + host + ":" + port); //設定密碼 singleServerConfig.setPassword(password); //設定對於master節點的連線池中連線數最大為500 singleServerConfig.setConnectionPoolSize(redissonProperties.getConnectionPoolSize()); //如果當前連線池裡的連線數量超過了最小空閒連線數,而同時有連線空閒時間超過了該數值,那麼這些連線將會自動被關閉, // 並從連線池裡去掉。時間單位是毫秒。 singleServerConfig.setIdleConnectionTimeout(redissonProperties.getIdleConnectionTimeout()); //同任何節點建立連線時的等待超時。時間單位是毫秒。 singleServerConfig.setConnectTimeout(redissonProperties.getConnectTimeout()); //等待節點回覆命令的時間。該時間從命令傳送成功時開始計時。 singleServerConfig.setTimeout(redissonProperties.getTimeout()); singleServerConfig.setPingConnectionInterval(redissonProperties.getPingTimeout()); } RedissonClient redisClient = Redisson.create(config); return redisClient; } }
自己寫了一個redisson的配置類,如下:
package com.example.moonlight.common.config.redis; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; @Data @ConfigurationProperties(prefix = "demo.boot.redisson")//批量配置,字首相同則可以自動設定,具體用到了beanPostProcess,設定了預設值,不配置引數也可以 public class RedissonProperties { /** * 設定對於master節點的連線池中連線數最大為500 */ private Integer connectionPoolSize = 500; /** * 如果當前連線池裡的連線數量超過了最小空閒連線數,而同時有連線空閒時間超過了該數值, * 那麼這些連線將會自動被關閉,並從連線池裡去掉。時間單位是毫秒。 */ private Integer idleConnectionTimeout = 10000; /** * 同任何節點建立連線時的等待超時。時間單位是毫秒。 */ private Integer connectTimeout = 30000; /** * 等待節點回覆命令的時間。該時間從命令傳送成功時開始計時。 */ private Integer timeout = 3000; /** * ping不通的時間 */ private Integer pingTimeout = 30000; /** * 當與某個節點的連線斷開時,等待與其重新建立連線的時間間隔。時間單位是毫秒。 */ private Integer reconnectionTimeout = 3000; }
application.properties的配置如下,主要是redis的資訊
#redis配置
# Redis資料庫索引(預設為0)
spring.redis.database=0
# Redis伺服器地址
spring.redis.host=127.0.0.1
# Redis伺服器連線埠
spring.redis.port=6379
# Redis伺服器連線密碼(預設為空,填寫redis的密碼就可以)
spring.redis.password=12345
# 連線池最大連線數(使用負值表示沒有限制)
spring.redis.pool.max-active=200
# 連線池最大阻塞等待時間(使用負值表示沒有限制)
spring.redis.pool.max-wait=-1
# 連線池中的最大空閒連線
spring.redis.pool.max-idle=10
# 連線池中的最小空閒連線
spring.redis.pool.min-idle=0
3、使用
前面的配置完成後,如果沒有問題,此時就可以正常的在程式碼中使用了,下面給出使用例子:
兩個方法一個是tryLock()一個是lock(),tryLock()會有返回值,如果加鎖失敗會返回false,此時可以根據返回值做一些事情,而lock()會一直阻塞,直到自己獲取到鎖,具體使用哪種,看具體情況而定。
package com.example.moonlight.modules.user.examples.redis;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@Service
public class RedisService {
private static final Logger logger = LoggerFactory.getLogger(RedisService.class);
@Autowired
private RedissonClient redissonClient;
/**
* 測試redis分散式鎖
*/
public void testRedisTryLock(CountDownLatch countDownLatch) {
RLock lock = redissonClient.getLock("this is lock");
try {
//考慮加鎖異常,但是實際加鎖成功這種情況,所以lock時需要在try-catch裡面,不然會出現異常沒有被捕獲而無法解鎖的問題
boolean isSuccess = lock.tryLock(10L, 30000, TimeUnit.MILLISECONDS);
if (isSuccess) {
logger.info(Thread.currentThread().getName() + " get lock is success");
//模擬執行業務程式碼
Thread.sleep(100);
} else {
logger.info(Thread.currentThread().getName() + " get lock is failed");
}
} catch (Exception e) {
logger.error("lock lock error", e);
} finally {
//判斷是否被當前執行緒持有
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
logger.info(Thread.currentThread().getName() + " unlock is success");
}
countDownLatch.countDown();
}
}
/**
* 測試redis分散式鎖
*/
public void testRedisLock(CountDownLatch countDownLatch) {
RLock lock = redissonClient.getLock("this is lock");
try {
//考慮加鎖異常,但是實際加鎖成功這種情況,所以lock時需要在try-catch裡面,不然會出現異常沒有被捕獲而無法解鎖的問題
lock.lock(30000, TimeUnit.MILLISECONDS);
logger.info(Thread.currentThread().getName() + " get lock is success");
//模擬執行業務程式碼
Thread.sleep(100);
} catch (Exception e) {
logger.error("lock lock error", e);
} finally {
//判斷是否被當前執行緒持有
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
logger.info(Thread.currentThread().getName() + " unlock is success");
}
countDownLatch.countDown();
}
}
}
然後寫一個Test測試一下,程式碼如下:
package com.example.moonlight.start;
import com.example.moonlight.modules.user.examples.multithreading.count_down_latch.TCountDownLatch;
import com.example.moonlight.modules.user.examples.redis.RedisService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.CountDownLatch;
@Slf4j
@SpringBootTest
class StartApplicationTests {
@Autowired
TCountDownLatch tCountDownLatch;
@Autowired
ThreadPoolTaskExecutor taskExecutor;
@Autowired
private RedisService redisService;
/**
* 測試redis的分散式鎖
*/
@Test
void testRedisTryLock() throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
taskExecutor.submit(() -> {
try {
redisService.testRedisTryLock(countDownLatch);
} catch (Exception e) {
log.error("error ", e);
}
});
}
//等待主執行緒中的子執行緒執行完,不然這個Test執行完,程式就關閉了,會報異常,在正常的程式中不會出現這個問題
countDownLatch.await();
}
/**
* 測試redis的分散式鎖
*/
@Test
void testRedisLock() throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
//多執行緒呼叫
taskExecutor.submit(() -> {
try {
redisService.testRedisLock(countDownLatch);
} catch (Exception e) {
log.error("error ", e);
}
});
}
//等待主執行緒中的子執行緒執行完,不然這個Test執行完,程式就關閉了,會報異常,在正常的程式中不會出現這個問題
countDownLatch.await();
}
}
這裡採用了執行緒池去呼叫方法,模擬多執行緒呼叫。
testRedisTryLock()執行結果如下圖,因為tryLock()到時間就會返回,所以根據返回結果執行了不同的程式碼,只有執行緒moonlight2獲取鎖成功,其他的執行緒獲取鎖失敗,最後moonlight2解鎖成功。
testRedisLock()執行結果如下圖,因為lock()會一直阻塞,所以執行緒依次的加鎖和解鎖成功。
注意:在解鎖時,加了一個if判斷,程式碼如下:
//判斷是否被當前執行緒持有
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
logger.info(Thread.currentThread().getName() + " unlock is success");
}
其中isHeldByCurrentThread()判斷鎖是否被當前執行緒持有,防止鎖被其他的執行緒解鎖,如果不加這個判斷,其他執行緒也是無法解鎖的,redisson已經做了這個處理,但是非持有鎖的執行緒解鎖時,會丟擲異常,異常截圖如下:
所以要不就是解鎖前先判斷一下,或者對解鎖捕獲異常並處理,不然程式可能會出現問題。
好啦!基本的使用就是這些,水平有限,如有錯誤及時指正。