redis分佈式鎖實例

什麼是分佈式鎖

        分佈式鎖是控制分佈式系統之間同步訪問共享資源的一種方式。在分佈式系統中,常常需要協調他們的動作。如果不同的系統或是同一個系統的不同主機之間共享了一個或一組資源,那麼訪問這些資源的時候,往往需要互斥來防止彼此干擾來保證一致性,在這種情況下,便需要使用到分佈式鎖。

redis分佈式鎖具有什麼特點

  1. redis爲單進程單線程模式,採用隊列模式將併發訪問變成串行訪問,且多客戶端對Redis的連接並不存在競爭關係。
  2. 高可用、高性能的加鎖和解鎖。
  3. SETNX指令和lua腳本,都是原子性操作。
  4. 具備鎖失效機制,通過失效時間,可以有效的避免異常時的死鎖問題。
  5. 具有重入性,如何操作可以通過lua腳本對KEY值進行判斷,實現重入和鎖有效時間的更新,具體可以看下Redisson中的這段代碼。
<T> Future<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId,
                RedisStrictCommand<T> command) {
    internalLockLeaseTime = unit.toMillis(leaseTime);
    //使用 EVAL 命令執行 Lua 腳本獲取鎖
    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
              "if (redis.call('exists', KEYS[1]) == 0) then " +
                  "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                  "return nil; " +
              "end; " +
              "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                  "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                  "return nil; " +
              "end; " +
              "return redis.call('pttl', KEYS[1]);",
                Collections.<Object>singletonList(getName()), internalLockLeaseTime,
                        getLockName(threadId));
}

使用時注意的問題

1、一些例子分開多步進行key值和有效期的操作,是錯誤的寫法,不能保證原子性 。

2、value值是判斷鎖使用者身份的證明,最好使用唯一值(如:uuid)的數據,提高識別度,避免出現問題。

 

redis鎖在單機環境多線程下的模擬運用

       概念的東西不多做介紹,有許多文章的概念會更加標準和官方,今天的文章以DEMO爲主,有的小夥伴會說DEMO爲單機環境演示,比較不具備參考性,後續有時間我會補一個實際分佈式DEMO更加直觀有參考性和學習性。本DEMO爲JDK1.8的springboot工程,不會創建springboot和本地沒有啓動redis的小夥伴我回頭弄個簡單的教程,先自己查找下教程。

DEMO目錄結構圖

實例結構

 

代碼部分

pom.xml文件引入依賴如下

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.10</version>
        </dependency>

配置項目redis環境,redis.properties內容

# Redis服務器地址
spring.redis.host=localhost
# Redis服務器連接端口
spring.redis.port=6379
# Redis服務器連接密碼(默認爲空)
spring.redis.password=
# 連接池最大連接數(使用負值表示沒有限制)
spring.redis.jedis.pool.max-active=200
# 連接池最大阻塞等待時間(使用負值表示沒有限制)
spring.redis.jedis.pool.max-wait=10000
# 連接池中的最大空閒連接
spring.redis.jedis.pool.max-idle=200
# 連接池中的最小空閒連接
spring.redis.jedis.pool.min-idle=0
# 連接超時時間(毫秒)
spring.redis.timeout=10000
#redis配置結束
spring.redis.block-when-exhausted=true

 RedisConfig.java

package com.example.redislock.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

@Configuration
@PropertySource("classpath:config/redis.properties")
@Slf4j
public class RedisConfig {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int port;

    @Value("${spring.redis.timeout}")
    private int timeout;

    @Value("${spring.redis.jedis.pool.max-idle}")
    private int maxIdle;

    @Value("${spring.redis.jedis.pool.max-wait}")
    private long maxWaitMillis;

    @Value("${spring.redis.password}")
    private String password;

    @Value("${spring.redis.block-when-exhausted}")
    private boolean  blockWhenExhausted;

    @Value("${spring.redis.jedis.pool.max-active}")
    private int maxActive;

    @Value("${spring.redis.jedis.pool.min-idle}")
    private int minIdle;

    @Bean
    public JedisPool redisPoolFactory()  throws Exception{
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxIdle(maxIdle);
        jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
        jedisPoolConfig.setMaxTotal(maxActive);
        jedisPoolConfig.setMinIdle(minIdle);
        JedisPool jedisPool = new JedisPool(jedisPoolConfig,host,port,timeout,null);

        log.info("JedisPool注入成功!");
        log.info("redis地址:" + host + ":" + port);
        return  jedisPool;
    }
}

 RedisService.java

package com.example.redislock.service;

public interface RedisService {

    boolean  lock(String key, String uuid);

    boolean  unlock(String key, String uuid);
}

 RedisServiceImpl.java

package com.example.redislock.service.impl;

import com.example.redislock.service.RedisService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.Collections;

@Service("redisService")
public class RedisServiceImpl implements RedisService {

    private static final Logger log = LoggerFactory.getLogger(RedisServiceImpl.class);
    @Autowired
    private JedisPool jedisPool;

    private static final String LOCK_SUCCESS = "OK";
    private static final Long RELEASE_SUCCESS = 1L;
    //鎖過期時間
    private static final long expireTime = 30000;
    //獲取鎖的超時時間
    private long timeout = 900000;

