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();
}
}
跑起来,可以看到执行效率会慢一点,但是业务是正确的,不会出现库存为负数的情况