热点活动-秒杀功能设计

news2024/11/14 20:22:46

一、需求描述

秒杀活动是电子商务兴起后出现的一种新型的购物方式,通过网上APP、小程序等平台推出一些低于市场价格的商品,提升购买率的营销活动,所有买家在同一时间网上抢购的一种销售方式。对比其他的营销活动,秒杀限时性更强,抢购氛围更浓,可营造出一种不是所有人都能抢到的刺激感。 

秒杀与限时促销功能的区别在于应用的场景不同,限时促销往往是被商家作为一种日常的促销(如我们商城版本的买两年送两年),而秒杀倾向于作为一种阶段性(限时感更强)的营销。同时,两种功能的营销效果不同,秒杀的作用是拉新促活,不是所有人都能抢到低价的商品,刺激客户及时下单购买,而限时促销呢,商家一般会提供充足的库存,基本上都能抢到,注重的是打造促销专场,吸引用户点击进入。

二、使用角色说明

运营人员:创建秒杀商品,创建秒杀活动,创建活动参与人数。

用户:抢购下单

三、关键功能点

1、秒杀活动商品库存数量set进入redis。

2、用户下单,使用redis redLock锁,判断商品库存是否还有。

3、如果库存还剩,为用户生成秒杀订单。

4、如果库存已购完,则抛异常,并关闭秒杀功能。

四、数据库设计

easy_product_seckill,秒杀活动表。记录开始时间,结束时间,秒杀商品,秒杀商品库存。

CREATE TABLE `easy_product_seckill` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '秒杀产品表id',
  `product_id` bigint(20) unsigned NOT NULL COMMENT '商品id',
  `image` varchar(255) NOT NULL COMMENT '推荐图',
  `images` varchar(2000) NOT NULL COMMENT '轮播图',
  `title` varchar(255) NOT NULL COMMENT '活动标题',
  `info` varchar(255) NOT NULL COMMENT '简介',
  `price` decimal(10,2) unsigned DEFAULT NULL COMMENT '价格',
  `ot_price` decimal(10,2) unsigned DEFAULT NULL COMMENT '原价',
  `give_integral` decimal(10,2) unsigned DEFAULT NULL COMMENT '返多少券积分',
  `sort` int(10) unsigned NOT NULL COMMENT '排序',
  `stock` int(10) unsigned NOT NULL COMMENT '库存',
  `sales` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '销量',
  `unit_name` varchar(16) NOT NULL COMMENT '单位名',
  `description` text COMMENT '内容',
  `start_time` date NOT NULL COMMENT '开始时间',
  `stop_time` date NOT NULL COMMENT '结束时间',
  `create_time` datetime NOT NULL COMMENT '添加时间',
  `create_user_id` bigint(20) unsigned NULL COMMENT '添加人id',
  `update_time` datetime DEFAULT NULL,
  `update_user_id` bigint(20) unsigned NULL COMMENT '更新人id',
  `status` tinyint(1) unsigned NOT NULL COMMENT '产品状态',
  `is_del` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '删除 0未删除1已删除',
  `num` int(11) unsigned NOT NULL COMMENT '最多秒杀几个',
  `is_show` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '显示',
  `time_id` int(10) unsigned DEFAULT '0' COMMENT '时间段id',
  `spec_type` tinyint(1) DEFAULT NULL COMMENT '规格 0单 1多',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `product_id` (`product_id`) USING BTREE,
  KEY `start_time` (`start_time`,`stop_time`) USING BTREE,
  KEY `is_del` (`is_del`) USING BTREE,
  KEY `is_show` (`status`) USING BTREE,
  KEY `add_time` (`create_time`) USING BTREE,
  KEY `sort` (`sort`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='商品秒杀产品表';

五、核心代码

5.1 引入redis依赖

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

5.2 配置redis

spring:
 redis:
 host: localhost
 port: 6379

5.3 编写加锁和解锁的方法

package com.xx.xx.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
/**
 * Created by on 2018/4/5.
 */
@Component
public class RedisLock {
    Logger logger = LoggerFactory.getLogger(this.getClass());
    @Autowired
    private StringRedisTemplate redisTemplate;
    /**
     * 加锁
     * @param key 商品id
     * @param value 当前时间+超时时间
     * @return
     */
    public boolean lock(String key, String value) {
        //这个其实就是setnx命令,只不过在java这边稍有变化,返回的是boolea
        if (redisTemplate.opsForValue().setIfAbsent(key, value)) { 
            return true;
        }
        //避免死锁,且只让一个线程拿到锁
        String currentValue = redisTemplate.opsForValue().get(key);
        //如果锁过期了
        if (!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()) {
            //获取上一个锁的时间
            String oldValues = redisTemplate.opsForValue().getAndSet(key, value);
             /*
             只会让一个线程拿到锁
             如果旧的value和currentValue相等,只会有一个线程达成条件,因为第二个线程拿到的oldValue已经和currentValue不一样了
             */
            if (!StringUtils.isEmpty(oldValues) && oldValues.equals(currentValue)) {
                return true;
            }
        }
        return false;
    }
    /**
     * 解锁
     * @param key
     * @param value
     */
    public void unlock(String key, String value) {
        try {
            String currentValue = redisTemplate.opsForValue().get(key);
            if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)) {
                redisTemplate.opsForValue().getOperations().delete(key);
            }
        } catch (Exception e) {
            logger.error("『redis分布式锁』解锁异常,{}", e);
        }
    }
}

