溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何用redis來實現(xiàn)分布式鎖

發(fā)布時間:2021-10-14 11:51:17 來源:億速云 閱讀:154 作者:iii 欄目:編程語言

本篇內(nèi)容主要講解“如何用redis來實現(xiàn)分布式鎖”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學(xué)習(xí)“如何用redis來實現(xiàn)分布式鎖”吧!

一、建Module

boot_redis01

boot_redis02

二、改POM

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.lau</groupId>
    <artifactId>boot_redis01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>boot_redis01</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-actuator -->
        <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
        <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 -->
        <dependency>
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-pool2</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
        <dependency>
             <groupId>redis.clients</groupId>
             <artifactId>jedis</artifactId>
             <version>3.1.0</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
        <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
        <dependency>
             <groupId>org.redisson</groupId>
             <artifactId>redisson</artifactId>
             <version>3.13.4</version>
        </dependency>
        <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-devtools</artifactId>
             <scope>runtime</scope>
             <optional>true</optional>
        </dependency>
        <dependency>
             <groupId>org.projectlombok</groupId>
             <artifactId>lombok</artifactId>
             <optional>true</optional>
        </dependency>
        <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
             <version>4.12</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-maven-plugin</artifactId>
             </plugin>
        </plugins>
    </build>
</project>

三、寫YML

server.port=1111

spring.redis.database=0
spring.redis.host=localhost
spring.redis.port=6379
#連接池最大連接數(shù)(使用負值表示沒有限制)默認8
spring.redis.lettuce.pool.max-active=8
#連接池最大阻塞等待時間(使用負值表示沒有限制)默認-1
spring.redis.lettuce.pool.max-wait=-1
#連接池中的最大空閑連接默認8
spring.redis.lettuce.pool.max-idle=8
#連接池中的最小空閑連接默認0
spring.redis.lettuce.pool.min-idle=0
?

四、主啟動

package com.lau.boot_redis01;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;

@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class})
public class BootRedis01Application {

    public static void main(String[] args) {
        SpringApplication.run(BootRedis01Application.class, args);
    }

}

五、業(yè)務(wù)類

1、RedisConfig配置類

package com.lau.boot_redis01.config;

import org.redisson.Redisson;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.io.Serializable;

@Configuration
public class RedisConfig {
    @Value("${spring.redis.host}")
    private String redisHost;

    /**
     *保證不是序列化后的亂碼配置
     */
    @Bean
    public RedisTemplate<String, Serializable>redisTemplate(LettuceConnectionFactory connectionFactory){
        RedisTemplate<String,Serializable> redisTemplate =new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setConnectionFactory(connectionFactory);
        return redisTemplate;
    }

    @Bean
    public Redisson redisson(){
       Config config = new Config();
            config.useSingleServer().setAddress("redis://"+redisHost+":6379").setDatabase(0);
       return (Redisson) Redisson.create(config);
    }
}

2、GoodController.java

package com.lau.boot_redis01.controller;

import com.lau.boot_redis01.util.RedisUtil;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import redis.clients.jedis.Jedis;

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

@RestController
public class GoodController {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Value("${server.port}")
    private String serverPort;

    private static final String REDIS_LOCK = "atguigulock";

