1. 程式人生 > 資訊 >今日清明節:萬物生長此時,皆清潔而明淨

今日清明節:萬物生長此時,皆清潔而明淨

說到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已經做了這個處理,但是非持有鎖的執行緒解鎖時,會丟擲異常,異常截圖如下:

所以要不就是解鎖前先判斷一下,或者對解鎖捕獲異常並處理,不然程式可能會出現問題。

好啦!基本的使用就是這些,水平有限,如有錯誤及時指正。