为什么要有避免死锁的一步呢?

假设没有『避免死锁』这一步,结果在执行到下单代码的时候出了问题,毕竟操作数据库、网络、io的时候抛了个异常,这个异常是偶然抛出来的,就那么偶尔一次,那么会导致解锁步骤不去执行,这时候就没有解锁,后面的请求进来自然也或得不到锁,这就被称之为死锁。

而这里的『避免死锁』,就是给锁加了一个过期时间,如果锁超时了,就返回true,解开之前的那个死锁。

5.4 下单代码中引入加锁和解锁,确保只有一个线程操作 

@Autowired
private RedisLock redisLock;

@Override
@Transactional
public String seckill(Integer id)throws RuntimeException {
 //加锁
 long time = System.currentTimeMillis() + 1000*10; //超时时间:10秒,最好设为常量
 boolean isLock = redisLock.lock(String.valueOf(id), String.valueOf(time));
 if(!isLock){
     throw new RuntimeException("人太多了,换个时间再试试~");
 }
 //查秒杀库存 
 Product product = productMapper.findById(id);
 if(storeSckill.getStock()==0){
     throw new RuntimeException("已经卖光");
 }
 //写入订单表
 Order order=new Order();
 order.setProductId(product.getId());
 order.setProductName(product.getName());
 orderMapper.add(order);
 //减库存
 product.setPrice(null);
 product.setName(null);
 product.setStock(product.getStock()-1);
 productMapper.update(product);
 //解锁
 redisLock.unlock(String.valueOf(id),String.valueOf(time));
 return findProductInfo(id);
}

封装-----

    @Autowired
    private RedisTemplate redisTemplate;
    /**
     * @Title: lock
     * @Description: 加锁机制
     * @param @param lock 锁的名称
     * @param @param expire 锁占有的时长(毫秒)
     * @param @return 设定文件
     * @return Boolean 返回类型
     * @throws
     */
    @SuppressWarnings("unchecked")
    public Boolean lock(final String lock, final int expire) {
        return (Boolean) redisTemplate.execute(new RedisCallback<Boolean>() {
            @Override
            public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                boolean locked = false;
                byte[] lockValue = redisTemplate.getValueSerializer().serialize(DateUtil.getDateAddMillSecond(null, expire));
                byte[] lockName = redisTemplate.getStringSerializer().serialize(lock);
                locked = connection.setNX(lockName, lockValue);
                if (locked) {
                    connection.expire(lockName, TimeoutUtils.toSeconds(expire, TimeUnit.MILLISECONDS));
                }
                return locked;
            }
        });
    }

    /**
     * @Title: unDieLock
     * @Description: 处理发生的死锁
     * @param @param lock 是锁的名称
     * @param @return 设定文件
     * @return Boolean 返回类型
     * @throws
     */
    @SuppressWarnings("unchecked")
    public Boolean unDieLock(final String lock) {
        boolean unLock = false;
        Date lockValue = (Date) redisTemplate.opsForValue().get(lock);
        if (lockValue != null && lockValue.getTime() <= (System.currentTimeMillis())) {
            redisTemplate.delete(lock);
            unLock = true;
        }
        return unLock;
    }

