跳至主要內容

优惠券服务

稀客大大大约 29 分钟

[TOC]

提示

认证服务:注册、登录、密码找回、密码修改、查询我的、查看登录信息等

用户服务:用户详情、完善、修改、足迹、历史、收藏等

会员服务:会员等级、积分变动、新增、消费、充值会员

签到服务:签到、校验、签到记录、抽奖、奖励等

优惠券服务:生成、领取、查询、抵扣等

订单服务:下单、查询、状态等

秒杀服务:下单、秒杀活动、状态等

评价服务:评价、查询、状态等

健身、健康、医疗、教育、社区、社交、电商、旅游、房产等等

通用型:任何项目基本上都可以使用

背景

0.1 优惠劵的目的

优惠券是我们系统中最常见的活跃用户的一种方式,简单的设计就能带来巨大的客流量。通常在活动、促销、甩卖等场景中,我们最常用到的手段无疑是优惠券了

0.2 优惠劵的意义

image.png
image.png

1、提升用户活跃度

人们总是对 “降价”、“打折” 这样的字眼充满了兴趣,用户也习惯于在了解到商品的价格及优惠力度后再决定购买,所以有优惠的商品才更具有吸引力。

2、增加产品曝光量

用户一券在手,总是让人忍不住翻看可以使用的商品,这无形中增加了平台商品的曝光量。同时好的优惠券会在用户的口口相传中得到推广,这对平台、商家和产品来说,都是一个很好的展现自己口碑的机会。

3、刺激用户的潜在购买需求

当用户的购买行为背后没有充分的购买动机,交易就会轻易的受到其他因素的影响而中断。优惠券的出现满足了用户 “赚到” 的心理,用户就愿意为潜在的购物需求买单。

4、提升用户的购买力

用户的购买力和收入水平成正比,和商品价格呈反比,当价格受到优惠时,用户的购买力也可以得到相应改善。

0.3 优惠劵使用条件的分类

1.体验券

image.png
image.png

一般针对新品或测试产品向用户免费发放的体验券,意在吸引用户的关注,倾听用户的意见,有时体验券也会以邀请码的形式出现。

2.代金券(又称现金券)

image.png
image.png

一般使用门槛较低,不会有金额、数量等方面的要求,可以直接使用,若购买商品不够券面金额,通常情况下是不退还差额的,如:新人大礼包、无门槛红包和员工福利等。

3.满减券

image.png
image.png

通常会有订购数量、订单总价、产品种类等方面的要求,满足条件的订单才可享受满减,如:生活缴费商品满 ¥100 减 ¥2 优惠券。

4.打折券

image.png
image.png
image.png
image.png

是直接对商品进行打折,一般商品较贵,购买的用户较少,或者用户订购量大会采用此类型优惠券,如:8.8 折优惠券等。

0.4 优惠劵使用范围分类

1.单品券

image.png
image.png

为购买单一商品时使用的优惠券

2.系列产品券

image.png
image.png

为购买某种特定系列产品时所使用的优惠券,用户只需要购买指定系列的产品就可以享用这张优惠券,如:购买无线宝 WiFi5 系列产品优惠券等

3.品类券

image.png
image.png

为购买某一类商品时使用的优惠券,如:购买清洁类、医药类、生鲜类等优惠券;

4.品牌券

image.png
image.png

为购买某一品牌商品时使用的优惠券,如:购买华为、京东云等品牌产品所用的优惠券。

0.5优惠劵发放主体分类

1、店铺优惠券

image.png
image.png

则是店铺自行发放的,如:关注有礼、抽奖、新老顾客回馈等;

2、平台优惠券

image.png
image.png

是由平台直接发放给用户的优惠券,针对的目标群体范围较广,如:购物津贴、百亿补贴等;

3.政企消费券

image.png
image.png

成本由政府、企业和平台共同承担,意在提升某些地区消费者的消费能力和消费水平,如:北京消费券等

一、需求

优惠券模板是由运营人员根据一定的条件来设定的,优惠券必须有数量限制并且必须有优惠券码

image.png
image.png

优惠券模板创建

image.png
image.png

二、分析

明白需求是什么?

模仿--使用功能

基于优惠券模板实现优惠券的发放,主要实现的是平台券,发放的途径:1.用户抢 2.系统发放

优惠券模板功能:

1.创建优惠券接口,设置各种条件

2.审核优惠券活动接口,根据情况,缓存

3.查询优惠券模板接口

难点:1.系统发放 线程池+分片算法 2.优惠券的缓存

用户优惠券功能:

1.领取接口,实现优惠券领取-超领

2.查询我的优惠券接口

3.查看优惠券抵扣接口

4.抵扣优惠券接口

三、设计

3.1 数据库脚本

CREATE TABLE `t_coupon_template`  (
`id` int(11) AUTO_INCREMENT,
`flag` int(11) COMMENT '状态:41.未审核 42.审核通过 43.审核失败',
`name` varchar(64) COMMENT '名字',
`logo` varchar(256) ,
`intro` varchar(256) COMMENT '简介',
`category` int(11) COMMENT '种类: 51-满减;52-折扣;53-立减',
`scope` int(11) COMMENT '使用范围:61-单品;62-商品类型;63-全品',
`scope_id` int(11) COMMENT '对应的id:单品id;商品类型id;全品为0',
`expire_time` datetime COMMENT '优惠券发放结束日期',
`coupon_count` int(11) COMMENT '优惠券发放数量',
`create_time` datetime COMMENT '创建时间',
`user_id` int(11) COMMENT '创建人的ID,后台内部员工',
`user_audit` varchar(100) COMMENT '审核意见',
`template_key` varchar(128) COMMENT '优惠券模板的识别码(有一定的识别度)',
`target` int(11) COMMENT '优惠券作用的人群:71-全体;72-会员等级 73-新用户 74-收费会员',
`target_level` int(11) COMMENT '用户等级要求,默认0',
`send_type` int comment '发放类型:81.用户领取 82.系统发放',
`start_time` datetime comment '优惠券生效日期',
`end_time` datetime comment '优惠券失效日期',
`limitmoney` decimal(10, 2) comment '优惠券可以使用的金额,满减、满折等',
`discount` double comment '减免或折扣' ,
PRIMARY KEY (`id`)
) comment '8.优惠券模板表';

