当前位置: 首页 > article >正文

谷粒商城の秒杀服务

文章目录

  • 前言
  • 一、秒杀系统的设计
  • 二、缓存预热
    • 1.缓存结构设计
    • 2、上架
  • 三、秒杀业务实现


前言

  本篇基于谷粒商城的秒杀服务,介绍设计一个秒杀系统的要素,包括缓存预热商品随机码动静分离消息队列削峰等。对应视频P311-P325(只介绍系统设计和后端代码的关键部分)


一、秒杀系统的设计

  对于短时间内高并发的秒杀场景,在系统的架构方面,首先应该做到服务自治。即拆分一个专门的微服务去应对秒杀相关的业务请求,具体创建订单,扣减库存,支付可以远程调用其他服务。这样做的目的是为了即使秒杀服务扛不住压力崩溃了,也不会对其他的服务造成影响,也是单一职责的体现。
  其次在安全方面,需要对秒杀的链接进行加密,或为每一个秒杀的商品生成随机码,用户请求时不仅需要带着商品id,还需要加上随机码,防止恶意攻击,以及在网关层识别非法攻击请求并且拦截。
  在流量控制方面,可以使用验证码等手段进行流量分担(用户输入验证码的速度有快有慢),以及引入消息队列,只将请求的关键信息放入消息队列,然后返回给用户提示信息,让队列自己去消费。最后还应该做好熔断降级
  除了上述几点,为了提高系统的响应速度,还需要进行缓存预热,将秒杀的商品信息,场次信息,库存信息提前存入Redis中,避免大量的请求全部访问数据库,以及Nginx做好动静分离
  附一个使用AES实现链接加密的简单案例:
  AES加密工具类:

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class AesEncryptionUtil {
    private static final String ALGORITHM = "AES";

    public static String encrypt(String data, String secret) throws Exception {
        SecretKeySpec key = new SecretKeySpec(secret.getBytes(), ALGORITHM);
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, key);
        byte[] encrypted = cipher.doFinal(data.getBytes());
        return Base64.getEncoder().encodeToString(encrypted);
    }

    public static String decrypt(String encryptedData, String secret) throws Exception {
        SecretKeySpec key = new SecretKeySpec(secret.getBytes(), ALGORITHM);
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, key);
        byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(encryptedData));
        return new String(decrypted);
    }
}

  Controller:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ResourceController {

    private static final String SECRET_KEY = "1234567890123456"; // 16位密钥

    @GetMapping("/encrypt")
    public String encrypt(@RequestParam String id) {
        try {
            String encryptedId = AesEncryptionUtil.encrypt(id, SECRET_KEY);
            return "Encrypted ID: " + encryptedId;
        } catch (Exception e) {
            e.printStackTrace();
            return "Error encrypting ID";
        }
    }

    @GetMapping("/resource")
    public String getResource(@RequestParam String id) {
        try {
            String decryptedId = AesEncryptionUtil.decrypt(id, SECRET_KEY);
            return "Resource ID: " + decryptedId;
        } catch (Exception e) {
            e.printStackTrace();
            return "Error decrypting ID";
        }
    }
}

  在访问资源接口/resource前,首先访问/encrypt连接获取加密ID:

http://localhost:8080/encrypt?id=123

  前端保存加密后的ID,带着这个ID去访问资源接口:

http://localhost:8080/resource?id=<encryptedId>

二、缓存预热

1.缓存结构设计

  在本项目中,选择将秒杀场次、库存量、商品信息进行缓存预热:

  • 秒杀场次设计为List结构,key是开始事件的毫秒值_结束时间的毫秒值,value是场次_skuId。
  • 库存量设计为String结构,key是固定前缀:随机码,value则是具体的库存。
  • 商品信息设计为hash结构,key是场次_skuId,value则是具体商品信息的对象。

