如何用redis来实现分布式锁

本篇内容主要讲解“如何用redis来实现分布式锁”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“如何用redis来实现分布式锁”吧!

创新互联建站从2013年创立,先为吉安等服务建站,吉安等地企业,进行企业商务咨询服务。为吉安企业网站制作PC+手机+微官网三网同步一站式服务解决您的所有建站问题。

一、建Module

boot_redis01

boot_redis02

二、改POM



    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        2.4.4
         
    
    com.lau
    boot_redis01
    0.0.1-SNAPSHOT
    boot_redis01
    Demo project for Spring Boot
    
        1.8
    
    
        
             org.springframework.boot
             spring-boot-starter-web
        
        
        
             org.springframework.boot
             spring-boot-starter-actuator
        
        
        
             org.springframework.boot
             spring-boot-starter-data-redis
        
        
        
             org.apache.commons
             commons-pool2
        
        
        
             redis.clients
             jedis
             3.1.0
        
        
        
             org.springframework.boot
             spring-boot-starter-aop
        
        
        
             org.redisson
             redisson
             3.13.4
        
        
             org.springframework.boot
             spring-boot-devtools
             runtime
             true
        
        
             org.projectlombok
             lombok
             true
        
        
             junit
             junit
             4.12
        
    
    
        
            
                 org.springframework.boot
                 spring-boot-maven-plugin
             
        
    

三、写YML

server.port=1111

spring.redis.database=0
spring.redis.host=localhost
spring.redis.port=6379
#连接池最大连接数(使用负值表示没有限制)默认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);
    }

}

五、业务类

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 RedisTemplateredisTemplate(LettuceConnectionFactory connectionFactory){
        RedisTemplate 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与锁过期两条命令合二为一,是为了解决命令分开执行引发的原子性问题:
            //setnx  中间会被其它redis客户端命令加塞   2、expire
            //3①、为了避免线程执行业务时间大于锁过期时间导致窜行操作,再释放锁时应判断是否是自己加的锁;
            //还有另外一种解决方案:锁续期——额外开启一个守护线程定时给当前key加超时时间(如5s到期,每2.5s ttl判断一次,并加2.5s超时时间,不断续期,线程将使用主动删除key命令的方式释放锁;另,当此redis客户端命令宕机后,此守护线程会自动随之消亡,不会再主动续期——此机制使得其它redis客户端可以获得锁,不会发生死锁或长期等待)
            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);//setnx

            if(!flag){
                return "获取锁失败!";
            }

            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("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort);

                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort;
            }

            System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort);

            return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort;
        }
        finally {
//            if(stringRedisTemplate.opsForValue().get(REDIS_LOCK).equals(value)){
//                stringRedisTemplate.delete(REDIS_LOCK);
//            }

              //3②这里也存在命令的原子问题:获取当前key经相等判断后与删除对应key是两个不同命令,中间会被加塞
              //解决方法1:redis事务
//            stringRedisTemplate.watch(REDIS_LOCK);
//            while(true){
//                if(stringRedisTemplate.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)){
//                    stringRedisTemplate.setEnableTransactionSupport(true);
//                    stringRedisTemplate.multi();
//                    stringRedisTemplate.delete(REDIS_LOCK);
//
//                    List 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、单机版没加锁

 问题:没有加锁,并发下数字不对,会出现超卖现象

① synchronized 不见不散

② ReentrantLock 过时不候

 
在单机环境下,可以使用synchronized或Lock来实现。
 
但是在分布式系统中,因为竞争的线程可能不在同一个节点上(同一个jvm中),所以需要一个让所有进程都能访问到的锁来实现,比如redis或者zookeeper来构建;
 
不同进程jvm层面的锁就不管用了,那么可以利用第三方的一个组件,来获取锁,未获取到锁,则阻塞当前想要运行的线程

2、使用Nginx配置负载均衡

注:分布式部署后,单机锁还是出现超卖现象,需要分布式锁

启动两个微服务1111和2222,访问使用:http://localhost/buy_goods(即通过nginx轮询方式访问1111和2222两个微服务)

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{#反向代理的服务器列表,权重相同,即负载均衡使用轮训策略
		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、异常将导致锁不会释放

① 出异常的话,可能无法释放锁, 必须要在代码层面finally释放锁 

② 加锁解锁,lock/unlock必须同时出现并保证调用

4、宕机

① 部署了微服务jar包的机器挂了,代码层面根本没有走到finally这块, 没办法保证解锁,这个key没有被删除,需要加入一个过期时间限定key

② 需要对lockKey有过期时间的设定

5、设置key+过期时间分开

① 设置key+过期时间分开了,必须要合并成一行具备原子性

6、张冠李戴,删除了别人的锁

① 设置锁失效时间不合理

7、finally块的判断+del删除操作不是原子性的

① 用redis自身的事务

i 未使用watch前:

如何用redis来实现分布式锁

如何用redis来实现分布式锁

ii使用watch后:

如何用redis来实现分布式锁

如何用redis来实现分布式锁

② 用Lua脚本

Redis可以通过eval命令保证代码执行的原子性

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分布式锁如何续期? 确保redisLock过期时间大于业务执行时间的问题(锁续期)

② redis单点故障——redis异步复制造成的锁丢失, 比如:主节点没来的及把刚刚set进来这条数据给从节点,就挂了。(zk/cp、redis/ap)(redis集群)

确保redisLock过期时间大于业务执行时间的问题;redis集群环境下,我们自己写的也不OK, 直接上RedLock之Redisson落地实现

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("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort);

                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort;
            }

            System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort);

            return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort;
        }
        finally {
            //IllegalMonitorStateException:attempt unlock lock,not locked by current thread by node_id
            if(lock.isLocked() && lock.isHeldByCurrentThread()){
                lock.unlock();
            }
        }
    }
}

注:在并发多的时候就可能会遇到这种错误,可能会被重新抢占

 如何用redis来实现分布式锁

到此,相信大家对“如何用redis来实现分布式锁”有了更深的了解,不妨来实际操作一番吧!这里是创新互联网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!


本文名称:如何用redis来实现分布式锁
URL网址:http://pwwzsj.com/article/pcjiph.html