Redis系列之incr和decr命令是线程安全的?

Redis是一个单线程的服务,所以正常来说redis的命令是会排队执行的。incr/decr命令是redis提供的可以实现递增递减的命令,所以这两个命令也是具有原子性的?是线程安全的?这个也是互联网公司面试的常见题,话不多说,动手实践一下吧,假设这两个命令是线程安全的,既然是线程安全的,那么来模拟实现高并发场景的秒杀减库存业务

软件环境:

  • JDK 1.8

  • SpringBoot 2.2.1

  • Maven 3.2+

  • Mysql 8.0.26

  • spring-boot-starter-data-redis 2.2.1

  • redisson-spring-boot-starter 3.1.5.6

  • 开发工具

    • IntelliJ IDEA

    • smartGit

项目搭建

使用Spring官网的https://start.spring.io快速创建Spring Initializr项目
在这里插入图片描述
选择maven、jdk版本
在这里插入图片描述
选择需要的依赖
在这里插入图片描述

redisson的没搜到,那就手动配置

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

application.yml加上配置:

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password:
    database: 0
    redisson:
      config: |
        singleServerConfig:
          idleConnectionTimeout: 10000
          connectTimeout: 10000
          timeout: 3000
          retryAttempts: 3
          retryInterval: 1500
          password: null
          subscriptionsPerConnection: 5
          clientName: null
          address: "redis://127.0.0.1:6379"
          subscriptionConnectionMinimumIdleSize: 1
          subscriptionConnectionPoolSize: 50
          connectionMinimumIdleSize: 32
          connectionPoolSize: 64
          database: 0
          dnsMonitoringInterval: 5000
        threads: 0
        nettyThreads: 0
        codec: !<org.redisson.codec.JsonJacksonCodec> {}
        transportMode: "NIO"

动手实践

新建一个Redis配置类:

package com.example.redis.configuration;

import cn.hutool.core.util.StrUtil;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.spring.data.connection.RedissonConnectionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfiguration {
   

  
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
   
        return new RedissonConnectionFactory();
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
   
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        template.setConnectionFactory(redisConnectionFactory());
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }

    @Bean
    public RedissonClient redissonClient() {
   
        RedissonClient redissonClient = Redisson.create();
        return redissonClient;
    }


}

写一个测试类,初始化商品库存为1000,然后开启1024个线程去抢

package com.example.redis;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import com.example.redis.configuration.RedisConfiguration;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;

import javax.annotation.Resource;
import java.util.Date;
import java.util.concurrent.CountDownLatch;
import java.util.stream.IntStream;

@Slf4j
@SpringBootTest
@ContextConfiguration(classes = RedisConfiguration.class)
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS)
public class SpringbootRedisIncrTests {
   

    private static final String REDIS_ID_KEY = "testKey:id:%s";
    private static final String REDIS_LOCK_KEY = "testKey:lock";

    @Resource
    private RedisTemplate redisTemplate;

    @Resource
    private RedissonClient redissonClient;

    @BeforeEach
    public void init(){
   
        log.info("init...");
        // 初始化库存为1000
        String idKey = String.format(REDIS_ID_KEY,  DateUtil.format(new Date() , DatePattern.PURE_DATE_PATTERN));
        redisTemplate.opsForValue().set(idKey, 1000);
    }

    @Test
    public void testIncr() throws InterruptedException {
   
        // 开启1024个线程去抢
        CountDownLatch countDownLatch = new CountDownLatch(1024);
        IntStream.range(0, 1024).forEach(e->{
   
            new Thread(new RunnableTest(countDownLatch)).start();
        });

        countDownLatch.await();
    }

    class RunnableTest implements Runnable {
   

        CountDownLatch countDownLatch;

        public RunnableTest(CountDownLatch countDownLatch) {
   
            this.countDownLatch = countDownLatch;
        }

        @SneakyThrows
        @Override
        public void run() {
   
            invoke();
            countDownLatch.countDown();
        }
    }


    private void invoke() throws InterruptedException {
      
        String idKey = String.format(REDIS_ID_KEY, DateUtil.format(new Date(), DatePattern.PURE_DATE_PATTERN));
        long incr = Convert.toLong(redisTemplate.opsForValue().get(idKey));
        if (incr > 0) {
   
            redisTemplate.opsForValue().decrement(idKey);
        }
        log.info("increment:{}", incr);
        
    }




}

跑起来,测试,这个库存数量竟然为负数了,这个业务场景肯定是不合理的,所以这两个命令也不是线程安全的,不可以用来做秒杀减库存业务

在这里插入图片描述

所以,我们可以用分布式锁来简单改造一下代码

private void invoke() throws InterruptedException {
   
    RLock rLock = redissonClient.getLock(REDIS_LOCK_KEY);
    rLock.lock();
    try {
   
        String idKey = String.format(REDIS_ID_KEY, DateUtil.format(new Date(), DatePattern.PURE_DATE_PATTERN));
        long incr = Convert.toLong(redisTemplate.opsForValue().get(idKey));
        if (incr > 0) {
   
            redisTemplate.opsForValue().decrement(idKey);
        }
        log.info("increment:{}", incr);
    } finally {
   
        rLock.unlock();
    }
}

跑起来,可以看到执行效率会慢一点,但是业务是正确的,不会出现库存为负数的情况

在这里插入图片描述

相关推荐

  1. CopyOnWriteArrayList线安全吗?

    2023-12-08 12:10:03       43 阅读
  2. ArrayList 线安全么?

    2023-12-08 12:10:03       42 阅读
  3. Spring 中 Bean 线安全

    2023-12-08 12:10:03       57 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2023-12-08 12:10:03       98 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2023-12-08 12:10:03       106 阅读
  3. 在Django里面运行非项目文件

    2023-12-08 12:10:03       87 阅读
  4. Python语言-面向对象

    2023-12-08 12:10:03       96 阅读

热门阅读

  1. Docker 简介及其常用命令详解

    2023-12-08 12:10:03       46 阅读
  2. angular新版本未生成app.module.ts

    2023-12-08 12:10:03       65 阅读
  3. github代码大纲

    2023-12-08 12:10:03       57 阅读
  4. sed 只用来替换文本?一文掌握 sed 更多用法

    2023-12-08 12:10:03       50 阅读
  5. 常见代码优化案例记录

    2023-12-08 12:10:03       40 阅读
  6. SQL 中连接类型大全

    2023-12-08 12:10:03       48 阅读
  7. PHP案例:探究MySQL应用开发喜好的网络调查

    2023-12-08 12:10:03       58 阅读
  8. MySQL 数字函数

    2023-12-08 12:10:03       41 阅读
  9. vuex实现持久化存储

    2023-12-08 12:10:03       71 阅读