1. 程式人生 > >分散式鎖之redis鎖及實現

分散式鎖之redis鎖及實現

分散式鎖有幾種常用的實現方式:zookeeper、memcached、redis、mysql。這裡介紹一下redis的實現方式,並在最後附上了一個Demo小工具:

眾所周知,reids鎖是通過setnx + expire的方式實現的,setnx保證只有在key不存在時才能set成功,expire保證鎖在非正常釋放的情況下不會形成死鎖。基本原理就是這個,但實際操作中我們需要注意幾個問題:

  1. setnx與expire是非原子性的,那麼如果setnx執行成功、但expire未執行,那麼鎖也就無法過期自動刪除了。解決方案:redis提供命令set(key,1,30,nx)
    一步到位設定超時時間。
  2. 如果執行緒A先獲得了鎖,但是執行時間超過了鎖的過期時間,鎖自動釋放了,那麼執行緒B獲得鎖,不僅有可能導致執行結果出現併發不一致問題,如果A執行完了,那麼還會刪除掉B的鎖。解決方案:(1)使用一個請求標識作為鎖的value值,在刪除前判斷一下。(2)使用一個守護執行緒,不斷的更新鎖過期時間,保證執行過程中鎖不會釋放。
  3. 判斷value值與刪除鎖不是原子的。解決方案:使用lua指令碼,保證判斷與刪除原子操作。
public class RedisLockUtil {

    private static final Jedis DEFAULT_CLIENT = RedisClient.getInstance();
    private
static final String LOCK_SUCCESS="OK"; private static final Long RELEASE_SUCCESS=1L; private static final Long EXPIRE_SUCCESS=1L; /** * 加鎖 * @param client 可自己傳入redis-client,如果是null,則用預設的client * @param lockId 當前鎖的key值 * @param requestId 當前加鎖的任務的請求標識,可以用執行緒id,或者分散式唯一id, * 可以保證全域性唯一即可,作為當前鎖的value值使用,用作刪除或 * 延時是判斷是否是持有鎖的任務 * @param
second 過期時間,避免鎖未正常釋放時形成死鎖 * @return true:加鎖成功,flase:加鎖失敗 */
public static boolean lock(Jedis client, String lockId, String requestId,int second) { if (client == null) client = DEFAULT_CLIENT; // client.setnx(lockId,"1"); // client.expire(lockId,60); //加鎖和超時設定分為了兩步,沒法保證原子性,所以可以直接用下面的命令 String rst = client.set(lockId,requestId,"nx","ex",second); if (LOCK_SUCCESS.equals(rst)) { return true; } return false; } /** * 釋放鎖 * @param client 可自己傳入redis-client,如果是null,則用預設的client * @param lockId 當前鎖的key值 * @param requestId 請求標識,刪除前判斷是否是持有鎖的任務 * @return true:加鎖成功,flase:加鎖失敗 */ public static boolean unlock(Jedis client, String lockId, String requestId) { if (client == null) client = DEFAULT_CLIENT; String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] " + "then return redis.call('del', KEYS[1]) " + "else return 0 end"; Object rst = client.eval(luaScript, Collections.singletonList(lockId), Collections.singletonList(requestId)); if (RELEASE_SUCCESS.equals(rst)) { return true; } return false; } /** * 延遲加鎖時間 * @param client 可自己傳入redis-client,如果是null,則用預設的client * @param lockId 當前鎖的key值 * @param requestId 請求標識,延時前判斷是否是持有鎖的任務 * @param second 申請延遲的時間 * @return true:加鎖成功,flase:加鎖失敗 */ public static boolean expired(Jedis client, String lockId, String requestId, int second) { if (client == null) client = DEFAULT_CLIENT; String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] " + "then return redis.call('expire',KEYS[1],ARGV[2]) " + "else return 0 end"; Object rst = client.eval(luaScript,Collections.singletonList(lockId), Arrays.asList(requestId,String.valueOf(second))); if (EXPIRE_SUCCESS.equals(rst)) { return true; } return false; } } class RedisClient{ private static volatile Jedis client = null; static Jedis getInstance(){ String host = "127.0.0.1"; int port = 6379; synchronized (RedisClient.class) { if (client == null) { client = new Jedis(host,port); } } return client; } }