DateUtil类的方法

 /**
     * 日期相减(返回秒值)
     * @param date Date
     * @param date1 Date
     * @return int
     * @author
     */
    public static Long diffDateTime(Date date, Date date1) {
        return (Long) ((getMillis(date) - getMillis(date1))/1000);
    }

    public static long getMillis(Date date) {
        Calendar c = Calendar.getInstance();
        c.setTime(date);
        return c.getTimeInMillis();
    }

    /**
     * 获取 指定日期 后 指定毫秒后的 Date
     * @param date
     * @param millSecond
     * @return
     */
    public static Date getDateAddMillSecond(Date date, int millSecond) {
        Calendar cal = Calendar.getInstance();
        // 没有 就取当前时间
        if (null != date) {
            cal.setTime(date);
        }
        cal.add(Calendar.MILLISECOND, millSecond);
        return cal.getTime();
    }

新补充

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.TimeoutUtils;
import org.springframework.stereotype.Component;

import java.util.Calendar;
import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * @ClassName: LockRetry
 * @Description: 此功能只用于促销组
 * @date 2017年7月29日 上午11:54:54
 */
@SuppressWarnings("rawtypes")
@Component("lockRetry")
public class LockRetry {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired

    private RedisTemplate redisTemplate;

    /**
     * @param @param  lock 名称
     * @param @param  expire 锁定时长(秒),建议10秒内
     * @param @param  num 取锁重试试数,建议不大于3
     * @param @param  interval 重试时长
     * @param @param  forceLock 强制取锁,不建议;
     * @param @return
     * @param @throws Exception 设定文件
     * @return Boolean 返回类型
     * @throws
     * @Title: retry
     * @Description: 重入锁
     */
    @SuppressWarnings("unchecked")
    public Boolean retryLock(final String lock, final int expire, final int num, final long interval, final boolean forceLock) throws Exception {
        Date lockValue = (Date) redisTemplate.opsForValue().get(lock);
        if (forceLock) {
            RedisUtils.remove(lock);
        }
        if (num <= 0) {
            if (null != lockValue && lockValue.getTime() >= (System.currentTimeMillis())) {
                logger.debug(String.valueOf((lockValue.getTime() - System.currentTimeMillis())));
                Thread.sleep(lockValue.getTime() - System.currentTimeMillis());
                RedisUtils.remove(lock);
                return retryLock(lock, expire, 1, interval, forceLock);
            }
            return false;
        } else {
            return (Boolean) redisTemplate.execute(new RedisCallback<Boolean>() {
                @Override
                public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                    boolean locked = false;
                    byte[] lockValue = redisTemplate.getValueSerializer().serialize(DateUtils.getDateAdd(null, expire, Calendar.SECOND));
                    byte[] lockName = redisTemplate.getStringSerializer().serialize(lock);
                    logger.debug(lockValue.toString());
                    locked = connection.setNX(lockName, lockValue);
                    if (locked) {
                        return connection.expire(lockName, TimeoutUtils.toSeconds(expire, TimeUnit.SECONDS));
                    } else {
                        try {
                            Thread.sleep(interval);
                            return retryLock(lock, expire, num - 1, interval, forceLock);
                        } catch (Exception e) {
                            e.printStackTrace();
                            return locked;
                        }
                    }
                }
            });
        }
    }
}
public class RedisUtils{
    /**
     * @param @param  date
     * @param @param  millSecond
     * @param @return 设定文件
     * @return Date 返回类型
     * @throws
     * @Title: getDateAddMillSecond
     * @Description: (TODO)取将来时间
     */
    public static Date getDateAdd(Date date, int expire, int idate) {
        Calendar calendar = Calendar.getInstance();
        // 默认当前时间
        if (null != date) {
            calendar.setTime(date);
        }
        calendar.add(idate, expire);
        return calendar.getTime();
    }

    /**
     * 删除对应的value
     *
     * @param key
     */
    public static void remove(final String key) {
        if (exists(key)) {
            stringRedisTemplate.delete(key);
        }
    }

