Java项目:支持并发的秒杀项目(基于Redis)

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
简介: Java项目:支持并发的秒杀项目(基于Redis)

高并发秒杀系统

分析需求

场景分析

  • 秒杀时大量用户会在同一时间同时进行抢购,网站瞬时访问流量激增。
  • 秒杀一般是访问请求数量远远大于库存数量,只有少部分用户能够秒杀成功。
  • 秒杀业务流程比较简单,一般就是下订单减库存。

问题分析

秒杀系统一般要注意的问题就是 :

  1. 库存少卖,超卖问题(原子性)
  2. 流量削峰,这里我们设定的时候每个用户只能秒杀一次所以比较好处理

执行流程

  • 初始化数据,提前预热要秒杀的商品(项目里设置为启动,如果秒杀列表有就预热)
  • 使用 redis 缓存秒杀的商品信息,使用redis来承担秒杀的压力最后生产秒杀到的用户,再到mysql生成订单
  • 在秒杀时使用(事务,分布式锁两种方式都实现)对商品库存,保证原子性

设计思路图

秒杀系统

技术选型

开发语言 : Java 8 或 Java 11

框架技术: SpringBoot2.x ,Mybatis-plus ,Thymeleaf

中间件: Redis

数据库:MySQL 8.0

数据源: druid 1.16

测试工具: apache jmeter

数据库表设计

三张表,分别是

  1. 商品表:  id 商品id  商品name  商品图片  商品类别  商品价格  库存
  2. 秒杀商品表 : id 商品id  秒杀开始时间  秒杀结束时间  秒杀价  可秒杀的数量
  3. 订单表  id 订单id 商品id  秒杀价格   用户id  地址 电话

sql表

