详解分布式锁

知识点: 

单体锁存在的问题:

  • 单体锁,即单体应用中的锁,通过加单体锁(synchronized或RentranLock)可以保证单个实例并发安全

  • 单体锁是JVM层面的锁,只能保证单个实例上的并发访问安全

  • 如果将单体应用部署到多个tomcat实例上,由负载均衡将请求分发到不同的实例

  • 每个tomocat实例都是一个JVM进程,多实例下会存在数据一致性问题。

分布式锁:

  • 分布式应用中所有线程都去获取同一把锁,但只有一个线程可以成功的获得锁,其他没有获得锁的线程必须全部等待,直到持有锁的线程释放锁。

  • 分布式锁是可以跨越多个tomcat实例,多个JVM进程的锁,所以分布式锁都是设计在第三方组件中的

  • 分布式锁都是通过第三方组件来实现的,目前主流的解决方案是使用Redis或Zookeeper来实现分布式锁

存在的问题:

出现用户超买,商家超卖的问题
具体案例:

添加相关依赖:

        <!--   redis     -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

配置application.yml

# 添加redis数据库
spring:
  redis:
    port: 6379
    database: 1
    host: 127.0.0.1

编写具体实例:

@RestController
@RequiredArgsConstructor
public class LockController {

    private final StringRedisTemplate redisTemplate;

    @SneakyThrows
    @GetMapping("/deductStock")
    public String deductStock() {
        System.out.println("用户正在下单……");

        /**
         * 单体锁
         */
        int total = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
        if (total > 0) {
            total = total - 1;
            Thread.sleep(3000);
            redisTemplate.opsForValue().set("stock", String.valueOf(total));
            System.out.println("下单成功!剩余库存为:" + total);
            return "下单成功!剩余库存为:" + total;
        }
        System.out.println("用户下单失败!");
        return "下单失败!剩余库存为:" + total;
    }
}

测试(点击要快):

我们模拟了系统休眠 ,多线程同时进入一个方法体中,此时,票100同时卖给了两个用户!

解决方案:
 

单体锁:

使用synchronized关键字:修改方法或代码块,用于实现同步控制。当一个线程进入synchronized修饰的方法或代码块时,其他线程需要等待该线程执行完毕后才能进入。

 其中,this关键字指的是,该类的具体实例,即LockController类的具体实例:

@RestController
@RequiredArgsConstructor
public class LockController {

    private final StringRedisTemplate redisTemplate;

    @SneakyThrows
    @GetMapping("/deductStock")
    public String deductStock() {
        System.out.println("用户正在下单……");

        /**
         * 单体锁
         */
        synchronized (this) {
            int total = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
            if (total > 0) {
                total = total - 1;
                Thread.sleep(3000);
                redisTemplate.opsForValue().set("stock", String.valueOf(total));
                System.out.println("下单成功!剩余库存为:" + total);
                return "下单成功!剩余库存为:" + total;
            }
            System.out.println("用户下单失败!");
            return "下单失败!剩余库存为:" + total;
        }
    }
}

测试结果:

虽然单体锁,解决了在同一个类中,多线程进入方法的问题,但是,当LockController并非单例,也会出现超卖现象: 

存在问题:当项目部署到集群服务器中,由反向代理服务器,负载均衡。会导致出现多个LockController实例。

解决方法(使用分布式锁):

分布式锁:

首先创建一个工具类,用于注入静态的组件:

@Component
public class ApplicationContextHolder implements ApplicationContextAware {
    private static ApplicationContext ac;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.ac = applicationContext;
    }

    public static <T> T getBean(Class<T> clazz){
        return ac.getBean(clazz);
    }

    public static Object getBean(String name){
        return ac.getBean(name);
    }
}

定义一个工具类,用于获取锁,释放锁:

/**
 * 分布式锁工具类
 */
public class LockUtil {
    private static StringRedisTemplate redisTemplate = ApplicationContextHolder.getBean(StringRedisTemplate.class);

    //获取锁的超时时间(自旋重试时间)
    private static long waitTimeout = 10000L;