    /**
     * 判断缓存中是否有对应的value
     *
     * @param key
     * @return
     */
    public static boolean exists(final String key) {
        return stringRedisTemplate.hasKey(key);
    }

    private static StringRedisTemplate stringRedisTemplate = ((StringRedisTemplate) SpringContextHolder.getBean("stringRedisTemplate"));
}

六、自测&压测 

详见今日头条。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/798908.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

地平线J5芯片部署参考算法(2023.07.27)

本文主要是记录地平线官方提供的可在J5芯片上部署的参考算法。 参考算法数据集FPSPointPillarsKITTI116 (双核)CenterPointNuscenes98.72&#xff08;双核&#xff09;FCOS3DNuscenes589 (双核)GANetCULane2431&#xff08;双核&#xff09;Swin TransformerImageNet133&#…

网络加速技巧

某APP限制网速&#xff0c;可以这么做&#xff1a; &#xff08;1&#xff09;把网络禁用 &#xff08;2&#xff09;在APP的设置里面&#xff0c;把优化速率打开 &#xff08;3&#xff09;启用网络 2023年7月27日亲测有用&#xff0c;开启优化速率之前是100k/s&#xff0c;开…

机器学习---混淆矩阵代码

1. 导包&#xff1a; import pandas as pd from sklearn.preprocessing import LabelEncoder from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline from sklearn.svm import SVC …

共用体类型

共用体&#xff08;union&#xff09;是一种成员共享存储空间的结构体类型。 union 共用体类型名 {成员列表 } 共用体内存长度是所有成员内存长度的最大值。 #include <iostream> using namespace std;int main() {//先声明共用体类型再定义共用体对象 union A {int m,…

11-2_Qt 5.9 C++开发指南_QSqlQueryModel的使用(QSqlQueryModel 只能作为只读数据源使用,不可以编辑数据)

文章目录 1 QSqlQueryModel 功能概述2 使用 QSqlQueryModel 实现数据查询2.1 实例功能2.2 可视化UI设计2.3 主窗口类定义&#xff08;去除自动生成的槽函数&#xff09;2.4 打开数据库2.5 记录移动 1 QSqlQueryModel 功能概述 从下图中可以看到&#xff0c;QSqlQueryModel 是 …

代码随想录算法训练营day13 | 239. 滑动窗口最大值,347. 前 K 个高频元素

239. 滑动窗口最大值 目录 239. 滑动窗口最大值 347. 前 K 个高频元素 239. 滑动窗口最大值 难度&#xff1a;hard 类型&#xff1a;队列&#xff0c;单调队列&#xff0c;滑动窗口 思路&#xff1a; 构造单调队列&#xff0c;维护大小为k的队列。队列里的元素始终是单调递…

无法加载文件 C:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本。npm.ps1 cannot be loaded

目录 原因 解决方法 提示 查看当前的执行策略命令 改回默认值 "Restricted"命令 这个错误提示是因为您的系统禁止执行 PowerShell 脚本。 原因 现用执行策略是 Restricted&#xff08;默认设置&#xff09; 解决方法 以管理员身份运行 PowerShell&#xff1a;右键…

AICodeConvert网站,可以用AI把代码从一种语言转换为另一种语言实现,代码开源了,从 6.24 到现在一个月, 没有主动推广,居然9.8K 访问量

这是我一个之前周六 6.24 开始验证思路的项目&#xff0c;验证的感觉差不多&#xff0c;不做主动推广到现在一个月&#xff0c;访问量 9.8K 。 源码开源了&#xff0c;github.com 网址&#xff1a;AICodeConvert 另一个在佛系验证中的还有这个&#xff1a;Base64.kr&#xf…

gedit更改字体大小颜色、行号、更改各种属性

最近在linux&#xff08;CentOS&#xff09;中运行gedit时发现&#xff1a; 如果用普通用户运行&#xff0c;不会报错&#xff0c;但是不会出现Preferences &#xff08;首选项&#xff09;等选项&#xff0c;不能进行基本属性参数的更改&#xff1b;如果采用su、sudo 运行则会…

机器学习之十大经典算法