CREATE DATABASE /*!32312 IF NOT EXISTS*/`seckill` /*!40100 DEFAULT CHARACTER SET utf8 */ 
USE `seckill`;
DROP TABLE IF EXISTS `goods`;
CREATE TABLE `goods` (
  `id` int NOT NULL AUTO_INCREMENT,
  `goods_id` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '商品id',
  `goods_name` varchar(100) DEFAULT NULL COMMENT '商品名字',
  `goodtype` varchar(100) DEFAULT NULL COMMENT '商品类别',
  `price` double DEFAULT NULL COMMENT '商品价格',
  `img_path` varchar(200) DEFAULT NULL COMMENT '商品图片地址',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb3;
/*Data for the table `goods` */
insert  into `goods`(`id`,`goods_id`,`goods_name`,`goodtype`,`price`,`img_path`) values 
(5,'1001','华为手机','电子用品',5000,'/img/huaweiphone.jpg');
/*Table structure for table `goodsorder` */
DROP TABLE IF EXISTS `goodsorder`;
CREATE TABLE `goodsorder` (
  `id` int NOT NULL AUTO_INCREMENT,
  `order_id` varchar(100) DEFAULT NULL,
  `user_id` varchar(100) DEFAULT NULL,
  `goods_id` varchar(100) DEFAULT NULL,
  `telephone` varchar(20) DEFAULT NULL,
  `address` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb3;
/*Data for the table `goodsorder` */
/*Table structure for table `seckillgoods` */
DROP TABLE IF EXISTS `seckillgoods`;
CREATE TABLE `seckillgoods` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `goods_id` varchar(100) DEFAULT NULL,
  `seckill_price` double DEFAULT NULL COMMENT '秒杀价格',
  `stock_num` int DEFAULT NULL COMMENT '秒杀库存',
  `start_time` datetime DEFAULT NULL COMMENT '开始时间',
  `end_time` datetime DEFAULT NULL COMMENT '结束时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3;
/*Data for the table `seckillgoods` */
insert  into `seckillgoods`(`id`,`goods_id`,`seckill_price`,`stock_num`,`start_time`,`end_time`) values 
(1,'1001',3599,10,'2022-03-07 00:00:00','2022-03-09 00:00:00');

项目配置文件

application.yml

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/seckill?    userSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
    driver-class-name: com.mysql.cj.jdbc.Driver
  redis:
    database: 0
    host: 120.79.14.203
    port: 6379
    password: root
    timeout: 1000
  thymeleaf:
    cache: false
    prefix: classpath:/templates/
    suffix: .html
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: false
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  #  configuration:
  #    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath:mapper/*

redis 配置类

@Configuration
public class RedisConfig {
    /**
     * @author 冷环渊 Doomwatcher
     * @context:redis 存储数据 序列化方式
     * @date: 2022/3/7 21:19
     * @param redisConnectionFactory
     * @return: org.springframework.data.redis.core.RedisTemplate<java.lang.String, java.lang.Object>
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory
                                                               redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //声明 key 和 value 的序列化方式
        StringRedisSerializer keySerializer = new StringRedisSerializer();
        GenericFastJsonRedisSerializer valueSerializer = new GenericFastJsonRedisSerializer();
        redisTemplate.setKeySerializer(keySerializer);
        redisTemplate.setValueSerializer(valueSerializer);
        redisTemplate.setHashKeySerializer(keySerializer);
        redisTemplate.setValueSerializer(valueSerializer);
        //多种的序列化方式 最好是谁序列化的谁处理
        //GenericFastJsonRedisSerializer
        //Jackson2JsonRedisSerializer
        return redisTemplate;
    }
}

redis工具类

package com.hyc.seckillsystem.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
 * @projectName: seckillsystem
 * @package: com.hyc.seckillsystem.util
 * @className: RedisUtil
 * @author: 冷环渊 doomwatcher
 * @description: TODO
 * @date: 2022/3/7 21:25
 * @version: 1.0
 */
@Component
public class RedisUtil {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    //    设置缓存
    public void set(String key, Object value) {
        redisTemplate.opsForValue().set(key, value);
    }
//设置缓存和超时时间
    public void set(String key, Object value, long timeout) {
        redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
    }
    //获取命中key 的内容
    public Object get(String key) {
        if (!redisTemplate.hasKey(key)) {
            return null;
        }
        return redisTemplate.opsForValue().get(key);
    }
    //递减操作
    public void decr(String key) {
        redisTemplate.opsForValue().decrement(key);
    }
    //触发 提交 参数里的sessionCallback 队列里的命令
    public Object execute(SessionCallback sessionCallback) {
        return redisTemplate.execute(sessionCallback);
    }
    // 使用脚本语言
    public Object execute(RedisScript script, List keylist) {
        return redisTemplate.execute(script, keylist);
    }
// 是否存在 key
    public boolean hasKey(String key) {
        return redisTemplate.hasKey(key);
    }
}

商品列表

商品列表界面,是比较简单的,设计思路还是基于传统的三层开发

Controller -> servier -> dao这样的分层开发模式

实体类

这里为了方面展示,就展示实体类的属性,小伙伴们自行补充getter/setter以及构造方法等

public class goods {
    @Id
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String goods_id;
    private String goods_name;
    private String goodtype;
    private double price;
    private String img_path;
    @TableField(exist = false)
    private SeckillGoods seckillgoods;
}

DAO

虽然使用了 mybatis-plus 但是我们还是根据自己的查询需求新增了自己的方法,

@Repository
public interface GoodsMapper extends BaseMapper<goods> {
    goods getGoodsByGoodsId(String goodId);
    List<goods> selectGoods();
    List<goods> getGoodList();
}

映射文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hyc.seckillsystem.dao.GoodsMapper">
    <select id="getGoodsByGoodsId" resultType="com.hyc.seckillsystem.pojo.goods">
        select seckill.goods.goods_id,
               seckill.goods.goods_name,
               seckill.goods.goodtype,
               seckill.goods.price,
               seckill.goods.img_path
        from seckill.goods
        where goods_id = #{goodId};
    </select>
    <select id="getGoodList" resultType="com.hyc.seckillsystem.pojo.goods">
        select seckill.goods.goods_id,
               seckill.goods.goods_name,
               seckill.goods.goodtype,
               seckill.goods.price,
               seckill.goods.img_path
        from seckill.goods
    </select>
    <resultMap id="goodsResult" type="com.hyc.seckillsystem.pojo.goods">
        <id property="id" column="id"/>
        <result property="goods_id" column="goods_id"/>
        <result property="goods_name" column="goods_name"/>
        <result property="goodtype" column="goods_type"/>
        <result property="price" column="price"/>
        <result property="img_path" column="img_path"/>
        <association property="seckillgoods" javaType="com.hyc.seckillsystem.pojo.SeckillGoods">
            <result property="seckill_price" column="seckill_price"/>
            <result property="stock_num" column="stock_num"/>
        </association>
    </resultMap>
    <select id="selectGoods" resultMap="goodsResult">
        SELECT a.`goods_id`,
               a.`goods_name`,
               a.`goodtype`,
               a.`price`,
               a.`img_path`,
               b.`seckill_price`,
               b.`stock_num`
        FROM seckill.goods AS a
                 LEFT JOIN seckill.seckillgoods AS b
                           ON a.`goods_id` = b.`goods_id`
    </select>
</mapper>

service 实现类

两种查找商品数据的方法

  1. getlist:逐个表 匹配查询
  2. selectGoods链表查询

返回商品详情(包括秒杀价格等)VO getGoodsDetail(String goodId)

service 层的设计思路就是 调用DAO层接口 实现对数据库中取出数据的处理,并且提供给controller封装好的接口

@Service
public class GoodsServiceImpl implements GoodsService {
    @Autowired
    GoodsMapper goodsMapper;
    @Autowired
    SeckillGoodsMapper seckillGoodsMapper;
    /**
     * @author 冷环渊 Doomwatcher
     * @context:逐个表 匹配查询
     * @date: 2022/3/7 16:20
     * @param
     * @return: java.util.List<com.hyc.seckillsystem.vo.GoodVo>
     */
    @Override
    public List<GoodVo> getlist() {
        /* 两种方式
         * 每个表单独查询
         * 从第一个表查多个数据,再根据数据去第二个表中查询
         * */
        List<goods> goods = goodsMapper.getGoodList();
        ArrayList<GoodVo> result = new ArrayList<>();
        for (goods good : goods) {
            Map<String, Object> map = new HashMap();
            map.put("goods_id", good.getGoods_id());
            SeckillGoods seckillGoods = seckillGoodsMapper.getSeckillByGoodsId(good.getGoods_id());
            GoodVo vo = new GoodVo();
            vo.setGoodsId(good.getGoods_id());
            vo.setGoodsName(good.getGoods_name());
            vo.setGoodType(good.getGoodtype());
            vo.setPrice(good.getPrice());
            vo.setImgPath(good.getImg_path());
            vo.setSeckillPrice(seckillGoods.getSeckill_price());
            vo.setStockNum(seckillGoods.getStock_num());
            result.add(vo);
        }
        return result;
    }
    @Override
    //返回秒杀商品详情页面
    public GoodsDetailVo getGoodsDetail(String goodId) {
        SeckillGoods seckillGoods = seckillGoodsMapper.getSeckillByGoodsId(goodId);
        goods good = goodsMapper.getGoodsByGoodsId(goodId);
        //商品表信息1
        GoodsDetailVo goodsDetailVo = new GoodsDetailVo();
        goodsDetailVo.setGoodsId(good.getGoods_id());
        goodsDetailVo.setGoodsName(good.getGoods_name());
        goodsDetailVo.setGoodType(good.getGoodtype());
        goodsDetailVo.setPrice(good.getPrice());
        goodsDetailVo.setImgPath(good.getImg_path());
        //秒杀表信息
        goodsDetailVo.setSeckillPrice(seckillGoods.getSeckill_price());
        goodsDetailVo.setStockNum(seckillGoods.getStock_num());
        goodsDetailVo.setStartTime(seckillGoods.getStart_time());
        goodsDetailVo.setEndTime(seckillGoods.getEnd_time());
        return goodsDetailVo;
    }
    /**
     * @author 冷环渊 Doomwatcher
     * @context: 联表查询
     * @date: 2022/3/7 16:20
     * @param
     * @return: com.hyc.seckillsystem.vo.GoodVo
     */
    @Override
    public List<GoodVo> selectGoods() {
        List<goods> goods = goodsMapper.selectGoods();
        ArrayList<GoodVo> result = new ArrayList<>();
        for (goods good : goods) {
            GoodVo vo = new GoodVo();
            vo.setGoodsId(good.getGoods_id());
            vo.setGoodsName(good.getGoods_name());
            vo.setGoodType(good.getGoodtype());
            vo.setPrice(good.getPrice());
            vo.setImgPath(good.getImg_path());
            vo.setSeckillPrice(good.getSeckillGoods().getSeckill_price());
            vo.setStockNum(good.getSeckillGoods().getStock_num());
            result.add(vo);
        }
        return result;
    }
}

Controller

Controller层,负责控制接口数据和渲染界面的值传递 , 跳转 , 这里基本上不包含业务代码

尽可能的再service层 封装好方法 让Controller只负责调用

@Controller
@Slf4j
public class GoodsController {
    @Autowired
    GoodsService goodsService;
    @GetMapping(value = "/index")
    public String list(Model model) {
        //    逐层调用
        List<GoodVo> getlist = goodsService.getlist();
        model.addAttribute("goodslist", getlist);
        return "list";
    }
    @GetMapping(value = "/goodDetail/{goodsId}")
    public String getGoodDetail(@PathVariable String goodsId, Model model) {
        GoodsDetailVo goodsDetail = goodsService.getGoodsDetail(goodsId);
        model.addAttribute("GoodsDetail", goodsDetail);
        //获取到时间
        Date startTime = goodsDetail.getStartTime();
        Date endTime = goodsDetail.getEndTime();
        Date nowtime = new Date();
        /*设置状态值
         * 0 未开始
         * 1 正在秒杀
         * 2 秒杀结束
         * */
        int status;
        int remainSeconds = -1;
        if (nowtime.before(startTime)) {
            //    秒杀未开始
            status = 0;
            remainSeconds = (int) ((startTime.getTime() - nowtime.getTime()) / 1000);
        } else if (nowtime.after(endTime)) {
            //秒杀已经结束
            status = 2;
        } else {
            //正在秒杀中
            status = 1;
        }
        model.addAttribute("status", status);
        model.addAttribute("remainSeconds", remainSeconds);
        return "Detail";
    }
}

秒杀商品

这里才是我们的重头戏这里我们主要讲解使用思路,不过多的去展示无用代码如实体类等,我们这里从最开始的

  1. 直接处理
  2. redis 事务处理
  3. 分布式锁 Lua处理

三种方式 由浅至深的来理解秒杀的思路和超卖问题的解决

直接处理

  • 判断用户id 的有效性 我们没有用户
  • 判断goodsid的有效性
  • 判断当前是否处于可以秒杀的状态
  • 判断是否有剩余库存
  • 判断用户的秒杀权限(是否秒杀过)
  • 减少库存
  • 生成新的订单
public String handle(String userId, String goodsId) {
    String stockkey = goodsId + "_count";
    String startTimekey = goodsId + "_startTime";
    String userkey = goodsId + "_" + userId;
    String dateStr = (String) redisUtil.get(startTimekey);
    SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    Date startTime = null;
    try {
        startTime = format.parse(dateStr);
    } catch (ParseException e) {
        e.printStackTrace();
    }
    if (startTime == null || new Date().before(startTime)) {
        return "秒杀还没开始";
    }
    int stockNum = (int) redisUtil.get(stockkey);
    if (stockNum <= 0) {
        return "秒杀一空";
    }
    if (redisUtil.get(userkey) != null) {
        return "用户秒杀成功过了";
    }
    //减少库存
    redisUtil.decr(stockkey);
    //限额 1 可以这么处理
    //如果限额 多个 可以获取value 自增
    redisUtil.set(goodsId + "_" + userId, 1);
    return userId + "用户秒杀成功";
}

测试

这个时候启动 jmeter 来测试一下接口的结果

  • 库存 10
  • 一百个线程 抢这 10个 手机

查看 redis 中 库存 key 的数量 为 -4

再次测试

通过测试和查看日志可以看到,我们直接处理,系统不行不能在第一时间反应过来是否超过了库存,导致特价商品被超额卖出,那这个问题怎么解决呢?

事务处理

优秀成熟的数据库 一定会有对事务的支持, redis 也不例外

Redis的事务在不出现异常的时候是原子操作,exec是触发事务执行的命令

相关命令:

  • watch  设置一个key 表示开始对这个key的命令监听
  • multi   将 后续的命令 加入到执行队列里
  • exec  出发执行队列里的命令
  • discard 停止提交,redis不能回滚

使用事务

public String handleByTrans(String userId, String goodsId) {
        String stockkey = goodsId + "_count";
        String startTimekey = goodsId + "_startTime";
        String userkey = goodsId + "_" + userId;
        String dateStr = (String) redisUtil.get(startTimekey);
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date startTime = null;
        try {
            startTime = format.parse(dateStr);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        if (startTime == null || new Date().before(startTime)) {
            return "秒杀还没开始";
        }
        if (redisUtil.get(userkey) != null) {
            return "用户秒杀成功过了";
        }
        SessionCallback sessionCallback = new SessionCallback() {
            @Override
            public Object execute(RedisOperations redisOperations) throws DataAccessException {
                // 编写事务逻辑
                // 调用 watch 开始事务, mutil 开始将之后的命令加入队列 exec 执行队列里的命令
                redisOperations.watch(stockkey);
                int stockNum = (int) redisUtil.get(stockkey);
                if (stockNum <= 0) {
                    return "秒杀一空";
                }
                redisOperations.multi();
                redisUtil.decr(stockkey);
                redisUtil.set(userkey, 1);
                return redisOperations.exec();
            }
        };
        redisUtil.execute(sessionCallback);
        if (redisUtil.hasKey(userkey)) {
            return userId;
        }
        return userId + "秒杀失败";
    }

测试

和之前一样的案例 100 抢 10

可以发现使用了 事务之后我们解决了超卖的问题

举一反三

那除了事务 还有什么方式可以解决超卖问题呢?

脚本语言 : lua

redis是支持执行脚本语言的且一段脚本执行的时候是不会被其他操作影响的,保证原子性

编写lua 脚本

--接受到参数1
local userId = KEYS[1];
--接收到参数2
local goodsId = KEYS[2];
--redis中的 key 存储为 局部变量
--秒杀商品数量
--.. 是lua 里面字符串的拼接
local cntkey = goodsId .. "_count";
--秒杀用户 id
local orderkey = goodsId .. "_" .. userId;
--判断是否秒杀过
local orderExists = redis.call("get", orderkey);
if (orderExists and tonumber(orderExists) == 1) then
    return 2;
end
--判断库存是否为空
local num = redis.call("get", cntkey);
if (num and tonumber(num) <= 0) then
    return 0;
else
    --    秒杀成功
    redis.call("decr", cntkey);
    redis.call("set", orderkey, 1);
end
return 1

实例代码

我们可以使用 DefaultRedisScript 类 来执行我们写在 resouce 下的lua脚本

script.setScriptSource(new ResourceScriptSource( new ClassPathResource("lua/sekill.lua")));

设置脚本的位置,之后用之前工具类封装的redis方法将脚本和需要的参数list 传入执行即可

//使用lua 脚本 保证原子性 (分布式锁)
    @Override
    public String handleByLua(String userId, String goodsId) {
        String startTimekey = goodsId + "_startTime";
        String dateStr = (String) redisUtil.get(startTimekey);
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date startTime = null;
        try {
            startTime = format.parse(dateStr);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        if (startTime == null || new Date().before(startTime)) {
            return "秒杀还没开始";
        }
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        //设置返回脚本类型
        script.setResultType(Long.class);
        script.setScriptSource(new ResourceScriptSource(
                new ClassPathResource("lua/sekill.lua")));
        List<String> keylist = new ArrayList<>();
        keylist.add(userId);
        keylist.add(goodsId);
        Object execute = redisUtil.execute(script, keylist);
        String resultStr = String.valueOf(execute);
        if ("0".equals(resultStr)) {
            return "秒杀库存已经空了";
        } else if ("1".equals(resultStr)) {
            return userId;
        } else if ("2".equals(resultStr)) {
            return "该用户秒杀过了 =>" + userId;
        } else {
            return "秒杀和抢购可能出了点问题,正在紧急维护";
        }
    }

测试

同样的 100 -》 10 ,从结果来看,也是保证了不超卖,不少卖的

解决了秒杀的问题,剩下就是将秒杀之后的成功用户信息和商品信息传给订单模块即可

Controller

@Controller
@Slf4j
public class redisController {
    @Autowired
    redisService redisService;
    @Autowired
    RedisUtil redisUtil;
    // 为了 方面测试 每次重启项目都会重置秒杀次数
    @PostConstruct
    public void initdata() {
        redisService.initdata();
    }
    @PostMapping(value = "/seckillAPI")
    public String seckill(String userId, String goodsId, Model model) {
        String key = goodsId + "_" + userId;
        if (userId == null || goodsId == null) {
            return "参数异常";
        }
        String result = redisService.seckill(userId, goodsId);
        if (redisUtil.hasKey(key)) {
            log.info(result);
            model.addAttribute("userId", result);
            model.addAttribute("goodsId", goodsId);
        }
        return "order";
    }
}

生成订单

解决了秒杀的问题,生成订单其实就简单和常规的不少

就是查询 必要的信息生成一个订单信息

service 实现类

@Service
public class orderServiceImpl implements orderService {
    @Autowired
    GoodsMapper goodsMapper;
    @Autowired
    SeckillGoodsMapper seckillGoodsMapper;
    @Autowired
    orderMapper ordermapper;
    @Override
    public orderVo createOrder(order order) {
        int insert = ordermapper.insert(order);
        if (insert != 0) {
            List<order> orders = ordermapper.selectList(null);
            for (order o : orders) {
                goods goods = goodsMapper.getGoodsByGoodsId(o.getGoods_id());
                SeckillGoods seckillGoods = seckillGoodsMapper.getSeckillByGoodsId(o.getGoods_id());
                orderVo orderVo = new orderVo();
                orderVo.setOrderId(String.valueOf(o.getId()));
                orderVo.setUserId(o.getUser_id());
                orderVo.setGoodsId(o.getGoods_id());
                orderVo.setTelephone(o.getTelephone());
                orderVo.setAddress(o.getAddress());
                orderVo.setImgPath(goods.getImg_path());
                orderVo.setSeckillPrice(seckillGoods.getSeckill_price());
                return orderVo;
            }
        }
        return null;
    }
}

Controller

等待用户 确认信息之后 就可以生成订单 同步到数据库了

@Controller
public class orderController {
    @Autowired
    orderService orderService;
    @PostMapping(value = "/create")
    public String createOrder(order order, Model model) {
        orderVo order1 = orderService.createOrder(order);
        model.addAttribute("orderinfo", order1);
        return "success";
    }
}

流程展示

总结

设计一个秒杀项目 其实要考虑的东西十分的多,我们这次的系统也不是最终的版本,先做出来的核心的,

套用鱼皮的话 先有 再调优 追求更好

拓展

  • 页面动静分离
  • nginx ip 分流
  • MQ 流量削峰,异步任务
  • 前端验证码
  • 数据库与缓存同步策略(MQ redis 都可以实现)

上述都是我学过的知识点暂时并且实战过,会在今后不断的将优化功能实现出来

这些都是我在考虑的范围内,今后会不断的学习 晚上其中的内容,

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
29天前
|
NoSQL 安全 测试技术
Redis游戏积分排行榜项目中通义灵码的应用实战
Redis游戏积分排行榜项目中通义灵码的应用实战
52 4
|
12天前
|
NoSQL Java 关系型数据库
Liunx部署java项目Tomcat、Redis、Mysql教程
本文详细介绍了如何在 Linux 服务器上安装和配置 Tomcat、MySQL 和 Redis,并部署 Java 项目。通过这些步骤,您可以搭建一个高效稳定的 Java 应用运行环境。希望本文能为您在实际操作中提供有价值的参考。
73 26
|
24天前
|
XML Java 测试技术
从零开始学 Maven:简化 Java 项目的构建与管理
Maven 是一个由 Apache 软件基金会开发的项目管理和构建自动化工具。它主要用在 Java 项目中,但也可以用于其他类型的项目。
36 1
从零开始学 Maven:简化 Java 项目的构建与管理
|
1月前
|
缓存 NoSQL 关系型数据库
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
本文详解缓存雪崩、缓存穿透、缓存并发及缓存预热等问题,提供高可用解决方案,帮助你在大厂面试和实际工作中应对这些常见并发场景。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
|
23天前
|
Java
Java项目中高精度数值计算:为何BigDecimal优于Double
在Java项目开发中,涉及金额计算、面积计算等高精度数值操作时,应选择 `BigDecimal` 而非 `Double`。`BigDecimal` 提供任意精度的小数运算、多种舍入模式和良好的可读性,确保计算结果的准确性和可靠性。例如,在金额计算中,`BigDecimal` 可以精确到小数点后两位,而 `Double` 可能因精度问题导致结果不准确。
|
1月前
|
Java Android开发
Eclipse 创建 Java 项目
Eclipse 创建 Java 项目
40 4
|
弹性计算 数据可视化 关系型数据库
阿里云服务器部署Java Web项目和连接MySQL数据库全流程
阿里云服务器部署Java Web项目和连接MySQL数据库全流程
6037 0
阿里云服务器部署Java Web项目和连接MySQL数据库全流程
|
4月前
|
Java 应用服务中间件 Windows
【应用服务 App Service】App Service 中部署Java项目,查看Tomcat配置及上传自定义版本
【应用服务 App Service】App Service 中部署Java项目,查看Tomcat配置及上传自定义版本
|
7月前
|
存储 Java Maven
使用 nohup 部署和后台运行Java 项目
使用 nohup 部署和后台运行Java 项目
437 1
使用 nohup 部署和后台运行Java 项目
|
4月前
|
Java 开发工具 git
【Azure 应用服务】本地Git部署Java项目到App Server,访问无效的原因
【Azure 应用服务】本地Git部署Java项目到App Server,访问无效的原因
下一篇
DataWorks