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.1port: 6379password:database: 0redisson:config: |singleServerConfig:idleConnectionTimeout: 10000connectTimeout: 10000timeout: 3000retryAttempts: 3retryInterval: 1500password: nullsubscriptionsPerConnection: 5clientName: nulladdress: "redis://127.0.0.1:6379"subscriptionConnectionMinimumIdleSize: 1subscriptionConnectionPoolSize: 50connectionMinimumIdleSize: 32connectionPoolSize: 64database: 0dnsMonitoringInterval: 5000threads: 0nettyThreads: 0codec: !<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 {@Beanpublic RedisConnectionFactory redisConnectionFactory() {return new RedissonConnectionFactory();}@Beanpublic 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;}@Beanpublic 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";@Resourceprivate RedisTemplate redisTemplate;@Resourceprivate RedissonClient redissonClient;@BeforeEachpublic void init(){log.info("init...");// 初始化库存为1000String idKey = String.format(REDIS_ID_KEY, DateUtil.format(new Date() , DatePattern.PURE_DATE_PATTERN));redisTemplate.opsForValue().set(idKey, 1000);}@Testpublic 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@Overridepublic 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();}
}
跑起来,可以看到执行效率会慢一点,但是业务是正确的,不会出现库存为负数的情况