SpringBoot之使用Redis实现分布式锁(秒杀系统)

作者:水能载舟,亦能覆舟 时间:2022-09-30 15:45:08 

一、Redis分布式锁概念篇

建议直接采用Redis的官方推荐的Redisson作为redis的分布式锁

1.1、为什么要使用分布式锁

    我们在开发应用的时候,如果需要对某一个共享变量进行多线程同步访问的时候,可以使用我们学到的Java多线程的18般武艺进行处理,并且可以完美的运行,毫无Bug!

    注意这是单机应用,也就是所有的请求都会分配到当前服务器的JVM内部,然后映射为操作系统的线程进行处理!而这个共享变量只是在这个JVM内部的一块内存空间!

    后来业务发展,需要做集群,一个应用需要部署到几台机器上然后做负载均衡,大致如下图:

SpringBoot之使用Redis实现分布式锁(秒杀系统)

    上图可以看到,变量A存在JVM1、JVM2、JVM3三个JVM内存中(这个变量A主要体现是在一个类中的一个成员变量,是一个有状态的对象,例如:UserController控制器中的一个整形类型的成员变量),如果不加任何控制的话,变量A同时都会在JVM分配一块内存,三个请求发过来同时对这个变量操作,显然结果是不对的!即使不是同时发过来,三个请求分别操作三个不同JVM内存区域的数据,变量A之间不存在共享,也不具有可见性,处理的结果也是不对的!

    如果我们业务中确实存在这个场景的话,我们就需要一种方法解决这个问题!

    为了保证一个方法或属性在高并 * 况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLock或Synchronized)进行互斥控制。在单机环境中,Java中提供了很多并发处理相关的API。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!

1.2、分布式锁应具备哪些条件

    在分析分布式锁的三种实现方式之前,先了解一下分布式锁应该具备哪些条件:

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

    2、高可用的获取锁与释放锁;

    3、高性能的获取锁与释放锁;

    4、具备可重入特性;

    5、具备锁失效机制,防止死锁;

    6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。

1.3、分布式锁的三种实现方式

    目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。

    在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。有的时候,我们需要保证一个方法在同一时间内只能被同一个线程执行。

    1、基于数据库实现分布式锁;

    2、基于缓存(Redis等)实现分布式锁;

    3、基于Zookeeper实现分布式锁;

    尽管有这三种方案,但是不同的业务也要根据自己的情况进行选型,他们之间没有最好只有更适合!

二、Redis分布式锁实战篇

2.1、导入依赖


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-redis</artifactId>
    <version>1.4.7.RELEASE</version>
</dependency>

2.2、配置Redis配置信息


spring
    redis:
      port: 6379
      host: 127.0.0.1
      password:
      database: 0

2.3、配置RedisConfig属性、如果需要使用FastJSON来序列化你的对象可以看看我前面写的一篇文章


@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate initRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws Exception {
        RedisTemplate redisTemplate = new RedisTemplate();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());
        redisTemplate.setDefaultSerializer(new StringRedisSerializer());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

2.4、写一个RedisLock工具类


/**
* @Description //直接使用Redis进行分布式锁
* 这是简易版本  如果要使用Redis原生锁记得加过期时间,防止死锁 最好使用Redisson操作简单更加方便
* @Date $ $
* @Author huangwb
**/

