使用Redis實現分佈式鎖

1.實現分佈式鎖的幾種方案

    1.Redis實現   (推薦)

    2.Zookeeper實現

    3.數據庫實現

Redis實現分佈式鎖
*
* 在集羣等多服務器中經常使用到同步處理一下業務,這是普通的事務是滿足不了業務需求,需要分佈式鎖
*
* 分佈式鎖的常用3種實現:
*               0.數據庫樂觀鎖實現
*               1.Redis實現   ---  使用redis的setnx()、get()、getset()方法,用於分佈式鎖,解決死鎖問題
*               2.Zookeeper實現
*                      參考:http://surlymo.iteye.com/blog/2082684
*                            http://www.jb51.net/article/103617.htm
*                            http://www.hollischuang.com/archives/1716?utm_source=tuicool&utm_medium=referral
*                   1、實現原理:
基於zookeeper瞬時有序節點實現的分佈式鎖,其主要邏輯如下(該圖來自於IBM網站)。大致思想即爲:每個客戶端對某個功能加鎖時,在zookeeper上的與該功能對應的指定節點的目錄下,生成一個唯一的瞬時有序節點。判斷是否獲取鎖的方式很簡單,只需要判斷有序節點中序號最小的一個。當釋放鎖的時候,只需將這個瞬時節點刪除即可。同時,其可以避免服務宕機導致的鎖無法釋放,而產生的死鎖問題。
2、優點
鎖安全性高,zk可持久化
3、缺點
性能開銷比較高。因爲其需要動態產生、銷燬瞬時節點來實現鎖功能。
4、實現
可以直接採用zookeeper第三方庫curator即可方便地實現分佈式鎖
*
* Redis實現分佈式鎖的原理:
*   1.通過setnx(lock_timeout)實現,如果設置了鎖返回1, 已經有值沒有設置成功返回0
*   2.死鎖問題:通過實踐來判斷是否過期,如果已經過期,獲取到過期時間get(lockKey),然後getset(lock_timeout)判斷是否和get相同,
*     相同則證明已經加鎖成功,因爲可能導致多線程同時執行getset(lock_timeout)方法,這可能導致多線程都只需getset後,對於判斷加鎖成功的線程,
*     再加expire(lockKey, LOCK_TIMEOUT, TimeUnit.MILLISECONDS)過期時間,防止多個線程同時疊加時間,導致鎖時效時間翻倍
*   3.針對集羣服務器時間不一致問題,可以調用redis的time()獲取當前時間

2.Redis分分佈式鎖的代碼實現

   1.定義鎖接口

package com.jay.service.redis;

/**
 * Redis分佈式鎖接口
 * Created by hetiewei on 2017/4/7.
 */
public interface RedisDistributionLock {
    /**
     * 加鎖成功,返回加鎖時間
     * @param lockKey
     * @param threadName
     * @return
     */
    public long lock(String lockKey, String threadName);

    /**
     * 解鎖, 需要更新加鎖時間,判斷是否有權限
     * @param lockKey
     * @param lockValue
     * @param threadName
     */
    public void unlock(String lockKey, long lockValue, String threadName);

    /**
     * 多服務器集羣,使用下面的方法,代替System.currentTimeMillis(),獲取redis時間,避免多服務的時間不一致問題!!!
     * @return
     */
    public long currtTimeForRedis();
}


   2.定義鎖實現

package com.jay.service.redis.impl;

import com.jay.service.redis.RedisDistributionLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;

import java.util.concurrent.TimeUnit;

/**
 * Created by hetiewei on 2017/4/7.
 */
public class RedisLockImpl implements RedisDistributionLock {

    //加鎖超時時間,單位毫秒, 即:加鎖時間內執行完操作,如果未完成會有並發現象
    private static final long LOCK_TIMEOUT = 5*1000;

    private static final Logger LOG = LoggerFactory.getLogger(RedisLockImpl.class);

    private StringRedisTemplate redisTemplate;

