【秒杀系统】秒杀系统和拓展优化(2)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 【秒杀系统】秒杀系统和拓展优化(2)

秒杀商品

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


直接处理

redis 事务处理

分布式锁 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个 手机

2.png


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

2.png



再次测试

3.png

4.png




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


事务处理

优秀成熟的数据库 一定会有对事务的支持, 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

2.png

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


举一反三

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


脚本语言 : 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 ,从结果来看,也是保证了不超卖,不少卖的

2.png


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


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";
    }
}

流程展示

2.png

3.png

2.png3.png

总结

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


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


拓展


页面动静分离

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
相关文章
|
2月前
|
缓存 前端开发 NoSQL
如何设计一个秒杀系统?
本文详细介绍了秒杀系统的原理与设计方法,包括高性能、一致性、高可用性和可扩展性等方面的要求。文中通过前端和后端的设计方案,探讨了如何实现秒杀系统的高并发处理,例如页面静态化、限流、降级策略及缓存优化等。此外,还分享了实际项目中的库存系统架构设计经验,并提供了面试中如何回答此类问题的建议。
181 2
|
2月前
|
缓存 NoSQL 应用服务中间件
【开发系列】秒杀系统的设计
【开发系列】秒杀系统的设计
|
缓存 NoSQL 数据库
【高并发】秒杀系统设计思路
【高并发】秒杀系统设计思路
216 0
|
消息中间件 缓存 NoSQL
如何设计一个秒杀系统???
如何设计一个秒杀系统???
187 0
|
消息中间件 缓存 安全
秒杀系统(1)——秒杀功能设计理念
秒杀系统(1)——秒杀功能设计理念
185 0
|
消息中间件 缓存 JavaScript
如何设计一个秒杀系统
如何设计一个秒杀系统
|
缓存 NoSQL 安全
秒杀系统的设计思路
你好看官,里面请!今天笔者讲的是秒杀系统的设计思路。不懂或者觉得我写的有问题可以在评论区留言,我看到会及时回复。 注意:本文仅用于学习参考,不可用于商业用途,如需转载请跟我联系。
434 2
|
数据采集 缓存 前端开发
如何设计一个秒杀系统(上)
秒杀大家都不陌生。自2011年首次出现以来,无论是双十一购物还是 12306 抢票,秒杀场景已随处可见。简单来说,秒杀就是在同一时刻大量请求争抢购买同一商品并完成交易的过程。从架构视角来看,秒杀系统本质是一个高性能、高一致、高可用的三高系统。而打造并维护一个超大流量的秒杀系统需要进行哪些关注,就是本文讨论的话题。
553 0
如何设计一个秒杀系统(上)
|
消息中间件 缓存 运维
如何设计一个秒杀系统(下)
这里我们讲解最后一部分
324 0
如何设计一个秒杀系统(下)
|
SQL 存储 缓存
如何设计一个秒杀系统(中)
我们接着上篇继续讲,这篇主要讲一致性
308 0