redis分佈式鎖的幾種實現方式,以及Redisson的配置和使用

最近在開發中涉及到了多個客戶端的對redis的某個key同時進行增刪的問題。這裏就會涉及一個問題:鎖java

先舉例在分佈式系統中不加鎖會出現問題:

  redis中存放了某個用戶的帳戶餘額 ,例如100 (用戶id:餘額)git

  A端須要對用戶扣費-1,須要兩步:github

    A1.將該用戶的目前餘額取出來(100)redis

    A2.將餘額扣除一部分(99)後再插入到redis中算法

  B端須要對用戶充值+10,須要兩步:spring

    B1.將該用戶的目前餘額取出來(99)數據庫

    B2.將餘額添加充值額度(109)後再插入到redis中api

  咱們的指望執行順序是A一、A二、B一、B2  結果就會是109服務器

  可是若是不加鎖,就會出現A一、B一、A二、B2(110)或者其餘各類隨機狀況,這樣就會形成數據錯誤。mybatis

Redis加鎖的幾種實現方式

  方式一,本身造輪子

    以前參考的不少博客,關於redis加鎖都是先setNX()獲取鎖,而後再setExpire()設置鎖的有效時間。

    然而這樣的話獲取鎖的操做就不是原子性的了,若是setNX後系統宕機,就會形成鎖死,系統阻塞。

    根據官方的推薦(https://redis.io/topics/distlock),最好使用set命令:SET key value [EX seconds] [PX milliseconds] [NX|XX]       

    EX PX設置有效時間    NX屬性的做用就是若是key存在就返回失敗,不然插入數據。

    須要注意的是:

      在Redis 2.6.12以前,set只能返回OK,因此沒法判斷操做是否成功,因此也就不適用。

      若是使用的是spring-boot-starter-data-redis依賴,那麼在2.x版本以前的接口也不支持上述的set操做

    java代碼:

//獲取鎖
        //鎖的鍵值須要具備標誌性。
        //例如,如今有兩個系統須要對key=user_id,value=user_balance進行操做,這時就能夠設計這個鍵的鎖爲user_id+"_key"
        String user_id="1";
        String key=user_id+"_key";
        //值設置爲一個隨機數(下面講緣由)
        String random_value=UUID.randomUUID().toString();
        redisTemplate.execute((RedisCallback<Boolean>) (RedisConnection connection)->{
            //只有2.0以上的版本才支持set返回插入結果Boolean
            //此命令的意思是隻有key不存在,才插入值,而且設置有效時間爲10s
            connection.set(key.getBytes(), random_value.getBytes(), Expiration.seconds(10), SetOption.SET_IF_ABSENT);
            //本示例因爲依賴版本低於2.0,因此沒法接受set設置結果
            Boolean result=true;
            return result;
        });
        
        //進行更新操做...
        
        //釋放鎖
        //爲何釋放以前要比較一下?
        //這是爲了防止刪除掉別人的鎖,例如此場景中:若是咱們的中間操做超過了10s那麼鎖會自動釋放,這時別人會再獲取鎖。
        //若是咱們執行完中間就直接刪除鎖的話,就會把別人的鎖刪除
        if(redisTemplate.opsForValue().get(key)==random_value) {
            redisTemplate.delete(key);
        }

能夠發現,若是本身來實現的話,受限不少。而且這仍是最基本的操做,包括出錯重試等功能都沒有。

因此咱們要學習redis推薦的reids工具redisson

  方式二:集成redissonhttps://github.com/redisson/redisson/wiki/2.-%E9%85%8D%E7%BD%AE%E6%96%B9%E6%B3%95

  一.添加依賴

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

  二.resources文件夾添加配置文件redisson.yml

singleServerConfig:
  #鏈接空閒超時,單位:毫秒
  idleConnectionTimeout: 10000
  pingTimeout: 1000
  #鏈接超時,單位:毫秒
  connectTimeout: 10000
  #命令等待超時,單位:毫秒
  timeout: 3000
  #命令失敗重試次數
  retryAttempts: 3
  #命令重試發送時間間隔,單位:毫秒
  retryInterval: 1500
  #從新鏈接時間間隔,單位:毫秒
  reconnectionTimeout: 3000
  #執行失敗最大次數
  failedAttempts: 3
  #單個鏈接最大訂閱數量
  subscriptionsPerConnection: 5
  #客戶端名稱
  clientName: null
  #地址
  address: "redis://192.168.1.16:6379"
  #數據庫編號
  database: 0
  #密碼
  password: xiaokong
  #發佈和訂閱鏈接的最小空閒鏈接數
  subscriptionConnectionMinimumIdleSize: 1 
  #發佈和訂閱鏈接池大小
  subscriptionConnectionPoolSize: 50
  #最小空閒鏈接數
  connectionMinimumIdleSize: 32
  #鏈接池大小
  connectionPoolSize: 64
  #是否啓用DNS監測
  dnsMonitoring: false
  #DNS監測時間間隔,單位:毫秒
  dnsMonitoringInterval: 5000
threads: 0
nettyThreads: 0
codec: !<org.redisson.codec.JsonJacksonCodec> {}
transportMode : "NIO"

  三.Application中設置RedissonClient

import org.mybatis.spring.annotation.MapperScan;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@SpringBootApplication
@EnableTransactionManagement
@MapperScan("com.xxx.mapper")
public class Application {
    public static void main(String [] args) {
        SpringApplication.run(Application.class, args);
    }
    @Bean(destroyMethod="shutdown")
    public RedissonClient redisson() throws IOException {
        RedissonClient redisson = Redisson.create(
                Config.fromYAML(new ClassPathResource("redisson.yml").getInputStream()));
        return redisson;
    }
}

 

  四.在代碼中使用

@Autowired
    private RedissonClient redisson;
    @Test 
    public void redisson() {
        String user_id="1";
        String key=user_id+"_key";
        //獲取鎖
        RLock lock = redisson.getLock(key);
        lock.lock();
            //執行具體邏輯...
        
        RBucket<Object> bucket = redisson.getBucket("a");
        bucket.set("bb");
        
        lock.unlock();
    }

須要注意的是redisson的使用和redisTemplate有比較大的區別,這裏簡單介紹一下幾個特性:(剛用時迷了好久,但願你們能少走些彎路)

1.redisson中不須要set指令,舉個例子:

RBucket<Object> bucket = redisson.getBucket("a");
bucket.set("bb");

在這兩條語句中,咱們只獲取了key="a"的bucket類型對象(裏面能夠裝一個任意對象)。而後修改bucket裏面一個值,其實這時["a","bb"]已經被存入redis了

2.全部的值都是結構體

和上例的RBucket結構體同樣,redisson提供了十幾種結構體(https://github.com/redisson/redisson/wiki/7.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%9B%86%E5%90%88)供咱們使用,當取值時,redisson也會自動將值轉換成對應的結構體。因此若是使用redisson取redisTemplate放入的值,就要當心報錯

方式三.基於redlock的算法討論

這種我尚未具體實現過,爲何會出現這種算法,主要是應對redis服務器宕機的問題。當redis宕機時,即便有主從,可是依然會有一個同步間隔。這樣就會形成數據流失。

固然,更爲嚴重的是,在分佈式狀況下,丟失的是鎖,咱們知道通常用鎖的數據都是比較重要的。

一個場景:A在向主機1請求到鎖成功後,主機1宕機了。如今從機1a變成了主機。可是數據沒有同步,從機1a是沒有A的鎖的。那麼B又能夠得到一個鎖。這樣就會形成數據錯誤。

redlock主要思想就是作數據冗餘。創建5臺獨立的集羣,當咱們發送一個數據的時候,要保證3臺(n/2+1)以上的機器接受成功纔算成功,不然重試或報錯。

固然具體是很複雜,想研究的能夠看看(https://redis.io/topics/distlock)

方式四.使用zookeeper+redis來管理鎖

就像以前討論的,方式2只能保證客戶端的正確,卻沒法保證服務端的宕機數據丟失。方式三的數據完整性很高,可是管理起來很複雜。這時就有了一個折中的作法:

將鎖存放在zookeeper中,因爲zookeeper與redis的場景不一樣,因此zookeeper的算法對數據的完整性要求很高。在分佈式的zookeeper中,數據是很難丟失的。

這樣,咱們就能夠把鎖放到zookeeper中,來保證鎖的完整性。

好吧,這個我也沒有實驗過(羞恥),不過網上又不少這方面的博客,之後用到再說吧~~~通常項目用方式二就能夠啦,

相關文章
相關標籤/搜索