redis分佈式鎖


 關於分佈式鎖的概念網上太多了,這裏就不羅嗦了。對於開發者來講,最關心的應該是什麼狀況下使用分佈式鎖。java

使用分佈式鎖,通常要知足如下幾個條件:mysql

· 分佈式系統(關鍵是分佈式)redis

· 共享資源(各系統訪問同一資源,資源的載體多是傳統關係型數據庫或者NoSQL)sql

· 同步訪問(沒有同步訪問,誰管你資源競爭不競爭)數據庫

場景案例

· 光說不練假把式,上點乾貨。管理後臺的部署架構(多臺tomcat服務器+redis+mysql)就知足使用分佈式鎖的條件。多臺服務器要訪問redis全局緩存的資源,若是不使用分佈式鎖就會出現問題。 看以下僞代碼:緩存

long N=0L;
//N從redis獲取值
if(N<5){
N++;
//N寫回redis
}


· 從redis獲取值N,對數值N進行邊界檢查,自加1,而後N寫回redis中。 這種應用場景很常見,像秒殺,全局遞增ID、IP訪問限制等。以IP訪問限制來講,惡意攻擊者可能發起無限次訪問,併發量比較大,分佈式環境下對N的邊界檢查就不可靠,由於從redis讀的N可能已是髒數據。傳統的加鎖的作法(如java的synchronized和Lock)也沒用,由於這是分佈式環境,這個同步問題的救火隊員也一籌莫展。在這危急存亡之秋,分佈式鎖終於有用武之地了。tomcat

所謂知己知彼,百戰百勝。要想用好他,首先你要了解他。分佈式鎖能夠基於不少種方式實現,好比zookeeper、redis...。無論哪一種方式,他的基本原理是不變的:用一個狀態值表示鎖,對鎖的佔用和釋放經過狀態值來標識。服務器

這裏主要講如何用redis實現分佈式鎖。架構

實現原理

Redis爲單進程單線程模式,採用隊列模式將併發訪問變成串行訪問,且多客戶端對Redis的鏈接並不存在競爭關係。redis的SETNX命令能夠方便的實現分佈式鎖。併發

SETNX命令(SET if Not eXists) 語法: SETNX key value 功能: 當且僅當 key 不存在,將 key 的值設爲 value ,並返回1;若給定的 key 已經存在,則 SETNX 不作任何動做,並返回0。 有了以上Redis知識了,寫個簡單的分佈式鎖就不是什麼難事。

直接貼java代碼!

public class RedisLock implements Lock {

    @Autowired

    protected StringRedisTemplate redisTemplate;

    private static final Logger logger = Logger.getLogger(RedisLock.class);

    // lock flag stored in redis
    private static final String LOCKED = "TRUE";

    // timeout(ms)
    private static final long TIME_OUT = 30000;

    // lock expire time(s)
    public static final int EXPIRE = 60;

    // private Jedis jedis;
    private String key;

    // state flag
    private volatile boolean locked = false;

    private static ConcurrentMap<String, RedisLock> map = Maps.newConcurrentMap();

    public RedisLock(String key) {
        this.key = "_LOCK_" + key;
        redisTemplate = (StringRedisTemplate) ApplicationContextHolder.getBean("redisTemplate");
    }

    public static RedisLock getInstance(String key) {
        return map.getOrDefault(key, new RedisLock(key));
    }

    public void lock(long timeout) {
        long nano = System.nanoTime();
        timeout *= 1000000;
        final Random r = new Random();
        try {
            while ((System.nanoTime() - nano) < timeout) {
                if (redisTemplate.getConnectionFactory().getConnection().setNX(key.getBytes(), LOCKED.getBytes())) {
                    redisTemplate.expire(key, EXPIRE, TimeUnit.SECONDS);
                    locked = true;
                    logger.debug("add RedisLock[" + key + "].");
                    break;
                }
                Thread.sleep(3, r.nextInt(500));
            }
        } catch (Exception e) {
        }
    }

    @Override
    public void unlock() {
        if (locked) {
            logger.debug("release RedisLock[" + key + "].");
            redisTemplate.delete(key);
        }
    }

    @Override
    public void lock() {
        lock(TIME_OUT);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public Condition newCondition() {
        return null;
    }

    @Override
    public boolean tryLock() {
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }
}

基準測試

飯已OK,快來MIXI啦。

~~~等等,還得讓評審來評價下這飯作得好很差吃。

寫一段測試代碼來對比測試下。

public void concurrentTest() {
        final Printer outer = new Printer();
        new Thread(new Runnable() {
            @Override
            public void run() {
                outer.output("I am a boy.");
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                outer.output("You are a girl.");
            }
        }).start();
    }
?
不加鎖的Printer:
?
class Printer {
      public void output(String name) {

          for (int i = 0; i < name.length(); i++) {
              System.out.print(name.charAt(i));
              try {
                  Thread.sleep(10);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      }
  }
?
?使用java內置的鎖ReentrantLock:
Lock lock=new ReentrantLock();class Printer {
      public void output(String name) {
          lock.lock();
          for (int i = 0; i < name.length(); i++) {
              System.out.print(name.charAt(i));
              try {
                  Thread.sleep(10);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
          lock.unlock();
      }
  }
?
?使用分佈式鎖RedisLock:
Lock lock=new ReentrantLock();class Printer {
      public void output(String name) {
         Lock lock = new RedisLock("lock1");
          lock.lock();
          for (int i = 0; i < name.length(); i++) {
              System.out.print(name.charAt(i));
              try {
                  Thread.sleep(10);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
          lock.unlock();
      }
  }

對比測試結果以下:

項目

不加鎖

java內置鎖ReentrantLock

加分佈式鎖RedisLock

測試結果

IY oau m aar eb oa yg.irl.

I am a boy.You are a girl.

You are a girl.I am a boy.

寫在後面

話說RedisLock可以正常使用了,也達到了預期效果。是否是這個分佈式鎖就萬無一失呢?

這是一個悲觀鎖,RedisLock會不斷嘗試去獲取鎖,直到超時。也就是說,若是長時間獲取不到,就會獲取鎖失敗,至關於沒加鎖!具體的超時時間設置爲多長,有待後期驗證,再作優化。

相關文章
相關標籤/搜索