分布式实战常见的电商项目中秒杀商品模块编写思路

分布式实战 (dubbo+zookeeper,redis,elastic search+kibana,mysql,jwt) 常见的电商项目中秒杀商品模块编写思路

image

秒杀服务模块

"秒杀"是网络卖家发布的一种超低价格的商品,所有买家在同一时间抢购的一种消费方式。秒杀技术实现的核心思想是使用缓存技术减轻数据库的压力。在秒杀是首先会将秒杀商品从数据库同步到缓存中,用户在缓存中查询秒杀商品redis的读写效率要比mysql高抢购时减少缓存中的商品数量。秒杀商品的
用户访问量以及并发量是比较大的,数据库并承受不了那么大的压力,产生的秒杀商品用户先写入缓存,当用户完成付款后,再将订单数据保存到数据库。

image

单点登录

单点登录,在用户登录一次后,可以访问所有相互信任的应用系统

JWT分布式鉴权

一般的电商项目在用户完成登录后会在头部显示欢迎回来xxx,在分布式项目中使用session保存用户信息是不行的,session只能保存在当前服务器,而分布式项目是有很多服务器的,此时我们需要使用单点登录这一技术来获取用户信息。
单点登录功能编写:

  1. 编写JWT工具类(生成令牌和解析令牌的方法)
  2. 用户登录成功后返回令牌字符串给前端
  3. 前端每次请求都在请求头加上令牌字符串(token)
  4. 编写拦截器,解析请求头中的令牌字符串
  5. 在api模块配置拦截器,指定需要拦截哪些接口,即需要登录才能访问的接口。一般都是拦截所有接口,放行个别接口

在后台管理系统我们可以通过Spring Security技术来获取认证用户的信息

秒杀商品服务接口(编写在通用模块)

我们采用的是dubbo+zookeeper这样的一个注册中心,使用Dubbo时服务的生产者和消费者都需要引入服务接口,所以我们要构建一个通用模块。除了编写服务接口还可以编写一些工具类,实体类,统一异常处理器等等。

  1. 将mysql中秒杀商品数据同步到redis中
  2. 根据商品id查询秒杀订单
  3. 生成订单数据保存到redis中
  4. 根据订单id查询订单数据
  5. 支付秒杀订单
// 秒杀商品服务
public interface SeckillService {

    Page<SeckillGoods> findPageByRedis(int page, int size);

    SeckillGoods findSeckillGoodsByRedis(Long goodsId);

    Orders createOrder(Orders orders);

    Orders findOrder(String id);

    Orders pay(String orderId);

}

秒杀商品服务实现类(dubbo服务提供者)

将数据库中的秒杀商品同步到redis中

这里使用的是Spring Task定时任务需要在spring的启动类上方添加EnableScheduling注解开启定时任务,在开发环境中为了方便测试,每5秒同步一次数据,在生产环境可以设置成每分钟同步一次数据。

将秒杀商品同步到reids中,需要自己构建查询条件,秒杀商品即startTime<当前时间<endTime并且库存>0,也就是说用户看到的秒杀商品都是正在秒杀的商品且有库存的。

@Scheduled(cron = "0/5 * * * * *")
    public void refreshRedis(){
        // 将redis中的商品库存数同步到mysql中
        List<SeckillGoods> seckillGoodsList = redisTemplate.boundHashOps("seckillGoods").values();
        for (SeckillGoods seckillGoods : seckillGoodsList) {
            SeckillGoods goods = seckillGoodsMapper.selectById(seckillGoods.getId());
            goods.setStockCount(seckillGoods.getStockCount());
            seckillGoodsMapper.updateById(goods);
        }
        System.out.println("将mysql中的数据同步到redis中");
        QueryWrapper<SeckillGoods> queryWrapper = new QueryWrapper();
        Date date = new Date();
        String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date);
        queryWrapper.le("startTime",now)
                .ge("endTime",now)
                .gt("stockCount",0);
        // 查询出正在秒杀的商品
        List<SeckillGoods> seckillGoods = seckillGoodsMapper.selectList(queryWrapper);
        // 删除redis中的秒杀商品数据
        redisTemplate.delete("seckillGoods");
        // 将数据同步到redis中
        for (SeckillGoods seckillGood : seckillGoods) {
            redisTemplate.boundHashOps("seckillGoods").put(seckillGood.getGoodsId(),seckillGood);
        }
    }

在Redis中查询所有秒杀订单

前端还是比较喜欢处理Mybatis-Plus的分页数据的,MP的页数是从1开始的补充:ES的分页是从0开始的,这里为了方便前端操作我们需要自己构建MP的分页对象,即当前页,每页条数,总条数,返回结果集。

@Override
    public Page<SeckillGoods> findPageByRedis(int page, int size) {

        List<SeckillGoods> seckillGoodsList = redisTemplate.boundHashOps("seckillGoods").values();

        int start = (page - 1) * size; 

        int end = start + size > seckillGoodsList.size() ? seckillGoodsList.size() : start + size;

        List<SeckillGoods> seckillGoods = seckillGoodsList.subList(start, end);

        Page<SeckillGoods> page1 = new Page();
        page1.setCurrent(page)
                .setSize(size)
                .setTotal(seckillGoodsList.size())
                .setRecords(seckillGoods);
        return page1;
    }

