千家信息网

如何用redis来实现分布式锁

发表于:2024-11-12 作者:千家信息网编辑
千家信息网最后更新 2024年11月12日,本篇内容主要讲解"如何用redis来实现分布式锁",感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习"如何用redis来实现分布式锁"吧!一、建Moduleboo
千家信息网最后更新 2024年11月12日如何用redis来实现分布式锁

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

一、建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=1111spring.redis.database=0spring.redis.host=localhostspring.redis.port=6379#连接池最大连接数(使用负值表示没有限制)默认8spring.redis.lettuce.pool.max-active=8#连接池最大阻塞等待时间(使用负值表示没有限制)默认-1spring.redis.lettuce.pool.max-wait=-1#连接池中的最大空闲连接默认8spring.redis.lettuce.pool.max-idle=8#连接池中的最小空闲连接默认0spring.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;@Configurationpublic 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;@RestControllerpublic 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前:

ii使用watch后:

② 用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;@RestControllerpublic 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来实现分布式锁"有了更深的了解,不妨来实际操作一番吧!这里是网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

0