CREATE TABLE `t_usercoupon`  (
`id` int(11) AUTO_INCREMENT,
`template_id` int(11) COMMENT '优惠券模板ID',
`user_id` int(11) COMMENT '前端用户ID',
`coupon_code` varchar(70) COMMENT '优惠券码',
`assign_date` datetime COMMENT '优惠券分发时间',  
`status` int(11) COMMENT '优惠券状态',
PRIMARY KEY (`id`)
) comment '9.用户优惠券表';

3.2 Entity和DTO

实体对象Entity

//优惠券模板对象
//和数据库中的表一一对应
package org.qf.cloudcoupon.entity;

import java.util.Date;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;

import java.io.Serializable;

/**
 * 8.优惠券模板表(TCouponTemplate)实体类
 *
 * @author makejava
 * @since 2022-09-26 14:46:36
 */
@Data
public class TCouponTemplate implements Serializable {
    private static final long serialVersionUID = 778554933659508581L;

    private Integer id;
    /**
     * 状态:41.未审核 42.审核通过 43.审核失败
     */
    private Integer flag;
    /**
     * 名字
     */
    private String name;

    private String logo;
    /**
     * 简介
     */
    private String intro;
    /**
     * 种类: 51-满减;52-折扣;53-立减
     */
    private Integer category;
    /**
     * 使用范围:61-单品;62-商品类型;63-全品
     */
    private Integer scope;
    /**
     * 对应的id:单品id;商品类型id;全品为0
     */
    private Integer scopeId;
    /**
     * 优惠券发放结束日期
     */
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date expireTime;
    /**
     * 优惠券发放数量
     */
    private Integer couponCount;
    /**
     * 创建时间
     */
    // 解析参数的,前端传递的是string类型的字串时 string->date
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    // 实体对象转换为json字串的互转 json <-> date
    // 前端传参如果是json字串 就按照JsonFormat进行反序列化
    // 后端如果需要返回json字串 就按照JsonFormat进行序列化
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;
    /**
     * 创建人的ID,后台内部员工
     */
    private Integer userId;
    /**
     * 审核意见
     */
    private String userAudit;
    /**
     * 优惠券模板的识别码(有一定的识别度)
     */
    private String templateKey;
    /**
     * 优惠券作用的人群:71-全体;72-会员等级 73-新用户 74-收费会员
     */
    private Integer target;
    /**
     * 用户等级要求,默认0
     */
    private Integer targetLevel;
    /**
     * 发放类型:81.用户领取 82.系统发放
     */
    private Integer sendType;
    /**
     * 优惠券生效日期
     */
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date startTime;
    /**
     * 优惠券失效日期
     */
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date endTime;
    /**
     * 优惠券可以使用的金额,满减、满折等
     */
    private Double limitmoney;
    /**
     * 减免或折扣
     */
    private Double discount;

}
package org.qf.cloudcoupon.entity;

import java.util.Date;

import lombok.Data;
import org.qf.cloudcoupon.config.SystemConfig;

import java.io.Serializable;

/**
 * 9.用户优惠券表(TUsercoupon)实体类
 *
 * @author makejava
 * @since 2022-09-26 14:47:20
 */
@Data
public class TUsercoupon implements Serializable {
    private static final long serialVersionUID = -71212782766340228L;

    private Integer id;
    /**
     * 优惠券模板ID
     */
    private Integer templateId;
    /**
     * 前端用户ID
     */
    private Integer userId;
    /**
     * 优惠券码
     */
    private String couponCode;
    /**
     * 优惠券分发时间
     */
    private Date assignDate;
    /**
     * 优惠券状态
     */
    private Integer status;

    // 在构造方法中 把固定的属性直接赋值
    public TUsercoupon(Integer templateId, Integer userId, String couponCode) {
        this.templateId = templateId;
        this.userId = userId;
        this.couponCode = couponCode;
        this.assignDate=new Date();
        this.status= SystemConfig.USER_COUPON_NO;
    }

}

数据传输对象DTO

// 优惠券模板对象
@Data
public class CouponAddDto {
    private String name;
    private String logo;
    private String intro;
    private Integer category;
    private Integer scope;
    private Integer scopeId;
//    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date expireTime;
    private Integer couponCount;
    private Integer target;
    private Integer targetLevel;
    private Integer sendType;
//    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date startTime;
//    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date endTime;
    private double limitmoney;
    private double discount;
}
// 用户优惠券对象
@Data
public class UserCouponDto {
    private Integer id;//用户优惠券id
    private Integer tid;//用户优惠券模板id
    private Integer uid;//用户id
    private String couponCode;//用户优惠券券码
    private Integer status;//用户优惠券id
    private Date startTime;//优惠券生效起始时间
    private Date endTime;//优惠券生效结束时间
    private double limitmoney;//要求,满减
    private double discount;//减免的金额或者打折
    private Integer category;//优惠券减免额类型:种类: 51-满减;52-折扣;53-立减
    private String name;//优惠券名称
    private String logo;//优惠券显示的logo

}
// 优惠券审核对象
@Data
public class CouponAuditDto {
    // 优惠券模板ID
    private Integer id;
    // 创建人ID 后台内部员工
    private Integer aid;
    // 优惠券状态
    private Integer flag;
    // 审核意见
    private String info;
}

3.3 优惠券模板状态

/**
 * 优惠券模板审核状态
 * 枚举类 不需要set方法 枚举的几个值是固定
 * 需要通过构造方法 设置枚举的几个值
 */
public enum CouponAudit {
    未审核(41),审核通过(42),审核拒绝(43);

    CouponAudit(int code) {
        this.code = code;
    }

    private int code;

    public int getCode() {
        return code;
    }
}

3.4 系统字典

系统配置常量

// 系统字典
public interface SystemConfig {
    //自定义uid的消息头
    String HEADER_UID="cp_uid";
    //优惠券的发送类型 81:用户优惠券 82:系统优惠券
    int COUPON_SEND_USER=81;
    int COUPON_SEND_SYSTEM=82;
    //优惠券作用的人群:71-全体;72-会员等级 73-新用户 74-收费会员
    int COUPON_TARGET_ALL=71;
    int COUPON_TARGET_LEVEL=72;
    int COUPON_TARGET_NEW=73;
    int COUPON_TARGET_PLUS=74;
    //批量新增用户优惠券数据的数量  线程池的任务处理的数量
    int THREAD_COUPON_BATCH=1000;
    
