SpringCloudAlibaba 综合项目实战工业级PaaS云平台第四课 优惠券模块的分布式设计和压力测试

发布于:2022-12-10 ⋅ 阅读:(572) ⋅ 点赞:(0)

第十三章 电商优惠券微服务业务介绍和模块开发

第1集 互联网公司中优惠券微服务业务介绍和效果体验

简介:介绍常见的优惠券业务和课程案例效果体验

  • 互联网企业最重要的是拉新业务,产品经理 自然诞生了很多玩法
    • 砍价
    • 拼团
    • 裂变
    • 优惠券

在这里插入图片描述

  • 电商优惠券逻辑,优惠券的玩法很多,主要讲一下比较常见的优惠券

    • 获取方式维度

      • 被动:新人优惠券

        • 无门槛现金劵
        • …其他
        第一次登录注册某平台,登录成功后进入到首个页面弹出新人红包或者某固定位置领取新人红包,前端领取位置及细节不做详细讲解,根据实际业务场景而定
        
      • 主动:领取优惠券

        • 满减劵

        • …其他

        促进商品订单成交,而设置的优惠券,提高下单支付率
        
    • 使用门槛维度

      • 无门槛现金劵
      • 满减劵
      • 满减折扣卷
      • 运费抵扣券
      • 兑换券
      • 店铺劵
      • 单品劵
      • …其他
      按照多维度设计,就需要用到规则引擎,这类一般是大厂的营销中台设计的,复杂且容易出问题
      
  • 优惠券常见属性

    • 类型:无门槛、满减、单品、折扣等
    • 每人领劵次数
    • 发券总量
    • 优惠券开始时间和结束时间
  • 天猫超市图

logo
  • 课程案例图

在这里插入图片描述

在这里插入图片描述

第2集 1024电商平台优惠券微服务业务介绍和数据库建立

简介:优惠券微服务介绍和效果体验

  • 大课优惠券业务需求介绍

    • 新用户注册-发放后端配置的新人优惠券
    • 用户可以主动领取优惠券
    • 下单可以选择对应的优惠券抵扣
    • 支持满减优惠券-无门槛优惠券两种
    • 多种元数据配置
      • 类型:无门槛、满减等
      • 每人领劵次数限制
      • 发券总量控制
      • 优惠券开始时间和结束时间
      • 优惠券状态配置
  • 核心知识:

    • 高并发下扣减劵库存
      • 超发
      • 单人超领取
  • 原生分布式锁+redisson框架分布锁使用

    • 分布式锁+最佳实践
  • 数据库表介绍

#优惠券表
CREATE TABLE `coupon` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `category` varchar(11) DEFAULT NULL COMMENT '优惠卷类型[NEW_USER注册赠券,TASK任务卷,PROMOTION促销劵]',
  `publish` varchar(11) DEFAULT NULL COMMENT '发布状态, PUBLISH发布,DRAFT草稿,OFFLINE下线',
  `coupon_img` varchar(524) DEFAULT NULL COMMENT '优惠券图片',
  `coupon_title` varchar(128) DEFAULT NULL COMMENT '优惠券标题',
  `price` decimal(16,2) DEFAULT NULL COMMENT '抵扣价格',
  `user_limit` int(11) DEFAULT NULL COMMENT '每人限制张数',
  `start_time` datetime DEFAULT NULL COMMENT '优惠券开始有效时间',
  `end_time` datetime DEFAULT NULL COMMENT '优惠券失效时间',
  `publish_count` int(11) DEFAULT NULL COMMENT '优惠券总量',
  `stock` int(11) DEFAULT '0' COMMENT '库存',
  `create_time` datetime DEFAULT NULL,
  `condition_price` decimal(16,2) DEFAULT NULL COMMENT '满多少才可以使用',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=23 DEFAULT CHARSET=utf8mb4;


