redis 集羣部署及分佈式鎖的實現

一、redis集羣的部署

  • 安裝redis
    確保安裝文件夾有redis-trib.rb文件,通過rudy構建redis集羣
  • 安裝ruby環境
    配置好環境變量,gem install redis 安裝redis依賴

詳細環境安裝教程:點擊打開鏈接

集羣搭建

      redis集羣最小包含3個主節點,並且每個節點都應該部署在不同的服務器上,這裏測試建立3個主節點和三個從節點的redis集羣,並部署在本地機器上,分別監聽不同的端口(7000,7001,7100,7101,7200,7201),7000、7100、7200爲三個主節點,7001、7101、7201分別爲它們對應的從節點。

     在redis安裝目錄建立cluster文件夾,在這個目錄下爲這六個redis節點新建六個文件夾,名稱爲各自的端口名,再在建好的文件裏創建如redis_7000.conf配置文件,內容爲:

port 7000
#綁定監聽的IP地址,默認爲127.0.0.1
#bind 172.16.10.49
appendonly yes
appendfilename "appendonly_7000.aof"

# // 如果要設置最大內存空間,可添加如下兩句
maxmemory 200mb
maxmemory-policy allkeys-lru

cluster-enabled yes
cluster-config-file nodes_7000.conf
cluster-node-timeout 15000
cluster-slave-validity-factor 10
cluster-migration-barrier 1
cluster-require-full-coverage yes

端口號7000都要改成各自對應的端口號,IP地址這裏測試就用默認127.0.0.1,建議使用電腦的本地ip地址。cluster-config-file參數指定了redis集羣節點的配置文件名稱,創建集羣后生成,內容如下:

0adf165a965376913d6a4d426784371e6b0d7cf6 127.0.0.1:7101 slave 2ec1abbf3bcd7758281bcc7b6762532084c0b60c 0 1521124105758 5 connected
8d8f817d997b57ccb10049208738d5ebfdd5c2fe 127.0.0.1:7001 master - 0 1521124104759 7 connected 0-5460
9334416471942fd042f704ae8a345ab6c1adcd37 127.0.0.1:7201 slave b7fedf227aec4fc2911bdf7de2566b73e0a88045 0 1521124107358 9 connected
1030b137672c69175e7cf1d99be325329e016422 127.0.0.1:7000 myself,slave 8d8f817d997b57ccb10049208738d5ebfdd5c2fe 0 0 1 connected
b7fedf227aec4fc2911bdf7de2566b73e0a88045 127.0.0.1:7200 master - 0 1521124100754 9 connected 10923-16383
2ec1abbf3bcd7758281bcc7b6762532084c0b60c 127.0.0.1:7100 master - 0 1521124106758 2 connected 5461-10922
vars currentEpoch 9 lastVoteEpoch 0

如果配置集羣后,想修改各節點的信息,如IP地址端口等,可以在這裏修改,但必須每個節點下的都要改。


然後使用如下命令安裝各服務,並啓動

  安裝   redis-server --service-install cluster/7100/redis_7100.conf --service-name redis7100   

  啓動   redis-server --service-start --service-name redis7100                                               

刪除服務 redis-server --service-uninstall --service-name redis7100

現在已經啓動了六個redis服務,但六個redis並沒有聯繫,沒有實現集羣。

創建集羣:

ruby redis-trib.rb create --replicas 1 127.0.0.1:700 0 127.0.0.1:7100 127.0.0.1:7200 127.0.0.1:7001 127.0.0.1:7101 127.0.0.1:7201

使用redis-trib.rb命令創建集羣,ruby可以去掉。命令運行成功,則集羣就創建成功了。

連接集羣:

redis-cli -c -p 7200 -h 127.0.0.1

這裏連接任何一個節點都可以。

redis集羣工作

set keya valuea
1、先對keya 計算值
2、對16384(總槽點數)取餘(得到槽點)
3、通過槽點找到對應的節點
4、在這個節點執行set keya valuea