    //优惠券的使用状态 91-未使用 92-已使用 93-无效
    int USER_COUPON_NO=91;
    int USER_COUPON_USED=92;
    int USER_COUPON_DEAD=93;
}

MQ消息配置常量

public interface RabbitMQConstConfig {
    //队列名
    String Q_USERINIT="cp-userinit";
    String Q_USERSCORE="cp-userscore";

    String Q_COUPONSYS="cp-couponsys";
    String Q_COUPONUSE="cp-couponuse";

    String Q_ORDER_SYNC="cp-ordersync";//订单同步:redis-mysql
    String Q_ORDER_DEAD="cp-orderdead";//订单死信消息
    String Q_ORDER_TIMEOUT="cp-ordertimeout";//订单延迟

    String Q_SKILLORDER_SYNC="cp-skillordersync";//秒杀订单同步:redis-mysql
    String Q_SKILLORDER_DEAD="cp-skillorderdead";//秒杀订单死信消息
    String Q_SKILLORDER_TIMEOUT="cp-skillordertimeout";//秒杀订单延迟

    //交换器名
    String EX_USERADD="ex-d-useradd";
    String EX_COUPONTEM="ex-d-coupontem";

    String EX_ORDERADD="ex-f-orderadd";//转发下单数据
    String EX_DEAD="ex-d-dead";//死信交换器

    String EX_SKILLORDERADD="ex-f-skillorderadd";//转发秒杀下单数据

    //路由关键字
    String RK_USERADD="rk-useradd";
    // 路由key
    String RK_COUPONSYS="rk-coupon-sys";
    String RK_COUPONUSE="rk-coupon-use";

    String RK_DEAD_ORDERTO="rk-order-timeout";
    String RK_DEAD_SKILLORDERTO="rk-skillorder-timeout";

    //MQ消息类型
    int MQTYPE_USERADD=1;
    int MQTYPE_USERSIGN=2;
    int MQTYPE_USERLOFIN=3;
    int MQTYPE_COUPONSYS=4;//优惠券模板 系统发放
    int MQTYPE_COUPONUSE=5;//优惠券模板 用户领取
    int MQTYPE_ORDERADD=6;//下单
    int MQTYPE_SKILLORDERADD=8;//秒杀下单
    int MQTYPE_ORDERSYNC=7;//订单同步
    int MQTYPE_ORDERCOMMENT=9;//订单评价

}

redis的key的常量

// 记录所有的key
public interface RedisKeyConfig {
    //存储优惠券信息
    //追加优惠券模板id,值存储数量,有效期为优惠券模板的领取的结束时间
    //public static final String COUPON_CACHE="cp:soupon:";//追加:模板id:等级id
    String COUPON_CACHE="cp:coupon:";//追加:模板id List类型,第一个元素:数量 第二个元素:等级要求
    //存储用户领过的优惠券,用来解决 用户领取优惠券的限领的问题,有效期:优惠券模板的领取的时间
    String COUPON_USERS="cp:coupon:users:";//追加模板id,Set类型 值记录uid,有效期

    //设置分布式锁的key,优惠券的领取 防止超领
    String COUPON_LOCK="cp:coupon:rl:";//追加模板id
    int COUPON_LOCK_TIME=10;//10秒
}

3.5 MQ消息对象

@Data
@NoArgsConstructor
@AllArgsConstructor
public class MqMsgBo implements Serializable {
    private long id;//唯一id,防止消息重复,雪花算法
    private int type;//类型
    private Object data;//消息内容
}

3.6 雪花算法的工具类

使用单例模式的工具类,获取全局唯一的ID:SnowFlowUtil.getInstance().nextId()

public class SnowFlowUtil {
    private SnowFlowUtil() {
    }
    private static class SnowFlowIodh {
        public static SnowFlowUtil obj=new SnowFlowUtil();
    }
    public static SnowFlowUtil getInstance(){
        return SnowFlowIodh.obj;
    }

    //因为二进制里第一个 bit 为如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。

    //机器ID  2进制5位  32位减掉1位 31个
    private long workerId;
    //机房ID 2进制5位  32位减掉1位 31个
    private long datacenterId;
    //代表一毫秒内生成的多个id的最新序号  12位 4096 -1 = 4095 个
    private long sequence;
    //设置一个时间初始值    2^41 - 1   差不多可以用69年
    private long twepoch = 1585644268888L;
    //5位的机器id
    private long workerIdBits = 5L;
    //5位的机房id;。‘
    private long datacenterIdBits = 5L;
    //每毫秒内产生的id数 2 的 12次方
    private long sequenceBits = 12L;
    // 这个是二进制运算,就是5 bit最多只能有31个数字,也就是说机器id最多只能是32以内
    private long maxWorkerId = -1L ^ (-1L << workerIdBits);
    // 这个是一个意思,就是5 bit最多只能有31个数字,机房id最多只能是32以内
    private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

    private long workerIdShift = sequenceBits;
    private long datacenterIdShift = sequenceBits + workerIdBits;
    private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    // -1L 二进制就是1111 1111  为什么?
    // -1 左移12位就是 1111  1111 0000 0000 0000 0000
    // 异或  相同为0 ,不同为1
    // 1111  1111  0000  0000  0000  0000
    // ^
    // 1111  1111  1111  1111  1111  1111
    // 0000 0000 1111 1111 1111 1111 换算成10进制就是4095
    private long sequenceMask = -1L ^ (-1L << sequenceBits);
    //记录产生时间毫秒数,判断是否是同1毫秒
    private long lastTimestamp = -1L;
    public long getWorkerId(){
        return workerId;
    }
    public long getDatacenterId() {
        return datacenterId;
    }
    public long getTimestamp() {
        return System.currentTimeMillis();
    }


