分佈式系統中,要保證某個資源在一時間段內只能有一個進程訪問,需要使用分佈式鎖。
方案一:基於redis的分佈式鎖
我們首先介紹三個使用到的redis方法SETNX()、GET()、GETSET()。
setnx ( key, value ):SET if Not Exists,該方法是原子的。若 key 不存在,則設置當前 key 成功,返回 1;若 key 已存在,則設置當前 key 失敗,返回 0。
getset ( key,newValue ):該方法是原子的,對 key 設置 newValue 新值,返回 key 原來的舊值。若原來無值,則返回null。
使用步驟
1、setnx(lockkey, 當前時間+過期超時時間),如果返回 1,則獲取鎖成功;如果返回 0 則沒有獲取到鎖,轉向 2
2、get(lockkey) 獲取值 oldExpireTime ,並將這個 value 值與當前的系統時間進行比較,如果小於當前系統時間,則認爲這個鎖已經超時,可以允許別的請求重新獲取,轉向 3
3、計算 newExpireTime = 當前時間+過期超時時間,然後 getset(lockkey, newExpireTime) ,返回當前 lockkey 的(舊)值currentExpireTime。
判斷 currentExpireTime 與 oldExpireTime 是否相等,如果相等,則當前 getset 設置成功,獲取到了鎖;如果不相等,說明這個鎖又被別的請求獲取走了,那麼當前請求可以直接返回失敗,或者繼續重試。
4、在獲取到鎖之後,當前線程可以開始自己的業務處理,處理完畢後,比較自己的處理時間和對於鎖設置的超時時間,如果小於鎖設置的超時時間,則直接執行 delete 釋放鎖;如果大於鎖設置的超時時間,無需處理。
//redis分佈式鎖
public final class RedisLockUtil {
private static final int defaultExpire = 60;
private RedisLockUtil() {
//
}
public static boolean lock(String key) {
return lock2(key, defaultExpire);
}
/**
* 加鎖
*
* @param key redis key
* @param expire 過期時間,單位秒
* @return true:加鎖成功,false,加鎖失敗
*/
public static boolean lock2(String key, int expire) {
RedisService redisService = SpringUtils.getBean(RedisService.class);
long value = System.currentTimeMillis() + expire;
long status = redisService.setnx(key, String.valueOf(value));
if (status == 1) {
return true;
}
// 獲取舊值
long oldExpireTime = Long.parseLong(redisService.get(key, "0"));
if (oldExpireTime < System.currentTimeMillis()) {
// 超時
long newExpireTime = System.currentTimeMillis() + expire;
// 當前舊值
long currentExpireTime = Long.parseLong(redisService.getSet(key, String.valueOf(newExpireTime)));
if (currentExpireTime == oldExpireTime) {
return true;
}
}
return false;
}
public static void unLock2(String key) {
RedisService redisService = SpringUtils.getBean(RedisService.class);
long oldExpireTime = Long.parseLong(redisService.get(key, "0"));
if (oldExpireTime > System.currentTimeMillis()) {
redisService.del(key);
}
}
//——————————————附:使用SETNX()、EXPIRE()做分佈式鎖——————————————————
// 此種方案不完善。比如在expire()命令執行成功前,發生了宕機的現象,那就會出現死鎖的問題
/**
* 加鎖
*
* @param key redis key
* @param expire 過期時間,單位秒
* @return true:加鎖成功,false,加鎖失敗
*/
public static boolean lock(String key, int expire) {
RedisService redisService = SpringUtils.getBean(RedisService.class);
long status = redisService.setnx(key, "1");
if (status == 1) {
redisService.expire(key, expire);
return true;
}
return false;
}
public static void unLock(String key) {
RedisService redisService = SpringUtils.getBean(RedisService.class);
redisService.del(key);
}
}
使用鎖:
public void drawRedPacket(long userId) {
String key = "draw.redpacket.userid:" + userId;
boolean lock = RedisLockUtil.lock2(key, 50);
if(lock) {
try {
//領取操作
} finally {
//釋放鎖
RedisLockUtil.unLock2(key);
}
} else {
new RuntimeException("重複領取獎勵");
}
}
優點: 性能高
缺點:失效時間設置多長時間爲好?如何設置的失效時間太短,方法沒等執行完,鎖就自動釋放了,那麼就會產生併發問題。如果設置的時間太長,其他獲取鎖的線程就可能要平白的多等一段時間。
方案二:基於zookeeper的分佈式鎖
1.在 /lock 節點下創建一個有序臨時節點 (EPHEMERAL_SEQUENTIAL)。
2.判斷創建的節點序號是否最小,如果是最小則獲取鎖成功。不是則取鎖失敗,然後 watch 序號比本身小的前一個節點。
3.當取鎖失敗,設置 watch 後則等待 watch 事件到來後,再次判斷是否序號最小。
4.取鎖成功則執行代碼,最後釋放鎖(刪除該節點)。
public class DistributedLock implements Lock, Watcher {
private ZooKeeper zk;
private String root = "/locks";// 根
private String lockName;// 競爭資源的標誌
private String waitNode;// 等待前一個鎖
private String myZnode;// 當前鎖
private CountDownLatch latch;// 計數器
private int sessionTimeout = 30000;
private List<Exception> exception = new ArrayList<Exception>();
/**
* 創建分佈式鎖,使用前請確認config配置的zookeeper服務可用
*
* @param config 127.0.0.1:2181
* @param lockName 競爭資源標誌,lockName中不能包含單詞lock
*/
public DistributedLock(String config, String lockName) {
this.lockName = lockName;
// 創建一個與服務器的連接
try {
zk = new ZooKeeper(config, sessionTimeout, this);
Stat stat = zk.exists(root, false);
if (stat == null) {
// 創建根節點
zk.create(root, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
} catch (IOException e) {
exception.add(e);
} catch (KeeperException e) {
exception.add(e);
} catch (InterruptedException e) {
exception.add(e);
}
}
/**
* zookeeper節點的監視器
*/
public void process(WatchedEvent event) {
if (this.latch != null) {
this.latch.countDown();
}
}
public void lock() {
if (exception.size() > 0) {
throw new LockException(exception.get(0));
}
try {
if (this.tryLock()) {
System.out.println("Thread-" + Thread.currentThread().getName() + " " + myZnode + " get lock true");
return;
} else {
waitForLock(waitNode, sessionTimeout);// 等待鎖
}
} catch (KeeperException e) {
throw new LockException(e);
} catch (InterruptedException e) {
throw new LockException(e);
}
}
public boolean tryLock() {
try {
String splitStr = "_lock_";
if (lockName.contains(splitStr))
throw new LockException("lockName can not contains \\u000B");
// 創建臨時子節點
myZnode = zk.create(root + "/" + lockName + splitStr, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println(myZnode + " is created ");
// 取出所有子節點
List<String> subNodes = zk.getChildren(root, false);
// 取出所有lockName的鎖
List<String> lockObjNodes = new ArrayList<String>();
for (String node : subNodes) {
String _node = node.split(splitStr)[0];
if (_node.equals(lockName)) {
lockObjNodes.add(node);
}
}
Collections.sort(lockObjNodes);
if (myZnode.equals(root + "/" + lockObjNodes.get(0))) {
// 如果是最小的節點,則表示取得鎖
return true;
}
// 如果不是最小的節點,找到比自己小1的節點
String subMyZnode = myZnode.substring(myZnode.lastIndexOf("/") + 1);
System.out.println("subMyZnode=" + subMyZnode);
waitNode = lockObjNodes.get(Collections.binarySearch(lockObjNodes, subMyZnode) - 1);
System.out.println("waitNode=" + waitNode);
} catch (KeeperException e) {
throw new LockException(e);
} catch (InterruptedException e) {
throw new LockException(e);
}
return false;
}
public boolean tryLock(long time, TimeUnit unit) {
try {
if (this.tryLock()) {
return true;
}
return waitForLock(waitNode, time);
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
private boolean waitForLock(String lower, long waitTime) throws InterruptedException, KeeperException {
// 判斷比自己小一個數的節點是否存在,如果不存在則無需等待鎖,同時註冊監聽
Stat stat = zk.exists(root + "/" + lower, true);
if (stat != null) {
System.out.println("Thread-" + Thread.currentThread().getName() + " waiting for " + root + "/" + lower);
this.latch = new CountDownLatch(1);
this.latch.await(waitTime, TimeUnit.MILLISECONDS);
this.latch = null;
}
return true;
}
public void unlock() {
try {
System.out.println(Thread.currentThread().getName() + " unlock " + myZnode);
zk.delete(myZnode, -1);
myZnode = null;
zk.close();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
public void lockInterruptibly() throws InterruptedException {
this.lock();
}
public Condition newCondition() {
return null;
}
public class LockException extends RuntimeException {
private static final long serialVersionUID = 1L;
public LockException(String e) {
super(e);
}
public LockException(Exception e) {
super(e);
}
}
public static void main(String[] args) {
final DistributedLock client1 = new DistributedLock("127.0.0.1", "lName");
final DistributedLock client2 = new DistributedLock("127.0.0.1", "lName");
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
client1.lock();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
client1.unlock();
}
}, "t1").start();
// 確保t1啓動
try {
Thread.sleep(2000);
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
client2.lock();
System.out.println("do something");
client2.unlock();
}
}, "t2").start();
}
}
優點:
有效的解決單點問題,不可重入問題,非阻塞問題以及鎖無法釋放的問題。實現起來較爲簡單。
缺點:
性能上可能並沒有緩存服務那麼高,因爲每次在創建鎖和釋放鎖的過程中,都要動態創建、銷燬臨時節點來實現鎖功能。ZK 中創建和刪除節點只能通過 Leader 服務器來執行,然後將數據同步到所有的 Follower 機器上。還需要對 ZK的原理有所瞭解。