基于Redis的分布式锁

本文详细介绍了在高并发场景下,JVM锁的局限性及分布式锁的必要性。通过实例展示了基于Redis的分布式锁实现,分析了存在的死锁问题并提出了解决方案,包括设置锁过期时间、使用唯一值标识锁以及通过定时器续时。最后提到了Redisson框架在解决分布式锁问题上的优势。
摘要由CSDN通过智能技术生成

1.使用场景

由于当今软件的部署方式大多数都是以分布式的方式进行部署的,那么由于这种部署形式的盛行,基于JVM的锁在并发量大的情况下仍有可能出现BUG。例如在秒杀,抢购等场景中,当所有线程都使用同一资源的情况下,JVM的锁是不再适用的。所以推行分布式锁进行资源管理。

2.架构图

分布式架构
由图可见,当每个请求发送到单个JVM的时候,是无法保证资源的同一性,在秒杀等一些场景当中,仍然会出现超卖现象。所以需要使用一个公有资源来完成锁上锁的行为。而基于Redis的分布式锁便可以很好的实现。

首先Redis本身就是单线程的,所以可以保证在任何一个时间节点下,只有一个线程可以访问;其次Redis是基于内存的存储单元,效率很高。所以使用Redis来做分布式锁是十分合适的。

3.分布式锁的实现

@RestController
public class RedisLockController {

    @Autowired
    RedisTemplate redisTemplate;

	@RequestMapping("/doSkillMethod")
    public String doLockServer(){
        //设置一个锁的key
        String lockKey = "lock";
        //使用redis的setnx方法来确定程序是否已经加锁
        boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockKey,"success");
        //如果已上锁,则直接返回
        if(!isLock){
            return "false";
        }
        //执行对应的业务
        Integer stock = (Integer) redisTemplate.opsForValue().get("stock");
        if(stock>0){
            stock --;
            redisTemplate.opsForValue().set("stock", stock);
        }else{
            return "库存不足";
        }
        //释放锁
        redisTemplate.delete("stock");
        return "true";
    }
}

该程序可以简单的实现分布式锁。逻辑主要是利用redis的setNX方法(如果key不存在才会设置,存在则不会设置),相当于在内存中加一个标记,当每个线程调用该方法后,首先都会先从redis中查看该标记是否已经被标记。

但是该分布式锁却存在很多BUG,不可以投入到生产环境中。

问题:
1.当程序加锁后,运行当中出现了异常,则会无法释放锁,导致死锁。
2.在程序运行途中,服务器宕机,导致程序没有跑完,锁没有释放而导致死锁。

解决方法:
1.在程序段上增加try,finally。保证一定会释放锁。
2.在设置锁的时候增加过期时间,到时间自动删除锁。

代码改进:

@RestController
public class RedisLockController {

    @Autowired
    RedisTemplate redisTemplate;
	
	@RequestMapping("/doSkillMethod")
    public String doLockServer(){
        //设置一个锁的key
        String lockKey = "lock";
        try{
            //使用redis的setnx方法来确定程序是否已经加锁,设置过期时间为10s
            boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockKey,"success", 10, TimeUnit.SECONDS);
            //如果已上锁,则直接返回
            if(!isLock){
                return "false";
            }
            //执行对应的业务
            Integer stock = (Integer) redisTemplate.opsForValue().get("stock");
            if(stock>0){
                stock --;
                redisTemplate.opsForValue().set("stock", stock);
            }else{
                return "库存不足";
            }
        }finally {
            //释放锁,若出现异常,则也会必定释放锁
            redisTemplate.delete("stock");
            return "true";
        }
    }
}

给锁设置了过期时间后又会出现新的BUG。例如当线程A先获得了锁,但是在10s的过期时间内由于各种原因并没有运行完程序,A上的锁则会自动消失;而此时线程B则会进入,加锁。而此时,线程A正好完成了所有的程序步骤,则会将线程B的锁释放掉。于是就会出现A释放B的锁,B释放C的锁。。。。导致程序紊乱,超卖显现再次发生。所以该方式的分布式锁也是不可以投入生产环境的。
于是我们给锁设置一个唯一的值,用于标记。保证每个线程只会删除自己的锁,不会误删其他线程的锁。

@RestController
public class RedisLockController {

    @Autowired
    RedisTemplate redisTemplate;