    @GetMapping("/buy_goods")
    public String buy_Goods() throws Exception {
        String value = UUID.randomUUID().toString() + Thread.currentThread().getName();

        try{
            //1、key加過期時間是因為如果redis客戶端宕機了會造成死鎖,其它客戶端永遠獲取不到鎖
            //2、這里將setnx與鎖過期兩條命令合二為一,是為了解決命令分開執(zhí)行引發(fā)的原子性問題:
            //setnx  中間會被其它redis客戶端命令加塞   2、expire
            //3①、為了避免線程執(zhí)行業(yè)務(wù)時間大于鎖過期時間導(dǎo)致竄行操作,再釋放鎖時應(yīng)判斷是否是自己加的鎖;
            //還有另外一種解決方案:鎖續(xù)期——額外開啟一個守護線程定時給當前key加超時時間(如5s到期,每2.5s ttl判斷一次,并加2.5s超時時間,不斷續(xù)期,線程將使用主動刪除key命令的方式釋放鎖;另,當此redis客戶端命令宕機后,此守護線程會自動隨之消亡,不會再主動續(xù)期——此機制使得其它redis客戶端可以獲得鎖,不會發(fā)生死鎖或長期等待)
            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);//setnx

            if(!flag){
                return "獲取鎖失?。?quot;;
            }

            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            if (goodsNumber > 0){
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");

                System.out.println("你已經(jīng)成功秒殺商品,此時還剩余:" + realNumber + "件"+"\t 服務(wù)器端口: "+serverPort);

                return "你已經(jīng)成功秒殺商品,此時還剩余:" + realNumber + "件"+"\t 服務(wù)器端口: "+serverPort;
            }

            System.out.println("商品已經(jīng)售罄/活動結(jié)束/調(diào)用超時,歡迎下次光臨"+"\t 服務(wù)器端口: "+serverPort);

            return "商品已經(jīng)售罄/活動結(jié)束/調(diào)用超時,歡迎下次光臨"+"\t 服務(wù)器端口: "+serverPort;
        }
        finally {
//            if(stringRedisTemplate.opsForValue().get(REDIS_LOCK).equals(value)){
//                stringRedisTemplate.delete(REDIS_LOCK);
//            }

              //3②這里也存在命令的原子問題:獲取當前key經(jīng)相等判斷后與刪除對應(yīng)key是兩個不同命令,中間會被加塞
              //解決方法1:redis事務(wù)
//            stringRedisTemplate.watch(REDIS_LOCK);
//            while(true){
//                if(stringRedisTemplate.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)){
//                    stringRedisTemplate.setEnableTransactionSupport(true);
//                    stringRedisTemplate.multi();
//                    stringRedisTemplate.delete(REDIS_LOCK);
//
//                    List<Object> list = stringRedisTemplate.exec();
//
//                    if(list == null){
//                        continue;
//                    }
//                }
//
//                stringRedisTemplate.unwatch();
//                break;
//            }

            //解決方法2:lua腳本——原子操作
            Jedis jedis = RedisUtil.getJedis();

            String script = "if redis.call('get', KEYS[1]) == ARGV[1]"+"then "
                    +"return redis.call('del', KEYS[1])"+"else "+ "  return 0 " + "end";
            try{
                Object result = jedis.eval(script, Collections.singletonList(REDIS_LOCK), Collections.singletonList(value));

                if ("1".equals(result.toString())){
                    System.out.println("------del REDIS_LOCK_KEY success");
                }
                else {
                    System.out.println("------del REDIS_LOCK_KEY error");
                }
            }finally {
                if (null != jedis){
                    jedis.close();
                }
            }
        }
    }
}

六、改造中的問題

1、單機版沒加鎖

 問題:沒有加鎖,并發(fā)下數(shù)字不對,會出現(xiàn)超賣現(xiàn)象

① synchronized 不見不散

② ReentrantLock 過時不候

 
在單機環(huán)境下,可以使用synchronized或Lock來實現(xiàn)。
 
但是在分布式系統(tǒng)中,因為競爭的線程可能不在同一個節(jié)點上(同一個jvm中),所以需要一個讓所有進程都能訪問到的鎖來實現(xiàn),比如redis或者zookeeper來構(gòu)建;
 
不同進程jvm層面的鎖就不管用了,那么可以利用第三方的一個組件,來獲取鎖,未獲取到鎖,則阻塞當前想要運行的線程

2、使用Nginx配置負載均衡

注:分布式部署后,單機鎖還是出現(xiàn)超賣現(xiàn)象,需要分布式鎖

啟動兩個微服務(wù)1111和2222,訪問使用:http://localhost/buy_goods(即通過nginx輪詢方式訪問1111和2222兩個微服務(wù))

nginx.conf配置

#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;
	upstream mynginx{#反向代理的服務(wù)器列表,權(quán)重相同,即負載均衡使用輪訓(xùn)策略
		server localhost:1111 weight=1;
		server localhost:2222 weight=1;
	}

    server {
        listen       80;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            #root   html;
            #index  index.html index.htm;
			proxy_pass http://mynginx;#配置反向代理
			index  index.html index.htm;
        }

        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
        #
        #location ~ \.php$ {
        #    proxy_pass   http://127.0.0.1;
        #}

        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        #location ~ \.php$ {
        #    root           html;
        #    fastcgi_pass   127.0.0.1:9000;
        #    fastcgi_index  index.php;
        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
        #    include        fastcgi_params;
        #}

        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /\.ht {
        #    deny  all;
        #}
    }


    # another virtual host using mix of IP-, name-, and port-based configuration
    #
    #server {
    #    listen       8000;
    #    listen       somename:8080;
    #    server_name  somename  alias  another.alias;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}


    # HTTPS server
    #
    #server {
    #    listen       443 ssl;
    #    server_name  localhost;

    #    ssl_certificate      cert.pem;
    #    ssl_certificate_key  cert.key;

    #    ssl_session_cache    shared:SSL:1m;
    #    ssl_session_timeout  5m;

    #    ssl_ciphers  HIGH:!aNULL:!MD5;
    #    ssl_prefer_server_ciphers  on;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}

}

3、異常將導(dǎo)致鎖不會釋放

① 出異常的話,可能無法釋放鎖, 必須要在代碼層面finally釋放鎖 

② 加鎖解鎖,lock/unlock必須同時出現(xiàn)并保證調(diào)用

4、宕機