    // 这个是核心方法,通过调用nextId()方法,
    // 让当前这台机器上的snowflake算法程序生成一个全局唯一的id
    public synchronized long nextId() {
        // 这儿就是获取当前时间戳,单位是毫秒
        long timestamp = timeGen();
        // 判断是否小于上次时间戳,如果小于的话,就抛出异常
        if (timestamp < lastTimestamp) {

            System.err.printf("clock is moving backwards. Rejecting requests until %d.", lastTimestamp);
            throw new RuntimeException(
                    String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
                            lastTimestamp - timestamp));
        }

        // 下面是说假设在同一个毫秒内,又发送了一个请求生成一个id
        // 这个时候就得把seqence序号给递增1,最多就是4096
        if (timestamp == lastTimestamp) {

            // 这个意思是说一个毫秒内最多只能有4096个数字,无论你传递多少进来,
            //这个位运算保证始终就是在4096这个范围内,避免你自己传递个sequence超过了4096这个范围
            sequence = (sequence + 1) & sequenceMask;
            //当某一毫秒的时间,产生的id数 超过4095,系统会进入等待,直到下一毫秒,系统继续产生ID
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }

        } else {
            sequence = 0;
        }
        // 这儿记录一下最近一次生成id的时间戳,单位是毫秒
        lastTimestamp = timestamp;
        // 这儿就是最核心的二进制位运算操作,生成一个64bit的id
        // 先将当前时间戳左移,放到41 bit那儿;将机房id左移放到5 bit那儿;将机器id左移放到5 bit那儿;将序号放最后12 bit
        // 最后拼接起来成一个64 bit的二进制数字,转换成10进制就是个long型
        return ((timestamp - twepoch) << timestampLeftShift) |
                (datacenterId << datacenterIdShift) |
                (workerId << workerIdShift) | sequence;
    }

    /**
     * 当某一毫秒的时间,产生的id数 超过4095,系统会进入等待,直到下一毫秒,系统继续产生ID
     * @param lastTimestamp
     * @return
     */
    private long tilNextMillis(long lastTimestamp) {

        long timestamp = timeGen();

        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }
    //获取当前时间戳
    private long timeGen(){
        return System.currentTimeMillis();
    }
}

雪花算法的原理就是生成一个的 64 位比特位的 long 类型的唯一 id。此算法与系统时间和机器码有关

  • 最高 1 位固定值 0,因为生成的 id 是正整数,如果是 1 就是负数了。

  • 接下来 41 位存储毫秒级时间戳,2^41/(1000606024365)=69,大概可以使用 69 年。

  • 再接下 10 位存储机器码,包括 5 位 datacenterId 和 5 位 workerId。最多可以部署 2^10=1024 台机器。

  • 最后 12 位存储序列号。同一毫秒时间戳时,通过这个递增的序列号来区分。即对于同一台机器而言,同一毫秒时间戳下,可以生成 2^12=4096 个不重复 id。

img
img

可以将雪花算法作为一个单独的服务进行部署,然后需要全局唯一 id 的系统,请求雪花算法服务获取 id 即可。

————————————————

SnowFlake 中文意思为雪花,故称为雪花算法。最早是 Twitter 公司在其内部用于分布式环境下生成唯一 ID。在2014年开源 scala 语言版本。

现在的服务基本是分布式、微服务形式的,而且大数据量也导致分库分表的产生,对于水平分表就需要保证表中 id 的全局唯一性。

对于 MySQL 而言,一个表中的主键 id 一般使用自增的方式,但是如果进行水平分表之后,多个表中会生成重复的 id 值。那么如何保证水平分表后的多张表中的 id 是全局唯一性的呢?

如果还是借助数据库主键自增的形式,那么可以让不同表初始化一个不同的初始值,然后按指定的步长进行自增。例如有3张拆分表,初始主键值为1,2,3,自增步长为3。

当然也有人使用 UUID 来作为主键,但是 UUID 生成的是一个无序的字符串,对于 MySQL 推荐使用增长的数值类型值作为主键来说不适合。

也可以使用 Redis 的自增原子性来生成唯一 id,但是这种方式业内比较少用。

当然还有其他解决方案,不同互联网公司也有自己内部的实现方案。雪花算法是其中一个用于解决分布式 id 的高效方案,也是许多互联网公司在推荐使用的。
————————————————

算法优缺点
雪花算法有以下几个优点:

高并发分布式环境下生成不重复 id,每秒可生成百万个不重复 id。
基于时间戳,以及同一时间戳下序列号自增,基本保证 id 有序递增。
不依赖第三方库或者中间件。
算法简单,在内存中进行,效率高。
雪花算法有如下缺点:

依赖服务器时间,服务器时钟回拨时可能会生成重复 id。算法中可通过记录最后一个生成 id 时的时间戳来解决,每次生成 id 之前比较当前服务器时钟是否被回拨,避免生成重复 id。

3.7 单例模式的线程池对象

/**
 * @Description: 单例模式 封装线程池
 * @Author: zed
 * @Date: 2022/5/5 11:37
 */