#优惠券领劵记录
CREATE TABLE `coupon_record` (
  `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT,
  `coupon_id` bigint(11) DEFAULT NULL COMMENT '优惠券id',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间获得时间',
  `use_state` varchar(32) DEFAULT NULL COMMENT '使用状态  可用 NEW,已使用USED,过期 EXPIRED;',
  `user_id` bigint(11) DEFAULT NULL COMMENT '用户id',
  `user_name` varchar(128) DEFAULT NULL COMMENT '用户昵称',
  `coupon_title` varchar(128) DEFAULT NULL COMMENT '优惠券标题',
  `start_time` datetime DEFAULT NULL COMMENT '开始时间',
  `end_time` datetime DEFAULT NULL COMMENT '结束时间',
  `order_id` bigint(11) DEFAULT NULL COMMENT '订单id',
  `price` decimal(16,2) DEFAULT NULL COMMENT '抵扣价格',
  `condition_price` decimal(16,2) DEFAULT NULL COMMENT '满多少才可以使用',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=141 DEFAULT CHARSET=utf8mb4;
  • 测试数据准备
INSERT INTO `coupon` (`id`, `category`, `publish`, `coupon_img`, `coupon_title`, `price`, `user_limit`, `start_time`, `end_time`, `publish_count`, `stock`, `create_time`, `condition_price`)
VALUES
	(18, 'NEW_USER', 'PUBLISH', 'https://file.xdclass.net/video/2020/alibabacloud/zt-alibabacloud.png', '永久有效-新人注册-0元满减-5元抵扣劵-限领取2张-不可叠加使用', 5.00, 2, '2000-01-01 00:00:00', '2099-01-29 00:00:00', 100000000, 99999991, '2020-12-26 16:33:02', 0.00),
	(19, 'PROMOTION', 'PUBLISH', 'https://file.xdclass.net/video/2020/alibabacloud/zt-alibabacloud.png', '有效中-21年1月到25年1月-20元满减-5元抵扣劵-限领取2张-不可叠加使用', 5.00, 2, '2000-01-29 00:00:00', '2025-01-29 00:00:00', 10, 3,  '2020-12-26 16:33:03', 20.00),
	(22, 'PROMOTION', 'PUBLISH', 'https://file.xdclass.net/video/2020/alibabacloud/zt-alibabacloud.png', '过期-20年8月到20年9月-商品id3-6元抵扣劵-限领取1张-可叠加使用', 6.00, 1, '2020-08-01 00:00:00', '2020-09-29 00:00:00', 100, 100, '2020-12-26 16:33:03', 0.00),
	(20, 'PROMOTION', 'PUBLISH', 'https://file.xdclass.net/video/2020/alibabacloud/zt-alibabacloud.png', '有效中-20年8月到21年9月-商品id1-8.8元抵扣劵-限领取2张-不可叠加使用', 8.80, 2, '2020-08-01 00:00:00', '2021-09-29 00:00:00', 100, 96, '2020-12-26 16:33:03', 0.00),
	(21, 'PROMOTION', 'PUBLISH', 'https://file.xdclass.net/video/2020/alibabacloud/zt-alibabacloud.png', '有效中-20年8月到21年9月-商品id2-9.9元抵扣劵-限领取2张-可叠加使用', 8.80, 2, '2020-08-01 00:00:00', '2021-09-29 00:00:00', 100, 96, '2020-12-26 16:33:03', 0.00);


第3集 优惠券微服务+MybatisPlusGenerator代码自动生成工具

简介:Mybatis-plus-generator代码自动化生成微服务相关类

  • 配置代码生成(标记TODO的记得修改)
public class MyBatisPlusGenerator {

    public static void main(String[] args) {
        //1. 全局配置
        GlobalConfig config = new GlobalConfig();

                // 作者
                config.setAuthor("二当家小D")
                // 生成路径,最好使用绝对路径,window路径是不一样的
                //TODO  TODO  TODO  TODO
                .setOutputDir("/Users/xdclass/Desktop/demo/src/main/java")
                // 文件覆盖
                .setFileOverride(true)
                // 主键策略
                .setIdType(IdType.AUTO)

                .setDateType(DateType.ONLY_DATE)
                // 设置生成的service接口的名字的首字母是否为I,默认Service是以I开头的
                .setServiceName("%sService")

                //实体类结尾名称
                .setEntityName("%sDO")

                //生成基本的resultMap
                .setBaseResultMap(true)

                //不使用AR模式
                .setActiveRecord(false)

                //生成基本的SQL片段
                .setBaseColumnList(true);

        //2. 数据源配置
        DataSourceConfig dsConfig = new DataSourceConfig();
        // 设置数据库类型
        dsConfig.setDbType(DbType.MYSQL)
                .setDriverName("com.mysql.cj.jdbc.Driver")
                //TODO  TODO  TODO  TODO
                .setUrl("jdbc:mysql://127.0.0.1:3306/xdclass_user?useSSL=false")
                .setUsername("root")
                .setPassword("xdclass.net");

        //3. 策略配置globalConfiguration中
        StrategyConfig stConfig = new StrategyConfig();

        //全局大写命名
        stConfig.setCapitalMode(true)
                // 数据库表映射到实体的命名策略
                .setNaming(NamingStrategy.underline_to_camel)

                //使用lombok
                .setEntityLombokModel(true)

                //使用restcontroller注解
                .setRestControllerStyle(true)

                // 生成的表, 支持多表一起生成,以数组形式填写
                //TODO  TODO  TODO  TODO
                .setInclude("user","address");

        //4. 包名策略配置
        PackageConfig pkConfig = new PackageConfig();
        pkConfig.setParent("net.xdclass")
                .setMapper("mapper")
                .setService("service")
                .setController("controller")
                .setEntity("model")
                .setXml("mapper");

        //5. 整合配置
        AutoGenerator ag = new AutoGenerator();
        ag.setGlobalConfig(config)
                .setDataSource(dsConfig)
                .setStrategy(stConfig)
                .setPackageInfo(pkConfig);

        //6. 执行操作
        ag.execute();
        System.out.println("======= 小滴课堂 Done 相关代码生成完毕  ========");
    }
}
  • 导入生成好的代码
    • model (为啥不放common项目,如果是确定每个服务都用到的依赖或者类才放到common项目)
    • mapper 类接口拷贝
    • resource/mapper文件夹 xml脚本拷贝
    • controller
    • service 不拷贝

第4集 新版MybatisPlus分页插件配置+优惠券分页列表接口

简介:Mybatis-plus-分页插件配置+优惠劵列表开发

  • MybatisPlus分页插件配置(Common项目配置)
@Configuration
public class MybatisPlusPageConfig {

    /*	旧版本配置
	@Bean
	public PaginationInterceptor paginationInterceptor(){
		return new PaginationInterceptor();
	}*/

    /**
     * 新的分页插件,一缓和二缓遵循mybatis的规则,
     * 需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }

}

  • 优惠券列表分页接口开发
 @Override
    public Map<String, Object> pageCouponActivity(int page, int size) {
        //第1页,每页2条
        Page<CouponDO> pageInfo = new Page<>(page, size);
        IPage<CouponDO> couponDOPage = couponMapper.selectPage(pageInfo, new QueryWrapper<CouponDO>()
                .eq("publish", CouponPublishEnum.PUBLISH)
                .eq("category", CouponCategoryEnum.PROMOTION)
                .orderByDesc("create_time"));

        Map<String, Object> pageMap = new HashMap<>(3);

        pageMap.put("total_record", couponDOPage.getTotal());
        pageMap.put("total_page", couponDOPage.getPages());
        pageMap.put("current_data", couponDOPage.getRecords().stream().map(obj -> beanProcess(obj)).collect(Collectors.toList()));

        return pageMap;
    }

第5集 优惠券微服务-登录拦截器配置-SwaggerUI配置

简介:登录拦截器配置和SwaggerUI接口文档配置

  • 配置登录拦截器
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {


    @Bean
    LoginInterceptor loginInterceptor() {
        return new LoginInterceptor();
    }


    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        registry.addInterceptor(loginInterceptor())
                .addPathPatterns("/api/coupon_record/*/**")
                .addPathPatterns("/api/coupon/*/**")

                //不拦截的路径              
                .excludePathPatterns("/api/coupon/*/page_coupon");

        WebMvcConfigurer.super.addInterceptors(registry);
    }


}

  • 领劵controller接口开发

  • SwaggerUI接口文档

第6集 优惠券微服务-C端领劵接口核心校验业务逻辑开发

简介:C端用户领劵接口核心业务逻辑开发

  • service层开发

    • 业务逻辑
    @Override
    public JsonData addCoupon(long couponId, String couponCategory) {
    
    LoginUser loginUser = LoginInterceptor.threadLocal.get();
    CouponDO couponDO = couponMapper.selectOne(new QueryWrapper<CouponDO>().eq("id", couponId)
                        .eq("category", couponCategory)
                        .eq("publish", CouponPublishEnum.PUBLISH));
    
                this.couponCheck(couponDO,loginUser.getId());
    
                CouponRecordDO couponRecordDO = new CouponRecordDO();
                BeanUtils.copyProperties(couponDO,couponRecordDO);
                couponRecordDO.setCreateTime(new Date());
                couponRecordDO.setUseState(CouponStateEnum.NEW.name());
                couponRecordDO.setUserId(loginUser.getId());
                couponRecordDO.setUserName(loginUser.getName());
                couponRecordDO.setCouponId(couponId);
                couponRecordDO.setId(null);
                
                //高并发下扣减劵库存,采用乐观锁,当前stock做版本号,延伸多种防止超卖的问题,一次只能领取1张,TODO
                int rows = couponMapper.reduceStock(couponId,couponDO.getStock());
                if(rows == 1){
                    //库存扣减成功才保存
                    couponRecordMapper.insert(couponRecordDO);
                }else {
                    log.warn("发放优惠券失败:{},用户:{}",couponDO,loginUser);
                    throw new BizException(BizCodeEnum.COUPON_NO_STOCK);
                }
    
            return JsonData.buildSuccess();
        }
    
    • 优惠券校验逻辑
     /**
         * 优惠券检查
         * @param couponDO
         * @param userId
         */
        private void couponCheck(CouponDO couponDO,long userId){
    
            //优惠券不存在
            if(couponDO == null){
                throw  new BizException(BizCodeEnum.COUPON_NO_EXITS);
            }
            //库存不足
            if(couponDO.getStock()<=0){
                throw  new BizException(BizCodeEnum.COUPON_NO_STOCK);
            }
            //是否在领取时间范围
            long time = CommonUtil.getCurrentTimestamp();
            long start = couponDO.getStartTime().getTime();
            long end = couponDO.getEndTime().getTime();
            if(time < start || time > end){
                throw  new BizException(BizCodeEnum.COUPON_OUT_OF_TIME);
            }
            //用户是否超过限制
            int recordNum = couponRecordMapper.selectCount(new QueryWrapper<CouponRecordDO>()
                    .eq("coupon_id",couponDO.getId())
                    .eq("user_id",userId));
    
            if(recordNum>=couponDO.getUserLimit()){
                throw  new BizException(BizCodeEnum.COUPON_OUT_OF_LIMIT);
            }
        }
    
    • 找出上面的业务可能存的问题,大家自行思考

第十四章 Jmeter5.X压力测试+高并发下领劵扣减库存的方式

第1集 接口压测和常用压力测试工具对比

简介:目前用的常用测试工具对比

  • LoadRunner

    • 性能稳定,压测结果及细粒度大,可以自定义脚本进行压测,但是太过于重大,功能比较繁多
  • Apache AB(单接口压测最方便)

    • 模拟多线程并发请求,ab命令对发出负载的计算机要求很低,既不会占用很多CPU,也不会占用太多的内存,但却会给目标服务器造成巨大的负载, 简单DDOS攻击等
  • Webbench

    • webbench首先fork出多个子进程,每个子进程都循环做web访问测试。子进程把访问的结果通过pipe告诉父进程,父进程做最终的统计结果。
  • Jmeter

    • 开源免费,功能强大,在互联网公司普遍使用
    • 压测不同的协议和应用
        1. Web - HTTP, HTTPS (Java, NodeJS, PHP, ASP.NET, …)
        1. SOAP / REST Webservices
        1. FTP
        1. Database via JDBC
        1. LDAP 轻量目录访问协议
        1. Message-oriented middleware (MOM) via JMS
        1. Mail - SMTP(S), POP3(S) and IMAP(S)
        1. TCP等等
    • 使用场景及优点
      • 1)功能测试
      • 2)压力测试
      • 3)分布式压力测试
      • 4)纯java开发
      • 5)上手容易,高性能
      • 4)提供测试数据分析
      • 5)各种报表数据图形展示
  • 压测工具本地快速安装Jmeter5.x
    • 需要安装JDK8 以上
    • 建议安装JDK环境,虽然JRE也可以,但是压测https需要JDK里面的 keytool工具
    • 快速下载 https://jmeter.apache.org/download_jmeter.cgi
    • 文档地址:http://jmeter.apache.org/usermanual/get-started.html

第2集 Jmeter5.x目录文件讲解+汉化操作

简介:讲解jmeter解压文件里面的各个目录,文件等

  • 目录

    bin:核心可执行文件,包含配置
            jmeter.bat: windows启动文件(window系统一定要配置显示文件拓展名)
            jmeter: mac或者linux启动文件
            jmeter-server:mac或者Liunx分布式压测使用的启动文件
            jmeter-server.bat:window分布式压测使用的启动文件
            jmeter.properties: 核心配置文件   
    extras:插件拓展的包
    
    lib:核心的依赖包
    
  • Jmeter语言版本中英文切换

    • 控制台修改 menu -> options -> choose language
  • 配置文件修改

    • bin目录 -> jmeter.properties
    • 默认 #language=en
    • 改为 language=zh_CN

第3集 Jmeter5.X基础功能组件介绍+线程组和Sampler

简介:讲解Jmeter里面GUI菜单栏主要组件

  • 添加->threads->线程组(控制总体并发)

    线程数:虚拟用户数。一个虚拟用户占用一个进程或线程
    
    准备时长(Ramp-Up Period(in seconds)):全部线程启动的时长,比如100个线程,20秒,则表示20秒内 100个线程都要启动完成,每秒启动5个线程
    
    循环次数:每个线程发送的次数,假如值为5,100个线程,则会发送500次请求,可以勾选永远循环
    
  • 线程组->添加-> Sampler(采样器) -> Http (一个线程组下面可以增加几个Sampler)

    名称:采样器名称
    注释:对这个采样器的描述
    web服务器:
      默认协议是http
      默认端口是80
      服务器名称或IP :请求的目标服务器名称或IP地址
    
    路径:服务器URL
    
  • 查看测试结果

    线程组->添加->监听器->察看结果树
    线程组->添加->监听器->聚合报告
    

第4集 Jmeter5.x实战之优惠券列表接口压测+聚合报告分析

简介:优惠券列表接口压测+结果聚合报告

  • 优惠券列表接口压测

  • 新增聚合报告:线程组->添加->监听器->聚合报告(Aggregate Report)

  lable: sampler的名称
  Samples: 一共发出去多少请求,例如10个用户,循环10次,则是 100
  Average: 平均响应时间
  Median: 中位数,也就是 50% 用户的响应时间


  90% Line : 90% 用户的响应不会超过该时间 (90% of the samples took no more than this time.     The remaining samples at least as long as this)
  95% Line : 95% 用户的响应不会超过该时间
  99% Line : 99% 用户的响应不会超过该时间
  min : 最小响应时间
  max : 最大响应时间

  

  Error%:错误的请求的数量/请求的总数
  Throughput: 吞吐量——默认情况下表示每秒完成的请求数(Request per Second) 可类比为qps、tps
  KB/Sec: 每秒接收数据量

第5集 优惠券领劵接口-Jmeter压测扣超发优惠券问题暴露

简介:Jmeter压测领劵接口-超发优惠券问题暴露

  • 新建接口压测计划

  • 压测领劵接口

  • 完成xml编写

    <!--扣减库存-->
        <update id="reduceStock">
            update coupon set stock=stock-1 where id = #{couponId}
        </update>
    
  • 问题

    • 扣减存储为负数,超发优惠券
    • 给公司造成资损

第6集 高并发下怎样优雅的保证扣减库存数据的正确性

简介:高并发下扣减库存的常见解决方案介绍

  • 这里不谈秒杀设计,不谈使用队列等使请求串行化,

    • 秒杀的话:限流、队列、异步,可以看小滴课堂专题视频
  • 我们谈下怎么用锁来保证数据正确,防止超发导致库存是负数,你能下想几种方式

    先看下面的是精简版的时序图

在这里插入图片描述

  • 同步代码块synchronized ,lock
public synchronized void reduceCouponStock(long couponId ,Integer num) {
//业务逻辑
}

问题:synchronized 作用范围是单个jvm实例, 如果做了集群分布式等,就失效了,且单机JVM加锁后就是串行等待问题
  • 分布式锁 zookeeper,redis (后续会讲到分布式锁的知识)
可以解决问题

问题:过于笨重,性能有所下降
  • 直接数据库更新扣减
update coupon set stock=stock - #{num} where id = #{couponId} and stock>0
//测试如果num大于已有库存,则会变负数
update coupon set stock=stock - #{num} where id = #{couponId} and (stock - #{num})>=0
或者
update coupon set stock=stock - #{num} where id = #{couponId} and stock >= #{num} 
//修复了负数问题

  • 如果扣减最多1个,则直接使用这种就行
update coupon set stock=stock-1 where id = #{couponId} and stock>0 

延伸
update coupon set stock=stock-1 where id = #{couponId} and stock = #{oldStock}
问题:扣减库存,如果别人补充库存,就存在ABA问题,看业务是否有这个限制,大课采用上面那种
比如
C线程查出来是10个
A线程扣减1个,剩9个
B线程更新了库存,变回10个
C更新的时候发现还是10个,则更新成功, 所以避免这个问题,要求不管谁修改了库存,一定要加个version递增版本号

update coupon set stock=stock-1,version=version+1 where id = #{couponId} and stock>0 and versoin=#{oldVersion}
  • 代码编写
<!--扣减库存,如果别人补充库存,则可能存在ABA问题-->
    <update id="reduceStock">
        update coupon set stock=stock-1 where id = #{couponId} and stock>0
    </update>

第7集 大厂面试热身赛-天猫超市-二面面试题-P7技术专家岗

简介:大厂面试题,高并发库存扣减超卖问题解决,多种sql适合场景

  • 题目:高并发库存扣减超卖问题,很多人加了乐观锁版本号去解决,那下面三种有什么区别,分别适合哪些场景使用
1)update product set stock=stock-1 where id = 1 and stock>0

2)update product set stock=stock-1 where stock=#{原先查询的库存}  and id = 1 and stock>0

3)update product set stock=stock-1,versioin = version+1 where  id = 1 and stock>0 and version=#{原先查询的版本号} 
  • 答案 : 核心是解决超卖的问题,就是防止库存为负数
方案一:id是主键索引的前提下,如果每次只是减少1个库存,则可以采用上面的方式,只做数据安全
校验,可以有效减库存,性能更高,避免大量无用sql,只要有库存就也可以操作成功.
场景:高并发场景下的取号器,优惠券发放扣减库存等


方案二:使用业务自身的条件做为乐观锁,但是存在ABA问题,对比方案三的好处是不用增加version版本字段。如果只是扣减库存且不在意ABA问题时,则可以采用上面的方式,但业务性能相对方案一就差了点,因为库存变动后sql就会无效


方案三:增加版本号主要是为了解决ABA问题,数据读取后,更新前数据被别人篡改过,version只能做递增
场景:商品秒杀、优惠券方法,需要记录库存操作前后的业务


三个方案各有利弊,看业务场景而定

第十五章 原生分布式锁-Redisson分布式锁防止个人超领优惠券

第1集 一行代码10个bug-单用户超领优惠券问题介绍

简介:讲解单用户优惠券超领业务问题和效果演示

  • 什么单用户超领优惠券

    • 优惠券限制1人限制1张,有些人却领了2张
    • 优惠券限制1人限制2张,有些人却领了3或者4张
  • 案例举例和问题来源

前面解决了,优惠券超发的问题,但是这个个人领取的时候,有张数限制,

有个生发洗发水100元,有个10元优惠券,每人限制领劵1张

小滴课堂-老王,使用时间暂停思维来发现问题,并发领劵

A线程原先查询出来没有领劵,要再插入领劵记录前暂停
然后B线程原先查询出来也没有领劵,则插入领劵记录,然后A线程也插入领劵记录
老王就有了两个优惠券

问题来源核心:对资源的修改没有加锁,导致多个线程可以同时操作,从而导致数据不正确

解决问题:分布式锁 或者 细粒度分布式锁

第2集 分布式核心技术-关于高并发下分布式锁你知道多少?

简介:分布式锁核心知识介绍和注意事项

  • 避免单人超领劵
    • 加锁
      • 本地锁:synchronize、lock等,锁在当前进程内,集群部署下依旧存在问题
      • 分布式锁:redis、zookeeper等实现,虽然还是锁,但是多个进程共用的锁标记,可以用Redis、Zookeeper、Mysql等都可以

在这里插入图片描述

  • 设计分布式锁应该考虑的东西
    • 排他性
      • 在分布式应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行
    • 容错性
      • 分布式锁一定能得到释放,比如客户端奔溃或者网络中断
    • 满足可重入、高性能、高可用
    • 注意分布式锁的开销、锁粒度

第3集 基于Redis实现分布式锁的几种坑你是否踩过《上》

简介:基于Redis实现分布式锁的几种坑

  • 实现分布式锁 可以用 Redis、Zookeeper、Mysql数据库这几种 , 性能最好的是Redis且是最容易理解

    • 分布式锁离不开 key - value 设置
    key 是锁的唯一标识,一般按业务来决定命名,比如想要给一种商品的秒杀活动加锁,key 命名为 “seckill_商品ID” 。value就可以使用固定值,比如设置成1

  • 基于redis实现分布式锁,文档:http://www.redis.cn/commands.html#string

    • 加锁 SETNX key value
    setnx 的含义就是 SET if Not Exists,有两个参数 setnx(key, value),该方法是原子性操作
    
    如果 key 不存在,则设置当前 key 成功,返回 1;
    
    如果当前 key 已经存在,则设置当前 key 失败,返回 0
    
    • 解锁 del (key)
    得到锁的线程执行完任务,需要释放锁,以便其他线程可以进入,调用 del(key)
    
    • 配置锁超时 expire (key,30s)
    客户端奔溃或者网络中断,资源将会永远被锁住,即死锁,因此需要给key配置过期时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放
    
    
    • 综合伪代码
    methodA(){
      String key = "coupon_66"
    
      if(setnx(key,1) == 1){
          expire(key,30,TimeUnit.MILLISECONDS)
          try {
              //做对应的业务逻辑
              //查询用户是否已经领券
              //如果没有则扣减库存
              //新增领劵记录
          } finally {
              del(key)
          }
      }else{
    
        //睡眠100毫秒,然后自旋调用本方法
    		methodA()
      }
    }
    
    • 存在哪些问题,大家自行思考下

第4集 基于Redis实现分布式锁的几种坑你是否踩过《下》

简介:手把手教你彻底掌握分布式锁+原生代码编写

    • 存在什么问题?

      • 多个命令之间不是原子性操作,如setnxexpire之间,如果setnx成功,但是expire失败,且宕机了,则这个资源就是死锁
      使用原子命令:设置和配置过期时间  setnx / setex
      如: set key 1 ex 30 nx
      java里面 redisTemplate.opsForValue().setIfAbsent("seckill_1",1,30,TimeUnit.MILLISECONDS)
      

在这里插入图片描述

* 业务超时,存在其他线程勿删,key 30秒过期,假如线程A执行很慢超过30秒,则key就被释放了,其他线程B就得到了锁,这个时候线程A执行完成,而B还没执行完成,结果就是线程A删除了线程B加的锁

```
可以在 del 释放锁之前做一个判断,验证当前的锁是不是自己加的锁, 那 value 应该是存当前线程的标识或者uuid

String key = "coupon_66"
String value = Thread.currentThread().getId()

if(setnx(key,value) == 1){
    expire(key,30,TimeUnit.MILLISECONDS)
    try {
        //做对应的业务逻辑
    } finally {
    	//删除锁,判断是否是当前线程加的
    	if(get(key).equals(value)){
					//还存在时间间隔
					del(key)
        }
    }
}else{
	
	//睡眠100毫秒,然后自旋调用本方法

}
```

* 进一步细化误删
  * 当线程A获取到正常值时,返回带代码中判断期间锁过期了,线程B刚好重新设置了新值,线程A那边有判断value是自己的标识,然后调用del方法,结果就是删除了新设置的线程B的值
  * 核心还是判断和删除命令 不是原子性操作导致



* 那如何解决呢?下集讲解

第5集 手把手教你彻底掌握分布式锁lua脚本+redis原生代码编写

简介:手把手教你彻底掌握分布式锁+原生代码编写

  • 前面说了redis做分布式锁存在的问题

    • 核心是保证多个指令原子性,加锁使用setnx setex 可以保证原子性,那解锁使用 判断和删除怎么保证原子性
    • 文档:http://www.redis.cn/commands/set.html
    • 多个命令的原子性:采用 lua脚本+redis, 由于【判断和删除】是lua脚本执行,所以要么全成功,要么全失败
    //获取lock的值和传递的值一样,调用删除操作返回1,否则返回0
    
    String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
    
    //Arrays.asList(lockKey)是key列表,uuid是参数
    Integer result = redisTemplate.execute(new DefaultRedisScript<>(script, Integer.class), Arrays.asList(lockKey), uuid);
    
    • 全部代码
    /**
    * 原生分布式锁 开始
    * 1、原子加锁 设置过期时间,防止宕机死锁
    * 2、原子解锁:需要判断是不是自己的锁
    */
    String uuid = CommonUtil.generateUUID();
    String lockKey = "lock:coupon:"+couponId;
    Boolean nativeLock=redisTemplate.opsForValue().setIfAbsent(lockKey,uuid,Duration.ofSeconds(30));
        if(nativeLock){
          //加锁成功
          log.info("加锁:{}",nativeLock);
          try {
               //执行业务  TODO
            }finally {
               String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
    
                    Integer result = redisTemplate.execute(new DefaultRedisScript<>(script, Integer.class), Arrays.asList(lockKey), uuid);
                    log.info("解锁:{}",result);
                }
    
            }else {
                //加锁失败,睡眠100毫秒,自旋重试
                try {
                    TimeUnit.MILLISECONDS.sleep(100L);
                } catch (InterruptedException e) { }
                return addCoupon( couponId, couponCategory);
            }
            //原生分布式锁 结束
    
    • 遗留一个问题,锁的过期时间,如何实现锁的自动续期 或者 避免业务执行时间过长,锁过期了?
      • 原生方式的话,一般把锁的过期时间设置久一点,比如10分钟时间

第6集 基于Redis官方推荐-分布式锁最佳实践介绍

简介:redis官方推荐-分布式锁最佳实践

  • 原生代码+redis实现分布式锁使用比较复杂,且有些锁续期问题更难处理

    • 官方推荐方式:https://redis.io/topics/distlock
    • 多种实现客户端框架

在这里插入图片描述

  • Redisson官方中文文档:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95

  • 聚合工程锁定版本,common项目添加依赖(多个服务都会用到分布式锁)

<!--分布式锁-->
<dependency>
      <groupId>org.redisson</groupId>
      <artifactId>redisson</artifactId>
      <version>3.10.1</version>
</dependency>

  • 创建redisson客户端
    @Value("${spring.redis.host}")
    private String redisHost;

    @Value("${spring.redis.port}")
    private String redisPort;

    @Value("${spring.redis.password}")
    private String redisPwd;
    
		/**
     * 配置分布式锁
     * @return
     */
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();

        //单机模式
        //config.useSingleServer().setPassword("123456").setAddress("redis://8.129.113.233:3308");
        config.useSingleServer().setPassword(redisPwd).setAddress("redis://"+redisHost+":"+redisPort);

        //集群模式
        //config.useClusterServers()
        //.setScanInterval(2000)
        //.addNodeAddress("redis://10.0.29.30:6379", "redis://10.0.29.95:6379")
        // .addNodeAddress("redis://127.0.0.1:6379");

        RedissonClient redisson = Redisson.create(config);

        return redisson;
    }

  • 模拟controller接口测试

第7集 实战Redisson实现优惠券微服务领劵接口的分布式锁

简介:redisson实现优惠券微服务领劵接口的分布式锁

  • 优惠券微服务,分布式锁实现方式
Lock lock = redisson.getLock("lock:coupon:"+couponId);
//阻塞式等待,一个线程获取锁后,其他线程只能等待,和原生的方式循环调用不一样
lock.lock();
        try {
            CouponDO couponDO = couponMapper.selectOne(new QueryWrapper<CouponDO>().eq("id", couponId)
                    .eq("category", couponCategory)
                    .eq("publish", CouponPublishEnum.PUBLISH));

            this.couponCheck(couponDO,loginUser.getId());

            CouponRecordDO couponRecordDO = new CouponRecordDO();
            BeanUtils.copyProperties(couponDO,couponRecordDO);
            couponRecordDO.setCreateTime(new Date());
            couponRecordDO.setUseState(CouponStateEnum.NEW.name());
            couponRecordDO.setUserId(loginUser.getId());
            couponRecordDO.setUserName(loginUser.getName());
            couponRecordDO.setCouponId(couponId);
            couponRecordDO.setId(null);
            //高并发下扣减劵库存,采用乐观锁,当前stock做版本号,一次只能领取1张
            int rows = couponMapper.reduceStock(couponId);

            if(rows == 1){
                //库存扣减成功才保存
                couponRecordMapper.insert(couponRecordDO);
            }else {
                log.warn("发放优惠券失败:{},用户:{}",couponDO,loginUser);
                throw new BizException(BizCodeEnum.COUPON_NO_STOCK);
            }

        }finally {
            lock.unlock();
        }

第8集 Redisson是怎样解决分布式锁的里面的坑

简介:redisson解决分布式锁里面的坑

  • 问题 : Redis锁的过期时间小于业务的执行时间该如何续期?

    • watch dog看门狗机制
    负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。或者业务执行时间过长导致锁过期,
    
    为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。
    
    Redisson中客户端一旦加锁成功,就会启动一个watch dog看门狗。watch dog是一个后台线程,会每隔10秒检查一下,如果客户端还持有锁key,那么就会不断的延长锁key的生存时间
    
    
    默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定
    
    • 指定加锁时间
    // 加锁以后10秒钟自动解锁
    // 无需调用unlock方法手动解锁
    lock.lock(10, TimeUnit.SECONDS);
    
    // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
    boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
    
    if (res) {
       try {
         ...
       } finally {
           lock.unlock();
       }
    }
    

第十六章 事务传播级别和隔离属性回顾+领劵记录模块开发

第1集 Spring 常见的事务管理-面试常考点

简介:讲解Spring常见的事务管理

  • 事务:多个操作,要么同时成功,要么失败后一起回滚

    • 具备ACID四种特性
      • Atomic(原子性)
      • Consistency(一致性)
      • Isolation(隔离性)
      • Durability(持久性)
  • 你知道常见的Spring事务管理方式吗

    • 编程式事务管理
    代码中调用beginTransaction()、commit()、rollback()等事务管理相关的方法,通过TransactionTempalte手动管理事务(用的少)
    
    • 声明式事务管理
    通过AOP实现,可配置文件方式或者注解方式实现事务的管理控制(用的多)
    
  • 你知道声明式事务管理本质吗:

    • 本质是对方法前后进行拦截,底层是建立在 AOP 的基础之上

    • 在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务

第2集 数据库事务传播级别和隔离属性知识回顾

简介:讲解Spring事务的传播属性和隔离级别

  • 事物传播行为介绍:

    • 如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为
    @Transactional(propagation=Propagation.REQUIRED) 如果有事务, 那么加入事务, 没有的话新建一个(默认情况下)
    
    @Transactional(propagation=Propagation.NOT_SUPPORTED) 不为这个方法开启事务
    
    @Transactional(propagation=Propagation.REQUIRES_NEW) 不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务
    
    @Transactional(propagation=Propagation.MANDATORY) 必须在一个已有的事务中执行,否则抛出异常
    
    @Transactional(propagation=Propagation.NEVER) 必须在一个没有的事务中执行,否则抛出异常(与Propagation.MANDATORY相反)
    
    @Transactional(propagation=Propagation.SUPPORTS) 如果其他bean调用这个方法,在其他bean中声明事务,那就用事务.如果其他bean没有声明事务,那就不用事务.
    
    @Transactional(propagation=Propagation.NESTED) 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行; 如果当前没有事务,则该取值等价于Propagation.REQUIRED。
    
  • 事务隔离级别

    • 是指若干个并发的事务之间的隔离程度
    @Transactional(isolation = Isolation.READ_UNCOMMITTED) 读取未提交数据(会出现脏读, 不可重复读) 基本不使用
    
    @Transactional(isolation = Isolation.READ_COMMITTED) 读取已提交数据(会出现不可重复读和幻读)
    
    @Transactional(isolation = Isolation.REPEATABLE_READ) 可重复读(会出现幻读)
    
    @Transactional(isolation = Isolation.SERIALIZABLE) 串行化
    
  • MYSQL: 默认为REPEATABLE_READ级别

  • 备注:基础知识不牢固,一定要补(面试专题第一季,300~500道面试题)

第3集 优惠券微服务领劵接口-本地事务配置

简介:领劵接口微服务本地事务配置

  • 领劵接口配置事务+测试

    • 启动类增加注解 @EnableTransactionManagement
    • 方法增加注解 @Transactional(rollbackFor=Exception.class,propagation=Propagation.REQUIRED)
    • 指定方法需要事务再添加,不能全局都使用
  • 分布式锁测试锁续期的时候

    • 不能使用debug模式,不然看到不到自动续期

第4集 优惠券微服务领劵记录分页模块开发

简介:微服务个人领券记录分页接口

  • 领劵记录查询开发
    @ApiOperation("分页查询我的优惠券列表")
    @GetMapping("page")
    public JsonData page(@RequestParam(value = "page",defaultValue = "1")int page,
                         @RequestParam(value = "size",defaultValue = "20")int size){

        Map<String,Object> pageInfo = couponRecordService.page(page,size);

        return JsonData.buildSuccess(pageInfo);
    }
    
    
     /**
     * 分页查询我的优惠券列表
     * @param page
     * @param size
     * @return
     */
    @Override
    public Map<String, Object> page(int page, int size) {
        LoginUser loginUser = LoginInterceptor.threadLocal.get();

        //第1页,每页2条
        Page<CouponRecordDO> pageInfo = new Page<>(page, size);
        IPage<CouponRecordDO> recordDOPage = couponRecordMapper.selectPage(pageInfo, new QueryWrapper<CouponRecordDO>().eq("user_id",loginUser.getId()).orderByDesc("create_time"));
        Map<String, Object> pageMap = new HashMap<>(3);

        pageMap.put("total_record", recordDOPage.getTotal());
        pageMap.put("total_page", recordDOPage.getPages());
        pageMap.put("current_data", recordDOPage.getRecords().stream().map(obj -> beanProcess(obj)).collect(Collectors.toList()));

        return pageMap;
    }

第5集 优惠券微服务领劵记录详情接口开发

简介:微服务个人领券记录详情接口

  • 领劵记录详情接口开发(防止水平权限攻击)
		/**
     * 查询优惠券记录信息
     * 水平权限攻击:也叫作访问控制攻击,Web应用程序接收到用户请求,修改某条数据时,没有判断数据的所属人,
     * 或者在判断数据所属人时从用户提交的表单参数中获取了userid。
     * 导致攻击者可以自行修改userid修改不属于自己的数据
     * @param recordId
     * @return
     */
    @ApiOperation("查询优惠券记录信息")
    @GetMapping("/detail/{record_id}")
    public JsonData findUserCouponRecordById(@PathVariable("record_id")long recordId ){

        CouponRecordVO couponRecordVO = couponRecordService.findById(recordId);
        return  couponRecordVO == null? JsonData.buildResult(BizCodeEnum.COUPON_NO_EXITS):JsonData.buildSuccess(couponRecordVO);
    }



    @Override
    public CouponRecordVO findById(long recordId) {
        LoginUser loginUser = LoginInterceptor.threadLocal.get();
        CouponRecordDO recordDO = couponRecordMapper.selectOne(new QueryWrapper<CouponRecordDO>().eq("id", recordId).eq("user_id", loginUser.getId()));
        if(recordDO == null){return null;}

        CouponRecordVO couponRecordVO = beanProcess(recordDO);
        return couponRecordVO;
    }

第十七章 架构师的思考+流量漏斗模型驱动解决方案

第1集 优惠券拉新-发放新人劵流程思路设计和对比

简介:优惠券拉新发放接口设计和思路

  • 用户注册->发放优惠券

    • RPC调用
      • 分布事务
    • 消息队列
      • 可靠性投递、幂等操作
  • 说下两种方式的优缺点和选择

    • rpc实时性高,强一致性的场景下比消息队列优秀
    • 上下游系统处理能力存在差距的时候消息队列可以很好的缓冲
    • 引入消息队列,系统可用性降低,假如队列宕机则系统就不能正常运行
    • 引入消息队列,系统复杂性增加,考虑多方面的问题,比如一致性问题、如何保证消息不被重复消费,如何保证保证消息可靠传输等

第2集 一线大厂必备分析方法论-流量漏斗模型介绍

简介:流量漏斗模型-大厂里面必备的分析方法论

  • 什么是流量漏斗模型

    • 谁会用:公司产品经理、运营、公司CTO、CEO

    • 那我们研发工程师为啥要用?

    • 举个例子大家就明白

      • 互联网产品其本身就是一个虚拟的漏斗,用户的行为路径有很多,举个淘宝、JD这电商例子
      • 首页->查看商品->添加购物车->注册->登录->下单->支付->确认收货

在这里插入图片描述

* 每一步都有折损,产品经理能安什么好心?

在这里插入图片描述

* 就是找事给我们做,不断提高每一步的转换率,降低每步的折损
  * 调整布局、用户体验、缩短链路等
  * 不定期的短信邮箱推送提醒、标题党等
  * 拼团、优惠券、积分等等需求就是这样来的

第3集 作为技术Leader的你-为什么要用流量漏斗模型

简介:作为技术Leader的你,为什么要用这个模型

在这里插入图片描述

  • 作为技术Leader的你,为什么要用?

    • 我们要有个做一个精益创业的思维 ”小步快跑,快速试错“
    • 把简单的事情想复杂,把复杂的事情做简单
    • 简历编写的时候,给些大厂的产品数据参考(非现象级爆款)
      • 日活1千万的用户,总用户量12亿,日新增510万
      • 日活1百万的用户,总用户量12千万,日新增12万
      • 日活5千,总用户量6080万,日新增1k3k之间
  • 通过前面流量漏斗分析其实多数公司里面,注册的量级不高(排除大量买量和现象级爆款)

    • 直接使用rpc调用即可,不引入消息队列

      • 这里会涉及到分布式事务问题,思考,假如发放优惠券失败应该怎么办?
        • 两个微服务同时进行回滚?
        • 不给注册的账号登录了吗?
        • 你这样会丢掉工作的的,任何方式都不能一刀切,一定要思考下具体场景具体而定
      • 假如RPC方式调用-优惠券发放失败,记录日志就行,可以人工补发或者定时任务重发
    • 前期不用引入相关的微服务组件

第4集 新用户注册-发放拉新领劵接口开发

简介:开发用户注册拉新领劵接口

  • 开发优惠券微服务-领劵接口controller

  • service层开发

用户注册->发放优惠券

  • RPC调用

    • 分布事务
  • 消息队列

    • 可靠性投递、幂等操作
  • 说下两种方式的优缺点和选择

    • rpc实时性高,强一致性的场景下比消息队列优秀
    • 上下游系统处理能力存在差距的时候消息队列可以很好的缓冲
    • 引入消息队列,系统可用性降低,假如队列宕机则系统就不能正常运行
    • 引入消息队列,系统复杂性增加,考虑多方面的问题,比如一致性问题、如何保证消息不被重复消费,如何保证保证消息可靠传输等

第2集 一线大厂必备分析方法论-流量漏斗模型介绍

简介:流量漏斗模型-大厂里面必备的分析方法论

  • 什么是流量漏斗模型

    • 谁会用:公司产品经理、运营、公司CTO、CEO

    • 那我们研发工程师为啥要用?

    • 举个例子大家就明白

      • 互联网产品其本身就是一个虚拟的漏斗,用户的行为路径有很多,举个淘宝、JD这电商例子
      • 首页->查看商品->添加购物车->注册->登录->下单->支付->确认收货

      [外链图片转存中…(img-FB7vRduS-1663813133844)]

      • 每一步都有折损,产品经理能安什么好心?

      [外链图片转存中…(img-HqjvUyd6-1663813133845)]

      • 就是找事给我们做,不断提高每一步的转换率,降低每步的折损
        • 调整布局、用户体验、缩短链路等
        • 不定期的短信邮箱推送提醒、标题党等
        • 拼团、优惠券、积分等等需求就是这样来的

第3集 作为技术Leader的你-为什么要用流量漏斗模型

简介:作为技术Leader的你,为什么要用这个模型

[外链图片转存中…(img-p9kYySH5-1663813133847)]

  • 作为技术Leader的你,为什么要用?

    • 我们要有个做一个精益创业的思维 ”小步快跑,快速试错“
    • 把简单的事情想复杂,把复杂的事情做简单
    • 简历编写的时候,给些大厂的产品数据参考(非现象级爆款)
      • 日活1千万的用户,总用户量12亿,日新增510万
      • 日活1百万的用户,总用户量12千万,日新增12万
      • 日活5千,总用户量6080万,日新增1k3k之间
  • 通过前面流量漏斗分析其实多数公司里面,注册的量级不高(排除大量买量和现象级爆款)

    • 直接使用rpc调用即可,不引入消息队列

      • 这里会涉及到分布式事务问题,思考,假如发放优惠券失败应该怎么办?
        • 两个微服务同时进行回滚?
        • 不给注册的账号登录了吗?
        • 你这样会丢掉工作的的,任何方式都不能一刀切,一定要思考下具体场景具体而定
      • 假如RPC方式调用-优惠券发放失败,记录日志就行,可以人工补发或者定时任务重发
    • 前期不用引入相关的微服务组件

第4集 新用户注册-发放拉新领劵接口开发

简介:开发用户注册拉新领劵接口

  • 开发优惠券微服务-领劵接口controller

  • service层开发

本文含有隐藏内容,请 开通VIP 后查看