Skip to content

[TOC]

TIP

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

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

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

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

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

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

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

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

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

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

背景

0.1 优惠劵的目的

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

0.2 优惠劵的意义

image.png

1、提升用户活跃度

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

2、增加产品曝光量

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

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

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

4、提升用户的购买力

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

0.3 优惠劵使用条件的分类

1.体验券

image.png

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

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

image.png

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

3.满减券

image.png

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

4.打折券

image.png

image.png

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

0.4 优惠劵使用范围分类

1.单品券

image.png

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

2.系列产品券

image.png

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

3.品类券

image.png

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

4.品牌券

image.png

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

0.5优惠劵发放主体分类

1、店铺优惠券

image.png

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

2、平台优惠券

image.png

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

3.政企消费券

image.png

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

一、需求

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

image.png

优惠券模板创建

image.png

二、分析

明白需求是什么?

模仿--使用功能

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

优惠券模板功能:

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

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

3.查询优惠券模板接口

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

用户优惠券功能:

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

2.查询我的优惠券接口

3.查看优惠券抵扣接口

4.抵扣优惠券接口

三、设计

3.1 数据库脚本

sql
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

java
//优惠券模板对象
//和数据库中的表一一对应
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;

}
java
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

java
// 优惠券模板对象
@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;
}
java
// 用户优惠券对象
@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

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

3.3 优惠券模板状态

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

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

    private int code;

    public int getCode() {
        return code;
    }
}

3.4 系统字典

系统配置常量

java
// 系统字典
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消息配置常量

java
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的常量

java
// 记录所有的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消息对象

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

3.6 雪花算法的工具类

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

java
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

可以将雪花算法作为一个单独的服务进行部署,然后需要全局唯一 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 单例模式的线程池对象

java
/**
 * @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 时间日期的工具类

java
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

3.查询优惠券活动接口

优惠券模板控制器接口

java
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);
    }
}

用户优惠券接口

java
@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层代码省略. 可以代码生成或手写

java
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新增依赖

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

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

yaml
spring:
  rabbitmq:
    addresses: 192.168.29.110:5672

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

java
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的消费端真正发放优惠券,消费端异步发送给用户优惠券

java
// 审核优惠券,审核通过后,可以直接发送优惠券,也可以使用异步发送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

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

java
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()

java
/*
 * 使用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异步实现用户级别优惠券派发:用户级别优惠券放入缓存

java
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("用户优惠券发放成功!");
        }
    }
}

总结:

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

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

五、测试

测试用例:

新增优惠券模板:

json
{
  "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

json
{
  "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

六、领取

领取优惠券

单机锁:synchronized和Lock

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

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

修饰静态方法--Class对象

修饰实例方法--this

修饰代码块--对象变量

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

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

非公平锁:抢占、竞争

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

java
//领取优惠券 ?难点-超领
@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就可以获取和释放锁单节点故障造成整个服务停止运行的情况;并且在多节点中锁的设计,及多节点同时崩溃等各种意外情况有自己独特的设计方法

七、测试

测试领取优惠券

用户优惠券控制器

java
// 根据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));
}
代码地址

代码仓库