節點,分爲主節點和從節點,一個主節點下可以有多個從節點。槽點值分配在主節點上,redis中的key-value就是
存儲在槽點上。
當主節點掛掉好,選舉一個從節點成爲主節點,若該主節點沒有從節點,則集羣處於fail狀態,或有半數以上的主
節點掛掉,集羣也處於fail狀態
主節點會把key—value寫入從節點

二、redis實現分佈式鎖

redis分佈式的實現原理:

    1、通過setNX操作,如果存在key,不操作;不存在,纔會set值,保證鎖的互斥性

    2、value設置鎖的到期時間,當鎖超時時,進行getAndSet操作,先get舊值,再set新值,避免發生死鎖。這裏也可以通過設置key的有效期來避免死鎖,但是setNx和exprise(設置有效期)操作非原子性,可能發生鎖沒有設置有效時間的問題,從而發生死鎖。

實現:

spring boot 通過jdeis連接redsi集羣

redis配置文件:

default.redis.maxRedirects=3
#連接池中最大連接數。高版本:maxTotal,低版本:maxActive
default.redis.maxTotal=20
#連接池中最大空閒的連接數
default.redis.maxIdle=10
#連接池中最少空閒的連接數
default.redis.minIdle=1
#當連接池資源耗盡時,調用者最大阻塞的時間,超時將跑出異常。單位,毫秒數;默認爲-1.表示永不超時。高版本:maxWaitMillis,低版本:maxWait
default.redis.maxWaitMillis=3000
#連接空閒的最小時間,達到此值後空閒連接將可能會被移除。負值(-1)表示不移除
default.redis.minEvictableIdleTimeMillis=-1
#對於“空閒鏈接”檢測線程而言,每次檢測的鏈接資源的個數。默認爲3
default.redis.numTestsPerEvictionRun=3
#“空閒鏈接”檢測線程,檢測的週期,毫秒數。如果爲負值,表示不運行“檢測線程”。默認爲-1
default.redis.timeBetweenEvictionRunsMillis=-1
#向調用者輸出“鏈接”資源時,是否檢測是有有效,如果無效則從連接池中移除,並嘗試獲取繼續獲取。默認爲false。建議保持默認值
default.redis.testOnBorrow=false
#連超時設置
default.redis.timeout=15000
#是否使用連接池
default.redis.usePool=true
#host&port
# 建議使用實際ip地址創建集羣
default.redis.nodes[0]=127.0.0.1:7000
default.redis.nodes[1]=127.0.0.1:7001
default.redis.nodes[2]=127.0.0.1:7100
default.redis.nodes[3]=127.0.0.1:7101
default.redis.nodes[4]=127.0.0.1:7200
default.redis.nodes[5]=127.0.0.1:7201

通過@ConfigurationProperties註解讀取配置信息:

@Component
@ConfigurationProperties(prefix = "default.redis")
public class RedisProperties {
    private int maxRedirects;
    private int maxTotal;
    private int maxIdle;
    private int minIdle;
    private int maxWaitMillis;
    private int minEvictableIdleTimeMillis;
    private int numTestsPerEvictionRun;
    private int timeBetweenEvictionRunsMillis;
    private boolean testOnBorrow;
    private int timeout;
    private boolean usePool;
    private List<String> nodes;


    public int getMaxRedirects() {
        return maxRedirects;
    }

    public void setMaxRedirects(int maxRedirects) {
        this.maxRedirects = maxRedirects;
    }

    public int getMaxTotal() {
        return maxTotal;
    }

    public void setMaxTotal(int maxTotal) {
        this.maxTotal = maxTotal;
    }

    public int getMaxIdle() {
        return maxIdle;
    }

    public void setMaxIdle(int maxIdle) {
        this.maxIdle = maxIdle;
    }

    public int getMinIdle() {
        return minIdle;
    }

    public void setMinIdle(int minIdle) {
        this.minIdle = minIdle;
    }

    public int getMaxWaitMillis() {
        return maxWaitMillis;
    }

    public void setMaxWaitMillis(int maxWaitMillis) {
        this.maxWaitMillis = maxWaitMillis;
    }

