redis使用redisson作为客户端配合springboot作为分布式锁的案例+原理

一、引入

作为编程人员,需要关注的一个事情,就是并发运行带来的问题
注意:分布式锁也是不可避免的事情,但是目前作为分布式锁的组件,主推redis,这是一个非常常见的分布式锁,也是程序员必须掌握的。

并发编程需要注意的:

  • 可见性:一个线程对共享变量的修改,另一个线程能立刻看到。缓存可导致可见性问题。
  • 原子性:一个或多个CPU执行操作不被中断。线程切换可导致原子性问题。
  • 有序性:编译器优化可能导致指令顺序发生改变。编译器优化可能导致有序性问题。

一个程序的线程问题

如下图,多个线程去抢占一个资源:
在这里插入图片描述
解决方法,对线程加锁。
在这里插入图片描述
总结来说,Lock与synchronized有以下区别:

  • Lock是一个接口,而synchronized是关键字。
  • synchronized会自动释放锁,而Lock必须手动释放锁。
  • Lock可以让等待锁的线程响应中断,而synchronized不会,线程会一直等待下去。
  • 通过Lock可以知道线程有没有拿到锁,而synchronized不能。
  • Lock能提高多个线程读操作的效率。
  • synchronized能锁住类、方法和代码块,而Lock是块范围内的

多个程序的线程问题

在这里插入图片描述
高可用的环境下,就会出现多个程序抢占一个资源的问题,普通的代码锁明显是无法解决这个问题,这个时候就必须采用分布式锁。
如下图,采用redis作为分布式锁:

在这里插入图片描述

二、什么是分布式锁

在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;

  • 高可用、高性能的获取锁与释放锁; (高可用)
    1、锁的颗粒度要尽量小
    2、锁的范围尽量要小
  • 具备可重入特性;(重入)
  • 具备锁失效机制、防止死锁;(过期)
  • 具备非阻塞锁特性,即没有获取到锁直接返回获取锁失败; (互斥)

分布式锁常见实现

分布式锁的实现又哪些?

1数据库层面的乐观锁;
2基于zookeeper的节点的实现;
3基于redis中间件的实现

性能比较:redis中间件>zookeeper=数据库

可靠性比较:zookeeper>redis中间件>数据库

为什么使用redis作为分布式锁

相信各位小伙伴在学习Redis时,都了解到Redis不仅仅是一个内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。
在这里插入图片描述

redis常见客户端:
Redis官方对Java 语言的封装框架推荐的有十多种(Redis 官网),主要是Jedis 、Redisson。
在这里插入图片描述
根据redis官网 https://redis.io/clients#java 所言,redisson是使用在 分布式和可伸缩的Java数据结构在Redis服务器上的

redisson实现分布式锁的原理

  • 加锁机制
    线程去获取锁,获取成功: 执行lua脚本,保存数据到redis数据库。
    线程去获取锁,获取失败: 一直通过while循环尝试获取锁,获取成功后,执行lua脚本,保存数据到redis数据库。
    在这里插入图片描述
  • 为啥要用lua脚本呢?
    这个不用多说,主要是如果你的业务逻辑复杂的话,通过封装在lua脚本中发送给redis,而且redis是单线程的,这样就保证这段复杂业务逻辑执行的原子性。

三、不加分布式锁的问题出现

3.1 创建springboot的项目,引入依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--springboot中的redis依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

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

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

    </dependencies>

3.2 代码

  • Controller
@RestController
public class NoDistributeController {

    /**
     * 无分布式锁
     */
    @Autowired
    NoDistributeService redisDistributeService;

    /**
     * 查询剩余订单结果接口
     * @return
     */
    @GetMapping("/query")
    public String query() {
        return redisDistributeService.queryMap();
    }

    /**
     * 下单接口
     * @return
     */
    @GetMapping("/order")
    public String order() {
        redisDistributeService.order();
        return redisDistributeService.queryMap();
    }
}
  • Service
@Service
public class NoDistributeService {

    /**
     * 模拟商品信息表
     */
    private static Map<String,Integer> products;

    /**
     * 模拟库存
     */
    private static Map<String,Integer> stock;

    /**
     * 订单
     */
    private static Map<String,String> orders;

    static {
        products = new HashMap<>();
        stock = new HashMap<>();
        orders = new HashMap<>();
        //模拟订单表数据 订单编号 苹果 库存 100000
        products.put("苹果",100000);
        //模拟库存表数据 订单编号 苹果 库存100000
        stock.put("苹果",100000);
    }

