基於Redis實現分佈式鎖

本文我的博客:https://abeille.top/blog/detail/AT811U9QOjava

使用場景:

搶購活動,限量供應;redis

首先第一步設計:將庫存信息放入redis進行緩存;緩存

public class DistributedRedis {

    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;

    private void buyGoods(){
        // 獲取key對應的數據
        Integer stock = redisTemplate.opsForValue().get("stock");
        // 若是庫存大於0,對庫存減1
        if(null != stock && stock > 0){
            int realStock = stock - 1;
            // 將修改後的庫存放入redis
            redisTemplate.opsForValue().set("stock", realStock);
            System.out.println("庫存扣減成功,當前庫存爲:" + realStock);
        } else {
            System.out.println("庫存扣減失敗");
        }
    }
}

問題一:高併發場景下會出現庫存扣減異常;

以上代碼存在一個問題:當高併發場景下,會有多個請求同時獲取到一樣的數據,而後進行操做,實際上操做了屢次,可是庫存只減了一次;
那這樣的場景的解決方案有:服務器

  1. 加synchronized鎖,示例以下:
public class DistributedRedis {

    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;

    private void buyGoods(){
           // 加同步鎖
        synchronized (this){
            // 獲取key對應的數據
            Integer stock = redisTemplate.opsForValue().get("stock");
            // 若是庫存大於0,對庫存減1
            if(null != stock && stock > 0){
                int realStock = stock - 1;
                // 將修改後的庫存放入redis
                redisTemplate.opsForValue().set("stock", realStock);
                System.out.println("庫存扣減成功,當前庫存爲:" + realStock);
            } else {
                System.out.println("庫存扣減失敗");
            }
        }

    }
}

問題二:多實例部署,synchronized鎖失效的問題;

當服務爲單機的狀況下,加synchronized是能夠解決問題的,可是若是多實例部署,那麼這個鎖就沒有效果了;針對新產生的這個問題,能夠經過使用redis.setnx()方法來解決;使用redistemplate,操做的是setIfAbsent()方法:網絡

public class DistributedRedis {

    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;

    private void buyGoods() {
//        synchronized (this){
        String lockKey = "stockLock";
        // 針對多實例部署,可使用setIfAbsent()方法,這個代碼實際是redis.setNx()方法
        Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(lockKey, 119);
        if (null == aBoolean || !aBoolean) {
            return;
        }
        // 獲取key對應的數據
        Integer stock = redisTemplate.opsForValue().get("stock");
        // 若是庫存大於0,對庫存減1
        if (null != stock && stock > 0) {
            int realStock = stock - 1;
            // 將修改後的庫存放入redis
            redisTemplate.opsForValue().set("stock", realStock);
            System.out.println("庫存扣減成功,當前庫存爲:" + realStock);
        } else {
            System.out.println("庫存扣減失敗");
        }
          // 操做完成以後,刪除鎖
        redisTemplate.delete(lockKey);
//        }

    }
}

問題三:當方法執行過程當中代碼執行出現異常,鎖沒法刪除的問題:

當方法執行時,若是中間某一步執行發生異常,那麼後面的代碼是沒法執行到的,那也就是說,redistemplate.delete()是執行不到的;這時候的解決方案是使用try-finally或者try-with-resource來解決,代碼示例以下;併發

public class DistributedRedis {

    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;

    private void buyGoods() {
//        synchronized (this){
        String lockKey = "stockLock";
        try {
            // 針對多實例部署,可使用setIfAbsent()方法,這個代碼實際是redis.setNx()方法
            Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(lockKey, 119);
            if (null == aBoolean || !aBoolean) {
                return;
            }
            // 獲取key對應的數據
            Integer stock = redisTemplate.opsForValue().get("stock");
            // 若是庫存大於0,對庫存減1
            if (null != stock && stock > 0) {
                int realStock = stock - 1;
                // 將修改後的庫存放入redis
                redisTemplate.opsForValue().set("stock", realStock);
                System.out.println("庫存扣減成功,當前庫存爲:" + realStock);
            } else {
                System.out.println("庫存扣減失敗");
            }
            
//        }
        } finally {
            // 操做完成以後,刪除鎖
            redisTemplate.delete(lockKey);
        }
    }
}

問題四:若是方法執行過程當中,服務整個掛掉了,那麼加的鎖會一直存在的問題;

當方法執行過程當中,服務掛掉了,或者重啓了,那沒有釋放掉的鎖會一直存在,解決方案是給這個鎖設置一個失效時間;框架

設置失效方法有兩種:

  1. 先設置鎖,在自行設置失效時間;
  2. 在設置鎖的同時(並不是真正同時,而是redis本身實現的原子操做)設置失效時間;
public class DistributedRedis {

    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;

