1. 程式人生 > 實用技巧 >使用Redis實現分散式鎖

使用Redis實現分散式鎖

分散式鎖

  在分散式的環境下,多個程序不再同一個系統中,控制多個程序對資源對訪問

使用場景

  一臺機器上多個不同執行緒搶佔同一個資源,多次執行會有異常,我們稱之為非執行緒安全, 可以通過synchronized或lock加鎖解決,但如果是多臺機器上多個不同執行緒搶佔同一個資源就需要使用分散式鎖。

實踐

  1、安裝Redis並啟動

  2、建立Spring boot專案,引入Redis依賴

<dependency>
  <groupId>redis.clients</groupId>
  <artifactId>jedis</artifactId>

  <version>2.9.0</version>
</dependency>

  3、建立Redis連線池

  Redis 是單程序單執行緒的,它利用佇列技術將併發訪問變為序列訪問,消除了傳統資料庫序列控制的開銷。

  Redis 是基於記憶體的資料庫,使用之前需要建立連線,建立斷開連線需要消耗大量的時間。

  連線池則可以實現在客戶端建立多個連線並且不釋放,當需要使用連線的時候通過一定的演算法獲取已經建立的連線,使用完了以後則還給連線池,這就免去了資料庫連線所佔用的時間。

  Redis 在收到多個連線後,採用的是非阻塞 IO,基於epoll的多路IO複用,然後採用佇列模式將併發訪問變為序列訪問。

@Configuration
public class RedisPool {
    private JedisPool jedisPool;

    @Value("${redis.pool.maxTotal}")
    private int maxTotal;//最大連線數

    @Value("${redis.pool.maxIdle}")
    private int maxIdle;//最大空閒連線數

    @Value("${redis.pool.minIdle}")
    private int minIdle;//最小空閒連線數

    @Value("${redis.pool.testOnBorrow}
") private boolean testOnBorrow;//在取連線時測試連線的可用性 @Value("${redis.pool.testOnReturn}") private boolean testOnReturn;//再還連線時不測試連線的可用性 @Value("${redis.pool.host}") private String host;//redis地址 @Value("${redis.pool.port}") private int port;// @Value("${redis.pool.timeout}") private int timeout; @Value("${redis.pool.password}") private String password; public Jedis getJedis() { return jedisPool.getResource(); } @Bean public JedisPool jedisPool() { JedisPoolConfig config = new JedisPoolConfig(); config.setMaxTotal(maxTotal); config.setMaxIdle(maxIdle); config.setMinIdle(minIdle); config.setTestOnBorrow(testOnBorrow); config.setTestOnReturn(testOnReturn); config.setBlockWhenExhausted(true); jedisPool = new JedisPool(config, host, port, timeout, null); return jedisPool; } }

  4、建立加鎖、解鎖方法

  加鎖就一行程式碼:jedis.set(String key, String value, String nxxx, String expx, int time)

  NX引數可以保證如果已有key存在,則函式不會呼叫成功,也就是隻有一個客戶端能持有鎖,滿足互斥性。
  對鎖設定了過期時間,即使鎖的持有者後續發生崩潰而沒有解鎖,鎖也會因為到了過期時間而自動解鎖,不會發生死鎖。
  將value賦值為requestId,代表加鎖的客戶端請求標識,那麼在客戶端在解鎖的時候就可以進行校驗是否是同一個客戶端

  

  解鎖需要兩行程式碼

  第一行程式碼,寫了一個簡單的Lua指令碼
  第二行程式碼,將Lua程式碼傳到jedis.eval()方法裡,並使引數KEYS[1]賦值為lockKey,ARGV[1]賦值為requestId。eval()方法是將Lua程式碼交給Redis服務端執行,eval()方法可以確保原子性

@Service
public class RedisPoolUtil {
    private static final String DEL_LUA = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    /**
     * NX:key不存在時才把key、value新增到redis
     * XX:key存在時才把key、value新增到redis
     */
    private static final String SET_IF_NOT_EXIST = "NX";
    /**
     * EX:單位秒
     * PX:單位毫秒
     */
    private static final String SET_WITH_EXPIRE_TIME = "EX";
    private static final Long ONE = 1L;
    private static final String OK = "OK";

    @Autowired
    private RedisPool redisPool;

    public boolean lock(String key, String requestId, int expireTime) {
        Object result = null;
        Jedis jedis = null;
        try {
            jedis = redisPool.getJedis();
            result = jedis.set(key, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
        return OK.equals(result);
    }

    public boolean unLock(String lockKey, String requestId) {
        Jedis jedis = null;
        Object result = null;
        try {
            jedis = redisPool.getJedis();
            result = jedis.eval(DEL_LUA, Collections.singletonList(lockKey), Collections.singletonList(requestId));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
        return ONE.equals(result);
    }
}