public class ThreadPoolSignle {
    //单例模式-IoDH实现
    public ThreadPoolExecutor poolExecutor;
    private ThreadPoolSignle(){
        //七大参数
        poolExecutor=new ThreadPoolExecutor(4,20,3, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(20),new DefaultManagedAwareThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
    }
    private static class PoolSingle{
        private static ThreadPoolSignle signle=new ThreadPoolSignle();
    }
    public static ThreadPoolSignle getInstance(){
        return PoolSingle.signle;
    }
}

3.8 时间日期的工具类

public class DateUtil {
    /**
     * 比较日期是否为同一天*/
    public static boolean eqDate(Date d1,Date d2){
        SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd");
        return sdf.format(d1).equals(sdf.format(d2));
    }
    /**
     * 计算2个日期相差的天数*/
    public static int diffDays(Date start,Date end){
        Calendar cal1 = Calendar.getInstance();
        cal1.setTime(start);
        Calendar cal2 = Calendar.getInstance();
        cal2.setTime(end);
        int day1= cal1.get(Calendar.DAY_OF_YEAR);
        int day2 = cal2.get(Calendar.DAY_OF_YEAR);
        int year1 = cal1.get(Calendar.YEAR);
        int year2 = cal2.get(Calendar.YEAR);
        if(year1 != year2) //同一年
        {
            int timeDistance = 0 ;
            for(int i = year1 ; i < year2 ; i ++)
            {
                if(i%4==0 && i%100!=0 || i%400==0) //闰年
                {
                    timeDistance += 366;
                }
                else //不是闰年
                {
                    timeDistance += 365;
                }
            }
            return timeDistance + (day2-day1) ;
        }
        else //不同年
        {
            return day2-day1;
        }
    }
    /**
     * 获取今日剩余的秒数*/
    public static long lastSeconds(){
        SimpleDateFormat sdf1=new SimpleDateFormat("yyyy-MM-dd");
        Date date=new Date();
        SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        try {
            Date date1=sdf.parse(sdf1.format(date)+" 23:59:59");
            return (date1.getTime()-new Date().getTime())/1000;
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return 0;
    }
    /**
     * 获取今日剩余的秒数*/
    public static long lastSeconds(Date date){
        return (date.getTime()-new Date().getTime())/1000;
    }

}

涉及技术栈:

Redis(1.缓存 2.分布式锁 3.倒计时)

RabbitMQ(1.削峰填谷,异步)

线程池(1.批处理,解决系统发放优惠券)

四、实现

基于微服务实现,优惠券服务

4.1 优惠券模板接口

1.新增优惠券活动接口 save

2.审核优惠券活动接口 audit

image.png
image.png

3.查询优惠券活动接口

优惠券模板控制器接口

package org.qf.cloudcoupon.controller;

import lombok.RequiredArgsConstructor;
import org.qf.cloudcoupon.entity.TCouponTemplate;
import org.qf.cloudcoupon.service.CouponTemplateService;
import org.qf.cloudentity.entity.Response;
import org.springframework.web.bind.annotation.*;

/**
 * @author zed
 * @date 2022/9/26
 * 优惠券模板控制器
 */
@RestController
@RequestMapping("couponTemplate")
@RequiredArgsConstructor
public class CouponTemController {

    private final CouponTemplateService couponTemplateService;

    // 新增优惠券模板
    @PostMapping("save")
    public Response save(@RequestBody TCouponTemplate couponTemplate) {
        return couponTemplateService.save(couponTemplate);
    }

    // 分页查看优惠券列表
    @GetMapping("queryAll")
    public Response queryAll(@RequestParam(value = "pageIndex", defaultValue = "1") Integer pageIndex,
                             @RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize) {
        return couponTemplateService.queryAll(pageIndex,pageSize);
    }
    
    // 优惠券审核接口
    @PostMapping("audit")
    public Response audit(@RequestBody CouponAuditDto dto) {
        return couponTemplateService.audit(dto);
    }
}

用户优惠券接口

@RestController
@RequestMapping("userCoupon")
@RequiredArgsConstructor
public class UserCouponController {

    private final UserCouponService service;

    // 根据优惠券状态查询,用户Id从请求头中获取
    @GetMapping("query")
    public Response query(@RequestParam int status, HttpServletRequest request){
        return service.queryMy(request.getIntHeader(SystemConfig.HEADER_UID),status);
    }
    // 根据ID查询用户优惠券
    @GetMapping("detail")
    public Response<UserCouponDto> detail(@RequestParam int id){
        return service.queryId(id);
    }
    // 用户领取优惠券
    // ul:用户等级 ctid:优惠券模板ID 
    @PostMapping("save")
    public Response save(@RequestParam int ul, @RequestParam int ctid,HttpServletRequest request){
        return service.save(request.getIntHeader(SystemConfig.HEADER_UID),ul,ctid);
    }
    // 更新优惠券状态
    @GetMapping("update")
    public Response update(@RequestParam int id,@RequestParam int flag){
        return service.update(id, flag);
    }
}

4.2业务层核心代码

新增优惠券

DAO层代码省略. 可以代码生成或手写

package org.qf.cloudcoupon.service.impl;

import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.qf.cloudcommon.util.RedissionUtil;
import org.qf.cloudcommon.util.SnowFlowUtil;
import org.qf.cloudcoupon.config.CouponAudit;
import org.qf.cloudcoupon.config.RabbitMQConstConfig;
import org.qf.cloudcoupon.config.RedisKeyConfig;
import org.qf.cloudcoupon.config.SystemConfig;
import org.qf.cloudcoupon.dao.TCouponTemplateDao;
import org.qf.cloudcoupon.dao.TUsercouponDao;
import org.qf.cloudcoupon.dto.CouponAuditDto;
import org.qf.cloudcoupon.dto.MqMsgBo;
import org.qf.cloudcoupon.entity.TCouponTemplate;
import org.qf.cloudcoupon.entity.TUsercoupon;
import org.qf.cloudcoupon.service.CouponTemplateService;
import org.qf.cloudentity.entity.Response;
import org.redisson.api.RLock;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @author zed
 * @date 2022/9/26
 * 优惠券模板业务实现
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class CouponTemplateServiceImpl implements CouponTemplateService {

    private final TCouponTemplateDao couponTemplateDao;

    private final RabbitTemplate rabbitTemplate;

    private final TUsercouponDao usercouponDao;

    @Override
    public Response<PageInfo<TCouponTemplate>> queryAll(Integer pageIndex, Integer pageSize) {
        // 在slf4j字串中使用占位符 {}
        log.info("查询的是第{},每页{}条数据", pageIndex, pageSize);
        PageHelper.startPage(pageIndex, pageSize);
        List<TCouponTemplate> list = couponTemplateDao.queryAll();
        PageInfo<TCouponTemplate> pageInfo = new PageInfo<>(list);
        return Response.ok(pageInfo);
    }

    @Override
    @Transactional
    public Response save(TCouponTemplate couponTemplate) {
        // 设置优惠券的状态是:41.未审核
        couponTemplate.setFlag(CouponAudit.未审核.getCode());
        // 优惠券创建时间
        couponTemplate.setCreateTime(new Date());
        // 优惠券模板的识别码(有一定的识别度)
        couponTemplate.setTemplateKey(UUID.randomUUID().toString());
        if (couponTemplateDao.insert(couponTemplate) > 0) {
            log.info("新增优惠券模板成功");
            return Response.ok();
        }
        return Response.fail("新增优惠券活动失败");
    }

    // 审核优惠券,审核通过后,可以直接发送优惠券,也可以使用异步发送MQ
    @Override
    @Transactional
    public Response audit(CouponAuditDto dto) {
        // 1、查询优惠券模板
        TCouponTemplate template = couponTemplateDao.queryById(dto.getId());
        // 2、查看优惠券模板的状态
        if (Objects.nonNull(template) && template.getFlag() == CouponAudit.未审核.getCode()) {
            // 3、更新优惠券
            template.setFlag(dto.getFlag());
            template.setUserId(dto.getAid());
            template.setUserAudit(dto.getInfo());
            if (couponTemplateDao.update(template) > 0) {
                // 审核操作成功
                // 4、验证优惠券活动是否审核成功
                if (dto.getFlag() == CouponAudit.审核通过.getCode()) {
                    // 发送优惠券,直接发送操作数据库也可以 但是需要操作数据库耗时较长
                    // 可以使用异步发送优惠券 把优惠券对象放入MQ中 在MQ的消费者中操作数据库
                    // 5、构建一个MQ的消息对象
                    MqMsgBo msgBo = new MqMsgBo();
                    msgBo.setId(SnowFlowUtil.getInstance().nextId());
                    // 设置优惠券类型需要根据模板中的类型来指定
                    // sendType 发放类型:81.用户领取 82.系统发放
                    msgBo.setData(template);
                    // MQ消息的路由key
                    String rk = "";
                    if (template.getSendType() == SystemConfig.COUPON_SEND_SYSTEM) {
                        // 系统发放的优惠券
                        msgBo.setType(RabbitMQConstConfig.MQTYPE_COUPONSYS);
                        rk = RabbitMQConstConfig.RK_COUPONSYS;
                    } else if (template.getSendType() == SystemConfig.COUPON_SEND_USER) {
                        // 用户领取的优惠券
                        msgBo.setType(RabbitMQConstConfig.MQTYPE_COUPONUSE);
                        rk = RabbitMQConstConfig.RK_COUPONUSE;
                    }
                    // 通过MQ发送消息
                    if (StringUtils.hasLength(rk)) {
                        rabbitTemplate.convertAndSend(RabbitMQConstConfig.EX_COUPONTEM, rk, msgBo);
                    }
                    return Response.ok();
                }

            }

        }
        return Response.fail("亲,不是合法的操作!");
    }

}

审核优惠券活动

因为要使用到MQ发送消息,所以需要