根据商品id查询出正在秒杀的商品

@Override
    public SeckillGoods findSeckillGoodsByRedis(Long goodsId) {
        SeckillGoods seckillGoods = (SeckillGoods) redisTemplate.boundHashOps("seckillGoods").get(goodsId);
        return seckillGoods;
    }

生成订单

订单的id为雪花算法生成的id是个字符串,在分布式的项目中使用雪花算法生成的id基本上不会重复,
这里需要返回一个订单对象给前端,前端会拿到订单id,方便下面完成根据订单id查询订单的功能。
在生成订单后需要在缓存中减少库存,并且在同步方法中修改mysql中的数据,避免秒杀商品无线循环。

为什么会出现秒杀商品无线循环的问题呢?

因为我们是将mysql的数据同步到redis中,用户生成订单后订单库存减少,此时redis中的库存发生变化,但mysql中的库存并没有发生变化,而我们的定时任务是每5秒进行同步数据,这时候就会出现一个问题,用户明明生成了订单但库存并没有变化,此时我们需要修改定时任务的方法,将redis的库存同步到mysql中。

订单超时回退库存解决方案

当redis中的数据过期时,我们仍然可以拿到key但拿不到value即订单详情,此时我们需要创建一个订单副本即订单过期时间要比原订单要长,当用户下单之后在5分钟内未付款删除redis中的订单数据并使商品回退库存。

创建redis配置类

@Configuration
public class RedisListenerConfig {

    @Bean
    public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory){
        RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
        redisMessageListenerContainer.setConnectionFactory(connectionFactory);
        return redisMessageListenerContainer;
    }
}

创建redis监听过期订单的方法

注意:当订单过期后触发该监听方法使商品回退库存,此时我们需要将副本也删除掉,副本也是有过期时间的当副本过期后触发该方法没有意义,在订单支付成功后我们也应该将副本删除。

/**
 * 监听过期订单
 */
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {

    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private SeckillService seckillService;

    public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
        super(listenerContainer);
    }

    @Override
    public void onMessage(Message message, byte[] pattern) {
        String orderId = message.toString();
        Orders orders = (Orders) redisTemplate.opsForValue().get(orderId + "_copy");
        Long goodId = orders.getCartGoods().get(0).getGoodId();
        Integer num = orders.getCartGoods().get(0).getNum();

        SeckillGoods seckillGoods = seckillService.findSeckillGoodsByRedis(goodId);

        seckillGoods.setStockCount(seckillGoods.getStockCount()+num);
        redisTemplate.boundHashOps("seckillGoods").put(goodId,seckillGoods);

        redisTemplate.delete(orderId+"_copy");
    }
}
@Override
    public Orders createOrder(Orders orders) {
        orders.setId(IdWorker.getIdStr());
        orders.setCreateTime(new Date());
        orders.setExpire(new Date(new Date().getTime()+1000*60*5));
        CartGoods cartGoods = orders.getCartGoods().get(0);
        Integer num = cartGoods.getNum();
        BigDecimal price = cartGoods.getPrice();
        BigDecimal sum = price.multiply(BigDecimal.valueOf(num));
        orders.setPayment(sum);

        SeckillGoods seckillGoods = findSeckillGoodsByRedis(cartGoods.getGoodId());
        Integer stockCount = seckillGoods.getStockCount();
        if(stockCount <= 0){
            throw new BusException(CodeEnum.STOCK_COUNT_ERROR);
        }
        seckillGoods.setStockCount(stockCount - num);
        redisTemplate.boundHashOps("seckillGoods").put(seckillGoods.getGoodsId(),seckillGoods);

        redisTemplate.setKeySerializer(new StringRedisSerializer());

        redisTemplate.opsForValue().set(orders.getId(),orders,1, TimeUnit.MINUTES);

        redisTemplate.opsForValue().set(orders.getId()+"_copy",orders,2,TimeUnit.MINUTES);
        return orders;
    }

根据订单id查询订单

@Override
    public Orders findOrder(String id) {
        Orders orders = (Orders) redisTemplate.opsForValue().get(id);
        return orders;
    }

支付订单

支付功能的编写:
使用的是一个支付宝沙箱环境进行测试,需要自己去申请,拿到支付宝公钥用于我们和支付宝交互,获取应用私钥用于支付宝和我们交互生成二维码需要封装请求参数out_trade_no、total_amount、subject这些参数分别是订单编号、支付金额、主题。
以下代码仅供参考:

 @Override
    public String pcPay(Orders orders) {
        // 创建支付请求
        AlipayTradePrecreateRequest alipayTradePrecreateRequest = new AlipayTradePrecreateRequest();
        // 设置请求内容
        alipayTradePrecreateRequest.setNotifyUrl(zfbPayConfig.getNotifyUrl()+zfbPayConfig.getPcNotify());
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("out_trade_no",orders.getId()); // 订单编号
        jsonObject.put("total_amount",orders.getPayment()); // 支付金额
        jsonObject.put("subject",orders.getCartGoods().get(0).getGoodsName()); // 订单标题
        alipayTradePrecreateRequest.setBizContent(jsonObject.toString());
        // 发送请求
        try {
            AlipayTradePrecreateResponse alipayTradePrecreateResponse = alipayClient.execute(alipayTradePrecreateRequest);
            // 生成二维码
            return alipayTradePrecreateResponse.getQrCode(); // 二维码字符串
        } catch (AlipayApiException e) {
            throw new BusException(CodeEnum.ZFB_PAY_ERROR);
        }

    }

    @Override
    public void checkSign(Map<String, Object> paramMap) {
        // 获取参数
        Map<String,String[]> requestParameterMap = (Map<String, String[]>) paramMap.get("requestParameterMap");
        // 验签
        boolean valid = ZfbVerifierUtils.isValid(requestParameterMap, zfbPayConfig.getPublicKey());
        if(!valid){
            throw new BusException(CodeEnum.CHECK_SIGN_ERROR);
        }
    }

	@PostMapping("/pcPay")
    public BaseResult<String> pcPay(String orderId){
        Orders orders = orderService.findById(orderId);
        String pay = zfbPayService.pcPay(orders);
        return BaseResult.ok(pay);
    }
    @PostMapping("/success/notify")
    public BaseResult successNotify(HttpServletRequest request){
        // 验签
        Map<String,Object> map = new HashMap();
        map.put("requestParameterMap",request.getParameterMap());
        zfbPayService.checkSign(map);

        String trade_status = request.getParameter("trade_status"); // 订单状态
        String out_trade_no = request.getParameter("out_trade_no"); // 订单编号

        // 付款成功
        if(trade_status.equals("TRADE_SUCCESS")){
            // 修改订单状态
            Orders orders = orderService.findById(out_trade_no);
            orders.setPaymentType(2); // 支付方式
            orders.setStatus(2); // 订单状态
            orders.setPaymentTime(new Date()); // 付款时间
            orderService.update(orders);
            // 新增交易记录
            Payment payment = new Payment();
            payment.setOrderId(out_trade_no);
            payment.setTransactionId(out_trade_no);
            payment.setTradeType("支付宝支付");
            payment.setTradeState(trade_status);
            payment.setContent(JSON.toJSONString(request.getParameterMap()));
            payment.setCreateTime(new Date());
            payment.setPayerTotal(orders.getPayment());
            zfbPayService.addPayment(payment);
        }
        return BaseResult.ok();
    }
@Override
    public Orders pay(String orderId) {
        // 查询订单设置订单数据
        Orders orders = (Orders) redisTemplate.opsForValue().get(orderId);
        if(orders == null){
            throw new BusException(CodeEnum.ORDER_PAY_ERROR);
        }
        orders.setPaymentTime(new Date());
        orders.setPaymentType(2);
        orders.setStatus(2);
        // 从redis中删除订单数据
        redisTemplate.delete(orderId);
        // 删除复制订单数据
        redisTemplate.delete(orderId+"_copy");
        return orders;
    }
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
项目是采用目前比较流行的SpringBoot/SpringCloud构建微服务电商项目项目叫 《果然新鲜》,实现一套串联的微服务电商项目。完全符合一线城市微服务电商的需求,对学习微服务电商架构,有非常大的帮助,该项目涵盖从微服务电商需求讨论、数据库设计、技术选型、互联网安全架构、整合SpringCloud各自组件、分布式基础设施等实现一套完整的微服务解决方案。 项目使用分布式微服务框架,涉及后台管理员服务、地址服务、物流服务、广告服务、商品服务、商品别服务、品牌服务、订单服务 、购物车服务、首页频道服务、公告服务、留言服务、搜索服务、会员服务等。  系统架构图   SpringBoot+SpringCloud+SSM构建微服务电商项目使用SpringCloud Eureka作为注册心,实现服务治理使用Zuul网关框架管理服务请求入口使用Ribbon实现本地负载均衡器和Feign HTTP客户端调用工具使用Hystrix服务保护框架(服务降级、隔离、熔断、限流)使用消息总线Stream RabbitMQ和 Kafka微服务API接口安全控制和单点登录系统CAS+JWT+OAuth2.0分布式基础设施构建分布式任务调度平台XXL-JOB分布式日志采集系统ELK分布式事务解决方案LCN分布式锁解决方案Zookeeper、Redis分布式配置心(携程Apollo)高并发分布式全局ID生成(雪花算法)分布式Session框架Spring-Session分布式服务追踪与调用链Zipkin项目运营与部署环境分布式设施环境,统一采用Docker安装使用jenkins+docker+k8s实现自动部署微服务API管理ApiSwagger使用GitLab代码管理(GitHub  GitEE)统一采用第三方云数据库使用七牛云服务器对静态资源实现加速 开发环境要求JDK统一要求:JDK1.8Maven统一管理依赖 统一采用Docker环境部署编码统一采用UTF-8开发工具IDEA 或者 Eclipse 

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序猿晓晓

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值