系统架构

客户端发起秒杀请求,请求经网关处理转发到对应的服务节点上,进行业务层处理,最后数据入库。

业务处理:

  •         验证秒杀活动是否已经开启;
  •         对流量进行限制;
  •         验证订单信息(验证重复秒杀、验证库存是否足够);
  •         订单数据异步入库; 

验证秒杀活动是否开启,需要用到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;
    }
    
    
}

Logo

开源、云原生的融合云平台

更多推荐