    @RequestMapping("/doSkillMethod")
    public String doLockServer(){
        //设置一个锁的key
        String lockKey = "lock";
        //设置唯一锁的值
        String uLockValue = UUID.randomUUID().toString();
        try{
            //使用redis的setnx方法来确定程序是否已经加锁,设置过期时间为10s
            boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockKey,uLockValue, 10, TimeUnit.SECONDS);
            //如果已上锁,则直接返回
            if(!isLock){
                return "false";
            }
            //执行对应的业务
            Integer stock = (Integer) redisTemplate.opsForValue().get("stock");
            if(stock>0){
                stock --;
                redisTemplate.opsForValue().set("stock", stock);
            }else{
                return "库存不足";
            }
        }finally {
            //判断当前锁是否是自己设置的锁
            if(uLockValue.equals(redisTemplate.opsForValue().get(lockKey))){
                //释放锁,若出现异常,则也会必定释放锁
                redisTemplate.delete("stock");
                return "true";
            }else{
                return "true";
            }
        }
    }
}

但是,该方法只能解决每个线程只能释放自己的锁,而没办法解决锁自动过期后其他线程进入程序的BUG。
面对这一问题,我们可以在设置锁后抛出一个新的线程,每过一段时间(小于锁的设置时间),查看当前锁是否还存在,如果存在则将锁的时间设置为原先的过期时间(给锁续时)。

@RestController
public class RedisLockController {

    @Autowired
    RedisTemplate redisTemplate;

    @RequestMapping("/doSkillMethod")
    public String doLockServer(){
        //设置一个锁的key
        String lockKey = "lock";
        //设置唯一锁的值
        String uLockValue = UUID.randomUUID().toString();
		//声明一个线程
        Thread thread = null;
        try{
            //使用redis的setnx方法来确定程序是否已经加锁,设置过期时间为10s
            boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockKey,uLockValue, 10, TimeUnit.SECONDS);
            //如果已上锁,则直接返回
            if(!isLock){
               Thread.sleep(100);
                doLockServer();
            }
            //给锁续时(抛出一个新的线程,在新的线程里面重新设置过期时间,时间间隔为过期时间的三分之一比较合适)
            thread = setTimer(lockKey, uLockValue);
            //执行对应的业务
            Integer stack = (Integer) redisTemplate.opsForValue().get("stackNum");

            System.out.println(stack);
            if(stack>0){
                stack --;
                System.out.println(stack);
                redisTemplate.opsForValue().set("stackNum", stack);
            }else{
                return "库存不足";
            }
        }finally {
            //结束计时器线程
            thread.stop();
            //判断当前锁是否是自己设置的锁
            if(uLockValue.equals(redisTemplate.opsForValue().get(lockKey))){
                //释放锁,若出现异常,则也会必定释放锁
                redisTemplate.delete("lockKey");
                return "true";
            }else{
                return "true";
            }
        }
    }

    public Thread setTimer(String lockKey, String uLockValue){
        final long timeInterval = 3000;
        Runnable runnable = new Runnable(){
            public void run() {
                while(true){
                    try {
                        Thread.sleep(timeInterval);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }finally {
                        if(uLockValue.equals(redisTemplate.opsForValue().get(lockKey))){
                            redisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);
                        }
                    }
                }
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
        return thread;
    }
}

添加一个计时器,可以有效的保证锁不会提前过期。一般在生产环境当中,大多数都会使用Redisson框架来实现分布式锁。

4.使用Redisson实现分布式锁

Redisson是一个基于Java实现的一个Redis客户端框架,主要运用于分布式架构。使用内部的锁架构可以有效的避免超卖等问题的发生。

1.添加Redisson的依赖

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

2.添加配置文件

@Configuration
public class RedissonConfig {

    @Bean
    public Redisson redisson(){
        Config config = new Config();
        //设置为单机地址的配置
        config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
        return (Redisson) Redisson.create(config);
    }
}

3.实现分布式锁

@RestController
public class RedissonLockController {
    @Autowired
    private Redisson redisson;

    @Autowired
    private RedisTemplate redisTemplate;

    @RequestMapping("/doSkillMethodRedisson")
    public String doRedissonLockServer(){
        //生成一个锁
        RLock redissonLock = redisson.getLock("lockKey");
        try{
            //给锁设置过期时间(没有参数会打开看门狗模式,从而启动一个守护线程,动态更新锁的时间)
            redissonLock.lock();
            //获取库存数量
            int stackNum = (int) redisTemplate.opsForValue().get("stackNum");
            if(stackNum>0){
                stackNum --;
                redisTemplate.opsForValue().set("stackNum", stackNum);
            }else{
                System.out.println("库存不足!");
            }
        }finally {
            //释放锁
            redissonLock.unlock();
        }
        return "true";
    }
}

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值