引言
如果有int count=0;
10000個線程都執行count++
,執行完之後count的值會是10000嗎?
public class Test {
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
for(int i = 0 ;i<10000;i++){
new Thread(()->{
count++;
}).start();
}
//等線程執行完
Thread.sleep(5000l);
System.out.println(count);
}
}
多跑幾次即可發現,有的時候得到的值不是自己想要的。這裏就是多線程安全問題了。在單機的情況下通過jdk自帶的synchronized
或者ReentrantLock
都可以解決如上問題(現在不考慮使用AtomicInteger
的情況下)。如果是多機器的情況下呢?jdk自帶的鎖就無法達到效果了。比如:
private IUserDao userDao;
public synchronized void addUser(String mobile,String name){
Boolean exists = userDao.getUserInfo(mobile);
if(exists){
userDao.addUser(mobile,name);
}
}
防止用戶操作過快,導致重複添加到數據庫中,在單機的情況下,通過synchronized
可防止用戶重複添加。但是如果是多個服務器的情況,用戶通過機器刷請求,這個時候jdk自帶的synchronized
也無法起到互斥的效果。這個時候就需要分佈式鎖來處理了。
上面的例子不算特別的好,大家腦補一下場景啦。
redis分佈式鎖
首先必須知道的幾個redis命令
redis 命令學習
setnx
SETNX 是【SET if Not eXists】(如果不存在,則 SET)的簡寫;
當且僅當 key 不存時,將 key 的值設爲 value 。若給定的 key 已經存在,則 SETNX 不做任何動作。
返回值:
設置成功,返回 1 。
設置失敗,返回 0 。
redis> EXISTS job # job 不存在
(integer) 0
redis> SETNX job "programmer" # job 設置成功
(integer) 1
redis> SETNX job "code-farmer" # 嘗試覆蓋 job ,失敗
(integer) 0
redis> GET job # 沒有被覆蓋
"programmer"
set
設置一個值到redis中。
使用setnx
命令的時候有一個不足,即是無法在設置一個值的同時設置key的過期時間,只能通過設置成功之後,再通過expire
設置過期時間,這個操作無法保證原子性。所以如果想在設置一個值的時候再設置過期時間保證它的原子性,只能通過set
命令。如下:
set key value [EX seconds] [PX milliseconds] [NX|XX]
EX seconds:設置失效時長,單位秒
PX milliseconds:設置失效時長,單位毫秒
NX:key不存在返回ok,如果key存在返回(nil)
XX:key存在返回ok,如果key不存在失敗返回(nil)
如此高仿setnx
命令,當job不存在時設置value爲aa,超時時間10秒
> set job aa ex 10 nx
OK
> ttl job
(integer) 7
> set job aa ex 10 nx
(nil)
getset
getset是【GETSET key value】
將給定 key 的值設爲 value ,並返回 key 的舊值(old value)。
當 key 存在但不是字符串類型時,返回一個錯誤。
> exists job
(integer) 0
> getset job aa
(nil)
> set job aa
OK
> getset job bb
"aa"
redis 鎖常見思路
通常情況下c0通過setnx進行設置值,如果設置成功,即可認爲獲取鎖成功,但是如果c0一直不釋放鎖,或者說c0線程異常,釋放鎖失敗,那就會導致其他線程一直無法獲取鎖。那必須得有異常處理的邏輯,所以思路如下
網上常見的實例:
實例一
實現思路
- c0通過
setnx
設置key爲lock
,如果設置成功,則通過expire
設置key過期時間,獲取鎖成功; - c1 ,c2(也可能是更多線程) 也同樣通過setnx設置key爲lock,這時應該設置失敗,因爲lock這個key已經被線程c0設置成功,此c1,c2獲取鎖失敗;
- c1 ,c2(也可能是更多線程) 也同樣通過setnx設置key爲
lock
,value爲鎖失效時間,這時應該設置失敗,因爲lock這個key已經被線程c0設置成功,此c1,c2獲取鎖失敗;
代碼
public boolean lock(final String key, final String value,Long expireTime) {
Object obj = null;
try {
obj = redisTemplate.execute(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
StringRedisSerializer serializer = new StringRedisSerializer();
Boolean success = connection.setNX(serializer.serialize(key), serializer.serialize(value));
connection.expire(serializer.serialize(key), expireTime);
connection.close();
return success;
}
});
} catch (Exception e) {
log.error("setNX redis error, key : {}", key,e);
}
return obj != null ? (Boolean) obj : false;
}
弊端:通過setNX和expire 並不是原子操作,如果在setnx設置成功之後系統崩盤,就會出現死鎖
實例二
實現思路
- c0通過setnx設置key爲
lock
,value爲鎖失效時間,可以爲當前時間向後10秒,設置成功,成功獲取鎖; - c1 ,c2(也可能是更多線程) 也同樣通過setnx設置key爲
lock
,value爲鎖失效時間,這時應該設置失敗,因爲lock這個key已經被線程c0設置成功,此c1,c2獲取鎖失敗; - c1,c2獲取lock的過期時間,發現鎖過期,進行再次爭奪鎖;
- c1,c2(或者更多線程)發現鎖已經過期這個時候每個線程中一定存有lock的value,通過getset 命令設置lock的value,value也是鎖的失效時間,如果返回的值和lock之前的value進行對比相同的話,即可認爲獲取鎖成功;
此實例算是實例一的補救;
代碼
/**
* 加鎖
*/
public long lock(String lockKey, String threadName) {
log.info(threadName + "開始執行加鎖");
//鎖時間 過期時間
Long lock_timeout = currtTimeForRedis() + lockTimeout + 1;
// 鎖失效時間爲10 秒,value裏存入過期時間,如果超過過期時間視爲無效鎖,其他線程可以重新獲取鎖
if (setNX(lockKey, String.valueOf(lock_timeout),10l)) {
//如果加鎖成功
log.info(threadName + "加鎖成功+1");
//設置超時時間,釋放內存
return lock_timeout;
} else {
//獲取redis裏面的時間
Object result = get(lockKey);
Long currt_lock_timeout_str = result == null ? null : Long.parseLong(result.toString());
//鎖已經失效
if (currt_lock_timeout_str != null && currt_lock_timeout_str < currtTimeForRedis()) {
//判斷是否爲空,不爲空時,如果被其他線程設置了值,則第二個條件判斷無法執行
//獲取上一個鎖到期時間,並設置現在的鎖到期時間
Long old_lock_timeout_Str = Long.valueOf(getSet(lockKey, String.valueOf(lock_timeout)));
if (old_lock_timeout_Str != null && old_lock_timeout_Str.equals(currt_lock_timeout_str)) {
//多線程運行時,多個線程簽好都到了這裏,但只有一個線程的設置值和當前值相同,它纔有權利獲取鎖
log.info(threadName + "加鎖成功+2");
//設置超時間,釋放內存 這個是 10 L
expire(lockKey, 10l);
//返回加鎖時間
return lock_timeout;
}
}
}
return -1;
}
public boolean setNX(final String key, final String value,Long expireTime) {
Object obj = null;
try {
obj = redisTemplate.execute(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
StringRedisSerializer serializer = new StringRedisSerializer();
Boolean success = connection.setNX(serializer.serialize(key), serializer.serialize(value));
connection.expire(serializer.serialize(key), expireTime);
connection.close();
return success;
}
});
} catch (Exception e) {
log.error("setNX redis error, key : {}", key,e);
}
return obj != null ? (Boolean) obj : false;
}
public String getSet(final String key, final String value) {
Object obj = null;
try {
obj = redisTemplate.execute(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
StringRedisSerializer serializer = new StringRedisSerializer();
byte[] data = connection.getSet(serializer.serialize(key), serializer.serialize(value));
connection.close();
return serializer.deserialize(data);
}
});
} catch (Exception e) {
log.error("getSet redis error, key : {}", key,e);
}
return obj != null ? obj.toString() : null;
}
/**
* 多服務器集羣,使用下面的方法,代替System.currentTimeMillis(),獲取redis時間,避免多服務的時間不一致問題!!!
*
* @return
*/
public long currtTimeForRedis() {
Object obj = redisTemplate.execute(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
return redisConnection.time();
}
});
return obj == null ? -1 : Long.parseLong(obj.toString());
}
public boolean expire(final String lockKey, final Long expireTime) {
Object obj = null;
try {
obj = redisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
StringRedisSerializer serializer = new StringRedisSerializer();
boolean bool = connection.expire(serializer.serialize(lockKey), expireTime);
connection.close();
return bool;
}
});
log.info("設置過期時間"+expireTime);
return (Boolean)obj;
} catch (Exception e) {
log.error("expire redis error, key : {}", lockKey,e);
}
return false;
}
弊端:多線程一起通過getset設置lock的value和之前的value進行比對,最後lock的value並不一定是成功獲取鎖線程設置的,即成功獲取鎖的線程的失效時間並不一定是自己想要的失效時間,但是誤差極小,而且不影響鎖的使用,所以可以忽略;由此設置的鎖不具有擁有者的標識,即任何線程都能對此鎖釋放;
注:在此列舉常見的兩個實例供大家參考;
redisson引入
貼一段概念:原爲地址 https://www.cnblogs.com/liyan492/p/9858548.html
概念:
Jedis:是Redis的Java實現客戶端,提供了比較全面的Redis命令的支持,
Redisson:實現了分佈式和可擴展的Java數據結構。
Lettuce:高級Redis客戶端,用於線程安全同步,異步和響應使用,支持集羣,Sentinel,管道和編碼器。
優點:
Jedis:比較全面的提供了Redis的操作特性
Redisson:促使使用者對Redis的關注分離,提供很多分佈式相關操作服務,例如,分佈式鎖,分佈式集合,可通過Redis支持延遲隊列
Lettuce:主要在一些分佈式緩存框架上使用比較多
可伸縮:
Jedis:使用阻塞的I/O,且其方法調用都是同步的,程序流需要等到sockets處理完I/O才能執行,不支持異步。Jedis客戶端實例不是線程安全的,所以需要通過連接池來使用Jedis。
Redisson:基於Netty框架的事件驅動的通信層,其方法調用是異步的。Redisson的API是線程安全的,所以可以操作單個Redisson連接來完成各種操作
Lettuce:基於Netty框架的事件驅動的通信層,其方法調用是異步的。Lettuce的API是線程安全的,所以可以操作單個Lettuce連接來完成各種操作
redisson RLock
RLock實現了java.util.concurrent.locks.Lock
接口(點擊跳轉->溫習lock接口)。而且RLock是使用Lua腳本完成分佈式鎖效率更高。
如下:
RLock lock = redisson.getLock("anyLock");
// 最常見的使用方法
lock.lock();
通過anyLock
key獲取鎖,如果負責儲存這個分佈式鎖的Redis節點宕機以後,而且這個鎖正好處於鎖住的狀態時,這個鎖會出現鎖死的狀態。爲了避免這種情況的發生,Redisson內部提供了一個監控鎖的看門狗,它的作用是在Redisson實例被關閉前,不斷的延長鎖的有效期。默認情況下,看門狗的檢查鎖的超時時間是30秒鐘,也可以通過修改Config.lockWatchdogTimeout來另行指定。
而且也可以通過leaseTime
自行控制超時時間
// 加鎖以後10秒鐘自動解鎖
// 無需調用unlock方法手動解鎖
lock.lock(10, TimeUnit.SECONDS);
// 嘗試加鎖,最多等待100秒,上鎖以後10秒自動解鎖
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
...//代碼邏輯
lock.unlock();
另外redisson還支持公平鎖,聯鎖,紅鎖,讀寫鎖,信號量,可過期性信號量,閉鎖
文檔地址:https://github.com/redisson/redisson#quick-start
推薦一個不錯的參考文章:https://yq.aliyun.com/articles/551423
Zookeeper分佈式鎖
zookeeper基礎
znode 一共4中類型:持久的,臨時的,有序持久,有序臨時
持久的znode,如/path,只能通過調用delete來進行刪除。臨時的znode與之相反,當創建該節點的客戶端崩潰或關閉了與ZooKeeper的連接時,這個節點就會被刪除。
一個znode還可以設置爲有序(sequential)節點。一個有序znode節
點被分配唯一個單調遞增的整數。當創建有序節點時,一個序號會被追
加到路徑之後。例如,如果一個客戶端創建了一個有序znode節點,其
路徑爲/tasks/task-,那麼ZooKeeper將會分配一個序號,如1,並將這個
數字追加到路徑之後,最後該znode節點爲/tasks/task-1。有序znode通過
提供了創建具有唯一名稱的znode的簡單方式。同時也通過這種方式可
以直觀地查看znode的創建順序。
zookeeper watch機制
當node節點有變化時,爲了替換客戶端的輪詢,我們選擇了基於通知(notification)的機制,客戶端向ZooKeeper註冊需要接收通知的
znode,通過對znode設置監視點(watch)來接收通知。 ZooKeeper可以定義不同類型的通知,這依賴於設置監視點對應的
通知類型。客戶端可以設置多種監視點,如監控znode的數據變化、監
控znode子節點的變化、監控znode的創建或刪除。
看了上面的一些官方的不知道會不會懵逼,簡單的說,就是觀察者模式,讓客戶端對node配置監聽,如果node有變回,就會有通知回來。
簡單測試
-
查看zk節點
[zk: localhost:2181(CONNECTED) 3] ls / [zookeeper]
-
創建/test節點
[zk: localhost:2181(CONNECTED) 4] create /test "" Created /test [zk: localhost:2181(CONNECTED) 5] ls / [zookeeper, test]
-
啓動客戶端2對test節點監聽
[zk: localhost:2181(CONNECTED) 1] ls / [zookeeper, test] [zk: localhost:2181(CONNECTED) 2] get /test true #true 是打開watch cZxid = 0x4d ctime = Fri Nov 22 21:41:15 CST 2019 mZxid = 0x4d mtime = Fri Nov 22 21:41:15 CST 2019 pZxid = 0x4d cversion = 0 dataVersion = 0 aclVersion = 0 ephemeralOwner = 0x0 dataLength = 0 numChildren = 0 [zk: localhost:2181(CONNECTED) 3]
-
第一個客戶端刪除/test節點
[zk: localhost:2181(CONNECTED) 6] delete /test [zk: localhost:2181(CONNECTED) 7]
-
接着馬上客戶端2就會有通知
[zk: localhost:2181(CONNECTED) 3] WATCHER:: WatchedEvent state:SyncConnected type:NodeDeleted path:/test
zookeeper分佈式鎖思路
思路一
假設有一個應用由n個進程組成,這些進程嘗試獲取一個鎖。再次
強調,ZooKeeper並未直接暴露原語,因此我們使用ZooKeeper的接口來
管理znode,以此來實現鎖。爲了獲得一個鎖,每個進程p嘗試創建
znode,名爲/lock。如果進程p成功創建了znode,就表示它獲得了鎖並
可以繼續執行其臨界區域的代碼。不過一個潛在的問題是進程p可能崩
潰,導致這個鎖永遠無法釋放。在這種情況下,沒有任何其他進程可以
再次獲得這個鎖,整個系統可能因死鎖而失靈。爲了避免這種情況,我
們不得不在創建這個節點時指定/lock爲臨時節點。
思路二
n個進程一起創建順序節點,如果發現當前線程創建的順序節點是第一個,則成功獲取鎖,如果不是則可以鑑定前面的鎖釋放,如果前面鎖釋放,則自己成功獲取鎖。
zookeeper Curator 框架
Curator是Netflix公司開源的一套Zookeeper客戶端框架。瞭解過Zookeeper原生API都會清楚其複雜度。Curator幫助我們在其基礎上進行封裝、實現一些開發細節,包括接連重連、反覆註冊Watcher和NodeExistsException等。目前已經作爲Apache的頂級項目出現,是最流行的Zookeeper客戶端之一。從編碼風格上來講,它提供了基於Fluent的編程風格支持。
除此之外,Curator還提供了Zookeeper的各種應用場景:Recipe、共享鎖服務、Master選舉機制和分佈式計數器等。
zookeeper 分佈式鎖通過acquire
進行獲取鎖,通過release
進行釋放鎖
curator框架分佈式鎖
共享可重入鎖
@RestController
public class ZookeeperController {
@Autowired
private CuratorFramework curatorFramework;
@RequestMapping("/zookeeper/makeorder")
public String makeOrder(Long userId){
// 生成鎖,防止用戶頻繁點擊
InterProcessMutex lock = new InterProcessMutex (curatorFramework, "/makeorder"+userId);
try {
// 如果可能請儘量設置超時時間,免得一直等待
lock.acquire();
System.out.println("獲取鎖成功");
//todo
Thread.sleep(10000L);
lock.release();
} catch (Exception e) {
e.printStackTrace();
}
return "true";
}
@RequestMapping("/zookeeper/makeorder/trylock")
public String makeOrderTrylock(Long userId){
// 生成鎖,防止用戶頻繁點擊
InterProcessMutex lock = new InterProcessMutex (curatorFramework, "/makeorder"+userId);
try {
Boolean getlock = lock.acquire(1000l, TimeUnit.MILLISECONDS);
if(getlock){
//todo
System.out.println("試着獲取鎖成功");
lock.release();
Thread.sleep(4000l);
return "true";
}else{
System.out.println("獲取鎖失敗,獲取超時");
return "false";
}
} catch (Exception e) {
e.printStackTrace();
}
return "error";
}
}
讀寫鎖
@RequestMapping("/zookeeper/makeorder/readlock")
// 讀取鎖
public String makeOrderReadlock(Long userId){
// 生成鎖,防止用戶頻繁點擊
InterProcessReadWriteLock interProcessReadWriteLock = new InterProcessReadWriteLock (curatorFramework, "/makeorderReadlock"+userId);
InterProcessMutex lock = interProcessReadWriteLock.readLock();
try {
Boolean getlock = lock.acquire(1000l, TimeUnit.MILLISECONDS);
if(getlock){
//todo
System.out.println("試着獲取readlock鎖成功");
lock.release();
return "true";
}else{
System.out.println("獲取鎖失敗,獲取超時");
return "false";
}
} catch (Exception e) {
e.printStackTrace();
}
return "error";
}
@RequestMapping("/zookeeper/makeorder/writelock")
// 寫入鎖
public String makeOrderWritelock(Long userId){
// 生成鎖,防止用戶頻繁點擊
InterProcessReadWriteLock interProcessReadWriteLock = new InterProcessReadWriteLock (curatorFramework, "/makeorderReadlock"+userId);
try {
InterProcessMutex lock = interProcessReadWriteLock.writeLock();
boolean getlock = lock.acquire(2000l, TimeUnit.MILLISECONDS);
if(getlock){
//todo
System.out.println("試着獲取writelock鎖成功");
Thread.sleep(10000l);
lock.release();
return "true";
}else{
System.out.println("獲取鎖失敗,獲取超時");
return "false";
}
} catch (Exception e) {
e.printStackTrace();
}
return "error";
}
curator對分佈式鎖分裝的比較完善,可以很輕鬆的實現分佈式鎖,另外還有共享鎖不可重入
,共享信號量
,聯鎖
,隊列
等等。
官網地址:http://curator.apache.org/curator-recipes/index.html
共享鎖一篇源碼分析,寫的好,供大家參考https://www.cnblogs.com/shileibrave/p/9854921.html