栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Redis分布式锁

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Redis分布式锁

初始分布式锁:

使用分布式锁的场景:

1、防止缓存击穿:缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。没有使用分布式锁的话,所有线程都是并发访问数据库的,如果使用分布式锁,那么所有的请求都会变成串行请求,大大减少了数据库的压力。

2、防止秒杀超卖:这种情况往往都会有大量线程并发访问,如果很多线程都进入if判断语句中,那么就会出现超卖的问题。

多个服务间 + 保证同一时刻内 + 同一用户只能有一个请求(防止关键业务出现数据冲突和并发错误)

创建两个Springboot模块,用来模拟两台机器卖一个商品,两个模块内容除了端口号之外,一模一样。

依赖

     
        
        
            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
        
        
            org.springframework.boot
            spring-boot-starter-testtest
            
                
                    org.junit.vintage
                    junit-vintage-engine
                
            
        
    

配置文件

package com.example.demo.config;

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 {
    @Bean
    public RedisTemplate redisTemplate(LettuceConnectionFactory connectionFactory){
        RedisTemplate redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return redisTemplate;
    }
}

业务逻辑

package com.example.demo.controller;

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;

@RestController
public class GoodController{

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

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

    @GetMapping("/buy_goods")
    public String buy_Goods(){

        // get key ====看看库存的数量够不够
        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", String.valueOf(realNumber));
            System.out.println("成功买到商品,库存还剩下: "+ realNumber + " 件" + "t服务提供端口" + serverPort);
            return "成功买到商品,库存还剩下:" + realNumber + " 件" + "t服务提供端口" + serverPort;
        }else{
            System.out.println("商品已经售完/活动结束/调用超时,欢迎下次光临" + "t服务提供端口" + serverPort);
        }

        return "商品已经售完/活动结束/调用超时,欢迎下次光临" + "t服务提供端口" + serverPort;
    }

}

功能简单测试:

redis:set goods:001 100 

目前功能正常

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

解决办法:

        1、加synchronized

        2、加ReentrantLock

        3、还是都可以

得看具体的业务逻辑

        1、如果加synchronized,那么必须等着进入synchronized的线程释放锁了其他线程才能进来,这样很容易导致线程积压和拥堵。

        2、加ReentrantLock,有一个tryLock()方法,可以避免因为之前的线程不释放锁而导致线程积压。后进来的线程有两种选择,一是觉得等待的时间太长了,放弃等待;二是给这个线程一个规定的时间,在规定的时间内拿不到锁在放弃等待。这样可以规避使用synchronized死等的情况

目前情况下,加了synchronized或者ReentrantLock,单机版情况下是没有问题的。

如果分布式部署以后,单机锁还是会出现超卖现象,需要使用分布式锁

        客户端发起请求,然后有nginx负责转发,后端服务器1加的锁对后端服务器2的锁不起作用

 采用Windows上的nginx做反向代理和负载均衡

 测试效果:目前来说 好像没有问题

 使用apache-jmeter做压力测试

 开始测试

发现出现两个模块卖出同一件商品,相当于出现超卖,有问题。单机锁无法解决分布式部署后的超卖问题。因此需要分布式锁。

使用redis的分布式锁 setNX

添加一个常量key

 生成一个随机值作为value,然后使用setNX保存到redis数据库中,后面执行业务逻辑。

 解锁 删除key 

 在使用redis分布式锁的时候,要注意释放锁,如果因为出现异常不能释放锁,会出现死锁的情况。必须要在代码层面加个finally来释放锁

即使是将解锁的代码放在finally代码块中,依旧会遇到一些极端的问题,比如部署这个模块的服务器宕机了,那么代码就运行不到finally代码块中,就释放不了锁,因此需要对这个REDIS_LOCK做一个过期时间,可以完成自动解锁。

这样如果代码能正常执行,就能走finally这里删除这个锁,不然就等着时间过期。