    public RedisLockImpl(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 加鎖
     * 取到鎖加鎖,取不到鎖一直等待知道獲得鎖
     * @param lockKey
     * @param threadName
     * @return
     */
    @Override
    public synchronized long lock(String lockKey, String threadName) {
        LOG.info(threadName+"開始執行加鎖");
        while (true){ //循環獲取鎖
            //鎖時間
            Long lock_timeout = currtTimeForRedis()+ LOCK_TIMEOUT +1;
            if (redisTemplate.execute(new RedisCallback<Boolean>() {
                @Override
                public Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException {
                    //定義序列化方式
                    RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
                    byte[] value = serializer.serialize(lock_timeout.toString());
                    boolean flag = redisConnection.setNX(lockKey.getBytes(), value);
                    return flag;
                }
            })){
                //如果加鎖成功
                LOG.info(threadName +"加鎖成功 ++++ 111111");
                //設置超時時間,釋放內存
                redisTemplate.expire(lockKey, LOCK_TIMEOUT, TimeUnit.MILLISECONDS);
                return lock_timeout;
            }else {
                //獲取redis裏面的時間
                String result = redisTemplate.opsForValue().get(lockKey);
                Long currt_lock_timeout_str = result==null?null:Long.parseLong(result);
                //鎖已經失效
                if (currt_lock_timeout_str != null && currt_lock_timeout_str < System.currentTimeMillis()){
                    //判斷是否爲空,不爲空時,說明已經失效,如果被其他線程設置了值,則第二個條件判斷無法執行
                    //獲取上一個鎖到期時間,並設置現在的鎖到期時間
                    Long old_lock_timeout_Str = Long.valueOf(redisTemplate.opsForValue().getAndSet(lockKey, lock_timeout.toString()));
                    if (old_lock_timeout_Str != null && old_lock_timeout_Str.equals(currt_lock_timeout_str)){
                        //多線程運行時,多個線程簽好都到了這裏,但只有一個線程的設置值和當前值相同,它纔有權利獲取鎖
                        LOG.info(threadName + "加鎖成功 ++++ 22222");
                        //設置超時間,釋放內存
                        redisTemplate.expire(lockKey, LOCK_TIMEOUT, TimeUnit.MILLISECONDS);

                        //返回加鎖時間
                        return lock_timeout;
                    }
                }
            }

            try {
                LOG.info(threadName +"等待加鎖, 睡眠100毫秒");
//                TimeUnit.MILLISECONDS.sleep(100);
                TimeUnit.MILLISECONDS.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 解鎖
     * @param lockKey
     * @param lockValue
     * @param threadName
     */
    @Override
    public synchronized void unlock(String lockKey, long lockValue, String threadName) {
        LOG.info(threadName + "執行解鎖==========");//正常直接刪除 如果異常關閉判斷加鎖會判斷過期時間
        //獲取redis中設置的時間
        String result = redisTemplate.opsForValue().get(lockKey);
        Long currt_lock_timeout_str = result ==null?null:Long.valueOf(result);

        //如果是加鎖者,則刪除鎖, 如果不是,則等待自動過期,重新競爭加鎖
        if (currt_lock_timeout_str !=null && currt_lock_timeout_str == lockValue){
            redisTemplate.delete(lockKey);
            LOG.info(threadName + "解鎖成功------------------");
        }
    }

    /**
     * 多服務器集羣,使用下面的方法,代替System.currentTimeMillis(),獲取redis時間,避免多服務的時間不一致問題!!!
     * @return
     */
    @Override
    public long currtTimeForRedis(){
        return redisTemplate.execute(new RedisCallback<Long>() {
            @Override
            public Long doInRedis(RedisConnection redisConnection) throws DataAccessException {
                return redisConnection.time();
            }
        });
    }

}


  3.分佈式鎖驗證

      
@RestController
@RequestMapping("/distribution/redis")
public class RedisLockController {

    private static final String LOCK_NO = "redis_distribution_lock_no_";

    private static int i = 0;

    private ExecutorService service;

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 模擬1000個線程同時執行業務,修改資源
     *
     * 使用線程池定義了20個線程
     *
     */
    @GetMapping("lock1")
    public void testRedisDistributionLock1(){

        service = Executors.newFixedThreadPool(20);

        for (int i=0;i<1000;i++){
            service.execute(new Runnable() {
                @Override
                public void run() {
                    task(Thread.currentThread().getName());
                }
            });
        }

    }

    @GetMapping("/{key}")
    public String getValue(@PathVariable("key") String key){
        Serializable result = redisTemplate.opsForValue().get(key);
        return result.toString();
    }

    private void task(String name) {
//        System.out.println(name + "任務執行中"+(i++));

        //創建一個redis分佈式鎖
        RedisLockImpl redisLock = new RedisLockImpl(redisTemplate);
        //加鎖時間
        Long lockTime;
        if ((lockTime = redisLock.lock((LOCK_NO+1)+"", name))!=null){
            //開始執行任務
            System.out.println(name + "任務執行中"+(i++));
            //任務執行完畢 關閉鎖
            redisLock.unlock((LOCK_NO+1)+"", lockTime, name);
        }

    }
}

4.結果驗證:

      在Controller中模擬了1000個線程,通過線程池方式提交,每次20個線程搶佔分佈式鎖,搶到分佈式鎖的執行代碼,沒搶到的等待

     結果如下:

   

2017-04-07 16:27:17.385  INFO 8652 --- [pool-2-thread-4] c.jay.service.redis.impl.RedisLockImpl   : pool-2-thread-4等待加鎖, 睡眠100毫秒
2017-04-07 16:27:17.385  INFO 8652 --- [pool-2-thread-7] c.jay.service.redis.impl.RedisLockImpl   : pool-2-thread-7解鎖成功------------------
        2017-04-07 16:27:17.391  INFO 8652 --- [pool-2-thread-5] c.jay.service.redis.impl.RedisLockImpl   : pool-2-thread-5加鎖成功 ++++ 111111
pool-2-thread-5任務執行中994
2017-04-07 16:27:17.391  INFO 8652 --- [pool-2-thread-5] c.jay.service.redis.impl.RedisLockImpl   : pool-2-thread-5執行解鎖==========
        2017-04-07 16:27:17.391  INFO 8652 --- [pool-2-thread-1] c.jay.service.redis.impl.RedisLockImpl   : pool-2-thread-1等待加鎖, 睡眠100毫秒
2017-04-07 16:27:17.391  INFO 8652 --- [pool-2-thread-5] c.jay.service.redis.impl.RedisLockImpl   : pool-2-thread-5解鎖成功------------------
        2017-04-07 16:27:17.397  INFO 8652 --- [pool-2-thread-6] c.jay.service.redis.impl.RedisLockImpl   : pool-2-thread-6加鎖成功 ++++ 111111
pool-2-thread-6任務執行中995
2017-04-07 16:27:17.398  INFO 8652 --- [pool-2-thread-6] c.jay.service.redis.impl.RedisLockImpl   : pool-2-thread-6執行解鎖==========
        2017-04-07 16:27:17.398  INFO 8652 --- [pool-2-thread-6] c.jay.service.redis.impl.RedisLockImpl   : pool-2-thread-6解鎖成功------------------
        2017-04-07 16:27:17.400  INFO 8652 --- [ool-2-thread-19] c.jay.service.redis.impl.RedisLockImpl   : pool-2-thread-19加鎖成功 ++++ 111111
pool-2-thread-19任務執行中996
2017-04-07 16:27:17.400  INFO 8652 --- [ool-2-thread-19] c.jay.service.redis.impl.RedisLockImpl   : pool-2-thread-19執行解鎖==========
        2017-04-07 16:27:17.400  INFO 8652 --- [ool-2-thread-19] c.jay.service.redis.impl.RedisLockImpl   : pool-2-thread-19解鎖成功------------------
        2017-04-07 16:27:17.571  INFO 8652 --- [ool-2-thread-11] c.jay.service.redis.impl.RedisLockImpl   : pool-2-thread-11加鎖成功 ++++ 111111
pool-2-thread-11任務執行中997
2017-04-07 16:27:17.572  INFO 8652 --- [ool-2-thread-11] c.jay.service.redis.impl.RedisLockImpl   : pool-2-thread-11執行解鎖==========
        2017-04-07 16:27:17.572  INFO 8652 --- [ool-2-thread-11] c.jay.service.redis.impl.RedisLockImpl   : pool-2-thread-11解鎖成功------------------
        2017-04-07 16:27:17.585  INFO 8652 --- [pool-2-thread-4] c.jay.service.redis.impl.RedisLockImpl   : pool-2-thread-4加鎖成功 ++++ 111111
pool-2-thread-4任務執行中998
2017-04-07 16:27:17.586  INFO 8652 --- [pool-2-thread-4] c.jay.service.redis.impl.RedisLockImpl   : pool-2-thread-4執行解鎖==========
        2017-04-07 16:27:17.586  INFO 8652 --- [pool-2-thread-4] c.jay.service.redis.impl.RedisLockImpl   : pool-2-thread-4解鎖成功------------------
        2017-04-07 16:27:17.591  INFO 8652 --- [pool-2-thread-1] c.jay.service.redis.impl.RedisLockImpl   : pool-2-thread-1加鎖成功 ++++ 111111
pool-2-thread-1任務執行中999
2017-04-07 16:27:17.591  INFO 8652 --- [pool-2-thread-1] c.jay.service.redis.impl.RedisLockImpl   : pool-2-thread-1執行解鎖==========
        2017-04-07 16:27:17.591  INFO 8652 --- [pool-2-thread-1] c.jay.service.redis.impl.RedisLockImpl   : pool-2-thread-1解鎖成功------------------






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