秒殺業務:使用redis處理分佈式鎖的問題

分佈式鎖會在高併發的業務被使用到:

   一、 分佈式鎖的處理一般可以有兩種處理方式:

               1.利用zookepeer的數據結構以及特性來處理分佈式鎖。

                      zookeeper可以創建臨時有序的數據節點,同時每個數據節點可以對比其小的數據節點進行監控。只有當比自己點               排序更小的數據節點被刪除之後纔會,zk纔會處理下一個節點。利用此特性,將高併發的訪問寫入到zk上的不同的有序                 的節點。這時候各個請求就會有序的執行,只有在序列號比自己小的請求執行完畢之後纔會執行,可以實現一個分鎖。

 

               2.利用redis的可以對每一個key可以執行一個設置鎖的操作,以及該鎖的過期時間可以實現。但是在實現過程中會出現一些問題,今天我就整理自己的一些見解以及使用代碼來完成使用redis處理分佈式鎖。

   

   二、使用reids處理分佈式鎖的實戰

             1.原生api處理分佈式鎖(效果不好,適用於併發訪問低的網站,這裏不再演示)

                  關鍵字:setNx    expire

             2.使用redisTemplate模板操作(效果較好,適用訪問量較大的公司)

                <1>.第一步:pom文件中引入相關依賴

             <dependency>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-starter-data-redis</artifactId>
             </dependency>

                <2>.第二步:applicaiton.properties中配置redis的相關鏈接配置              

               redis.hostName=127.0.0.1
               redis.port=6379
               redis.password=https://blog.hhui.top
               # 連接超時時間
               redis.timeout=10000

               #最大空閒數
               redis.maxIdle=300
               #控制一個pool可分配多少個jedis實例,用來替換上面的redis.maxActive,如果是jedis 2.4以後用該屬性
               redis.maxTotal=1000
               #最大建立連接等待時間。如果超過此時間將接到異常。設爲-1表示無限制。
               redis.maxWaitMillis=1000
               #連接的最小空閒時間 默認1800000毫秒(30分鐘)
               redis.minEvictableIdleTimeMillis=300000
               #每次釋放連接的最大數目,默認3
               redis.numTestsPerEvictionRun=1024
               #逐出掃描的時間間隔(毫秒) 如果爲負數,則不運行逐出線程, 默認-1
               redis.timeBetweenEvictionRunsMillis=30000
               #是否在從池中取出連接前進行檢驗,如果檢驗失敗,則從池中去除連接並嘗試取出另一個
               redis.testOnBorrow=true
               #在空閒時檢查有效性, 默認false
               redis.testWhileIdle=true

               <3>.第三步:分佈式鎖代碼實現

package com.example.demo.redis;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 *  商品操作類
 */