问题又来了,这样只能保证加锁,不能保证原子性。设置key+过期时间分开了,必须要合并成一行具备原子性。setIfAbsent本身具有原子性。

 这样虽然能保存设置key+过期时间操作的原子性,但是还有一个问题,如果一个线程在本次操作中加了锁,然后在调用其他方法,但是这次调用其他方法花费了很长时间,平时5秒就能完成的,现在需要12秒,这样就超过了设置的过期时间10秒,会释放锁,其他线程就能抢到锁,但是这个线程还没有有操作完,等这个线程走到finally去删除锁的时候,就会把别人的线程的上的锁删掉了。

为了避免这个问题,需要在删除锁的时候,判断一下删除的是不是自己加的锁,因为之前加锁设置key-value的时候,value是唯一的,因此删除key的时候,先通过key查询value,然后判断一下这个value是否是自己生成的,在进行删除操作。

 即使删除key时进行了判断,但是判断和删除不具有原子性(个人理解的一种情况:刚好线程A从redis里获取key的时间是10秒,然后就自动删除了,这时候线程B来创建了一个key,但是之前线程A刚好判断满足删除key的条件,依旧会把线程B创建的key删除)。问题就在于finally语句块中的判断和del删除操作不是原子性的。可以使用lua脚本,也可以使用redis本身的事务。

        采用redis事务保证原子性

        采用lua脚本:Redis调用Lua脚本通过eval命令保证代码执行的原子性

        创建一个utils

package com.example.demo.utils;


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

public class RedisUtils {

    private static JedisPool jedisPool;

    static {
        JedisPoolConfig jpc = new JedisPoolConfig();
        jpc.setMaxTotal(20);
        jpc.setMaxIdle(10);
        jedisPool = new JedisPool(jpc,"192.168.25.135",6379);
    }

    public static Jedis getJedis() throws Exception{
        if(jedisPool == null)
            throw new NullPointerException("JedisPool is not OK.");
        return jedisPool.getResource();
    }

}

还有问题需要保证:确保redisLock过期时间大于业务执行时间的问题——redis分布式锁如何续期?

集群 + CAP对比ZooKeeper 对比ZooKeeper,重点,CAP

  • Redis - AP -redis异步复制造成的锁丢失,比如:主节点没来的及把刚刚set进来这条数据给从节点,就挂了。
  • ZooKeeper - CP

CAP

  • C:Consistency(强一致性)
  • A:Availability(可用性)
  • P:Partition tolerance(分区容错性)

综上所述

Redis集群环境下,我们自己写的也不OK,直接上RedLock之Redisson落地实现。

在RedisConfig中添加配置

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

修改业务逻辑代码

package com.example.demo.controller;

import com.example.demo.utils.RedisUtils;
import org.redisson.Redisson;
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 redis.clients.jedis.JedisPool;

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

@RestController
public class GoodController{
    private final ReentrantLock lock=new ReentrantLock();

    @Autowired
    private Redisson redisson;

    public static final String REDIS_LOCK="LOCK";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

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

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

        RLock redissonLock = redisson.getLock(REDIS_LOCK);

        redissonLock.lock();//加锁

        try {

            

//            //相当于加锁
//            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value,10l, TimeUnit.SECONDS);//setNX+设置过期时间
//            //stringRedisTemplate.expire(REDIS_LOCK,10l, TimeUnit.SECONDS);//10秒后自动解锁
//
//            if (!flag){
//                //进入到这里,就说明抢锁失败
//                return "抢锁失败!";
//            }

            String result = stringRedisTemplate.opsForValue().get("goods:001");// get key ====看看库存的数量够不够
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);
            if(goodsNumber > 0){
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
                System.out.println("成功买到商品,库存还剩下: "+ realNumber + " 件" + "t服务提供端口" + serverPort);
                return "成功买到商品,库存还剩下:" + realNumber + " 件" + "t服务提供端口" + serverPort;
            }else{
                System.out.println("商品已经售完/活动结束/调用超时,欢迎下次光临" + "t服务提供端口" + serverPort);
            }