2、上架

  本项目中使用缓存预热的方式是定时任务,提前将今明后三天的秒杀信息放入缓存,并且设计上使用了双检锁模式。在定时任务执行处使用分布式缓存锁,防止多实例同时运行,并且在执行相关业务代码的时候再次进行了判断,如果缓存中已经有了对应的key,则不再重复向Redis中保存。限流也使用了Redisson中的semaphore防止并发问题。

    // 每天凌晨 3 点执行
    @Scheduled(cron = "0 0 3 * * *")
    public void executeTaskAt3AMUpSeckillSku() {
        //加分布式锁,防止多实例重复执行
        RLock lock = redissonClient.getLock(UPLOAD_LOCK);
        try {
            lock.lock();
            secKillSkuService.uploadSecKillSkuInfo();
        } finally {
            lock.unlock();
        }
    }
    @Override
    public void uploadSecKillSkuInfo() {
        List<SeckillSessionPojo> lasted3SeckillInfo = couponRemoteServiceClient.getLasted3SeckillInfo();

        if (!CollectionUtils.isEmpty(lasted3SeckillInfo)) {
            //将活动信息进行缓存 key:前缀:开始事件_结束时间 value SkuId
            this.saveSessionInfos(lasted3SeckillInfo);

            //保存商品信息 key:前缀 value 商品信息
            this.saveSessionSkuInfos(lasted3SeckillInfo);
        }
    }
    
    private void saveSessionInfos(List<SeckillSessionPojo> lasted3SeckillInfo) {
        lasted3SeckillInfo.forEach(seckillSessionPojo -> {
            long start = seckillSessionPojo.getStartTime().getTime();
            long end = seckillSessionPojo.getEndTime().getTime();
            String key = SESSION_CACHE_PREFIX + start + "_" + end;
            Boolean hasKey = stringRedisTemplate.hasKey(key);
            if (!hasKey) {
                //活动场次id_商品id
                List<String> ids = seckillSessionPojo.getSkuRelationEntities().stream().map(seckillSkuRelationPojo ->
                        seckillSkuRelationPojo.getPromotionSessionId().toString()+"_"+seckillSkuRelationPojo.getSkuId().toString()).collect(Collectors.toList());
                stringRedisTemplate.opsForList().leftPushAll(key, ids);
            }
        });
    }

    private void saveSessionSkuInfos(List<SeckillSessionPojo> lasted3SeckillInfo) {

        lasted3SeckillInfo.forEach(seckillSessionPojo -> {
            //准备hash操作,一个活动场次一个hash操作
            BoundHashOperations<String, Object, Object> operations = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
            List<SeckillSkuRelationEntity> relations = seckillSessionPojo.getSkuRelationEntities();
            relations.forEach(relation -> {
                String token = UUID.randomUUID().toString().replace("-", "");
                if (Boolean.FALSE.equals(operations.hasKey(relation.getPromotionSessionId().toString()+"_"+relation.getSkuId().toString()))) {
                    //缓存商品,一个活动场次对应的具体商品
                    SecKillSkuRedisTO secKillSkuRedisTO = new SecKillSkuRedisTO();
                    BeanUtils.copyProperties(relation, secKillSkuRedisTO);
                    //还应该设置商品详细信息 远程调用product服务
                    SkuInfoPojo skuInfo = null;
                    try {
                        skuInfo = productRemoteServiceClient.getSkuInfo(relation.getSkuId());
                    } catch (Exception e) {
                        log.info("根据skuId:{}查询商品服务错误:", relation.getSkuId(), e);
                    }
                    secKillSkuRedisTO.setSkuInfo(skuInfo);
                    //设置商品的开始事件和结束时间
                    secKillSkuRedisTO.setStartTime(seckillSessionPojo.getStartTime().getTime());
                    secKillSkuRedisTO.setEndTime(seckillSessionPojo.getEndTime().getTime());
                    //设置随机码
                    secKillSkuRedisTO.setRandomCode(token);
                    String result = JSON.toJSONString(secKillSkuRedisTO);
                    operations.put(relation.getPromotionSessionId().toString()+"_"+relation.getSkuId().toString(), result);


                    //限流 相比较于固定的skuId,每次的随机码都不一样
                    RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                    //商品可以秒杀的总量作为信号量
                    semaphore.trySetPermits(relation.getSeckillCount().intValue());

                }
            });
        });
    }

