1. redis分布式鎖-java實現(xiàn)

        共 20173字,需瀏覽 41分鐘

         ·

        2021-05-25 21:08

        點擊上方藍(lán)色字體,選擇“標(biāo)星公眾號”

        優(yōu)質(zhì)文章,第一時間送達(dá)

          作者 |  _否極泰來

        來源 |  urlify.cn/QRbiem

        1、為什么要使用分布式鎖

        如果在一個分布式系統(tǒng)中,我們從數(shù)據(jù)庫中讀取一個數(shù)據(jù),然后修改保存,這種情況很容易遇到并發(fā)問題。因為讀取和更新保存不是一個原子操作,在并發(fā)時就會導(dǎo)致數(shù)據(jù)的不正確。這種場景其實并不少見,比如電商秒殺活動,庫存數(shù)量的更新就會遇到。如果是單機(jī)應(yīng)用,直接使用本地鎖就可以避免。如果是分布式應(yīng)用,本地鎖派不上用場,這時就需要引入分布式鎖來解決。

        由此可見分布式鎖的目的其實很簡單,就是為了保證多臺服務(wù)器在執(zhí)行某一段代碼時保證只有一臺服務(wù)器執(zhí)行。

        2、為了保證分布式鎖的可用性,至少要確保鎖的實現(xiàn)要同時滿足以下幾點
        • 互斥性。在任何時刻,保證只有一個客戶端持有鎖。

        • 不能出現(xiàn)死鎖。如果在一個客戶端持有鎖的期間,這個客戶端崩潰了,也要保證后續(xù)的其他客戶端可以上鎖。

        • 保證上鎖和解鎖都是同一個客戶端。

        3、一般來說,實現(xiàn)分布式鎖的方式有以下幾種
        • 使用MySQL,基于唯一索引。

        • 使用ZooKeeper,基于臨時有序節(jié)點。

        • 使用Redis,基于set命令(2.6.12 版本開始)。

        本篇文章主要講解Redis的實現(xiàn)方式。

        4、用到的redis命令

        鎖的實現(xiàn)主要基于redis的SET命令(SET詳細(xì)解釋參考這里),我們來看SET的解釋:

        SET key value [EX seconds] [PX milliseconds] [NX|XX]

        • 將字符串值 value 關(guān)聯(lián)到 key 。

        • 如果 key 已經(jīng)持有其他值, SET 就覆寫舊值,無視類型。

        • 對于某個原本帶有生存時間(TTL)的鍵來說, 當(dāng) SET 命令成功在這個鍵上執(zhí)行時, 這個鍵原有的 TTL 將被清除。
          可選參數(shù)

        從 Redis 2.6.12 版本開始, SET 命令的行為可以通過一系列參數(shù)來修改:

        EX second :設(shè)置鍵的過期時間為 second 秒。SET key value EX second 效果等同于 SETEX key second value 。
        PX millisecond :設(shè)置鍵的過期時間為 millisecond 毫秒。SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
        NX :只在鍵不存在時,才對鍵進(jìn)行設(shè)置操作。SET key value NX 效果等同于 SETNX key value 。
        XX :只在鍵已經(jīng)存在時,才對鍵進(jìn)行設(shè)置操作。

        加鎖:使用SET key value [PX milliseconds] [NX]命令,如果key不存在,設(shè)置value,并設(shè)置過期時間(加鎖成功)。如果已經(jīng)存在lock(也就是有客戶端持有鎖了),則設(shè)置失敗(加鎖失敗)。

        解鎖:使用del命令,通過刪除鍵值釋放鎖。釋放鎖之后,其他客戶端可以通過set命令進(jìn)行加鎖。

        5、上面第二項,說了分布式鎖,要考慮的問題,下面講解一下

        5.1、互斥性。在任何時刻,保證只有一個客戶端持有鎖

        redis命令是原子性的,只要客戶端調(diào)用redis的命令SET key value [PX milliseconds] [NX] 執(zhí)行成功,就算加鎖成功了

        5.2、不能出現(xiàn)死鎖。如果在一個客戶端持有鎖的期間,這個客戶端崩潰了,也要保證后續(xù)的其他客戶端可以上鎖。

        set命令px設(shè)置了過期時間,key過期失效了,就能避免死鎖了

        5.3保證上鎖和解鎖都是同一個客戶端。

        釋放鎖(刪除key)的時候,只要確保是當(dāng)前客戶端設(shè)置的value才去刪除key即可,采用lua腳本來實現(xiàn)

        在Redis中,執(zhí)行Lua語言是原子性,也就是說Redis執(zhí)行Lua的時候是不會被中斷的,具備原子性,這個特性有助于Redis對并發(fā)數(shù)據(jù)一致性的支持。


        6、java代碼實現(xiàn)

        先把需要的jar包引入
                <dependency>
                    <groupId>redis.clients</groupId>
                    <artifactId>jedis</artifactId>
                    <version>2.9.3</version>
                </dependency>
        加鎖設(shè)置參數(shù)的實體類
        import lombok.Data;

        //加鎖設(shè)置的參數(shù)
        @Data
        public class LockParam {
            //鎖的key
            private String lockKey;
            //嘗試獲得鎖的時間(單位:毫秒),默認(rèn)值:3000毫秒
            private Long tryLockTime;
            //嘗試獲得鎖后,持有鎖的時間(單位:毫秒),默認(rèn)值:5000毫秒
            private Long holdLockTime;

            public LockParam(String lockKey){
                this(lockKey,1000*3L,1000*5L);
            };
            public LockParam(String lockKey,Long tryLockTime){
                this(lockKey,tryLockTime,1000*5L);
            };
            public LockParam(String lockKey,Long tryLockTime,Long holdLockTime){
                this.lockKey = lockKey;
                this.tryLockTime = tryLockTime;
                this.holdLockTime = holdLockTime;
            };
        }
        redis分布式具體代碼實現(xiàn)
        import lombok.extern.slf4j.Slf4j;
        import redis.clients.jedis.Jedis;

        import java.util.Collections;
        import java.util.UUID;

        /**
         * redis分布式鎖
         */
        @Slf4j
        public class RedisLock {

            //鎖key的前綴
            private final static String prefix_key = "redisLock:";
            //釋放鎖的lua腳本
            private final static  String unLockScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            //執(zhí)行unLockScript腳本,釋放鎖成功值
            private final static  Long unLockSuccess = 1L;


            //加鎖設(shè)置的參數(shù)(key值、超時時間、持有鎖的時間)
            private LockParam lockParam;
            //嘗試獲得鎖的截止時間【lockParam.getTryLockTime()+System.currentTimeMillis()】
            private Long tryLockEndTime;
            //redis加鎖的key
            private String redisLockKey;
            //redis加鎖的vlaus
            private String redisLockValue;
            //redis加鎖的成功標(biāo)示
            private Boolean holdLockSuccess= Boolean.FALSE;


            //jedis實例
            private Jedis jedis;
            //獲取jedis實例
            private Jedis getJedis(){
                return this.jedis;
            }
            //關(guān)閉jedis
            private void closeJedis(Jedis jedis){
                jedis.close();
                jedis = null;
            }

            public RedisLock(LockParam lockParam){
                if(lockParam==null){
                    new RuntimeException("lockParam is null");
                }
                if(lockParam.getLockKey()==null || lockParam.getLockKey().trim().length()==0){
                    new RuntimeException("lockParam lockKey is error");
                }
                this.lockParam = lockParam;

                this.tryLockEndTime = lockParam.getTryLockTime()+System.currentTimeMillis();
                this.redisLockKey = prefix_key.concat(lockParam.getLockKey());
                this.redisLockValue = UUID.randomUUID().toString().replaceAll("-","");

                //todo 到時候可以更換獲取Jedis實例的實現(xiàn)
                jedis = new Jedis("127.0.0.1",6379);
            }

            /**
             * 加鎖
             * @return 成功返回true,失敗返回false
             */
            public boolean lock() {
                while(true){
                    //判斷是否超過了,嘗試獲取鎖的時間
                    if(System.currentTimeMillis()>tryLockEndTime){
                        return false;
                    }
                    //嘗試獲取鎖
                    holdLockSuccess = tryLock();
                    if(Boolean.TRUE.equals(holdLockSuccess)){
                        return true;//獲取鎖成功
                    }

                    try {
                        //獲得鎖失敗,休眠50毫秒再去嘗試獲得鎖,避免一直請求redis,導(dǎo)致redis cpu飆升
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }

            /**
             * 執(zhí)行一次加鎖操作:成功返回true 失敗返回false
             * @return 成功返回true,失敗返回false
             */
            private boolean tryLock() {
                try {
                    String result = getJedis().set(redisLockKey,redisLockValue, "NX""PX", lockParam.getHoldLockTime());
                    if ("OK".equals(result)) {
                        return true;
                    }
                }catch (Exception e){
                    log.warn("tryLock failure redisLockKey:{} redisLockValue:{} lockParam:{}",redisLockKey,redisLockValue,lockParam,e);
                }
                return false;
            }

            /**
             * 解鎖
             * @return 成功返回true,失敗返回false
             */
            public Boolean unlock() {
                Object result = null;
                try {
                    //獲得鎖成功,才執(zhí)行l(wèi)ua腳本
                    if(Boolean.TRUE.equals(holdLockSuccess)){
                        //執(zhí)行Lua腳本
                        result = getJedis().eval(unLockScript, Collections.singletonList(redisLockKey), Collections.singletonList(redisLockValue));
                        if (unLockSuccess.equals(result)) {//釋放成功
                            return true;
                        }
                    }
                } catch (Exception e) {
                    log.warn("unlock failure redisLockKey:{} redisLockValue:{} lockParam:{} result:{}",redisLockKey,redisLockValue,lockParam,result,e);
                } finally {
                    this.closeJedis(jedis);
                }
                return false;
            }
        }
        redis分布式鎖使用
        import lombok.extern.slf4j.Slf4j;

        @Slf4j
        public class test {
            static String lockKey = "666";
            public static void main(String[] args) throws InterruptedException {
                log.info("下面測試兩個線程同時,搶占鎖的結(jié)果");
                Thread thread1 = new Thread(()->{
                    testRedisLock();
                });
                thread1.setName("我是線程1");
                Thread thread2 = new Thread(()->{
                    testRedisLock();
                });
                thread2.setName("我是線程2");

                //同時啟動線程
                thread1.start();
                thread2.start();

                Thread.sleep(1000*20);
                log.info("-----------------我是一條分割線----------------");
                log.info("");
                log.info("");
                log.info("");


                log.info("下面是測試  一個線程獲取鎖成功后,由于業(yè)務(wù)執(zhí)行時間超過了設(shè)置持有鎖的時間,是否會把其他線程持有的鎖給釋放掉");
                Thread thread3 = new Thread(()->{
                    testRedisLock2();
                });
                thread3.setName("我是線程3");
                thread3.start();

                Thread.sleep(1000*1);//暫停一秒是為了讓線程3獲的到鎖
                Thread thread4 = new Thread(()->{
                    testRedisLock();
                });
                thread4.setName("我是線程4");
                thread4.start();
            }

            public static void testRedisLock(){
                LockParam lockParam = new LockParam(lockKey);
                lockParam.setTryLockTime(2000L);//2秒時間嘗試獲得鎖
                lockParam.setHoldLockTime(1000*10L);//獲得鎖成功后持有鎖10秒時間
                RedisLock redisLock = new RedisLock(lockParam);
                try {
                    Boolean lockFlag = redisLock.lock();
                    log.info("加鎖結(jié)果:{}",lockFlag);
                    if(lockFlag){
                        try {
                            //20秒模擬處理業(yè)務(wù)代碼時間
                            Thread.sleep(1000*5L);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }catch (Exception e) {
                    log.info("testRedisLock e---->",e);
                }finally {
                    boolean unlockResp = redisLock.unlock();
                    log.info("釋放鎖結(jié)果:{}",unlockResp);
                }
            }


            public static void testRedisLock2(){
                LockParam lockParam = new LockParam(lockKey);
                lockParam.setTryLockTime(1000*2L);//2秒時間嘗試獲得鎖
                lockParam.setHoldLockTime(1000*2L);//獲得鎖成功后持有鎖2秒時間
                RedisLock redisLock = new RedisLock(lockParam);
                try {
                    Boolean lockFlag = redisLock.lock();
                    log.info("加鎖結(jié)果:{}",lockFlag);
                    if(lockFlag){
                        try {
                            //10秒模擬處理業(yè)務(wù)代碼時間
                            Thread.sleep(1000*10L);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }catch (Exception e) {
                    log.info("testRedisLock e---->",e);
                }finally {
                    boolean unlockResp = redisLock.unlock();
                    log.info("釋放鎖結(jié)果:{}",unlockResp);
                }
            }
        }


        這是代碼在執(zhí)行過程中,通過redis可視化工具看到的效果,可以參考一下~

        控制臺日志打印結(jié)果
        15:02:28.569 [main] INFO com.test.test - 下面測試兩個線程同時,搶占鎖的結(jié)果
        15:02:28.645 [我是線程2] INFO com.test.test - 加鎖結(jié)果:true
        15:02:30.618 [我是線程1] INFO com.test.test - 加鎖結(jié)果:false
        15:02:30.620 [我是線程1] INFO com.test.test - 釋放鎖結(jié)果:false
        15:02:33.652 [我是線程2] INFO com.test.test - 釋放鎖結(jié)果:true
        15:02:48.614 [main] INFO com.test.test - -----------------我是一條分割線----------------
        15:02:48.614 [main] INFO com.test.test - 
        15:02:48.614 [main] INFO com.test.test - 
        15:02:48.614 [main] INFO com.test.test - 
        15:02:48.614 [main] INFO com.test.test - 下面是測試  一個線程獲取鎖成功后,由于業(yè)務(wù)執(zhí)行時間超過了設(shè)置持有鎖的時間,是否會把其他線程持有的鎖給釋放掉
        15:02:48.616 [我是線程3] INFO com.test.test - 加鎖結(jié)果:true
        15:02:50.645 [我是線程4] INFO com.test.test - 加鎖結(jié)果:true
        15:02:55.647 [我是線程4] INFO com.test.test - 釋放鎖結(jié)果:true
        15:02:58.621 [我是線程3] INFO com.test.test - 釋放鎖結(jié)果:false
        • 可以看到多個線程競爭一把鎖的時候,保證了只有一個線程持有鎖

        • 分割線下面的日志也能看出,一個線程持有了鎖,由于處理業(yè)務(wù)代碼時間,超過了設(shè)置持有鎖的時間,通過lua腳本釋放鎖的時候,也不會把其他線程持有的鎖給釋放掉,保證了安全釋放了鎖





        瀏覽 129
        點贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
          
          

            1. 国产一级α片 | 亚洲AV永久无码精品国产精 | 男人操女人的app | 操逼一级毛片 | 午夜精品一区二区三区免费视频 |