  • 启动MQ服务 在虚拟机中启动RabbitMQ
  • 添加AMQP依赖
  • 增加MQ配置

maven新增依赖

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

配置文件新增 5672:内部通讯端口 15672:WEB页面端口

spring:
  rabbitmq:
    addresses: 192.168.29.110:5672

配置优惠券相关的MQ的队列&交换机&路由key及绑定关系

package org.qf.cloudcoupon.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author zed
 * @date 2022/9/27
 * RabbitMQ的配置类
 * 指定交换机/路由key和队列的绑定关系
 */
@Configuration
public class RabbitMQConfig {

    // 系统优惠券的队列
    @Bean
    public Queue createSys(){
        return new Queue(RabbitMQConstConfig.Q_COUPONSYS);
    }

    // 用户优惠券的队列
    @Bean
    public Queue createUser(){
        return new Queue(RabbitMQConstConfig.Q_COUPONUSE);
    }

    // 优惠券模板专用的交换机
    @Bean
    public DirectExchange directExchange(){
        return new DirectExchange(RabbitMQConstConfig.EX_COUPONTEM);
    }

    // 系统优惠券队列的绑定
    @Bean
    public Binding bindSys(DirectExchange e){
        return BindingBuilder.bind(createSys()).to(e).with(RabbitMQConstConfig.RK_COUPONSYS);
    }

    // 用户优惠券队列的绑定
    @Bean
    public Binding bindUser(DirectExchange e){
        return BindingBuilder.bind(createUser()).to(e).with(RabbitMQConstConfig.RK_COUPONUSE);
    }
}

难点在于MQ发送优惠券对象后,在MQ的消费端真正发放优惠券,消费端异步发送给用户优惠券

// 审核优惠券,审核通过后,可以直接发送优惠券,也可以使用异步发送MQ
@Override
@Transactional
public Response audit(CouponAuditDto dto) {
    // 1、查询优惠券模板
    TCouponTemplate template = couponTemplateDao.queryById(dto.getId());
    // 2、查看优惠券模板的状态
    if (Objects.nonNull(template) && template.getFlag() == CouponAudit.未审核.getCode()) {
        // 3、更新优惠券
        template.setFlag(dto.getFlag());
        template.setUserId(dto.getAid());
        template.setUserAudit(dto.getInfo());
        if (couponTemplateDao.update(template) > 0) {
            // 审核操作成功
            // 4、验证优惠券活动是否审核成功
            if (dto.getFlag() == CouponAudit.审核通过.getCode()) {
                // 发送优惠券,直接发送操作数据库也可以 但是需要操作数据库耗时较长
                // 可以使用异步发送优惠券 把优惠券对象放入MQ中 在MQ的消费者中操作数据库
                // 5、构建一个MQ的消息对象
                MqMsgBo msgBo = new MqMsgBo();
                msgBo.setId(SnowFlowUtil.getInstance().nextId());
                // 设置优惠券类型需要根据模板中的类型来指定
                // sendType 发放类型:81.用户领取 82.系统发放
                msgBo.setData(template);
                // MQ消息的路由key
                String rk = "";
                if (template.getSendType() == SystemConfig.COUPON_SEND_SYSTEM) {
                    // 系统发放的优惠券
                    msgBo.setType(RabbitMQConstConfig.MQTYPE_COUPONSYS);
                    rk = RabbitMQConstConfig.RK_COUPONSYS;
                } else if (template.getSendType() == SystemConfig.COUPON_SEND_USER) {
                    // 用户领取的优惠券
                    msgBo.setType(RabbitMQConstConfig.MQTYPE_COUPONUSE);
                    rk = RabbitMQConstConfig.RK_COUPONUSE;
                }
                // 通过MQ发送消息
                if (StringUtils.hasLength(rk)) {
                    rabbitTemplate.convertAndSend(RabbitMQConstConfig.EX_COUPONTEM, rk, msgBo);
                }
                return Response.ok();
            }

        }

    }
    return Response.fail("亲,不是合法的操作!");
}

基于MQ异步实现优惠券系统派发:线程池处理业务

系统发送的优惠券:

1、如果是给全体所有用户发放的,就获取所有用户的ID

2、如果是给某个级别用户发送的优惠券,就获取这个等级的所有用户的ID

然后给这些人发送对应的优惠券!本质上就是到用户优惠券表中批量新增记录!

package org.qf.cloudcoupon.listener;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.qf.cloudcommon.util.SnowFlowUtil;
import org.qf.cloudcommon.util.ThreadPoolSignle;
import org.qf.cloudcoupon.config.RabbitMQConstConfig;
import org.qf.cloudcoupon.config.SystemConfig;
import org.qf.cloudcoupon.dao.TUsercouponDao;
import org.qf.cloudcoupon.dto.MqMsgBo;
import org.qf.cloudcoupon.entity.TCouponTemplate;
import org.qf.cloudcoupon.entity.TUsercoupon;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @author zed
 * @date 2022/9/27
 * 系统级别MQ的监听器-消费者
 */
@Slf4j
@Component
@RequiredArgsConstructor
@RabbitListener(queues = RabbitMQConstConfig.Q_COUPONSYS)
public class CouponTemSystemListener {