            return "商品已经售完/活动结束/调用超时,欢迎下次光临" + "t服务提供端口" + serverPort;
        }finally {

            redissonLock.unlock();//解锁


//            //redis调用lua脚本
//            Jedis jedis = RedisUtils.getJedis();
//            //Lua脚本,来自于官网
//            String script = "if redis.call('get', KEYS[1]) == ARGV[1] "
//                    + "then "
//                    + "    return redis.call('del', KEYS[1]) "
//                    + "else "
//                    + "    return 0 "
//                    + "end";
//
//            try {
//                //执行脚本
//                Object o = jedis.eval(script, Collections.singletonList(REDIS_LOCK),
//                        Collections.singletonList(value));
//
//                if("1".equals(o.toString())) {
//                    System.out.println("---del redis lock ok.");
//                }else {
//                    System.out.println("---del redis lock error.");
//                }
//
//            }finally {
//                //释放资源
//                if (jedis!=null){
//                    jedis.close();
//                }
//            }

//            //采用redis事务
//            while (true){
//                //watch 相当于一个乐观锁,用来监控REDIS_LOCK
//                stringRedisTemplate.watch(REDIS_LOCK);
//                if (stringRedisTemplate.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)){
//                    //设置支持事务
//                    stringRedisTemplate.setEnableTransactionSupport(true);
//                    //开启事务
//                    stringRedisTemplate.multi();
//                    //命令入队
//                    stringRedisTemplate.delete(REDIS_LOCK);
//                    List list = stringRedisTemplate.exec();
//                    if (list==null){//说明之前watch监控的REDIS_LOCK已经被人修改了
//                        continue;
//                    }
//
//                }
//                stringRedisTemplate.unwatch();
//                break;
//            }

//            //解锁
//            if (stringRedisTemplate.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)){
//                stringRedisTemplate.delete(REDIS_LOCK);
//            }

        }

    }
}
 

在进行并发测试,发现没有出现超卖问题。

目前看来是没有问题的,但是可能在超高并发的情况下,会出现一个问题

IllegalMonitorStateException: attempt to unlock lock,not loked by current thread by node id:da6385f-81a5-4e6c-b8c0

这个错误就是有可能当前线程和解锁的线程不是同一个。

IllegalMonitorStateException:非法监视器状态异常。

问题出在finally的redissonLock.unlock();这里。

解决办法:在finally语句块中添加判断

redis分布式锁总结回顾:

synchronized单机版oK,nginx分布式微服务下单机锁不行;

取消单机锁,上Redis分布式锁setnx;

只加了锁,没有释放锁,出异常的话,可能无法释放锁,必须要在代码层面finally释放锁;

宕机了,部署了微服务代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要有lockKey的过期时间设定;

为redis的分布式锁key,增加过期时间,此外,还必须要setnx+过期时间必须同一行

必须规定只能自己删除自己的锁,你不能把别人的锁删除了,防止张冠李戴,1删2,2删3;

Redis集群环境下,我们自己写的也不oK,直接上RedLock之Redisson落地实现。

=========================================================================

Redis分布式锁源码解析:

https://www.cnblogs.com/kiko2014551511/p/11527108.html

使用redisson加分布式锁就比较简洁,三行代码,加锁解锁就两行代码。

构造方法:

@Autowired
private Redisson redisson;

public static final String REDIS_LOCK="LOCK";

//获取锁
RLock redissonLock = redisson.getLock(REDIS_LOCK);

调用的构造方法:主要是创建一个Rlock对象,同时初始化了一个看门狗的时间,默认为30秒

//这个name就是传进来的key
public RLock getLock(String name) {
       return new RedissonLock(this.connectionManager.getCommandExecutor(), name);
}

