基于Redis实现秒杀系统
系统架构客户端发起秒杀请求,请求经网关处理转发到对应的服务节点上,进行业务层处理,最后数据入库。业务处理:验证秒杀活动是否已经开启;对流量进行限制;验证订单信息(验证重复秒杀、验证库存是否足够);订单数据异步入库;验证秒杀活动是否开启,需要用到Redis实现,因为服务是分布式的,多个节点上的系统时间可能存在略微差异,可以采用Redis来管理秒杀活动的开启状态。由于秒杀开启后,会有大量流量进入,需要
系统架构

客户端发起秒杀请求,请求经网关处理转发到对应的服务节点上,进行业务层处理,最后数据入库。
业务处理:
- 验证秒杀活动是否已经开启;
- 对流量进行限制;
- 验证订单信息(验证重复秒杀、验证库存是否足够);
- 订单数据异步入库;
验证秒杀活动是否开启,需要用到Redis实现,因为服务是分布式的,多个节点上的系统时间可能存在略微差异,可以采用Redis来管理秒杀活动的开启状态。
由于秒杀开启后,会有大量流量进入,需要对访问流量进行限制,可以通过Redis实现,总体设计是在Redis初始化一个最大限制,每进入一个秒杀请求对于当前流量数加1,如果当前流量数达到最大限制流量数,则进行限流处理。限流处理只要能限制住一部分流量即可,结合性能考虑,限流操作没必要做成原子性操作。
重复秒杀限制:一个用户只能进行一次秒杀,采用布隆过滤器,判断用户是否进行过秒杀,布隆过滤器具有占用存储空间小、查询高效等特点。
验证库存是否足够: 为了防止超卖,需要验证库存,当库存不足时直接返回秒杀失败,验证库存需要先在Redis读取当前购买量,然后和库存数比较,如果库存足够,则对当前购买量incr,验证成功。
为了减轻数据库压力,采用异步入库,对于验证通过的数据写入MQ,多线程并发在MQ消费订单数据进行入库。
依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
#Redis
spring.redis.host=192.168.50.134
spring.redis.port=6379
lua脚本:
bloomFilterAdd.lua
local bloomName = KEYS[1]
local value = KEYS[2]
local result_1 = redis.call('BF.ADD',bloomName,value)
return result_1
bloomFilterExist.lua
---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by lw.
--- DateTime: 2021/12/21 10:24
---
local bloomName = KEYS[1]
local value = KEYS[2]
local result_1 = redis.call('BF.EXISTS',bloomName,value)
return result_1
checkStock.lua
---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by lw.
--- DateTime: 2021/12/22 10:29
---读取当前已购买数量,判断如果小于库存总数10000 则递增已购买数量 返回true 否则返回false
---
local localKey = KEYS[1]
local result_1 = redis.call('GET',localKey)
if tonumber(result_1) < 10
then
redis.call('INCR',localKey)
return true
else
return false
end
package com.tech.seckill.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory){
RedisTemplate<String, String> redisTemplate = new RedisTemplate<String,String>();
redisTemplate.setConnectionFactory(factory);
// 使用Jackson2JsonRedisSerialize 替换默认序列化
/**Jackson序列化 json占用的内存最小 */
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
/**Jdk序列化 JdkSerializationRedisSerializer是最高效的*/
// JdkSerializationRedisSerializer jdkSerializationRedisSerializer = new JdkSerializationRedisSerializer();
/**String序列化*/
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
/**将key value 进行stringRedisSerializer序列化*/
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setValueSerializer(stringRedisSerializer);
/**将HashKey HashValue 进行序列化*/
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean
public KeyGenerator genValueKeyGenerator() {
return (o, method, objects) -> {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(o.getClass().getSimpleName());
stringBuilder.append(".");
stringBuilder.append(method.getName());
stringBuilder.append("[");
for (Object obj : objects) {
stringBuilder.append(obj.toString());
}
stringBuilder.append("]");
return stringBuilder.toString();
};
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
return new RedisCacheManager(
RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory),
this.getRedisCacheConfigurationWithTtl(600), // 默认策略,未配置的 key 会使用这个
this.getRedisCacheConfigurationMap() // 指定 key 策略
);
}
private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap() {
Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
redisCacheConfigurationMap.put("UserInfoList", this.getRedisCacheConfigurationWithTtl(100));
redisCacheConfigurationMap.put("UserInfoListAnother", this.getRedisCacheConfigurationWithTtl(18000));
return redisCacheConfigurationMap;
}
private RedisCacheConfiguration getRedisCacheConfigurationWithTtl(Integer seconds) {
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
redisCacheConfiguration = redisCacheConfiguration.serializeValuesWith(
RedisSerializationContext
.SerializationPair
.fromSerializer(jackson2JsonRedisSerializer)
).entryTtl(Duration.ofSeconds(seconds));
return redisCacheConfiguration;
}
}
package com.tech.seckill.controller;
import com.tech.seckill.service.SecKillService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author lw
* @since 2021/12/21
*/
@RestController
public class SecKillController {
@Autowired
private SecKillService secKillService;
@GetMapping("secKill")
String secKill(int uid,int skuId){
return secKillService.secKill(uid,skuId);
}
}
package com.tech.seckill.service;
/**
* @author lw
* @since 2021/12/21
*/
public interface SecKillService {
String secKill(int uid,int skuId);
}
package com.tech.seckill.service.impl;
import com.tech.seckill.service.SecKillService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.List;
/**
* @author lw
* @since 2021/12/21
*/
@Service
public class SecKillServiceImpl implements SecKillService {
//秒杀开始状态及时间(需初始化 0_秒杀开始时间戳【秒】)
private static final String secStartPrefix="skuId_start_";
//当前参与秒杀的流量数
private static final String secAccess="skuId_access_";
//流量限制总数(需初始化 根据需要可以设置成库存数的倍数,比如设置为库存数的1.5倍)
private static final String secCount="skuId_count_";
//布隆过滤器 防止重复秒杀
private static final String bloomFilterName="userIdBloomFilter";
//当前已购买数量(需初始化 0)
private static final String secBuyCount="skuId_buy_";
@Autowired
private RedisTemplate redisTemplate;
@Override
public String secKill(int uid, int skuId) {
//判断秒杀是否开始 状态位_开始时间 [0 未开始,1 开始] 防止多节点流量倾斜,当状态位为1表示可以启动秒杀了
String isStart = (String)redisTemplate.opsForValue().get(secStartPrefix + skuId);
if(!StringUtils.hasLength(isStart)){
return "活动未开始";
}
int startStatus = Integer.parseInt(isStart.split("_")[0]);
if(startStatus==0){
int planStartTime = Integer.parseInt(isStart.split("_")[1]);
if(getNow()<planStartTime){
return "活动未开始";
}else{
//开始秒杀
redisTemplate.opsForValue().set(secStartPrefix + skuId,isStart.replaceFirst("0","1"));
}
}
//流量拦截 读取限制流量 当前流量,判断当前流量是否达到限制流量,如果达到则进行拦截
// 两次读一次判断这三个操作并没有设计成一个原子性操作,因为是限流,过滤掉大部分流量即可,以及考虑到执行的性能,并没有使用lua脚本做成原子性操作
//信息校验层扣减库存采用了原子性操作
String skuAccessName=secAccess+skuId; //当前参与秒杀的流量数
String accessNumText = (String)redisTemplate.opsForValue().get(skuAccessName);
Integer accessNum=StringUtils.hasLength(accessNumText)?Integer.parseInt(accessNumText):0;
String countName = secCount + skuId; //最大允许的流量数
int maxCount = Integer.parseInt((String)redisTemplate.opsForValue().get(countName));
if(accessNum>maxCount){ //如果达到最大流量,进行限流
return "抢购已经完成,欢迎下次参与";
}else{
redisTemplate.opsForValue().increment(skuAccessName);
}
//信息校验层
if(redisIdExist(uid)){ //校验用户ID是否抢购过
return "抢购已完成,欢迎下次参与";
}else{ //如果没有抢购过,添加到布隆过滤器,下次不可抢购
redisIdAdd(uid);
}
//校验库存(lua脚本原子性执行)防止超卖
//读取当前购买数量,与初始化库存数比较,如果有剩余库存则抢购成功
String currentBuyCountKey=secBuyCount+skuId;
Boolean check = checkStock(currentBuyCountKey);
if(check){
return "恭喜您,抢购成功";
}else{
return "抢购已完成,欢迎下次参与";
}
//todo 完成校验后,将数据发送到MQ(异步 解耦 流量削峰)
//todo 多个监听程序消费数据进行入库
}
private long getNow() {
return System.currentTimeMillis()/1000;
}
Boolean redisIdAdd(int id){
DefaultRedisScript<Boolean> script = new DefaultRedisScript<>();
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("bloomFilterAdd.lua")));
script.setResultType(Boolean.class);
List<Object> keyList = new ArrayList<>();
keyList.add(bloomFilterName);
keyList.add(String.valueOf(id));
Boolean res = (Boolean) redisTemplate.execute(script, keyList);
return res;
}
Boolean redisIdExist(int id){
DefaultRedisScript<Boolean> script = new DefaultRedisScript<>();
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("bloomFilterExist.lua")));
script.setResultType(Boolean.class);
List<Object> keyList = new ArrayList<>();
keyList.add(bloomFilterName);
keyList.add(String.valueOf(id));
Boolean res = (Boolean) redisTemplate.execute(script, keyList);
return res;
}
Boolean checkStock(String key){
DefaultRedisScript<Boolean> script = new DefaultRedisScript<>();
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("checkStock.lua")));
script.setResultType(Boolean.class);
List<Object> keyList = new ArrayList<>();
keyList.add(key);
Boolean res = (Boolean) redisTemplate.execute(script, keyList);
return res;
}
}
更多推荐
所有评论(0)