    private final TUsercouponDao usercouponDao;

    @RabbitHandler
    public void handler(MqMsgBo msgBo) {
        // 1、判断消息类型 如果是系统级别的优惠券模板 就可以处理
        if (msgBo.getType() == RabbitMQConstConfig.MQTYPE_COUPONSYS) {
            TCouponTemplate template = (TCouponTemplate) msgBo.getData();
            // 2、发送给哪些用户的优惠券
            Integer target = template.getTarget();
            if (target == SystemConfig.COUPON_TARGET_ALL || target == SystemConfig.COUPON_TARGET_LEVEL) {
                // 3、根据用户级别 查询这个级别下的所有用户的ID
                List<Integer> uids = levels(target);
                // 4、给这些用户发送优惠券 使用线程池优化操作 分片操作

                // 5、分片处理
                int batchs = uids.size() / SystemConfig.THREAD_COUPON_BATCH;
                batchs = uids.size() % SystemConfig.THREAD_COUPON_BATCH == 0 ? batchs : batchs + 1;

                for (int i = 0; i < batchs; i++) {
                    int start = i * SystemConfig.THREAD_COUPON_BATCH;

                    // 6、使用线程池处理
                    ThreadPoolSignle.getInstance().poolExecutor.execute(() -> {
                        // 每一个线程要执行的任务
                        List<TUsercoupon> entities = new ArrayList<>();
                        // 7、使用stream流优化
                        List<Integer> list = uids.stream().skip(start).limit(SystemConfig.THREAD_COUPON_BATCH).collect(Collectors.toList());
                        list.forEach(uid->{
                            TUsercoupon usercoupon = new TUsercoupon(template.getId(), uid, "cp-sys-" + SnowFlowUtil.getInstance().nextId());
                            entities.add(usercoupon);
                        });
                        // 批量新增
                        usercouponDao.insertBatch(entities);
                    });
                }
            }
        }

    }

    // 模拟获取某个等级的所有用户的ID
    private List<Integer> levels(Integer level) {
        List<Integer> uids = new ArrayList<>();
        for (int i = 0; i < 5005; i++) {
            uids.add(i);
        }
        return uids;
    }


    /*public static void main(String[] args) {
        List<Integer> uids = new ArrayList<>();
        for (int i = 0; i < 5005; i++) {
            uids.add(i);
        }

        List<Integer> list = uids.stream().skip(5000).limit(1000).collect(Collectors.toList());
        System.out.println(list);
    }*/
}

使用stream流的批处理方式

stream().skip().limit()

/*
 * 使用stream流的实现方式
 */
int pages = uids.size() / SystemConfig.THREAD_COUPON_BATCH;
pages = uids.size() % SystemConfig.THREAD_COUPON_BATCH == 0 ? pages : pages + 1;
for (int i = 0; i < pages; i++) {
    List<Integer> list = uids.stream().skip(i * SystemConfig.THREAD_COUPON_BATCH).limit(SystemConfig.THREAD_COUPON_BATCH).collect(Collectors.toList());
    System.out.println(list);
}

基于Redis异步实现用户级别优惠券派发:用户级别优惠券放入缓存

package org.qf.cloudcoupon.listener;

import lombok.extern.slf4j.Slf4j;
import org.qf.cloudcommon.util.DateUtil;
import org.qf.cloudcommon.util.RedissionUtil;
import org.qf.cloudcoupon.config.RabbitMQConstConfig;
import org.qf.cloudcoupon.config.RedisKeyConfig;
import org.qf.cloudcoupon.dto.MqMsgBo;
import org.qf.cloudcoupon.entity.TCouponTemplate;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * @author zed
 * @date 2022/9/27
 */
@Slf4j
@Component
@RabbitListener(queues = RabbitMQConstConfig.Q_COUPONUSE)
public class CouponTemUserListener {