    public int getMinEvictableIdleTimeMillis() {
        return minEvictableIdleTimeMillis;
    }

    public void setMinEvictableIdleTimeMillis(int minEvictableIdleTimeMillis) {
        this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis;
    }

    public int getNumTestsPerEvictionRun() {
        return numTestsPerEvictionRun;
    }

    public void setNumTestsPerEvictionRun(int numTestsPerEvictionRun) {
        this.numTestsPerEvictionRun = numTestsPerEvictionRun;
    }

    public int getTimeBetweenEvictionRunsMillis() {
        return timeBetweenEvictionRunsMillis;
    }

    public void setTimeBetweenEvictionRunsMillis(int timeBetweenEvictionRunsMillis) {
        this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;
    }

    public boolean isTestOnBorrow() {
        return testOnBorrow;
    }

    public void setTestOnBorrow(boolean testOnBorrow) {
        this.testOnBorrow = testOnBorrow;
    }

    public int getTimeout() {
        return timeout;
    }

    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }

    public boolean isUsePool() {
        return usePool;
    }

    public void setUsePool(boolean usePool) {
        this.usePool = usePool;
    }

    public List<String> getNodes() {
        return nodes;
    }

    public void setNodes(List<String> nodes) {
        this.nodes = nodes;
    }

}

redis配置類,生成redisTemplate

@Configuration
public class RedisConfig {

    @Autowired
    private RedisProperties redisProperties;

    @Bean
    public RedisClusterConfiguration redisClusterConfiguration() {
        RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration();
        redisClusterConfiguration.setClusterNodes(getRedisNode());
        redisClusterConfiguration.setMaxRedirects(redisProperties.getMaxRedirects());
        return redisClusterConfiguration;
    }

    @Bean
    public JedisPoolConfig jedisPoolConfig() {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxIdle(redisProperties.getMaxIdle());
        jedisPoolConfig.setMaxTotal(redisProperties.getMaxTotal());
        jedisPoolConfig.setMinIdle(redisProperties.getMinIdle());
        jedisPoolConfig.setMaxWaitMillis(redisProperties.getMaxWaitMillis());
        jedisPoolConfig.setNumTestsPerEvictionRun(redisProperties.getNumTestsPerEvictionRun());
        jedisPoolConfig.setTimeBetweenEvictionRunsMillis(redisProperties.getTimeBetweenEvictionRunsMillis());
        jedisPoolConfig.setTestOnBorrow(redisProperties.isTestOnBorrow());
        return jedisPoolConfig;
    }

    @Bean
    public JedisConnectionFactory jedisConnectionFactory(RedisClusterConfiguration redisClusterConfiguration, JedisPoolConfig jedisPoolConfig) {
        JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(redisClusterConfiguration, jedisPoolConfig);
        jedisConnectionFactory.setTimeout(redisProperties.getTimeout());
        return jedisConnectionFactory;
    }

    @Bean
    public RedisTemplate redisTemplate(JedisConnectionFactory jedisConnectionFactory) {
        RedisTemplate redisTemplate = new RedisTemplate();
        redisTemplate.setConnectionFactory(jedisConnectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        return redisTemplate;
    }

    private List<RedisNode> getRedisNode() {
        List<String> nodes = redisProperties.getNodes();
        if (CommonUtils.isNotEmpty(nodes)) {
            List<RedisNode> redisNodes = nodes.stream().map(node -> {
                String[] ss = node.split(":");
                return new RedisNode(ss[0], Integer.valueOf(ss[1]));
            }).collect(Collectors.toList());
            return redisNodes;
        }
        return new ArrayList<>();
    }
}

redis鎖的實現:

@Component
public class RedisLock {