    private void buyGoods() {
//        synchronized (this){
        String lockKey = "stockLock";
        try {
            // 針對多實例部署,可使用setIfAbsent()方法,這個代碼實際是redis.setNx()方法
            // 給鎖設置失效時間,方法有兩種:一種是先設置鎖,而後在自行設置失效時間
//            Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(lockKey, 119);
//            redisTemplate.expire(lockKey, 15, TimeUnit.SECONDS);
            // 方法二:在設置鎖的同時設置其失效時間
//            Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(lockKey, 119, Duration.of(15, ChronoUnit.SECONDS)); // 此方法是對下面的額方法傳入的時間的一個封裝
            Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(lockKey, 119, 15, TimeUnit.SECONDS);
            
            if (null == aBoolean || !aBoolean) {
                return;
            }
            // 獲取key對應的數據
            Integer stock = redisTemplate.opsForValue().get("stock");
            // 若是庫存大於0,對庫存減1
            if (null != stock && stock > 0) {
                int realStock = stock - 1;
                // 將修改後的庫存放入redis
                redisTemplate.opsForValue().set("stock", realStock);
                System.out.println("庫存扣減成功,當前庫存爲:" + realStock);
            } else {
                System.out.println("庫存扣減失敗");
            }

//        }
        } finally {
            // 操做完成以後,刪除鎖
            redisTemplate.delete(lockKey);
        }
    }
}

問題五:當方法還沒執行完,鎖失效了的問題;

當服務多實例部署是,因爲網絡,服務器等緣由,方法執行時間不等,可能存在在釋放鎖的時候,這個鎖已經失效的狀況,或者釋放的鎖不是本次操做添加的鎖,那麼這個鎖也就失效了;解決方法是在加鎖時,添加一個身份標識,在釋放鎖時,判斷這個鎖是否本身添加的,示例代碼以下:dom

public class DistributedRedis {

    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;

    private void buyGoods() {
//        synchronized (this){
        String lockKey = "stockLock";
        int nextInt = 0;
        try {
            nextInt = new Random().nextInt(100);
            // 針對多實例部署,可使用setIfAbsent()方法,這個代碼實際是redis.setNx()方法
            // 給鎖設置失效時間,方法有兩種:一種是先設置鎖,而後在自行設置失效時間
//            Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(lockKey, 119);
//            redisTemplate.expire(lockKey, 15, TimeUnit.SECONDS);
            // 方法二:在設置鎖的同時設置其失效時間
//            Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(lockKey, 119, Duration.of(15, ChronoUnit.SECONDS)); // 此方法是對下面的額方法傳入的時間的一個封裝
            Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(lockKey, nextInt, 15, TimeUnit.SECONDS);

            if (null == aBoolean || !aBoolean) {
                return;
            }
            // 獲取key對應的數據
            Integer stock = redisTemplate.opsForValue().get("stock");
            // 若是庫存大於0,對庫存減1
            if (null != stock && stock > 0) {
                int realStock = stock - 1;
                // 將修改後的庫存放入redis
                redisTemplate.opsForValue().set("stock", realStock);
                System.out.println("庫存扣減成功,當前庫存爲:" + realStock);
            } else {
                System.out.println("庫存扣減失敗");
            }

//        }
        } finally {
            // 操做完成以後,刪除鎖
            Integer lockValue = redisTemplate.opsForValue().get(lockKey);
            if(0 != nextInt && null != lockValue && nextInt == lockValue){
                redisTemplate.delete(lockKey);
            }
        }
    }
}

問題六:失效時間的長短怎麼肯定,不合適的失效時間如何解決;

當一個方法中使用redis分佈式鎖,它的失效時間肯定多少爲合適?這個問題能夠經過使用redisson框架來解決,示例代碼以下;分佈式

public class DistributedRedis {

    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;
    @Autowired
    private Redisson redisson;

    private void buyGoods() {
//        synchronized (this){
        String lockKey = "stockLock";
//        int nextInt = 0;
        RLock lock = redisson.getLock(lockKey);
        try {
            // 默認失效時間爲-1,不失效
            lock.lock(15, TimeUnit.SECONDS);
//            nextInt = new Random().nextInt(100);
            // 針對多實例部署,可使用setIfAbsent()方法,這個代碼實際是redis.setNx()方法
            // 給鎖設置失效時間,方法有兩種:一種是先設置鎖,而後在自行設置失效時間
//            Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(lockKey, 119);
//            redisTemplate.expire(lockKey, 15, TimeUnit.SECONDS);
            // 方法二:在設置鎖的同時設置其失效時間
//            Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(lockKey, 119, Duration.of(15, ChronoUnit.SECONDS)); // 此方法是對下面的額方法傳入的時間的一個封裝
//            Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(lockKey, nextInt, 15, TimeUnit.SECONDS);
//
//            if (null == aBoolean || !aBoolean) {
//                return;
//            }
            // 獲取key對應的數據
            Integer stock = redisTemplate.opsForValue().get("stock");
            // 若是庫存大於0,對庫存減1
            if (null != stock && stock > 0) {
                int realStock = stock - 1;
                // 將修改後的庫存放入redis
                redisTemplate.opsForValue().set("stock", realStock);
                System.out.println("庫存扣減成功,當前庫存爲:" + realStock);
            } else {
                System.out.println("庫存扣減失敗");
            }

//        }
        } finally {
            // 操做完成以後,刪除鎖
//            Integer lockValue = redisTemplate.opsForValue().get(lockKey);
//            if(0 != nextInt && null != lockValue && nextInt == lockValue){
//                redisTemplate.delete(lockKey);
//            }
            lock.unlock();
        }
    }
}
相關文章
相關標籤/搜索