    //锁的过期时间,防止死锁
    private static long lockTimeout = 10L;

    /**
     * 获取分布式锁
     */
    public static boolean getLock(String lockName, String value) {
        //计算获取锁的超时时间
        long endTime = System.currentTimeMillis() + waitTimeout;
        //超时之前尝试获取锁
        while (System.currentTimeMillis() < endTime) {
            //判断是否能够获取锁,其实就是判断是否往redis中插入对应的key
            Boolean flag = redisTemplate.opsForValue().setIfAbsent(lockName, value, lockTimeout, TimeUnit.SECONDS);
            if (flag) {
                return true;
            }
        }
        return false;
    }

    /**
     * 释放分布式锁
     */
    public static void unlock(String lockName, String value) {
        if(value.equals(redisTemplate.opsForValue().get(lockName))){
            redisTemplate.delete(lockName);
        }
    }
}

使用分布式锁进行加锁:

@RestController
@RequiredArgsConstructor
public class LockController {

    private final StringRedisTemplate redisTemplate;

    @SneakyThrows
    @GetMapping("/deductStock")
    public String deductStock() {
        System.out.println(Thread.currentThread().getName() + "用户正在下单……");

        /**
         * 分布式锁
         */
        String lockName = "stock_lock";
        String value = UUID.randomUUID().toString();
        if (!LockUtil.getLock(lockName, value)) {
            return "获取锁失败……";
        }
        int total = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
        if (total > 0) {
            total = total - 1;
            Thread.sleep(3000);
            redisTemplate.opsForValue().set("stock", String.valueOf(total));
            System.out.println("下单成功!剩余库存为:" + total);
            LockUtil.unlock(lockName,value); //释放锁
            return "下单成功!剩余库存为:" + total;
        }
        System.out.println("用户下单失败!");
        LockUtil.unlock(lockName,value);
        return "下单失败!剩余库存为:" + total;
    }
}

测试结果:

 存在问题:当用户进入后,拿到锁后,执行后续代码,但是锁到期了,锁被释放出来。后续的用户,也是可以进入线程当中的。依旧会出现抄买现象。

解决方法(第三方库来实现分布式锁 ):判断当前用户是否完成后续操作,如果没有完成就自动续签(加时长),直到用户完成后续操作。

Redisson:

Redisson是一个基于Redis的Java驻留对象框架,它提供了一套易于使用的API,用于操作Redis的数据结构和执行分布式操作。

Redisson是Redis官网推荐实现分布式锁的一个第三方类库,用起来更简单。

执行流程:

  • 只要线程加锁成功(默认锁的超时时间为30s),Redisson就会启动一个用于监控锁的看门狗,它是一个守护线程,会每隔10秒检查一下,如果线程还持有锁,就会不断的延长锁的有效期(即每到20s就会自动续借成30s),也称为自动续期机制

  • 当业务执行完,释放锁后,会关闭守护线程。

  • 从而防止了线程业务还没执行完,而锁却过期的问题 。

首先引入相关依赖:

<!-- redisson -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.24.3</version>
</dependency>

编写代码,调用工具类:

@RestController
@RequiredArgsConstructor
public class LockController {

    private final StringRedisTemplate redisTemplate;
    private final RedissonClient redissonClient;

    @SneakyThrows
    @GetMapping("/deductStock")
    public String deductStock() {
        System.out.println(Thread.currentThread().getName() + "用户正在下单……");

        /**
         * 使用Redisson分布式锁
         */
        String lockName = "stock_lock";
        RLock rLock = redissonClient.getLock(lockName);
        rLock.lock(); //获取锁

        int total = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
        if (total > 0) {
            total = total - 1;
            Thread.sleep(3000);
            redisTemplate.opsForValue().set("stock", String.valueOf(total));
            System.out.println("下单成功!剩余库存为:" + total);
            rLock.unlock();
            return "下单成功!剩余库存为:" + total;
        }
        System.out.println("用户下单失败!");
        rLock.unlock();
        return "下单失败!剩余库存为:" + total;
    }
}

测试结果:

  • 8
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值