    private static final Logger log = LoggerFactory.getLogger(RedisLock.class);
    /* 默認鎖的有效時間30s */
    private static final int DEFAULT_LOCK_EXPIRSE_MILL_SECONDS = 30 * 1000;
    /* 默認請求鎖等待超時時間10s */
    private static final int DEFAULT_LOCK_WAIT_DEFAULT_TIME_OUT = 10 * 1000;
    /* 默認的輪詢獲取鎖的間隔時間 */
    private static final int DEFAULT_LOOP_WAIT_TIME = 150;
    /* 鎖的key前綴 */
    private static final String LOCK_PREFIX = "LOCK_";

    /* 是否獲得鎖的標誌 */
    private boolean lock = false;
    /* 鎖的key */
    private String lockKey;
    /* 鎖的有效時間(ms) */
    private int lockExpirseTimeout;
    /* 請求鎖的阻塞時間(ms) */
    private int lockWaitTimeout;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public boolean isLock() {
        return lock;
    }

    public void setLock(boolean lock) {
        this.lock = lock;
    }

    public String getLockKey() {
        return lockKey;
    }

    public void setLockKey(String lockKey) {
        this.lockKey = LOCK_PREFIX + lockKey;
    }

    public int getLockExpirseTimeout() {
        return lockExpirseTimeout;
    }

    public void setLockExpirseTimeout(int lockExpirseTimeout) {
        this.lockExpirseTimeout = lockExpirseTimeout;
    }

    public int getLockWaitTimeout() {
        return lockWaitTimeout;
    }

    public void setLockWaitTimeout(int lockWaitTimeout) {
        this.lockWaitTimeout = lockWaitTimeout;
    }

    public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public RedisLock() {
    }

    public RedisLock(String lockKey, int lockExpirseTimeout, int lockWaitTimeout) {
        this.lockKey = LOCK_PREFIX + lockKey;
        this.lockExpirseTimeout = lockExpirseTimeout;
        this.lockWaitTimeout = lockWaitTimeout;
    }

    public RedisLock newInstance(String lockKey) {
        RedisLock redisLock = new RedisLock(lockKey, DEFAULT_LOCK_EXPIRSE_MILL_SECONDS, DEFAULT_LOCK_WAIT_DEFAULT_TIME_OUT);
        redisLock.setRedisTemplate(this.redisTemplate);
        return redisLock;
    }

    public RedisLock newInstance(String lockKey, int lockExpirseTimeout, int lockWaitTimeout) {
        if (lockExpirseTimeout == 0 || lockWaitTimeout == 0) {
            lockExpirseTimeout = DEFAULT_LOCK_EXPIRSE_MILL_SECONDS;
            lockWaitTimeout = DEFAULT_LOCK_WAIT_DEFAULT_TIME_OUT;
        }
        RedisLock redisLock = new RedisLock(lockKey, lockExpirseTimeout, lockWaitTimeout);
        redisLock.setRedisTemplate(this.redisTemplate);
        return redisLock;
    }

    public boolean setIfAbsent(String expirseTimeStr) {
        // setIfAbsent通過jedis的setNx實現
        return this.redisTemplate.opsForValue().setIfAbsent(this.lockKey, expirseTimeStr);
    }

    public String getAndSet(String expiresTimeStr) {
        // 獲取原來的值,並設置新的值,原子操作
        return (String) this.redisTemplate.opsForValue().getAndSet(this.lockKey, expiresTimeStr);
    }