//构造一个RedissonLock对象,包括执行器commandExecutor和我们传入的lockKey
public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
        super(commandExecutor, name);
        this.commandExecutor = commandExecutor;
        this.id = commandExecutor.getConnectionManager().getId();
        //这里初始化了一个看门狗时间,watch dog,就是用来续命的watch dog,默认是30s,
        //这值是可以改的,在构建RedssionClient的时候可以动态传入
        //watch dog的续命时间
        this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
        this.entryName = this.id + ":" + name;
        this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub();
}

加锁

redissonLock.lock();//加锁

加锁底层的核心就是执行一段lua脚本。lua脚本是往redis中存储一个hash,redis的hash就相当于java中的>;

lua脚本的大概意思就是:首先判断一下需要加锁的key是否存在,如果不存在,就进行加锁,并设置一个过期时间30秒,然后返回。此时相当于是>;如果存在的话,就判断当前线程是不是持有锁的线程,如果是的话,就进行自增(可重入锁),并刷新过期时间,如果不是的话,就获取这个key的过期时间,然后返回这个过期时间。

 RFuture tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand command) {
        this.internalLockLeaseTime = unit.toMillis(leaseTime);
        return this.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command, 
    //这段就是需要执行的lua脚本
    "if (redis.call('exists', KEYS[1]) == 0) 
    then redis.call('hincrby', KEYS[1], ARGV[2], 1); 
    redis.call('pexpire', KEYS[1], ARGV[1]); 
    return nil; 
    end; 
    if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) 
    then redis.call('hincrby', KEYS[1], ARGV[2], 1); 
    redis.call('pexpire', KEYS[1], ARGV[1]); 
    return nil;
    end; 
    return redis.call('pttl', KEYS[1]);", 
    Collections.singletonList(this.getName()), this.internalLockLeaseTime,             
    this.getLockName(threadId));
}

这里需要考虑一个问题,锁有过期时间,那么线程在持有锁的过程中,锁过期了怎么办?redisson提供了一个watch dog机制(看门狗,默认10秒 执行一次),当执行完加锁的lua脚本加锁成功后,会执行一个异步操作,异步回调scheduleExpirationRenewal方法,该方法也是执行的lua脚本,先判断一下自己是否是锁的持有人,如果是的话,那么就刷新一下hash key的存活时间。

锁等待:

在while循环中,不断重新尝试获取锁,直到成功。当然轮询的时候会根据拿到的ttl,等待指定时间之后,再尝试去获取锁。

当线程尝试用lua脚本加锁的时候,发现已经被别的线程先加上锁了,那么就会返回这个hash key的存活时间(ttl),也就是这个锁的存活时间。如果这个锁的存活时间为null,说明当前线程就是当前加锁线程,可以直接跳出循环;如果存活时间大于等于0,说明锁还没有过期,当前线程会等待指定时间之后再去尝试获取锁;如果存活时间小于0,那么说明锁已过期,当前线程会立刻尝试去获取锁。

释放锁:

释放锁的核心代码也是执行的lua脚本。

protected RFuture unlockInnerAsync(long threadId) {
        return this.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, 
    "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then 
    return nil;
    end; 
    local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
     if (counter > 0) then 
    redis.call('pexpire', KEYS[1], ARGV[2]); 
    return 0; 
    else redis.call('del', KEYS[1]); 
    redis.call('publish', KEYS[2], ARGV[1]); 
    return 1; 
    end; 
    return nil;", 
    Arrays.asList(this.getName(), this.getChannelName()), LockPubSub.UNLOCK_MESSAGE, this.internalLockLeaseTime, this.getLockName(threadId));
}

如果不是当前持有锁的人,那么立刻返回;如果是,那么对hash的内部key进行减一操作,然后查看结果值,如果结果值大于0,那么刷新hash key的过期时间;如果结果值小于等于0,那么就删除hash key,并发布锁释放的消息。

转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号