三、秒杀业务实现

  在秒杀业务的具体实现上:

  1. 在拦截器中判断用户是否登录。
  2. 进行场次判断,是否在秒杀时间段中。
  3. 参数中的商品随机码和场次_skuId是否与缓存中预热的一致。
  4. 校验用户是否已经参加过该场次该商品的秒杀(使用Redis的setNX命令)。
  5. 从信号量中扣去库存(尝试扣去库存使用有超时时间的获取,超过时间获取不到就自己放弃,不会死等)。
  6. 向Rabbit MQ发送消息,订单服务监听,消费消息进行订单创建。
    @Override
    public String kill(String killId, String key, Integer num) {
        MemberRespVO memberRespVO = LoginInterceptor.threadLocal.get();
        Long userId = memberRespVO.getId();
        String timeId = IdWorker.getTimeId();
        //首先校验用户是否登录(在拦截器中已经实现)
        //校验信息是否合法
        BoundHashOperations<String, Object, Object> operations = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
        //获取killId的场次信息
        String json = (String) operations.get(killId);
        if (!StringUtils.isBlank(json)){
            SecKillSkuRedisTO secKillSkuRedisTO = JSON.parseObject(json, SecKillSkuRedisTO.class);
            Long startTime = secKillSkuRedisTO.getStartTime();
            Long endTime = secKillSkuRedisTO.getEndTime();
            long time = new Date().getTime();
            //校验时间
            if (startTime > time || endTime < time) {
                return null;
            }
            String randomCode = secKillSkuRedisTO.getRandomCode();
            Long promotionSessionId = secKillSkuRedisTO.getPromotionSessionId();
            Long skuId = secKillSkuRedisTO.getSkuId();
            //校验参数中的随机码和场次_skuId与redis中的是否一致
            if (!key.equals(randomCode) || !killId.equals(promotionSessionId+"_"+skuId)) {
                return null;
            }
            //校验该用户是否已经秒杀过
            String userKey = new StringBuffer().append(userId).append("_").append(promotionSessionId).append("_").append(skuId).toString();
            //setIfAbsent 只有不存在才会创建
            Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(userKey, num.toString(), 100, TimeUnit.MILLISECONDS);
            if (!aBoolean) {
                return null;
            }
            //扣减库存
            RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
            try {
                //利用有超时时间的获取,超过时间获取不到就自己放弃,不会死等
                boolean b = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
                if (!b){
                    return null;
                }
                //向rabbitMQ发消息,创建订单
                SecKillRabbitTO secKillRabbitTO = new SecKillRabbitTO();
                secKillRabbitTO.setMemberId(userId);
                secKillRabbitTO.setNum(num);
                secKillRabbitTO.setPromotionSessionId(promotionSessionId);
                secKillRabbitTO.setSkuId(skuId);
                secKillRabbitTO.setOrderNo(timeId);
                rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",secKillRabbitTO);
            } catch (InterruptedException e) {
                return null;
            }
        }
        return timeId;
    }


http://www.kler.cn/a/369453.html

相关文章:

  • centos8.5环境下openresty使用lua访问redis、本地缓存、获取get参数,请求头以及获取post body参数
  • 2024.10.25 软考学习笔记(知识点)
  • 雷池社区版OPEN API使用教程
  • 算法设计与分析:贪心算法思想的应用
  • 基于SpringCloud的WMS管理系统源码
  • Spring boot 配置文件的加载顺序
  • 深度学习超参数调优指南
  • Typst 平替Latex的新一代工具
  • vue3+ts实时播放视频,视频分屏
  • 【学术会议论文投稿】JavaScript在数据可视化领域的探索与实践
  • 若依框架前后端结构
  • 前端构建工具vite的优势
  • Java虚拟机JVM的简要工作原理
  • 从零学习大模型(三)-----GPT3(下)
  • 轻松拿捏!windows系统上安装Mamba
  • HarmonyOS 模块化设计
  • 机器人学习仿真框架
  • linux下xdg-open打开文件
  • 大厂面试真题-说说DDD中的领域驱动事件
  • CSS 常见选择器
  • 图像处理 -- 图像对比度的数学解析
  • 【python Arrow库】一个处理日期和时间的Python库
  • 【iOS】SDWebImage的使用
  • Linux 进程优先级 进程切换
  • 春秋云镜——SQL注入漏洞复现——CVE-2022-4230
  • Maven 空 JAR 的一个案例