机器学习算法是计算机科学和人工智能领域的关键组成部分&#xff0c;它们用于从数据中学习模式并作出预测或做出决策。本文将为大家介绍十大经典机器学习算法&#xff0c;其中包括了线性回归、逻辑回归、支持向量机、朴素贝叶斯、决策树等算法&#xff0c;每种算法都在特定的领…

云原生架构的定义

前言&#xff1a; 从技术的角度&#xff0c;云原生架构是基于云原生技术的一组架构原则和设计模式的集合&#xff0c;旨在将云应用中非业务代码的部分进行最大化的剥离&#xff0c;从而让云设施接管应用中原有的大量非功能特性&#xff08;如弹性、韧性、安全、可观测性、灰度…

MySQL中锁的简介——全局锁

1.锁的概述及分类 2.全局锁的介绍 给数据库加全局锁&#xff1a; flush tables with read lock;数据备份&#xff1a; mysqldump备份指令 root用户名 1234 密码 itcast数据库名称 itcast.sql备份文件名称 mysqldump -uroot -p1234 itcast >itcast.sql;数据库全局锁解锁&am…

复现YOLOv5改进最新MPDIoU:有效和准确的边界盒回归的损失,打败G/E/CIoU,效果明显!!!

MPDIoU: A Loss for Efficient and Accurate Bounding Box Regression 论文简介MPDIoU核心设计思路论文方法实验部分加入YOLOv5代码论文地址:https://arxiv.org/pdf/2307.07662.pdf 论文简介 边界盒回归(Bounding box regression, BBR)广泛应用于目标检测和实例分割,是目标…

word显示书签并给书签添加颜色

CTRg 定位书签 在 Word 的用户界面中&#xff0c;没有直接的选项可以批量为所有书签设置颜色。但你可以使用 VBA 宏或者编写自定义的功能来实现这个需求。这里给出一个简单的 VBA 宏&#xff0c;它可以设置当前文档中所有书签内文本的颜色&#xff1a;vba Sub ColorAllBookmark…

【高级数据结构】线段树

目录 最大数&#xff08;单点修改&#xff0c;区间查询&#xff09; 线段树1&#xff08;区间修改&#xff0c;区间查询&#xff09; 最大数&#xff08;单点修改&#xff0c;区间查询&#xff09; 洛谷&#xff1a;最大数https://www.luogu.com.cn/problem/P1198 题目描述 …

11-3_Qt 5.9 C++开发指南_QSqlQuery的使用(QSqlQuery 是能执行任意 SQL 语句的类)

文章目录 1. QSqlQuery基本用法2. QSqlQueryModel和QSqlQuery联合使用2.1 可视化UI设计框架2.1.1主窗口的可视化UI设计框架2.1.2 对话框的可视化UI设计框架 2.2 数据表显示2.3 编辑记录对话框2.4 编辑记录2.5 插入记录2.6 删除记录2.7 记录遍历2.8 程序框架及源码2.8.1 程序整体…

unity关于匀速移动某些值的方法

可能很多人会用到Verctor3.Lerp、Mathf.LerpUnclamped等等 这种其实不是匀速 看一下这个整体差不多的逻辑 public static float Lerp(float a, float b, float t){return a (b - a) * t;};这个逻辑就是&#xff0c;从a值到b值&#xff0c;返回一个a值加&#xff08;b值-a值&…

msvcp140.dll丢失怎么办?(详细解决方法)

1.msvcp140.dll有什么用&#xff1f; 运行C程序&#xff1a;msvcp140.dll文件包含了许多C程序所需的函数和资源&#xff0c;使得C程序能够在计算机上正确运行。 提供运行时库&#xff1a;msvcp140.dll文件包含了C程序在运行时所需的库文件&#xff0c;如输入/输出操作、内存管…

【梯度下降在波士顿房价预测中的应用】

数据准备 我们首先需要加载波士顿房价数据集。该数据集包含房屋特征信息和对应的房价标签。 import pandas as pd import numpy as npdata_url "http://lib.stat.cmu.edu/datasets/boston" raw_df pd.read_csv(data_url, sep"\s", skiprows22, headerN…

运算放大器基础(二)

5.4.4基于理想运放的放大倍数分析 集成运放两个工作区&#xff1a; 线性区、非线性区 集成运放的理想化参数&#xff1a; 理想运放在线性区的特点 集成运放工作在线性区的电路特征