巧用Redis中的原子性操作,解决秒杀并发中的问题,如商品一卖二
在分布式系统中,Redis 是一种非常流行的缓存和数据存储解决方案,它提供了丰富的数据结构和操作方法,方便我们快速处理各种业务场景。
在开始讲具体的技巧之前,先说说咱们再秒杀并发中遇到的问题:咱们在维护一个商城系统的时候,发现有个很奇怪的现象,明明通过分布式锁已经锁定了商品只能卖个一个用户,可实际情况,它还是能够卖给2个用户,当然这也不完全是后端的锅,前端在控制上也有些问题,另外就是加锁的时候,咱们是锁定了商品,可是商城中有个预约功能,一次可以预约多个商品,导致没办法直接通过锁商品来确保一对一,这就带来了很大的问题。
我们在修复了前端(原来是前端直接传参,没有经过后端,修改后,增加了后端的判断,如果商品数量不足了,就不能进行抢购了)后,对后端代码也进行了修改和调整。基于原来分布式锁的基础上,运用原子性操作,控制存量,当存量不足时,就禁止操作,从而避免了后面发生一卖二的情况。
下面先了解一下Redis的原子性操作,我们以RedisTemplate
为示例,具体了解一下怎么做到保持原子性的。
RedisTemplate
是 Spring 框架对 Redis 操作的一个高级封装,使得我们可以方便地在 Java 应用程序中使用 Redis。opsForHash
是 RedisTemplate
的一个操作哈希表的接口,它提供了一系列对哈希表的操作方法,包括 put
、get
、delete
等,而 increment
和 decrement
这两个方法则在处理数字类型的哈希表键值对时非常有用,它们可以实现原子性的增减操作,非常适合一些需要保证并发安全的场景,比如秒杀商品的库存管理。
秒杀商品的场景分析
在秒杀商品的业务场景中,商品的库存是一个关键因素,由于参与秒杀的用户数量众多,可能会在同一时刻有大量的用户请求购买同一件商品,因此需要保证库存的扣减操作是原子性的,避免超卖现象的发生。传统的数据库操作在高并发下可能会出现性能瓶颈和数据不一致的问题,而 Redis 的 increment
和 decrement
操作可以很好地解决这个问题。
我们可以将商品的库存存储在 Redis 的哈希表中,其中哈希表的键是商品的 ID,值是商品的库存数量。每次用户发起秒杀请求时,我们使用 decrement
操作来减少库存,如果库存减为负数,说明库存不足,秒杀失败;否则,秒杀成功。
示例代码
以下是一个简单的 Spring Boot 应用程序的代码示例,展示了如何使用 RedisTemplate
的 opsForHash
的 increment
和 decrement
方法来实现秒杀商品的库存管理:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class SeckillService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String STOCK_KEY = "seckill_stock";
// 初始化商品库存
public void initStock(String productId, int stock) {
HashOperations<String, String, Integer> hashOps = redisTemplate.opsForHash();
hashOps.put(STOCK_KEY, productId, stock);
}
// 尝试秒杀商品
public boolean seckill(String productId) {
HashOperations<String, String, Integer> hashOps = redisTemplate.opsForHash();
Long result = hashOps.decrement(STOCK_KEY, productId, 1);
if (result >= 0) {
System.out.println("秒杀成功! 剩余库存数量: " + result);
return true;
} else {
System.out.println("秒杀失败. 该商品已售罄.");
// 库存恢复,防止超卖后库存为负数
hashOps.increment(STOCK_KEY, productId, 1);
return false;
}
}
}
在上述代码中:
-
首先,我们使用
@Autowired
注解注入了RedisTemplate
对象,这是 Spring 框架自动为我们提供的 Redis 操作模板。 -
initStock
方法用于初始化商品的库存,将商品的库存存储在 Redis 的哈希表中。我们使用opsForHash
获取HashOperations
对象,然后使用put
方法将商品的 ID 和库存数量存储在名为seckill_stock
的哈希表中。 -
seckill
方法是实现秒杀操作的核心方法。当用户发起秒杀请求时,我们使用decrement
方法将商品的库存减 1。decrement
方法会返回操作后的结果,如果结果大于等于 0,表示秒杀成功,我们可以进行后续的订单处理等操作;如果结果小于 0,表示库存不足,秒杀失败,此时我们使用increment
方法将库存加 1 恢复,防止库存出现负数。
配置 RedisTemplate
为了使上述代码正常运行,我们还需要在 Spring Boot 应用程序中配置 RedisTemplate
,以下是一个简单的配置示例:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration;
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 RedisConfig {
@Bean
public JedisConnectionFactory jedisConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setHostName("localhost");
redisStandaloneConfiguration.setPort(6379);
JedisClientConfiguration.JedisClientConfigurationBuilder jedisClientConfiguration = JedisClientConfiguration.builder();
return new JedisConnectionFactory(redisStandaloneConfiguration, jedisClientConfiguration.build());
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(jedisConnectionFactory());
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
在这个配置类中:
-
我们使用
JedisConnectionFactory
配置了 Redis 的连接信息,这里假设 Redis 运行在本地的 6379 端口。 -
RedisTemplate
的配置中,我们使用StringRedisSerializer
对键进行序列化,使用GenericJackson2JsonRedisSerializer
对值进行序列化,以便存储不同类型的数据。
测试代码
以下是一个简单的测试代码,演示如何使用上述的 SeckillService
进行秒杀操作:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SeckillApplication implements CommandLineRunner {
@Autowired
private SeckillService seckillService;
public static void main(String[] args) {
SpringApplication.run(SeckillApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
// 初始化商品库存
seckillService.initStock("product1", 10);
// 模拟用户秒杀
for (int i = 0; i < 15; i++) {
boolean success = seckillService.seckill("product1");
System.out.println("User " + i + " seckill result: " + success);
}
}
}
在测试代码中,我们使用 CommandLineRunner
接口的 run
方法,在应用程序启动时进行库存的初始化和秒杀操作的模拟。我们将商品 product1
的库存初始化为 10,然后模拟 15 个用户发起秒杀请求,观察秒杀结果。
总结
通过使用 RedisTemplate 的 opsForHash
的 increment
和 decrement
方法,我们可以方便地实现秒杀商品的库存管理,利用 Redis 的原子性操作保证了高并发下库存数据的一致性和准确性。这种方法不仅提高了系统的性能,还避免了传统数据库操作在高并发场景下可能出现的超卖问题,为秒杀等业务场景提供了一种高效、可靠的解决方案。
需要注意的是,在实际应用中,我们还需要考虑更多的细节,比如秒杀的时间范围、用户的购买限制、订单的生成和处理等,同时要确保 Redis 的高可用性和性能优化,以应对大规模的并发请求。
这个示例代码展示了如何使用 Redis 进行简单的秒杀商品库存管理,你可以根据实际需求进行扩展和优化,例如加入用户 ID 的验证、使用 Redis 的事务或 Lua 脚本进一步增强操作的原子性等。
希望这篇文章能帮助你理解 RedisTemplate 的 opsForHash
的 increment
和 decrement
方法在秒杀商品场景中的应用,在实际开发中可以根据具体情况灵活运用这些方法,提高系统的并发处理能力和数据安全性。
通过上述代码和解释,你可以看到如何使用 RedisTemplate 的 opsForHash
操作在秒杀商品的场景下进行库存管理,利用 increment
和 decrement
方法的原子性操作保证数据的一致性和并发安全。你可以根据自己的需求对代码进行扩展和修改,以适应不同的业务场景。