系统架构

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

业务处理:

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

验证秒杀活动是否开启,需要用到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
thenredis.call('INCR',localKey)return true
elsereturn 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 {@Beanpublic 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;}@Beanpublic 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();};}@Beanpublic 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 {@Autowiredprivate 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_";@Autowiredprivate RedisTemplate redisTemplate;@Overridepublic 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;}}

基于Redis实现秒杀系统相关推荐

  1. Redis优化秒杀系统

    Redis优化秒杀系统 使用背景: 普通的基于mss框架的系统在并发量不是很高的情况下,对redis的需求不是很高.redis在系统中的角色相当于一个对象缓存器,在高并发的系统中(比如秒杀系统),在某 ...

  2. 基于redis实现秒杀并防止超卖

    基于redis实现秒杀并防止超卖 为什么基于redis 针对秒杀商品库存为一个的情况 setnx 代码实现 测试 针对有多个库存的商品 实现 测试 为什么基于redis 因为所有redis的操作(这里 ...

  3. 如何利用redis实现秒杀系统

    文章目录 题记 利用Watch实现Redis乐观锁 题记 在线思维导图总结:redis大纲 利用Watch实现Redis乐观锁 乐观锁基于CAS(Compare And Swap)思想(比较并替换), ...

  4. java设计前期工作基础和存在的困难_Java秒杀系统实战系列-基于Redisson的分布式锁优化秒杀逻辑...

    本文是"Java秒杀系统实战系列文章"的第十五篇,本文我们将借助综合中间件Redisson优化"秒杀系统中秒杀的核心业务逻辑",解决Redis的原子操作在优化秒 ...

  5. Java秒杀系统实战系列~基于Redisson的分布式锁优化秒杀逻辑

    摘要: 本篇博文是"Java秒杀系统实战系列文章"的第十五篇,本文我们将借助综合中间件Redisson优化"秒杀系统中秒杀的核心业务逻辑",解决Redis的原子 ...

  6. 【高并发】Redis如何助力高并发秒杀系统?看完这篇我彻底懂了!!

    秒杀业务 在电商领域,存在着典型的秒杀业务场景,那何谓秒杀场景呢.简单的来说就是一件商品的购买人数远远大于这件商品的库存,而且这件商品在很短的时间内就会被抢购一空.比如每年的618.双11大促,小米新 ...

  7. 实践出真知:全网最强秒杀系统架构解密!!

    很多小伙伴反馈说,高并发专题学了那么久,但是,在真正做项目时,仍然不知道如何下手处理高并发业务场景!甚至很多小伙伴仍然停留在只是简单的提供接口(CRUD)阶段,不知道学习的并发知识如何运用到实际项目中 ...

  8. 解密秒杀系统架构:不是所有的秒杀都是秒杀

    摘要:究竟什么样的系统算是高并发系统?今天,我们就一起解密高并发业务场景下典型的秒杀系统的架构. 本文分享自华为云社区<[高并发]秒杀系统架构解密,不是所有的秒杀都是秒杀(升级版)!!>, ...

  9. 电商项目的并发量一般是多少_【高并发】高并发秒杀系统架构解密,不是所有的秒杀都是秒杀!...

    写在前面 很多小伙伴反馈说,高并发专题学了那么久,但是,在真正做项目时,仍然不知道如何下手处理高并发业务场景!甚至很多小伙伴仍然停留在只是简单的提供接口(CRUD)阶段,不知道学习的并发知识如何运用到 ...

最新文章

  1. svn的一些相关资料
  2. 回顾2011年最热门的开源PHP项目
  3. setting an array element with a sequence.
  4. C# 总结const、 readonly、 static三者区别:
  5. graph driver-device mapper-04libdevmapper基本操作
  6. vscode远程无法更新
  7. response Headers与request Headers字段详解(收藏)
  8. java可以看懂php代码吗_同一段代码,在PHP里和Java里都能运行,输出结果相同,你能看懂其中的原理吗?...
  9. 下载《Hadoop权威指南》的气象数据
  10. 免费使用一年海外云服务器
  11. 从初学时整理的jq资料
  12. 企业上云要几步?中拓互联奉送企业上云全攻略
  13. Python中验证URL是否可以访问
  14. 对抗生成网络代码Generative Adversarial Networks (GANs),Vanilla GAN,Deeply Convolutional GANs
  15. 机器学习算法——概率类模型评估指标4(校准可靠性曲线及预测概率直方图)
  16. 风影ASP.NET基础教学 7用户控件编程
  17. [转]如何查看图片的长和宽(尺寸)
  18. 整理UML建模概念和图形~(啥?程序员不再写代码,变成画图工程师?)
  19. 圣经中有关天使的记载
  20. VI编辑器_终端编辑器(命令整理)

热门文章

  1. 广州王师傅揭秘未来20年最有前途的行业!——不要再错过下一个风口
  2. Elasticsearch的搜索命令
  3. iPhone中通过ics来添加农历与天气预报
  4. github 下载文件加速 https://moeyy.cn/gh-proxy/
  5. Python实现流星雨效果的代码
  6. 以前曾看到过一个期货童话故事,很有意思,发上来
  7. 淘宝购物如何找优惠券_淘宝上买衣服怎么省钱
  8. 情商比智商更能决定人的一生
  9. arcmap小技巧之获取行政区划及json格式转shp,json转csv
  10. cherry 键盘WIN键不生效问题