    @RabbitHandler
    public void handler(MqMsgBo msgBo) {
        // 判断优惠券的类型是用户级别的优惠券
        if (msgBo.getType() == RabbitMQConstConfig.MQTYPE_COUPONUSE) {
            // 发送优惠券,把优惠券放入缓存中
            TCouponTemplate template = (TCouponTemplate) msgBo.getData();
            Integer templateId = template.getId();

            // 放入缓存中的redis的key
            String key = RedisKeyConfig.COUPON_CACHE + templateId;
            RedissionUtil.setList(key, template.getCouponCount());
            RedissionUtil.setList(key, template.getTargetLevel());
            // 设置优惠券模板key的过期时间 领取的过期时间
            RedissionUtil.expire(key, DateUtil.lastSeconds(template.getExpireTime()));
            log.info("用户优惠券发放成功!");
        }
    }
}

总结:

系统级优惠券,无需用户领取自动发送

用户级别的优惠券,需要用户手动点击进行领取

五、测试

测试用例:

新增优惠券模板:

{
  "category": 52,
  "couponCount": 1000,
  "discount": 50,
  "endTime": "2022-05-19 23:59:59",
  "expireTime": "2022-05-17 23:59:59",
  "intro": "测试系统发送优惠券",
  "limitmoney": 100,
  "logo": "",
  "name": "测试方便面满100减50元",
  "scope": 63,
  "scopeId": 0,
  "sendType": 82,
  "startTime": "2022-05-18 00:00:00",
  "target": 71,
  "targetLevel": 0
}
image.png
image.png
{
  "category": 53,
  "couponCount": 10,
  "discount": 50,
  "endTime": "2022-05-20T10:08:55.000+08:00",
  "expireTime": "2022-05-18T10:08:55.000+08:00",
  "intro": "测试系统发送优惠券",
  "limitmoney": 100,
  "logo": "",
  "name": "水饺立减50元",
  "scope": 63,
  "scopeId": 0,
  "sendType": 81,
  "startTime": "2022-05-18T10:08:55.000+08:00",
  "target": 71,
  "targetLevel": 0
}
image.png
image.png

六、领取

领取优惠券

单机锁:synchronized和Lock

synchronized:关键字底层Object 监视器 ,进入的时候验证Object 不存在 加1 ,执行完减1,每次执行完都会校验计数器是否为0,为0,释放锁,在锁上等待的线程会进入就绪状态

用法:3种,锁的范围(粒度)

修饰静态方法--Class对象

修饰实例方法--this

修饰代码块--对象变量

可重入:同一个线程对同一把锁,可以使用多次

公平锁:所有线程,按次序获取锁,排队

非公平锁:抢占、竞争

Lock:接口,实现类:ReentrantLock,底层:AQS(AbstractQueuedSynchronizer)+CAS算法(ABA问题)

//领取优惠券 ?难点-超领
@Override
public Response save(int uid, int ul, int ctid) {
    //1.验证优惠券是否领取结束
    if (RedissionUtil.checkKey(RedisKeyConfig.COUPON_CACHE + ctid)) {
        //2.校验用户是否领过该优惠券--查询数据库,查询Redis
        boolean r = true; // 该用户是否领取过
        boolean istime = false;
        if (RedissionUtil.checkKey(RedisKeyConfig.COUPON_USERS + ctid)) {
            r = !RedissionUtil.exists(RedisKeyConfig.COUPON_USERS + ctid, uid);
        } else {
            istime = true;//第一次有人领取这个模板的优惠券
        }
        if (r) {//没有领取过
            //3.验证数量是否足够
            int count = (int) RedissionUtil.getList(RedisKeyConfig.COUPON_CACHE + ctid, 0);
            //获取分布式锁的对象
            RLock rLock = RedissionUtil.getLock(RedisKeyConfig.COUPON_LOCK + ctid);
            try {
                //尝试添加分布式锁
                if (rLock.tryLock(RedisKeyConfig.COUPON_LOCK_TIME, TimeUnit.SECONDS)) {
                    if (count > 0) {
                        //4.校验领取是否有用户等级的限制
                        int level = (int) RedissionUtil.getList(RedisKeyConfig.COUPON_CACHE + ctid, 1);
                        if (level > 0) {
                            //改优惠券的领取,要求用户等级,关于等级:1.前端直接传递 2.远程服务调用
                            //5.查询用户等级,校验是否满足规则
                            if (ul < level) {
                                return Response.fail("亲,你不满足领取的资格!");
                            }
                        }

                        //6.生成用户优惠券信息
                        TUsercoupon coupon = new TUsercoupon(ctid, uid, "user_" + SnowFlowUtil.getInstance().nextId());
                        //7.新增优惠券到数据库
                        if (usercouponDao.insert(coupon) > 0) {

                            //更改数量 index 0:表示List中的第一个数据
                            RedissionUtil.setList(RedisKeyConfig.COUPON_CACHE + ctid, 0, count - 1);
                            //记录当前用户已经领取
                            RedissionUtil.setSet(RedisKeyConfig.COUPON_USERS + ctid, uid + "");
                            //设置 有效期,优惠券模板的剩余时间
                            if (istime) {
                                RedissionUtil.expire(RedisKeyConfig.COUPON_USERS + ctid, RedissionUtil.ttl(RedisKeyConfig.COUPON_CACHE + ctid));
                            }

                            //返回结果
                            return Response.ok();
                        } else {
                            return Response.fail("系统故障,领取失败!");
                        }
                    } else {
                        return Response.fail("亲,优惠券已被领完!");
                    }
                }

            } catch (InterruptedException e) {
                e.printStackTrace();
                return Response.fail("系统故障,领取失败!");
            } finally {
                rLock.unlock();//释放锁
            }
        } else {
            //领取过
            return Response.fail("亲,你已经领取过了!");
        }
    } else {
        return Response.fail("亲,活动已结束!");
    }
    return Response.fail("系统故障,领取失败!");
}

分布式锁:解决集群下,同一资源排队访问

如果我们的项目是集群部署(多态服务器)下,防止线程安全,可以使用分布式锁

推荐使用:Redssion的RedLock

RedLock:Redis Distributed Lock;即使用redis实现的分布式锁

算法实现了多redis实例的情况,相对于单redis节点来说,优点在于防止了最低保证分布式锁的有效性及安全性的要求如下:

1.互斥;任何时刻只能有一个client获取锁

2.释放死锁;即使锁定资源的服务崩溃或者分区,仍然能释放锁

3.容错性;只要多数redis节点(一半以上)在使用,client就可以获取和释放锁单节点故障造成整个服务停止运行的情况;并且在多节点中锁的设计,及多节点同时崩溃等各种意外情况有自己独特的设计方法

七、测试

测试领取优惠券

用户优惠券控制器

// 根据ID查看优惠券详情
@GetMapping("detail")
public Response<UserCouponDto> detail(@RequestParam int id){
    return service.queryId(id);
}
// 用户领取优惠券
@PostMapping("save")
public Response save(@RequestParam int ul, 
                     @RequestParam int ctid,
                     HttpServletRequest request){
    return service.save(request.getIntHeader(SystemConfig.HEADER_UID),ul,ctid));
}
代码地址

代码仓库open in new window

上次编辑于:
贡献者: 稀客