首页 > 其他分享 >商品秒杀服务

商品秒杀服务

时间:2023-10-01 21:47:30浏览次数:24  
标签:服务 redisTo killId 信号量 商品 num 秒杀 String

 

1. 定时任务启动秒杀活动

  (1)使用定时任务开启秒杀活动;

  (2)在分布式场景中,需要先获取锁,然后执行上架操作,最后释放锁;

  (3)其他任务在获取锁后如果商品已经上架,那么就不用再次上架;

  (4)使用redis缓存秒杀活动信息和活动相关的商品信息;

@Slf4j
@Service
public class SeckillScheduled {

    @Autowired
    private SeckillService seckillService;

    @Autowired
    private RedissonClient redissonClient;

    //秒杀商品上架功能的锁
    private final String upload_lock = "seckill:upload:lock";

//    @Scheduled(cron = "0 0 3 * * ?")
//    @Scheduled(cron = "*/10 * * * * ?")
    public void uploadSeckillSkuLatest3Days() {
        log.info("商品秒杀信息上架");

        //在分布式的场景下,需要先获取到锁,然后执行上架操作,最后释放锁。
        //其他任务在获取锁后,如果商品已经上架,那么就不用再次上架了
        RLock lock = redissonClient.getLock(upload_lock);
        try {
            lock.lock(10, TimeUnit.SECONDS);
            seckillService.uploadSeckillSkuLatest3Days();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

    private final String SESSION__CACHE_PREFIX = "seckill:sessions:";

    private final String SECKILL_CHARE_PREFIX = "seckill:skus";

    private final String SKU_STOCK_SEMAPHORE = "seckill:stock:";    //+商品随机码

    @Override
    public void uploadSeckillSkuLatest3Days() {

        //1、扫描最近三天的商品需要参加秒杀的活动
        R lates3DaySession = couponFeignService.getLates3DaySession();

        if (lates3DaySession.getCode() == 0) {
            //上架商品
            List<SeckillSessionWithSkusVo> sessionData = lates3DaySession.getData("data", new TypeReference<List<SeckillSessionWithSkusVo>>() {
            });

            //缓存到Redis
            //1、缓存活动信息
            saveSessionInfos(sessionData);
            //2、缓存活动的关联商品信息
            saveSessionSkuInfo(sessionData);
        }
    }

    /**
     * 缓存秒杀活动信息
     * @param sessions
     */
    private void saveSessionInfos(List<SeckillSessionWithSkusVo> sessions) {
        sessions.stream().forEach(session -> {

            //获取当前活动的开始和结束时间的时间戳
            long startTime = session.getStartTime().getTime();
            long endTime = session.getEndTime().getTime();

            //存入到Redis中的key
            String key = SESSION__CACHE_PREFIX + startTime + "_" + endTime;

            //判断Redis中是否有该信息,如果没有才进行添加
            Boolean hasKey = redisTemplate.hasKey(key);
            //缓存活动信息
            if (!hasKey) {
                //获取到活动中所有商品的skuId
                List<String> skuIds = session.getRelationSkus().stream()
                        .map(item -> item.getPromotionSessionId() + "-" + item.getSkuId().toString()).collect(Collectors.toList());
                redisTemplate.opsForList().leftPushAll(key,skuIds);
            }
        });
    }

    /**
     * 缓存秒杀活动所关联的商品信息
     * @param sessions
     */
    private void saveSessionSkuInfo(List<SeckillSessionWithSkusVo> sessions) {
        sessions.stream().forEach(session -> {
            //准备hash操作,绑定hash
            BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
            session.getRelationSkus().stream().forEach(seckillSkuVo -> {
                //生成随机码
                String token = UUID.randomUUID().toString().replace("-", "");
                String redisKey = seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString();
                if (!operations.hasKey(redisKey)) {
                    //缓存我们商品信息
                    SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
                    Long skuId = seckillSkuVo.getSkuId();
                    //1、先查询sku的基本信息,调用远程服务
                    R info = productFeignService.getSkuInfo(skuId);
                    if (info.getCode() == 0) {
                        SkuInfoVo skuInfo = info.getData("skuInfo",new TypeReference<SkuInfoVo>(){});
                        redisTo.setSkuInfo(skuInfo);
                    }
                    //2、sku的秒杀信息
                    BeanUtils.copyProperties(seckillSkuVo,redisTo);

                    //3、设置当前商品的秒杀时间信息
                    redisTo.setStartTime(session.getStartTime().getTime());
                    redisTo.setEndTime(session.getEndTime().getTime());
                    //4、设置商品的随机码(防止恶意攻击)
                    redisTo.setRandomCode(token);
                    //序列化json格式存入Redis中
                    String seckillValue = JSON.toJSONString(redisTo);
                    operations.put(seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString(),seckillValue);

                    //5、使用库存作为分布式redisson信号量
                    //信号量有限流作用,只允许或许信号量的请求来执行库存操作,减轻数据库压力
                    RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                    //使用商品可以秒杀的数量作为信号量
                    semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
                }
            });

        });
    }

  (1)在秒杀实体缓存中设置随机码token,在秒杀前端请求时需要携带token才可以执行请求,用来防止恶意攻击;

  (2)使用库存量作为分布式redisson信息量,只有获取到信号量才可以执行库存操作,减轻数据库压力

 

2. 商品抢购

  (1)当商品在秒杀活动中时,执行抢购请求,不在活动中时执行普通请求。

  (2)在抢购请求中获取携带商品id,随机码,和商品数量信息;

                   <div class="box-btns-two"
                         th:if="${item.seckillSkuVo == null }">
                        <a class="addToCart" href="#" th:attr="skuId=${item.info.skuId}">
                            加入购物车
                        </a>
                    </div>

                    <div class="box-btns-two"
                         th:if="${item.seckillSkuVo != null && (#dates.createNow().getTime() >= item.seckillSkuVo.startTime && #dates.createNow().getTime() <= item.seckillSkuVo.endTime)}">
                        <a class="seckill" href="#"
                           th:attr="skuId=${item.info.skuId},sessionId=${item.seckillSkuVo.promotionSessionId},code=${item.seckillSkuVo.randomCode}">
                            立即抢购
                        </a>
                    </div>

        $(".seckill").click(function () {
        var isLogin = [[${session.loginUser != null}]];     //true
        if (isLogin) {
            var killId = $(this).attr("sessionid") + "-" + $(this).attr("skuid");
            var code = $(this).attr("code");
            var num = $("#productNum").val();
            location.href = "http://seckill.gulimall.com/kill?killId=" + killId + "&key=" + code + "&num=" + num;
        } else {
            alert("秒杀请先登录");
        }
        return false;
    });
    /**
     * 商品进行秒杀(秒杀开始)
     * @param killId
     * @param key
     * @param num
     * @return
     */
    @GetMapping(value = "/kill")
    public String seckill(@RequestParam("killId") String killId,
                          @RequestParam("num") Integer num,
                          Model model) {

        String orderSn = null;
        try {
            //1、判断是否登录
            orderSn = seckillService.kill(killId,num);
            model.addAttribute("orderSn",orderSn);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "success";
    }

    /**
     * 当前商品进行秒杀(秒杀开始)
     * @param killId
     * @param key
     * @param num
     * @return
     */
    @Override
    public String kill(String killId, String key, Integer num) throws InterruptedException {

        long s1 = System.currentTimeMillis();
        //获取当前用户的信息
        MemberResponseVo user = LoginUserInterceptor.loginUser.get();

        //1、获取当前秒杀商品的详细信息从Redis中获取
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
        String skuInfoValue = hashOps.get(killId);
        if (StringUtils.isEmpty(skuInfoValue)) {
            return null;
        }
        //(合法性效验)
        SeckillSkuRedisTo redisTo = JSON.parseObject(skuInfoValue, SeckillSkuRedisTo.class);
        Long startTime = redisTo.getStartTime();
        Long endTime = redisTo.getEndTime();
        long currentTime = System.currentTimeMillis();
        //判断当前这个秒杀请求是否在活动时间区间内(效验时间的合法性)
        if (currentTime >= startTime && currentTime <= endTime) {

            //2、效验随机码和商品id
            String randomCode = redisTo.getRandomCode();
            String skuId = redisTo.getPromotionSessionId() + "-" +redisTo.getSkuId();
            if (randomCode.equals(key) && killId.equals(skuId)) {
                //3、验证购物数量是否合理和库存量是否充足
                Integer seckillLimit = redisTo.getSeckillLimit();

                //获取信号量
                String seckillCount = redisTemplate.opsForValue().get(SKU_STOCK_SEMAPHORE + randomCode);
                Integer count = Integer.valueOf(seckillCount);
                //判断信号量是否大于0,并且买的数量不能超过库存
                if (count > 0 && num <= seckillLimit && count > num ) {
                    //4、验证这个人是否已经买过了(幂等性处理),如果秒杀成功,就去占位。userId-sessionId-skuId
                    //SETNX 原子性处理
                    String redisKey = user.getId() + "-" + skuId;
                    //设置自动过期(活动结束时间-当前时间)
                    Long ttl = endTime - currentTime;
                    Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
                    if (aBoolean) {
                        //占位成功说明从来没有买过,分布式锁(获取信号量-1)
                        RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
                        //TODO 秒杀成功,快速下单
                        boolean semaphoreCount = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
                        //保证Redis中还有商品库存
                        if (semaphoreCount) {
                            //创建订单号和订单信息发送给MQ
                            // 秒杀成功 快速下单 发送消息到 MQ 整个操作时间在 10ms 左右
                            String timeId = IdWorker.getTimeId();
                            SeckillOrderTo orderTo = new SeckillOrderTo();
                            orderTo.setOrderSn(timeId);
                            orderTo.setMemberId(user.getId());
                            orderTo.setNum(num);
                            orderTo.setPromotionSessionId(redisTo.getPromotionSessionId());
                            orderTo.setSkuId(redisTo.getSkuId());
                            orderTo.setSeckillPrice(redisTo.getSeckillPrice());
                            rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",orderTo);
                            long s2 = System.currentTimeMillis();
                            log.info("耗时..." + (s2 - s1));
                            return timeId;
                        }
                    }
                }
            }
        }
        long s3 = System.currentTimeMillis();
        log.info("耗时..." + (s3 - s1));
        return null;
    }

  (1)在秒杀时需要进行判断用户是否已经购买过,如果没有购买过才可以执行购买;

  (2)没有购买的用户可以获取信号量,获取到信号量后,信号量会减去当前购买的数量;

  (3)如果购买成功,会执行创建订单操作,保存订单项信息,保存商品sku信息等操作,整个操作会发送到mq中来执行,这样整个秒杀订单的请求都会在redis中来执行,耗时较短。

  每执行一次下单请求,信号量会-1,同时会保存当前userId-sessionId-skuId,记录当前用户是否已经抢购过。

标签:服务,redisTo,killId,信号量,商品,num,秒杀,String
From: https://www.cnblogs.com/homle/p/17739299.html

相关文章

  • 三丰 /////云免费云服务器的使用体验
    在网上看到了三风云主机的介绍,想着试试看就注册了账号搞了一个免费云服务器试试,怎么说呢,作为一个新手,之前完全没有接触过云服务器,使用了三f云的免费云服务器,整个过程非常简单,很快就可以完成服务器的设置和部署。官方也提供了带面板的和纯净的centos,对于我来说还是挺方便的,而且我是......
  • SpringCloud微服务学习笔记(二)【Feign,Gateway,Docker】
    Feign先来看我们以前利用RestTemplate发起远程调用的代码:存在下面的问题:•代码可读性差,编程体验不统一•参数复杂URL难以维护Feign是一个声明式的http客户端,官方地址:https://github.com/OpenFeign/feign其作用就是帮助我们优雅的实现http请求的发送,解决上面提到的问题。基......
  • 分析商品/商家上的某类型文案好坏的思路
    问题定义每个商品/商家卡片,上面有几个文案候选可以展示,最终随机展示一个文案,每个文案有一个文案类型,现在想分析出某类型文案的好坏,分析出某类型文案对于用户展示的好坏,分析出哪类文案好哪类文案略差。问题难点如果直接用曝光点击日志的所有商品/商家的某类型点击总和除以......
  • VScode中下载了插件但是无法找到SSH Target连接服务器的解决方法(CANNOT find SSH Targ
    VSCode版本vscodeversion:(version1.82)已下载扩展installedextensions:Remote-SSHv0.106.4Remote-SSH:EditingConfigurationFilesv0.86.0RemoteDevelopmentv0.24.0WSLv0.81.3几天前我从pycharm转战vscode,在连接服务器时遇到了一些问题。根据一些较为古早的......
  • Ubuntu服务器安全性提升:修改SSH默认端口号
    在Ubuntu服务器上,SSH(SecureShell)是一种至关重要的远程连接工具。它提供了一种安全的方式来远程连接和管理计算机系统,通过加密通信来确保数据的保密性和完整性。SSH协议广泛用于计算机网络中,用于远程管理、文件传输和安全通信等任务。然而,SSH默认使用的端口号是22,这也是黑客们常常......
  • 魅族云服务自动一键所有选择图片下载。
    魅族云服务的相册功能,没有一键选择所有的图片,就挺恶心的。魅族不一直提供云相册的服务了,就需要将图片全部下载。之前有大神写过油泼猴的脚本。今天拿来用,发现用不了。又在网上查一下了,有npm的开源下载工具。附上码云地址,没用过。https://gitee.com/moreant/mpcb但是部署起来太......
  • 解决服务器取证过程中宝塔强制绑定手机号的问题
    声明本文中提及的方式仅是为了便于服务器取证的研究,仅适用于无法出网的真实取证鉴定情况。请不要在生产环境随意修改宝塔服务的任何文件!分析目前,宝塔面板已经强制要求绑定手机号。这给取证工作带来很大的不便,尤其是在实际工作中,服务器是不可以连接互联网的,因此必须解决掉这个......
  • 通过python封装接口商品ID采集商品详情数据
    您可以使用Python中的requests库和json库发起HTTP请求并解析响应数据,来实现获取微店商品详情数据的操作。以下是一个简单的示例代码:importrequestsimportjsondeffetch_weidian_product_detail(product_id):#构造请求URLurl=f"https://api.vdian.com/api?param={json.dumps......
  • API商品数据接口调用
    一、API商品数据接口概述API商品数据接口是一种通用的数据交互方式,它允许不同系统之间进行数据传输和交互。API商品数据接口可以使用各种不同的协议和标准来实现,例如RESTfulAPI、SOAP、XML-RPC等。其中RESTfulAPI是最常用的一种,它基于HTTP协议和JSON格式进行数据传输,具有简单易用......
  • 微服务的设计涉及表的访问基本原则
    微服务的设计涉及表的访问基本原则1.微服务设计上是高于独立模块,提供服务能力的接口设计。多个微服务之间,如果涉及到访问同一个数据表的访问,更多的考虑将该表的sqlmapdao层的代码归结到某个具体的服务中,而不是在多个服务中都提供一套相同的代码,不便于表的管理。(高内聚,低耦合)其......