@Component
public class RedisLockCommon {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * Redis加锁的操作
     *
     * @param key
     * @param value
     * @return
     */
    public Boolean tryLock(String key, String value) {
        if (stringRedisTemplate.opsForValue().setIfAbsent(key, value)) {
            return true;
        }
        String currentValue = stringRedisTemplate.opsForValue().get(key);
        if (StringUtils.isNotEmpty(currentValue) && Long.valueOf(currentValue) < System.currentTimeMillis()) {
            //获取上一个锁的时间 如果高并发的情况可能会出现已经被修改的问题  所以多一次判断保证线程的安全
            String oldValue = stringRedisTemplate.opsForValue().getAndSet(key, value);
            if (StringUtils.isNotEmpty(oldValue) && oldValue.equals(currentValue)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Redis解锁的操作
     *
     * @param key
     * @param value
     */
    public void unlock(String key, String value) {
        String currentValue = stringRedisTemplate.opsForValue().get(key);
        try {
            if (StringUtils.isNotEmpty(currentValue) && currentValue.equals(value)) {
                stringRedisTemplate.opsForValue().getOperations().delete(key);
            }
        } catch (Exception e) {
        }
    }
}

2.5、减库存操作


@Override
   public boolean decrementProductStore(Long productId, Integer productQuantity) {
       String key = "dec_store_lock_" + productId;
       long time = System.currentTimeMillis();
       try {
           //如果加锁失败
           if (!redisLock.tryLock(key, String.valueOf(time))) {
               return false;
           }
           ProductInfo productInfo = productInfoMapper.selectByPrimaryKey(productId);
           //如果库存为空
           if (productInfo.getProductStock() == 0) {
               return false;
           }
           //减库存操作
           productInfo.setProductStock(productInfo.getProductStock() - 1);
           productInfoMapper.updateByPrimaryKey(productInfo);
       } catch (Exception e) {
           e.printStackTrace();
           return false;
       } finally {
           //解锁
           redisLock.unlock(key, String.valueOf(time));
       }
       return true;

}

2.6、测试接口


@GetMapping("test")
public String createOrderTest() {
    if (!productInfoService.decrementProductStore(1L, 1)) {
        return "库存不足";
    }
    OrderMaster orderMaster = new OrderMaster();
    //未支付
    orderMaster.setOrderStatus(0);
    //未支付
    orderMaster.setPayStatus(0);
    orderMaster.setBuyerName("张三");
    orderMaster.setBuyerAddress("湖南长沙");
    orderMaster.setBuyerPhone("18692794847");
    orderMaster.setOrderAmount(BigDecimal.ZERO);
    orderMaster.setCreateTime(DateUtils.getCurrentDate());
    orderMaster.setOrderId(UUID.randomUUID().toString().replaceAll("-", ""));
    orderMasterService.insert(orderMaster);
}

2.7、可以使用使用Apache ab进行http接口测试

    具体文章可以看看这篇文章 https://www.jb51.net/article/177250.htm

三、结尾

    文章概览篇来源于 https://blog.csdn.net/hxpjava1/article/details/81068355

来源:https://blog.csdn.net/qq_37892957/article/details/89322334

标签:SpringBoot,Redis,分布式锁
0
投稿

猜你喜欢

  • Android实现上传文件功能的方法

    2022-01-18 11:01:02
  • 解决Druid动态数据源配置重复刷错误日志的问题

    2021-06-06 17:44:51
  • Spring Security角色继承分析

    2022-02-03 03:39:12
  • 解决logback-classic 使用testCompile的打包问题

    2021-07-01 08:16:50
  • Android使用OkHttp发送post请求

    2022-12-04 13:24:57
  • Java基础之反射

    2021-12-20 17:20:20
  • C#环形缓冲区(队列)完全实现

    2022-06-26 08:05:48
  • C#读取或设置ScrollLock状态的方法

    2023-03-14 21:41:24
  • 升级dubbo2.7.4.1版本平滑迁移到注册中心nacos

    2021-11-17 04:03:34
  • C#图像处理之图像均值方差计算的方法

    2023-12-10 17:35:33
  • C# HttpClient 如何使用 Consul 发现服务

    2021-09-28 01:06:18
  • Spring如何使用注解的方式创建bean

    2022-01-29 03:45:49
  • 从零实现一个简单的Spring Bean容器的代码案例

    2022-07-24 11:42:16
  • Java中的"goto"语句妙用

    2023-10-10 03:54:32
  • Java实现经典游戏飞机大战-I的示例代码

    2023-07-30 15:45:22
  • Android使用系统相机进行拍照的步骤

    2023-10-07 16:05:05
  • Mybatis中where标签与if标签结合使用详细说明

    2021-07-27 08:15:53
  • Springboot 多租户SaaS搭建方案

    2022-06-04 00:23:32
  • Java实现抢红包功能

    2021-08-05 07:47:58
  • vscode设置Fira_Code字体及改变编辑器字体、背景颜色的代码详解

    2023-05-22 21:00:17
  • asp之家 软件编程 m.aspxhome.com