分佈式鎖
有的時候,我們需要保證一個方法在同 一時間內只能被同一個線程執行。在單機模式下,可以通過sychronized、鎖等方式來實現,
在分佈式環境下,有以下的解決方案:
數據庫鎖
1.通過一個一張表的一條記錄,來判斷資源的佔用情況
2.使用基於數據庫的排它鎖 (即select * from tb_User for update)
3.使用樂觀鎖的方式,即CAS操作(或version字段)
基於Redis的分佈式鎖(緩存鎖)
redis提供了可以用來實現分佈式鎖的方法,比如redis的setnx方法等。(即redis事務機制可以實現樂觀鎖CAS)
基於Zookeeper的分佈式鎖(這種方式最可靠)
基於zookeeper臨時有序節點可以實現的分佈式鎖。大致思想即爲:每個客戶端對某個方法加鎖時,在zookeeper上的與該方法對應的指定節點的目錄下,生成一個唯一的瞬時有序節點。
判斷是否獲取鎖的方式很簡單,只需要判斷有序節點中序號最小的一個。 當釋放鎖的時候,只需將這個瞬時節點刪除即可。
redis分佈式鎖和Zookeeper分佈式鎖的區別
- redis分佈式鎖,其實需要自己不斷去嘗試獲取鎖,比較消耗性能;
- zk分佈式鎖,獲取不到鎖,註冊個監聽器即可,不需要不斷主動嘗試獲取鎖,性能開銷較小。
- redis獲取鎖的那個客戶端bug了或者掛了,那麼只能等待超時時間之後才能釋放鎖;而zk的話,因爲創建的是臨時znode,只要客戶端掛了,znode就沒了,此時就自動釋放鎖。
數據庫的方式實現分佈式鎖實現方式
info:分佈式鎖表
CREATE TABLE `lock_info`
( `id` bigint(20) NOT NULL,
`expiration_time` datetime NOT NULL,
`status` int(11) NOT NULL, `tag` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tag` (`tag`) )
ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
- id:主鍵
- tag:鎖的標示,以訂單爲例,可以鎖訂單id
- expiration_time:過期時間
- status:鎖狀態,0,未鎖,1,已經上鎖
Redis實現分佈式鎖的方式
RedisLock:redis實現分佈式鎖工具類
/**
* redis實現分佈式鎖機制
*/
@Component
public class RedisLock {
//日誌記錄器
private static Logger logger = LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 加鎖
* @param key
* @param value 當前事件+超時事件
* @return
*/
public boolean lock(String key,String value){
//加鎖成功
if (redisTemplate.opsForValue().setIfAbsent(key,value)){
return true;
}
//假如currentValue=A先佔用了鎖 其他兩個線程的value都是B,保證其中一個線程拿到鎖
String currentValue = redisTemplate.opsForValue().get(key);
//鎖過期 防止出現死鎖
if (!StringUtils.isEmpty(currentValue) &&
Long.parseLong(currentValue) < System.currentTimeMillis()){
//獲取上一步鎖的時間
String oldValue = redisTemplate.opsForValue().getAndSet(key, value);
if (!StringUtils.isEmpty(oldValue) &&
oldValue.equals(currentValue)){
return true;
}
}
return false;
}
/**
* 解鎖
* @param key
* @param value
*/
public void unlock(String key,String value){
try {
String currentValue = redisTemplate.opsForValue().get(key);
if (!StringUtils.isEmpty(currentValue) &&
currentValue.equals(value)){
redisTemplate.opsForValue().getOperations().delete(key);
}
}catch (Exception e){
logger.error("【redis分佈式鎖】 解鎖異常,{}",e);
}
}
OrderService:消費者中的訂單操作
@Service
@Transactional
public class OrderService extends ServiceImpl<OrderPojoMapper, OrderPojo> implements IOrderService {
@Autowired
OrderPojoMapper orderPojoMapper;
/** 超時時間 */
private static final int TIMEOUT = 5000;
@Autowired
RedisLock redisLock;
@Autowired
RedisTemplate redisTemplate;
//日誌記錄器
private static Logger logger = LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);
/**
* 秒殺中訂單
* @param orderPojo
* @return
*/
@Override
public ResponseResult seckillNewOrder(OrderPojo orderPojo) {
String msg="";
String productId=orderPojo.getProductId();
long time = System.currentTimeMillis() + TIMEOUT;
//加鎖
String lockKey = "seckill:"+productId;
if (!redisLock.lock(lockKey,String.valueOf(time))){
return ResponseResult.error(ConstantsUtil.seckill_fail);
}
//進行商品搶購(商品數減一)
int stockNum = 0;
Object tmp =redisTemplate.opsForValue().get("seckill_"+productId);
if(tmp!=null){
String stockNumStr =tmp.toString();
if(StringUtils.isNotBlank(stockNumStr)){
stockNum = Integer.valueOf(stockNumStr);
}
if (stockNum == 0) {
//庫存不足
return ResponseResult.error(ConstantsUtil.seckill_fail2);
} else {
redisTemplate.opsForValue().set("seckill_"+productId,String.valueOf(stockNum-1));
msg="恭喜你搶到商品,剩餘商品數量爲"+(stockNum-1);
//創建訂單
orderPojoMapper.insert(orderPojo);
}
}else {
return ResponseResult.error(ConstantsUtil.seckill_fail3);
}
//解鎖
redisLock.unlock(lockKey, String.valueOf(time));
return ResponseResult.success(msg);
}
}
使用Zookeeper實現分佈式鎖
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
public class DistributedLock implements Watcher {
private int threadId;
private ZooKeeper zk = null;
private String selfPath;
private String waitPath;
private String LOG_PREFIX_OF_THREAD;
private static final int SESSION_TIMEOUT = 10000;
private static final String GROUP_PATH = "/locks";
private static final String SUB_PATH = "/locks/sub";
private static final String CONNECTION_STRING = "127.0.0.1:2181";
private static final int THREAD_NUM = 10;
// 確保連接zk成功;
private CountDownLatch connectedSemaphore = new CountDownLatch(1);
// 確保所有線程運行結束;
private static final CountDownLatch threadSemaphore = new CountDownLatch(
THREAD_NUM);
public DistributedLock(int id) {
this.threadId = id;
LOG_PREFIX_OF_THREAD = "【第" + threadId + "個線程】";
}
public static void main(String[] args) {
// 用多線程模擬分佈式環境
for (int i = 0; i < THREAD_NUM; i++) {
final int threadId = i + 1;
new Thread() {
@Override
public void run() {
try {
DistributedLock dc = new DistributedLock(threadId);
dc.createConnection(CONNECTION_STRING, SESSION_TIMEOUT);
// GROUP_PATH不存在的話,由一個線程創建即可;
synchronized (threadSemaphore) {
dc.createPath(GROUP_PATH, "該節點由線程" + threadId
+ "創建", true);
}
dc.getLock();
} catch (Exception e) {
System.out.println("【第" + threadId + "個線程】 拋出的異常:");
e.printStackTrace();
}
}
}.start();
}
try {
threadSemaphore.await();
System.out.println("所有線程運行結束!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 獲取鎖
*
* @return
*/
private void getLock() throws KeeperException, InterruptedException {
// 去創建臨時節點
selfPath = zk.create(SUB_PATH, null, ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println(LOG_PREFIX_OF_THREAD + "創建鎖路徑:" + selfPath);
if (checkMinPath()) {
getLockSuccess();
}
}
/**
* 創建節點
*
* @param path 節點path
* @param data 初始數據內容
* @return
*/
public boolean createPath(String path, String data, boolean needWatch)
throws KeeperException, InterruptedException {
if (zk.exists(path, needWatch) == null) {
System.out.println(LOG_PREFIX_OF_THREAD
+ "節點創建成功, Path: "
+ this.zk.create(path, data.getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT)
+ ", content: " + data);
}
return true;
}
/**
* 創建ZK連接
*
* @param connectString ZK服務器地址列表
* @param sessionTimeout Session超時時間
*/
public void createConnection(String connectString, int sessionTimeout)
throws IOException, InterruptedException {
zk = new ZooKeeper(connectString, sessionTimeout, this);
connectedSemaphore.await();
}
/**
* 獲取鎖成功
*/
public void getLockSuccess() throws KeeperException, InterruptedException {
if (zk.exists(this.selfPath, false) == null) {
System.out.println(LOG_PREFIX_OF_THREAD + "本節點已不在了...");
return;
}
System.out.println(LOG_PREFIX_OF_THREAD + "獲取鎖成功,趕緊幹活!");
Thread.sleep(2000);
System.out.println(LOG_PREFIX_OF_THREAD + "刪除本節點:" + selfPath);
zk.delete(this.selfPath, -1);
releaseConnection();
threadSemaphore.countDown();
}
/**
* 關閉ZK連接
*/
public void releaseConnection() {
if (this.zk != null) {
try {
this.zk.close();
} catch (InterruptedException e) {
}
}
System.out.println(LOG_PREFIX_OF_THREAD + "釋放連接");
}
/**
* 檢查自己是不是最小的節點
*
* @return
*/
public boolean checkMinPath() throws KeeperException, InterruptedException {
List<String> subNodes = zk.getChildren(GROUP_PATH, false);
Collections.sort(subNodes);
int index = subNodes.indexOf(selfPath.substring(GROUP_PATH.length() + 1));
switch (index) {
case -1: {
System.out.println(LOG_PREFIX_OF_THREAD + "本節點已不在了..." + selfPath);
return false;
}
case 0: {
System.out.println(LOG_PREFIX_OF_THREAD + "子節點中,我果然是老大...哈哈哈" + selfPath);
return true;
}
default: {
this.waitPath = GROUP_PATH + "/" + subNodes.get(index - 1);
System.out.println(LOG_PREFIX_OF_THREAD + "獲取子節點中,排在我前面的。。。"
+ waitPath);
try {
zk.getData(waitPath, true, new Stat());
return false;
} catch (KeeperException e) {
if (zk.exists(waitPath, false) == null) {
System.out.println(LOG_PREFIX_OF_THREAD + "子節點中,排在我前面的。。。"
+ waitPath + "已失蹤,幸福來得太突然?");
return checkMinPath();
} else {
throw e;
}
}
}
}
}
@Override
public void process(WatchedEvent event) {
// 監聽器處理事件
if (event == null) {
return;
}
Event.KeeperState keeperState = event.getState();
Event.EventType eventType = event.getType();
if (Event.KeeperState.SyncConnected == keeperState) {
if (Event.EventType.None == eventType) {
System.out.println(LOG_PREFIX_OF_THREAD + "成功連接上ZK服務器");
connectedSemaphore.countDown();
} else if (event.getType() == Event.EventType.NodeDeleted
&& event.getPath().equals(waitPath)) {
System.out.println(LOG_PREFIX_OF_THREAD
+ "收到情報,排我前面的傢伙已掛,我是不是可以出山了?");
try {
if (checkMinPath()) {
getLockSuccess();
}
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} else if (Event.KeeperState.Disconnected == keeperState) {
System.out.println(LOG_PREFIX_OF_THREAD + "與ZK服務器斷開連接");
} else if (Event.KeeperState.AuthFailed == keeperState) {
System.out.println(LOG_PREFIX_OF_THREAD + "權限檢查失敗");
} else if (Event.KeeperState.Expired == keeperState) {
System.out.println(LOG_PREFIX_OF_THREAD + "會話失效");
}
}
}