亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Redis分布式鎖介紹與使用

 更新時間:2022年09月16日 09:13:41   作者:扎哇太棗糕  
服務(wù)器集群項目中的鎖是無法精準的鎖住線程資源的,于是我們就是需要使用分布式鎖,分布式鎖該如何使用又有什么注意點呢?就讓我們進入接下來的學習

首先,使用idea模擬搭建一個tomcat服務(wù)器集群,并使用Nginx對集群中的服務(wù)器實現(xiàn)負載均衡

配置完負載均衡之后,發(fā)送兩次請求就會在idea的運行窗口中發(fā)現(xiàn),兩次請求的運行是分別在兩個服務(wù)器中完成,這就是集群的輪詢機制

分布式鎖

業(yè)務(wù)邏輯分析

  在單JVM虛擬機多線程執(zhí)行的情況下,可以使用JVM內(nèi)部的鎖機制來控制多進程的并發(fā)執(zhí)行,借此可以保證一個用戶只能下一個優(yōu)惠券訂單。但是在分布式的情況下,每一個JVM虛擬機都有一個鎖監(jiān)視器,不同JVM里的不同線程之間的訪問的并不是同一個鎖監(jiān)視器,所以說此時再使用synchronized鎖就無法滿足一個用戶限買一單的業(yè)務(wù)情況了,于是就需要使用分布式鎖

  分布式鎖就是滿足分布式系統(tǒng)或集群模式下多進程可見并且互斥的鎖。一般實現(xiàn)分布式鎖的技術(shù)主要就是MySQL、Redis和ZooKeeper,但是綜合對比來看的話,Redis作分布式鎖的性能更高一些,Redis是在JVM虛擬機之外的一種應(yīng)用可以滿足多線程都可見,互斥可以使用setnx這種的互斥命令來實現(xiàn),但是使用Redis會存在安全性問題,如果Redis崩潰的話會導致鎖無法釋放而出現(xiàn)死鎖現(xiàn)象,解決這一問題的方案就是使用TTL過期時間,就算崩潰也可以實現(xiàn)到期自動釋放。

Redis命令

  使用Redis實現(xiàn)分布式鎖的步驟主要就是使用setnx體現(xiàn)互斥鎖,然后expire過期時間防止宕機死鎖,但是如果服務(wù)在setnx之后expire之前宕機的話,依舊會造成死鎖現(xiàn)象。于是我們可以使用以下命令在互斥的同時設(shè)置超時時間,這樣的話即是在設(shè)置鎖之后宕機,依舊可以憑借超時時間釋放鎖

SET lock thread NX EX ttl超時時間

代碼實現(xiàn)

  將獲取鎖和釋放鎖業(yè)務(wù)抽取出來,使用接口和實現(xiàn)類來完成

public interface ILock {
    /**
     * 嘗試獲取鎖
     * @param timeoutSec 鎖的超時時間
     * @return 是否成功獲取鎖
     */
    boolean tryLock(long timeoutSec);
    /**
     * 釋放鎖
     */
    void unLock();
}
public class SimpleRedisLock implements ILock {
    private String name;
    /**
     *先獲取StringRedisTemplate對象,才能使用代碼操作Redis
     */
    private StringRedisTemplate stringRedisTemplate;
    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }
    @Override
    public boolean tryLock(long timeoutSec) {
        // 獲取當前操作線程的標識
        long threadId = Thread.currentThread().getId();
        // 獲取鎖
        Boolean res = stringRedisTemplate.opsForValue()
                .setIfAbsent(RedisConstants.KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
        // res是Boolean的包裝類,返回結(jié)果的時候涉及到拆箱問題,有可能存在結(jié)果為null的情況,此時就需要返回結(jié)果與true的比較,避免了空指針風險
        return Boolean.TRUE.equals(res);
    }
    @Override
    public void unLock() {
        // 釋放鎖
        stringRedisTemplate.delete(RedisConstants.KEY_PREFIX + name);
    }
}

  定義了分布式鎖的獲取和釋放,接下來就是在一人一單業(yè)務(wù)代碼中將鎖機制升級成多線程鎖了,主要修改的代碼為就是5~14行,由單體的synchronized鎖改為使用自定義的Redis鎖,并根據(jù)不同線程獲取鎖的不同結(jié)果定義了不同的業(yè)務(wù)