    /**
     * 1、獲得當前系統時間,計算鎖的到期時間
     * 2、setNx操作,加鎖
     * 3、如果,加鎖成功,設置鎖的到期時間,返回true;取鎖失敗,取出當前鎖的value(到期時間)
     * 4、如果value不爲空而且小於當前系統時間,進行getAndSet操作,重新設置value,並取出舊value;否則,等待間隔時間後,重複步驟2;
     * 5、如果步驟3和4取出的value一樣,加鎖成功,設置鎖的到期時間,返回true;否則,別人加鎖成功,恢復鎖的value,等待間隔時間後,重複步驟2。
     */
    public boolean lock() {
        log.info("{}-----嘗試獲取鎖...", Thread.currentThread().getName());
        int lockWaitMillSeconds = this.lockWaitTimeout;
        // key 的值,表示key的到期時間
        String redisValue = String.valueOf(System.currentTimeMillis() + this.lockExpirseTimeout);
        while (lockWaitMillSeconds > 0) {
            lock = setIfAbsent(redisValue);
            if (lock) {
                // 拿到鎖,設置鎖的有效期,這裏可能因爲故障沒有被執行,鎖會一直存在,這時就需要value的有效期去判斷鎖是否失效
                this.redisTemplate.expire(this.lockKey, lockExpirseTimeout, TimeUnit.MILLISECONDS);
                log.info("{}-----獲得鎖", Thread.currentThread().getName());
                return lock;
            } else {
                // 鎖存在,判斷鎖有沒有過期
                String oldValue = (String) this.redisTemplate.opsForValue().get(this.lockKey);
                if (CommonUtils.isNotEmpty(oldValue) && Long.parseLong(oldValue) < System.currentTimeMillis()) {
                    // 鎖的到期時間小於當前時間,說明鎖已失效, 修改value,獲得鎖
                    String currentRedisValue = getAndSet(String.valueOf(lockExpirseTimeout + System.currentTimeMillis()));
                    // 如果兩個值不相等,說明有另外一個線程拿到了鎖,阻塞
                    if (currentRedisValue.equals(oldValue)) {
                        // 如果修改的鎖的有效期之前沒被其他線程修改,則獲得鎖, 設置鎖的超時時間
                        redisTemplate.expire(this.lockKey, lockExpirseTimeout, TimeUnit.MILLISECONDS);
                        log.info("{}-----獲得鎖", Thread.currentThread().getName());
                        this.lock = true;
                        return this.lock;
                    } else {
                        // 有另外一個線程獲得了這個超時的鎖,不修改鎖的value
                        redisTemplate.opsForValue().set(this.lockKey, currentRedisValue);
                    }
                }
            }
            // 減掉固定輪詢獲取鎖的間隔時間
            lockWaitMillSeconds -= DEFAULT_LOOP_WAIT_TIME;
            try {
                log.info("{}-----等待{}ms後,再嘗試獲取鎖...", Thread.currentThread().getName(), DEFAULT_LOOP_WAIT_TIME);
                // 取鎖失敗時,應該在隨機延時後進行重試,避免不同客戶端同時重試導致誰都無法拿到鎖的情況出現,也可以採用等待隊列的方式
                Thread.sleep(DEFAULT_LOOP_WAIT_TIME);
            } catch (InterruptedException e) {
                log.error("redis 同步鎖出現未知異常", e);
            }
        }
        log.info("{}-----請求鎖超時,獲得鎖失敗", Thread.currentThread().getName());
        return false;
    }

    public void unlock() {
        if (lock) {
            this.redisTemplate.delete(this.lockKey);
            this.lock = false;
        }
    }

}

加鎖過程:

1、獲得當前系統時間,計算鎖的到期時間
2、setNx操作,加鎖
3、如果,加鎖成功,設置鎖的到期時間,返回true;取鎖失敗,取出當前鎖的value(到期時間)
4、如果value不爲空而且小於當前系統時間,進行getAndSet操作,重新設置value,並取出舊value;否則,等待間隔時間後,重複步驟2;
5、如果步驟3和4取出的value一樣,加鎖成功,設置鎖的到期時間,返回true;否則,別人加鎖成功,恢復鎖的value,等待間隔時間後,重複步驟2。

這裏設置鎖的到期時間,只是爲了減少後面複雜邏輯的執行

測試:

測試類
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = Application.class)
public class TestRedis {
    private CountDownLatch countDownLatch = new CountDownLatch(2);