@RestController
@RequestMapping("/redis")
public class ProductController {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @RequestMapping("/product_count")
    public String getProduct(){
        /**
         *  定義秒殺商品的redis中的key 以及 一個UUID 作爲鎖的value
         */
        String productName = "productId";
        String locKeyName = "lockKey";
        String value = UUID.randomUUID().toString();

        try{
            /**
             *  設置鎖,以及鎖的過期時間,最好使用四個參數的setifAbsent保證原子性
             */
            Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(locKeyName,value,30, TimeUnit.SECONDS);

            if(!aBoolean){
                return "此時鎖被別人佔領";
            }
            /**
             *  減庫存操作
             */
            int product_count = Integer.valueOf(redisTemplate.opsForValue().get(productName));
            if(product_count > 0){
                int count = product_count - 1;
                redisTemplate.opsForValue().set(productName,String.valueOf(count));
                System.out.println("扣除庫存成功!當前庫存爲" + count);
            }else{
                System.out.println("商品已售空!");
            }
        }finally {
            /**
             *  操作完畢,釋放當前線程的鎖
             */
            String concurrentLock = redisTemplate.opsForValue().get(locKeyName);
            if(value.equals(concurrentLock)){
                redisTemplate.delete("product_001");
            }
        }

        return "mission completed";
    }

}

             總結:使用 redisTemplate做分佈式鎖的注意事項。

                  注意事項:

                     1.在使用redisTemplate的setIfAbsent  API 的時候需要注意的是一定要使用key、value、long、timeUnit這參數                    都有的方法。因爲這樣才能夠保證get鎖和設置鎖的過期時間保持原子性。否則在這兩個操作之間可能會出現錯誤。

                     2.在使用redisTemplate做分佈式鎖的時候需要使用try +finally 來釋放鎖,因爲在程序執行的過程中可能會出現一                    些狀況,所以必須得保證每一個線程執行完畢之後釋放鎖。    

                     3.在釋放鎖的時候必須加上判斷保證釋放的是自己的鎖,否則有可能會釋放到後面線程的鎖導致鎖的永久失效。

       舉例說明:

                        

                  4.任務執行的時間問題還是無法解決,需要在主線程中new一個新的線程來增加lockKey的存活時間。知道主線程完                   成時纔將此線程關閉,才能夠保證鎖被別的線程獲取同時也能夠保證自己的任務執行完成。

                   有興趣的小夥伴可以知己動手嘗試着手寫一下。

 

     三、使用reids + Redisson處理分佈式鎖的實戰

1.加入依賴:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.7.4</version>
</dependency>

2.將redisson注入spring容器:

    

@Bean   // 創建好的redisson必須加上@Bean才能注入到spring容器中
public Redisson redisson(){
    /**
     *  使用redisson的config創建一個redisson的客戶端並連接
     *  並使用@Bean註解將其初始化到spring容器
     */
    Config config = new Config();
    config.useSingleServer().setAddress("127.0.0.1:6379").setDatabase(0);
    /**
     *  注意類型轉換
     */
    return (Redisson) Redisson.create(config);
}

注意:redisson的創建方式是使用redisson的config 來設置地址和數據庫。最後使用create()有參構造創建。

3.代碼實戰:

package com.example.demo.redis;


import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("/redisson")
public class RedissonController {


    @Autowired
    private Redisson redisson;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @RequestMapping("/product")
    public String getProduct(){

        String productName = "productId";
        String locKey = "lockKey";

        /**
         *  使用redisson獲取鎖並判斷其是否獲取鎖
         */
        RLock lock = redisson.getLock(locKey);
        boolean locked = lock.isLocked();
        if(locked){
            return "此時鎖被別的線程使用!";
        }

        /**
         *  設置鎖的過期時間: 鎖的默認過期時間爲30s
         *
         *  注:redission的lock方法的底層是一個rentreelock,同時它的底層也自動加上了一個線程
         *  作爲定時器來增加鎖的過期時間,知道業務執行完畢之後纔會關閉調該線程。保證在業務執行完成再釋放鎖!
         *
         */
        lock.lock(30, TimeUnit.SECONDS); // 解決了延長鎖的持有之間的問題

        /**
         *  業務處理
         */

        Integer count = Integer.valueOf(redisTemplate.opsForValue().get(productName));
        if(count > 0){
            int x = count - 1;
            redisTemplate.opsForValue().set(productName,String.valueOf(x));
            System.out.println("操作成功,庫存數量減一");
        }else{
            System.out.println("庫存不足,請稍後操作!");
        }
        lock.unlock();  // 注意:lock必須手動釋放!!!

        return "mission completed";
    }

}

 

注意:Lock lock = redision.getLock("name"); 獲取到的是一個可重入鎖:renetreeLock

  lock.lock(),方法中已經實現了加入定時增加的key的存活時間的線程,能夠保證資源不被浪費。保證在當前線程執行完畢之後才釋放鎖。

rentreelock必須釋放鎖!!!

 

這就已經用redis實現了分佈式鎖!

    

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章