    @Override
    public boolean lock(String key, String uuid) {

        Jedis jedis = null;
        long start = System.currentTimeMillis();
        try {
            jedis = jedisPool.getResource();
            while (true) {
                //使用setnx是爲了保持原子性
                String result = jedis.set(key, uuid, "NX", "PX", expireTime);

                //OK標示獲得鎖,null標示其他任務已經持有鎖
                if (LOCK_SUCCESS.equals(result)) {
                    return true;
                }
                //在timeout時間內仍未獲取到鎖,則獲取失敗
                long time = System.currentTimeMillis() - start;
                if (time >= timeout) {
                    return false;
                }
                //增加睡眠時間可能導致結果分散不均勻,測試時可以不用睡眠
//                Thread.sleep(1);
            }
//        }catch (InterruptedException e1) {
//            log.error("redis競爭鎖,線程sleep異常");
//            return false;
        } catch (Exception e) {
            log.error("redis競爭鎖失敗");
            throw e;
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
    }

    @Override
    public boolean unlock(String key, String uuid) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            //lua腳本,使用lua腳本是爲了保持原子性
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            Object result = jedis.eval(script, Collections.singletonList(key), Collections.singletonList(uuid));

            if (RELEASE_SUCCESS.equals(result)) {
                return true;
            }
            return false;
        } catch (Exception e) {
            log.error("redis解鎖失敗");
            throw e;
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
    }
}

 RedisLockController.java

package com.example.redislock.controller;

import com.example.redislock.service.RedisService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.UUID;

@RestController
public class RedisLockController {

    @Resource(name = "redisService")
    private RedisService redisService;

    //沒有連接db講車票設置爲類變量作爲共享資源,便於任務競爭進行DEMO展示
    private int trainNum = 500;

    /**
     * 無db,模擬補充車票
     * @param num
     */
    @RequestMapping(value = "/addTrainNum")
    public String addTrainNum(@RequestParam(defaultValue = "500") int num) {
        trainNum = trainNum + num;
        return "系統已補票,目前車票庫存:" + trainNum;
    }

    /**
     * 無鎖買票
     */
    @RequestMapping(value = "/noLock")
    public void noLock() {

        while (true) {
            if(trainNum > 0) {
                try {
                    Thread.sleep(20);
                    System.out.println(Thread.currentThread().getName() + "購買了車票,車票號:" + trainNum-- );
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                }
            } else {
                System.out.println("車票售完");
                break;
            }
        }
    }

    /**
     * redis鎖買票
     */
    @RequestMapping(value = "/redisLock")
    public void redisLock() {
        String uuid = UUID.randomUUID().toString();
        while (true) {
            redisService.lock("train_test", uuid);
            if(trainNum > 0) {
                try {
                    Thread.sleep(20);
                    System.out.println(Thread.currentThread().getName() + "購買了車票,車票號:" + trainNum-- + ",uuid:" + uuid);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    redisService.unlock("train_test", uuid);
                }
            } else {
                System.out.println("車票售完");
                redisService.unlock("train_test", uuid);
                break;
            }
        }
    }

    /**
     * 本地多線程模擬
     */
    @RequestMapping(value = "/redisLockTest")
    public void redisLockTest() {

        Train train = new Train(40);
        Thread thread1 = new Thread(train, "小明");
        Thread thread2 = new Thread(train, "小王");
        Thread thread3 = new Thread(train, "小李");
        thread1.start();
        thread2.start();
        thread3.start();
    }

    class Train implements Runnable {

        private int num;
        private ThreadLocal<String> localUUID = new ThreadLocal<>();

        public Train(int num) {
            this.num = num;
        }

        @Override
        public void run() {

            localUUID.set(UUID.randomUUID().toString());
            while (true) {
                redisService.lock("train_test", localUUID.get());
                if(num > 0) {

                    try {
                        Thread.sleep(100);
                        System.out.println(Thread.currentThread().getName() + "購買了車票,車票號:" + num-- + ",uuid:" + localUUID.get());

                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        redisService.unlock("train_test", localUUID.get());
                    }
                } else {
                    redisService.unlock("train_test", localUUID.get());
                    System.out.println("車票售完");
                    break;
                }
            }
        }
    }
}

實驗步驟

1、啓動RedislockApplication

2、網頁單次執行  http://localhost:8080/noLock  ,查看控制檯信息,如下圖,購票信息正常。

3、另開一個頁籤執行  http://localhost:8080/addTrainNum?num=500  補充車票,可以根據自己需要調整。

4、網頁快速多次執行(F5刷新)  http://localhost:8080/noLock ,查看控制檯信息,如下圖,購票信息異常。異常原因:類變量在堆中屬於共享變量,多線程任務情況下,操作同一資源造成的線程不安全現象。

5、再次打開頁籤執行  http://localhost:8080/addTrainNum?num=500  補充車票。

6、網頁快速多次執行(F5刷新)  http://localhost:8080/redisLock  查看控制檯信息,如下圖,購票信息正常,redis鎖生效。

 

本次DEMO演示到此結束,如果有問題歡迎給我留言,我會及時改正,後面我會補一更全的DEMO

 

 

 

 

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