public Result secKillVoucher(Long voucherId) {
    // 單用戶id(攔截器中做登錄驗證的用戶id)
    Long userId = UserHolder.getUser().getId();
    // 創(chuàng)建鎖對象
    SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
    // 獲取鎖
    boolean isLock = lock.tryLock(1200);
    // 判斷是否獲取鎖成功
    if (!isLock) {
        // 獲取鎖失敗,返回錯誤或者重試
        return Result.fail("不允許重復下單!" );
    }
    // 獲取鎖成功,繼續(xù)下單的業(yè)務(wù)邏輯
    try {
        // 查詢優(yōu)惠券
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
        // 獲取時間 判斷秒殺活動是否開始或者結(jié)束
        if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
            return Result.fail("活動暫未開始");
        } else if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
            return Result.fail("活動已經(jīng)結(jié)束");
        }
        // 判斷庫存是否充足
        if (seckillVoucher.getStock() < 1) {
            return Result.fail("庫存不足,活動結(jié)束");
        }
        // user_id和voucher_id聯(lián)合查詢訂單數(shù)
        int count = query().eq("user_id", userId)
                .eq("voucher_id", voucherId)
                .count();
        // 訂單數(shù)為1 就說明已經(jīng)下過單了
        if (count > 0) {
            return Result.fail("您已經(jīng)購買過該商品了");
        }
        // 扣減庫存
        boolean update = seckillVoucherService.update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherId)
                .gt("stock", 0)
                .update();
        if (!update) {
            return Result.fail("庫存不足!");
        }
        // 創(chuàng)建訂單 并返回id
        VoucherOrder order = new VoucherOrder();
        // 訂單id(redis全局唯一id) 下單用戶id(攔截器中做登錄驗證的用戶id) 優(yōu)惠券id(直接傳過來的id)
        long orderId = generator.nextId("order");
        order.setId(orderId);
        order.setUserId(userId);
        order.setVoucherId(voucherId);
        save(order);
        return Result.ok(orderId);
    } finally {
        // 釋放鎖
        lock.unLock();
    }
}

分布式鎖誤刪問題

問題原因分析

  這個問題出現(xiàn)在Redis鎖設(shè)置的超時時間上,由于設(shè)置了超時時間,所以可能出現(xiàn)一下情況:即當線程1獲取到鎖之后執(zhí)行下單業(yè)務(wù),但是由于業(yè)務(wù)堵塞鎖已經(jīng)超出TTL時間自動釋放;此時線程2趁機獲取Redis鎖成功執(zhí)行下單業(yè)務(wù),線程2的下單業(yè)務(wù)執(zhí)行到一半時線程1完成下單使用del命令釋放鎖;此時線程1釋放的是線程2的鎖,于是現(xiàn)在鎖又處于閑置狀態(tài),于是線程3來獲取Redis鎖成功執(zhí)行下單業(yè)務(wù);此時,一共有同一個用戶的兩個線程在同時操作

  為了解決以上出現(xiàn)的問題,需要在每次釋放鎖之前都通過鎖的線程標識(Redis鎖對應(yīng)的值)判斷一下是不是自己的鎖,如果是就使用del命令釋放鎖,否則就不做操作。但是有一點值得注意,之前鎖的線程標識使用的是線程的name,這樣的話很容易就造成不同JVM虛擬機里的線程name沖突影響判斷,于是可以使用UUID隨機生成一組數(shù)字加上線程name作為線程的標識,這樣更能確保唯一性

代碼實現(xiàn)

  綜上所述,一共有兩處需要改進的地方,一個是使用UUID加線程name作為線程標識(主要修改的是獲取鎖方法加上UUID的獲取),一個是在使用del釋放鎖之前判斷一下是否是自己的鎖

public static final String ID_PREFIX = UUID.randomUUID(true) + "-";
public boolean tryLock(long timeoutSec) {
    // 獲取當前操作線程的標識
    String threadId = RedisConstants.ID_PREFIX + Thread.currentThread().getId();
    // 獲取鎖
    Boolean res = stringRedisTemplate.opsForValue()
            .setIfAbsent(RedisConstants.KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
    // res是Boolean的包裝類,返回結(jié)果的時候涉及到拆箱問題,有可能存在結(jié)果為null的情況,此時就需要返回結(jié)果與true的比較,避免了空指針風險
    return Boolean.TRUE.equals(res);
}
public void unLock() {
    // 獲取當前操作線程的標識
    String threadId = RedisConstants.ID_PREFIX + Thread.currentThread().getId();
    // 通過鎖名 獲取redis中存儲的鎖對應(yīng)的標識
    String rid = stringRedisTemplate.opsForValue().get(RedisConstants.KEY_PREFIX + name);
    if (threadId.equals(rid)) {
        // 釋放鎖
        stringRedisTemplate.delete(RedisConstants.KEY_PREFIX + name);
    }
}

Lua腳本

  Redis提供了Lua腳本功能,在一個腳本中編寫多條Redis命令,確保多條命令執(zhí)行時的原子性。Lua是一種編程語言,它的基本語法大家可以參考網(wǎng)站:傳送門

使用Redis命令調(diào)用腳本的常見命令可以是:

EVAL “redis.call(‘set’, ‘key’, ‘value’)” num

  上述命令解釋為EVAL是調(diào)用,后面雙引號中就是所調(diào)用的腳本語句,而最后的num即腳本語句中的KEYS類型參數(shù)的個數(shù),num之外的就是ARGV(value)類型的參數(shù)。比如說,接下來這一個語句就代表著:setname為Rose,其中KEYS類型的參數(shù)有1個,就是num后面的第一個name,剩下的都是ARGV(value)類型的數(shù)據(jù),其中調(diào)用的是KEYS[1]和ARGV[2],也就是name和Rose

EVAL “redis.call(‘set’, ‘KEYS[1]’, ‘ARGV[2]’)” 1 name age Rose

到此這篇關(guān)于Redis分布式鎖介紹與使用的文章就介紹到這了,更多相關(guān)Redis分布式鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論