java中分佈式鎖的實現方式

分佈式鎖

有的時候,我們需要保證一個方法在同 一時間內只能被同一個線程執行。在單機模式下,可以通過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 + "會話失效");
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章