    @Test
    public void testRedisLock() throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                RedisLock lock = ((RedisLock) SpringContextUtil.getBean("redisLock")).newInstance("test");
                if (lock.lock()) {
                    System.out.println("work1獲得鎖");
                    System.out.println("work1 工作15s...");
                    try {
                        Thread.sleep(15000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("work1完成工作,釋放鎖");
                    lock.unlock();
                }
                countDownLatch.countDown();
            }
        },"work1").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                RedisLock lock = ((RedisLock) SpringContextUtil.getBean("redisLock")).newInstance("test");
                if (lock.lock()) {
                    System.out.println("work2獲得鎖");
                    System.out.println("work2 工作5s...");
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("work2完成工作,釋放鎖");
                    lock.unlock();
                }
                countDownLatch.countDown();
            }
        }, "work2").start();
        // 等待兩個線程完成,才完成主線程
        countDownLatch.await();
    }
}

這裏在單元測試中起兩個線程work1和work2,work1模擬工作15s,work2模擬工作5s。這裏爲了juint的主線程不會在兩個work線程完成工作之前就停止,用到了CountDownLatch,讓主線程在兩個work線程完成前等待。

上面測試有兩種結果,第一種,work2先拿到鎖,工作5s,work1等待(默認等待超時時間10s),等待過程中一值嘗試獲取鎖(默認間隔150ms),默認鎖的有效期30s,顯然,5s後work2完成工作釋放鎖,work1獲得鎖,work1和work2都正常完成了工作。結果如下:

2018-03-16 14:16:50,579 5576 [work1] INFO  c.s.component.redis.lock.RedisLock - work1-----嘗試獲取鎖...
2018-03-16 14:16:50,579 5576 [work2] INFO  c.s.component.redis.lock.RedisLock - work2-----嘗試獲取鎖...
2018-03-16 14:16:50,598 5595 [work1] INFO  c.s.component.redis.lock.RedisLock - work1-----等待150ms後,再嘗試獲取鎖...
2018-03-16 14:16:50,599 5596 [work2] INFO  c.s.component.redis.lock.RedisLock - work2-----獲得鎖
work2獲得鎖
work2 工作5s...
2018-03-16 14:16:50,748 5745 [work1] INFO  c.s.component.redis.lock.RedisLock - work1-----等待150ms後,再嘗試獲取鎖...
。。。省略重複嘗試獲取鎖日誌。。。
2018-03-16 14:16:55,571 10568 [work1] INFO  c.s.component.redis.lock.RedisLock - work1-----等待150ms後,再嘗試獲取鎖...
work2完成工作,釋放鎖
2018-03-16 14:16:55,722 10719 [work1] INFO  c.s.component.redis.lock.RedisLock - work1-----獲得鎖
work1獲得鎖
work1 工作15s...
work1完成工作,釋放鎖

第二種結果,work1先拿到鎖,工作15s,work2等待,這裏,因爲work1的工作時間超過了默認的等待超時時間10s,所以work2在work1完成工作釋放鎖之前就因爲等待超時而獲取鎖失敗,不能完成工作,結果如下:

2018-03-16 14:22:45,292 5448 [work1] INFO  c.s.component.redis.lock.RedisLock - work1-----嘗試獲取鎖...
2018-03-16 14:22:45,292 5448 [work2] INFO  c.s.component.redis.lock.RedisLock - work2-----嘗試獲取鎖...
2018-03-16 14:22:45,308 5464 [work2] INFO  c.s.component.redis.lock.RedisLock - work2-----等待150ms後,再嘗試獲取鎖...
2018-03-16 14:22:45,309 5465 [work1] INFO  c.s.component.redis.lock.RedisLock - work1-----獲得鎖
work1獲得鎖
work1 工作15s...
2018-03-16 14:22:45,459 5615 [work2] INFO  c.s.component.redis.lock.RedisLock - work2-----等待150ms後,再嘗試獲取鎖...
。。。省略重複嘗試獲取鎖日誌。。。
2018-03-16 14:22:55,251 15407 [work2] INFO  c.s.component.redis.lock.RedisLock - work2-----等待150ms後,再嘗試獲取鎖...
2018-03-16 14:22:55,401 15557 [work2] INFO  c.s.component.redis.lock.RedisLock - work2-----請求鎖超時,獲得鎖失敗
。。。這裏還要等待大概5s。。。
work1完成工作,釋放鎖


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章