    /**
     * 模拟查询秒杀成功返回的信息
     * @return  返回拼接的秒杀商品结果字符串
     */
    public String queryMap() {
        String pid = "苹果";
        return "秒杀商品限量:" +  products.get(pid) + "份,还剩:"+stock.get(pid) +"份,成功下单:"+orders.size() + "人";
    }

    /**
     * 下单
     */
    public void order() {
        String pid = "苹果";
        //从库存表中获取库存余量
        int stockNum = stock.get(pid);
        //如果库存为0 则输出库存不足
        if(stockNum == 0) {
            System.out.println("商品库存不足");
        }else{ //如果有库存
            //往订单表中插入数据 生成UUID作为用户ID pid
            orders.put(UUID.randomUUID().toString(),pid);
            //线程休眠 模拟其他操作
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //减库存操作
            stock.put(pid,stockNum-1);
        }
    }
}

3.3 Jmeter测试结果

在这里插入图片描述在这里插入图片描述
2秒,下单1000次
查看结果:http://localhost:8080/query

在这里插入图片描述
可以看出, 下单了994个人,而订单只下了29份,严重出现问题。

四、加入redisson分布式锁

4.1 伪代码

Config config = new Config();
config.useClusterServers()
    .setScanInterval(2000) // cluster state scan interval in milliseconds
    .addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001")
    .addNodeAddress("redis://127.0.0.1:7002");

RedissonClient redisson = Redisson.create(config);

RLock lock = redisson.getLock("myLock");

// traditional lock method
//lock方法是直接加锁,如果锁已被占用,则直接线程阻塞,进行等待,直到锁被占用方释放。
lock.lock();

// or acquire lock and automatically unlock it after 10 seconds
lock.lock(10, TimeUnit.SECONDS);

// or wait for lock aquisition up to 100 seconds 
// and automatically unlock it after 10 seconds
//tryLock方法则是设定了waitTime(等待时间),在这个等待时间没到前,也是线程阻塞并反复去获取锁,直到取到锁或等待时间超时,则返回false。
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
   try {
     ...
   } finally {
       lock.unlock();
   }
}

更多加锁方法可以参考 https://github.com/redisson/redisson/wiki/8.-distributed-locks-and-synchronizers

4.2 代码

package com.dislock.redis.lock;

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.config.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Service
public class NoDistributeService {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * 模拟商品信息表
     */
    private static Map<String, Integer> products;

    /**
     * 模拟库存
     */
    private static Map<String, Integer> stock;

    /**
     * 订单
     */
    private static Map<String, String> orders;

    /**
     * 获取锁
     */
    private static Redisson redisson;

    static {
        products = new HashMap<>();
        stock = new HashMap<>();
        orders = new HashMap<>();
        //模拟订单表数据 订单编号 苹果 库存 100000
        products.put("苹果", 100000);
        //模拟库存表数据 订单编号 苹果 库存100000
        stock.put("苹果", 100000);

        Config config = new Config();

        config.useSingleServer().setAddress("redis://localhost:6379");
        redisson = (Redisson) Redisson.create(config);

    }

    /**
     * 模拟查询秒杀成功返回的信息
     *
     * @return 返回拼接的秒杀商品结果字符串
     */
    public String queryMap() {
        String pid = "苹果";
        return "秒杀商品限量:" + products.get(pid) + "份,还剩:" + stock.get(pid) + "份,成功下单:" + orders.size() + "人";
    }

    /**
     * 下单
     */
    public void order() {
        String pid = "苹果";

        //从库存表中获取库存余量
        int stockNum = stock.get(pid);

        // 获得锁对象实例
        RLock lock = redisson.getLock("lock_Key");

        boolean res = false;

        try {
            res = lock.tryLock();
            //locked = lock.tryLock(1,2,TimeUnit.MINUTES);
            if (res) {
                try {
                    //如果库存为0 则输出库存不足
                    if (stockNum == 0) {
                        System.out.println("商品库存不足");
                    } else { //如果有库存
                        //往订单表中插入数据 生成UUID作为用户ID pid
                        orders.put(UUID.randomUUID().toString(), pid);
                        //线程休眠 模拟其他操作
                        try {
                            TimeUnit.MILLISECONDS.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //减库存操作
                        stock.put(pid, stockNum - 1);
                    }
                } finally {
                    logger.info("释放锁");
                    lock.unlock();
                }
            }
        } catch (Exception e) {
            logger.info("获取锁有误");
        }
    }
}

4.3 Jmeter测试结果

在这里插入图片描述
结果下单的人和剩余的人准确无误。

尾语

本人长期从事java开发,有疑问,请留言,我会及时改正, 谢谢

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值