① 部署了微服務(wù)jar包的機器掛了,代碼層面根本沒有走到finally這塊, 沒辦法保證解鎖,這個key沒有被刪除,需要加入一個過期時間限定key

② 需要對lockKey有過期時間的設(shè)定

5、設(shè)置key+過期時間分開

① 設(shè)置key+過期時間分開了,必須要合并成一行具備原子性

6、張冠李戴,刪除了別人的鎖

① 設(shè)置鎖失效時間不合理

7、finally塊的判斷+del刪除操作不是原子性的

① 用redis自身的事務(wù)

i 未使用watch前:

如何用redis來實現(xiàn)分布式鎖

如何用redis來實現(xiàn)分布式鎖

ii使用watch后:

如何用redis來實現(xiàn)分布式鎖

如何用redis來實現(xiàn)分布式鎖

② 用Lua腳本

Redis可以通過eval命令保證代碼執(zhí)行的原子性

java配置類:

package com.lau.boot_redis01.util;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class RedisUtil {
    private static JedisPool jedisPool;

  static {
   JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(20);
        jedisPoolConfig.setMaxIdle(10);

        jedisPool = new JedisPool(jedisPoolConfig,"127.0.0.1",6379,100000);
    }

    public static Jedis getJedis() throws Exception{
        if (null!=jedisPool){
            return jedisPool.getResource();
        }
        throw new Exception("Jedispool is not ok");
    }
}
Jedis jedis = RedisUtil.getJedis();

            String script = "if redis.call('get', KEYS[1]) == ARGV[1]"+"then "
                    +"return redis.call('del', KEYS[1])"+"else "+ "  return 0 " + "end";
            try{
                Object result = jedis.eval(script, Collections.singletonList(REDIS_LOCK), Collections.singletonList(value));

                if ("1".equals(result.toString())){
                    System.out.println("------del REDIS_LOCK_KEY success");
                }
                else {
                    System.out.println("------del REDIS_LOCK_KEY error");
                }
            }finally {
                if (null != jedis){
                    jedis.close();
                }
            }

8、仍然存在的問題(redisson得以解決)

① Redis分布式鎖如何續(xù)期? 確保redisLock過期時間大于業(yè)務(wù)執(zhí)行時間的問題(鎖續(xù)期)

② redis單點故障——redis異步復(fù)制造成的鎖丟失, 比如:主節(jié)點沒來的及把剛剛set進來這條數(shù)據(jù)給從節(jié)點,就掛了。(zk/cp、redis/ap)(redis集群)

確保redisLock過期時間大于業(yè)務(wù)執(zhí)行時間的問題;redis集群環(huán)境下,我們自己寫的也不OK, 直接上RedLock之Redisson落地實現(xiàn)

1、RedisConfig.java

    @Bean
    public Redisson redisson(){
       Config config = new Config();
            config.useSingleServer().setAddress("redis://"+redisHost+":6379").setDatabase(0);
       return (Redisson) Redisson.create(config);
    }

2、控制器類: 

package com.lau.boot_redis01.controller;

import com.lau.boot_redis01.util.RedisUtil;
import lombok.val;
import org.redisson.Redisson;
import org.redisson.RedissonLock;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import redis.clients.jedis.Jedis;

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

@RestController
public class GoodController_Redisson {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Value("${server.port}")
    private String serverPort;

    private static final String REDIS_LOCK = "atguigulock";

    @Autowired
    private Redisson redisson;

    @GetMapping("/buy_goods2")
    public String buy_Goods() throws Exception {
        String value = UUID.randomUUID().toString() + Thread.currentThread().getName();

        RLock lock = redisson.getLock(REDIS_LOCK);

        try{
            lock.lock();

            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            if (goodsNumber > 0){
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");

                System.out.println("你已經(jīng)成功秒殺商品,此時還剩余:" + realNumber + "件"+"\t 服務(wù)器端口: "+serverPort);

                return "你已經(jīng)成功秒殺商品,此時還剩余:" + realNumber + "件"+"\t 服務(wù)器端口: "+serverPort;
            }

            System.out.println("商品已經(jīng)售罄/活動結(jié)束/調(diào)用超時,歡迎下次光臨"+"\t 服務(wù)器端口: "+serverPort);

            return "商品已經(jīng)售罄/活動結(jié)束/調(diào)用超時,歡迎下次光臨"+"\t 服務(wù)器端口: "+serverPort;
        }
        finally {
            //IllegalMonitorStateException:attempt unlock lock,not locked by current thread by node_id
            if(lock.isLocked() && lock.isHeldByCurrentThread()){
                lock.unlock();
            }
        }
    }
}

注:在并發(fā)多的時候就可能會遇到這種錯誤,可能會被重新?lián)屨?/p>

 如何用redis來實現(xiàn)分布式鎖

到此,相信大家對“如何用redis來實現(xiàn)分布式鎖”有了更深的了解,不妨來實際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進入相關(guān)頻道進行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI