基于Vue+SpringCloudAlibaba微服务电商项目实战-022:基于Redis实现秒杀抢购

1 微服务秒杀抢购实现方案

今日课程任务

  1. 大型电商秒杀抢购有哪些技术实现方案
  2. 秒杀抢购如何防止库存超卖的问题
  3. 秒杀抢购如何保证接口的安全性
  4. 基于Redis令牌桶方式实现秒杀抢购
  5. 基于lua脚本与java方式实现秒杀抢购区别
  6. 基于kafka异步实现扣库存,减少数据库IO操作
  7. 前端如何定时获取秒杀抢购消费结果

2 秒杀抢购如何实现前端优化

秒杀接口的实现;实际上就是一个互联网高并发解决问题+亿万级别商品详情页面设计。
前端:

  1. 实现动静分离 将静态资源(js、img、css、html)和动态资源(ftl、接口)分开部署;
  2. 将静态资源部署到第三方cdn中存放,减少带宽传输距离,提高网页加载速度;
  3. 将一些静态资源实现压缩(img/css/js等)生成.min文件,减少带宽的传输;
  4. 前端需要将抢购按钮置灰,防止用户重复触发秒杀接口;
  5. 生成一个图像验证码防止机器模拟刷秒杀接口;
  6. 基于openresty+lua实现亿万级别商品详情页面;

作用:目的就是能够将秒杀的商品详情页面快速展示给用户。

3 基于mysql行锁机制防止库存超卖

后端:

  1. 使用网关对秒杀接口实现限流、服务降级、熔断,有效的保护秒杀接口,使用黑名单和白名单限制部分刷接口用户(sentinel/redis/guava);
  2. 先扣库存,库存扣成功后下订单,跳转到聚合支付;
    秒杀成功->扣库存->下订单 生成一个支付令牌跳转到聚合支付令牌

注意:扣库存是秒杀成功之后扣而不是用户支付成功后扣

  1. 秒杀成功就是不支付如何实现库存回滚?
    30分钟订单超时的问题 解决:基于redis过期key或者MQ延迟队列
  2. 假设当前库存为1的情况下,多个用户同时抢购最后一个库存,怎么防止库存超卖的问题?
    解决:库存>0或者乐观锁、数据库自带行锁机制实现
    update meite_seckill set inventory=inventory-1,version=version+1 where seckill_id=#{seckillId} and inventory>0
    mysql行锁 多个线程同时操作同一行数据的时候,最终只会有一个线程进行操作,目的是为了保证线程安全性问题,防止数据脏读。
    redis多线程同时操作同一个key做set操作,没有行锁机制。

4 基于乐观锁方式防止库存超卖

update meite_seckill set inventory=inventory-1,version=version+1 where seckill_id=#{seckillId} and inventory>0 and version=#{version}
乐观锁与悲观锁区别:
乐观锁:自旋形式,程序不会阻塞 类似java CAS无锁机制
悲观锁:会导致程序阻塞,效率较低 synchronized/lock
在这里插入图片描述
使用乐观锁机制防止库存马上被抢完,采用自旋的形式靠运气秒杀抢购。

5 基于Redis生成令牌桶方式实现秒杀

如果有10万人抢购商品,会对数据库做10万次的io操作,对数据库性能压力非常大。
库存如果有100个的情况下,只需要做100次减库存。
采用令牌桶方式实现。如果人为修改库存,令牌桶的令牌数量也需要修改。

  1. 基于Redis生成库存令牌桶,当前库存有100个就生成100个令牌,只要能够抢到令牌就可以去减库存,可以有效减少数据库的io操作。
    Key=秒杀的商品id value=list(token)

在这里插入图片描述

6 提供生成令牌桶接口

秒杀成功明细表

DROP TABLE IF EXISTS `meite_order`;
CREATE TABLE `meite_order` (
  `seckill_id` bigint(20) NOT NULL COMMENT '秒杀商品id',
  `user_phone` bigint(20) NOT NULL COMMENT '用户手机号',
  `state` tinyint(4) NOT NULL DEFAULT '-1' COMMENT '状态标示:-1:无效 0:成功 1:已付款 2:已发货',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='秒杀成功明细表';

INSERT INTO `meite_order` VALUES ('10000', '15528415441', '1', '2020-04-28 22:18:01');

秒杀库存表

DROP TABLE IF EXISTS `meite_seckill`;
CREATE TABLE `meite_seckill` (
  `seckill_id` bigint(20) NOT NULL COMMENT '商品库存id',
  `name` varchar(120) CHARACTER SET utf8 NOT NULL COMMENT '商品名称',
  `inventory` int(11) NOT NULL COMMENT '库存数量',
  `start_time` datetime NOT NULL COMMENT '秒杀开启时间',
  `end_time` datetime NOT NULL COMMENT '秒杀结束时间',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `version` bigint(20) NOT NULL DEFAULT '0',
  PRIMARY KEY (`seckill_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀库存表';
INSERT INTO `meite_seckill` VALUES ('10000', '蚂蚁课堂第七期微服务架构', '99', '2020-04-28 16:47:44', '2020-04-30 16:47:47', '2020-04-28 16:47:50', '2');

新建项目mt-shop-service-api-seckill、mt-shop-service-seckill

public interface OrderTokenService {

    /**
     * 新增对应商品库存令牌桶
     *
     * @seckillId 商品库存id
     */
    @GetMapping("/addSpikeToken")
    public BaseResponse<JSONObject> addSpikeToken(Long seckillId, Long tokenQuantity);
}
@RestController
public class OrderTokenServiceImpl extends BaseApiService implements OrderTokenService {
    @Autowired
    private SeckillMapper seckillMapper;
    @Autowired
    private TokenUtil tokenUtil;
    public BaseResponse<JSONObject> addSpikeToken(Long seckillId, Long tokenQuantity) {
        // 1.验证参数
        if (seckillId == null) {
            return setResultError("商品库存id不能为空!");
        }
        if (tokenQuantity == null) {
            return setResultError("token数量不能为空!");
        }
        SeckillEntity seckillEntity = seckillMapper.findBySeckillId(seckillId);
        if (seckillEntity == null) {
            return setResultError("商品信息不存在!");
        }
        // 2.使用多线程异步生产令牌
        createSeckillToken(seckillId, tokenQuantity);
        return setResultSuccess("令牌正在生成中.....");
    }


    private void createSeckillToken(Long seckillId, Long tokenQuantity) {
        tokenUtil.createListToken("seckill_", seckillId + "", tokenQuantity);
    }

}
TokenUtil

public void createListToken(String keyPrefix, String redisKey, Long tokenQuantity) {
    List<String> listToken = getListToken(keyPrefix, tokenQuantity);
    redisUtil.setList(redisKey, listToken);
}

public List<String> getListToken(String keyPrefix, Long tokenQuantity) {
    List<String> listToken = new ArrayList<>();
    for (int i = 0; i < tokenQuantity; i++) {
        String token = keyPrefix + UUID.randomUUID().toString().replace("-", "");
        listToken.add(token);
    }
    return listToken;

}

测试效果:
在这里插入图片描述

7 MQ异步消费如何获取消费结果

  1. 采用多线程或者MQ异步根据令牌修改库存,先返回“正在秒杀中”结果给客户端。
    问题:秒杀接口不能立即响应秒杀结果,所以前端ajax采用根据用户userId或者手机号码主动定时查询秒杀结果。
    案例:12306提示正在出票中。。过段时间提示出票成功或者出票失败

8 MQ消费者如何保证幂等性问题

@RestController
public class SpikeCommodityServiceImpl extends BaseApiService implements SpikeCommodityService {

    @Autowired
    private TokenUtil tokenUtil;
    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private SpikeManager spikeManager;

    @Override
    public BaseResponse<JSONObject> spike(String userPhone, Long seckillId) {
        // 1.验证参数
        if (userPhone == null) {
            return setResultError("userPhone不能为空");
        }
        if (seckillId == null) {
            return setResultError("seckillId不能为空");
        }
        // 2.对用户的频率实现限制 10s setnx key存在返回1 key如果不存在的情况0
        Boolean spikeNx = redisUtil.setNx(userPhone, seckillId + "", 10l);
        if (!spikeNx) {
            return setResultError("您当前在10s内访问频率过多,请稍后重试!");
        }
        // 3. 从Redis中获取令牌,
        String spikeToken = tokenUtil.getListKeyToken(seckillId + "");
        if (StringUtils.isEmpty(spikeToken)) {
            return setResultError("很抱歉,当前商品已经售空,请下次再来");
        }
        // 4.修改库存、下订单 肯定不同步 一定要是异步实现 MQ异步实现     // 投递消息到MQ中
        spikeManager.sendSpikeMsg(userPhone, seckillId, spikeToken);
        return setResultError("正在抢购中...");
    }
}
@Component
@EnableAsync
public class SpikeManager {
    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    public void sendOrderMsg(JSONObject data){
        kafkaTemplate.send("mayikt-topic-spike", null, data.toJSONString());
    }
    @Async
    public void sendSpikeMsg(String userPhone,Long seckillId,String spikeToken){
        JSONObject data = new JSONObject();
        data.put("userPhone",userPhone);
        data.put("seckillId",seckillId);
        data.put("spikeToken",spikeToken);
        // 投递消息到MQ中
        sendOrderMsg(data);
    }
}
@Component
public class InventoryConsumer {
    @Autowired
    private SeckillMapper seckillMapper;
    @Autowired
    private OrderMapper orderMapper;

    @KafkaListener(topics = "mayikt-topic-spike")
    public void receive(ConsumerRecord<?, ?> consumer) throws Exception {
        String json = (String) consumer.value();
        if (StringUtils.isEmpty(json)) {
            return;
        }
        JSONObject jsonObject = JSONObject.parseObject(json);
        String userPhone = jsonObject.getString("userPhone");
        Long seckillId = jsonObject.getLong("seckillId");
        // 根据userid+seckillId+msg自带的消息全局id 解决幂等性问题
        //  3.根据该id查询该商品是否存在
        SeckillEntity seckillEntity = seckillMapper.findBySeckillId(seckillId);
        if (seckillEntity == null) {
            return;
        }
        // 4.对库存实现减去1
        Long version = seckillEntity.getVersion();
        int resultSeckill = seckillMapper.inventoryDeduction(seckillId, version);
        if (resultSeckill <= 0) {
            return;
        }
        // 5.插入订单记录
        OrderEntity orderEntity = new OrderEntity(seckillId, userPhone);
        int resultOrder = orderMapper.insertOrder(orderEntity);
        if (resultOrder <= 0) {
            return;
        }
    }
}

效果测试:
在这里插入图片描述

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
该资源内项目源码是个人的课程设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。 该资源内项目源码是个人的课程设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。
本文介绍了一个基于Spring Boot、Spring CloudVue前后端分离的项目实战。这个项目是一个简单的在线商城,包含了用户注册、登录、商品展示、购物车、订单管理等功能。通过这个项目,读者可以深入理解前后端分离的架构模式和互联网应用的开发方式。 首先,文章介绍了前后端分离的基本概念和优势。前后端分离是将应用的前端和后端代码分开来开发,使得前端和后端具有独立的开发周期和技术栈,进而提高了开发效率和代码质量。同时,前后端分离还可以提供更好的用户体验和灵活性,对于互联网应用来说尤为重要。 接下来,文章介绍了项目的架构和技术栈。项目采用了Spring Boot和Spring Cloud框架来实现后端代码,采用MyBatis作为ORM框架和Redis作为缓存中间件。同时,项目还采用了Vue.js作为前端框架和Element UI组件库来实现前端页面。通过这些开源框架和组件,可以快速搭建一个前后端分离的互联网应用。 然后,文章介绍了项目的核心功能和代码实现。在用户注册和登录方面,项目采用了Spring Security框架和JWT令牌来实现用户认证和授权,保证了用户信息的安全性。在商品展示和购物车方面,项目采用了Vue.js来实现前端页面和事件处理。在订单管理方面,项目采用了MyBatis Plus来实现订单数据的持久化和分页查询。 最后,文章介绍了项目的测试和优化。通过对项目的压力测试和性能测试,文章发现项目还存在一些性能瓶颈和安全隐患,可以通过优化数据库查询、缓存配置和代码实现来提高应用的性能和安全性。 总之,这篇文章介绍了一个基于Spring Boot、Spring CloudVue前后端分离的项目实战,通过实现一个在线商城的功能,展示了前后端分离的开发模式和互联网应用的开发技术栈。本文可以作为前后端分离开发的入门教程,也可以作为互联网应用开发的参考文档。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值