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

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
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 都可以实现)


相关文章
|
6月前
|
人工智能 自然语言处理
AudioX:颠覆创作!多模态AI一键生成电影级音效+配乐,耳朵的终极盛宴
AudioX 是香港科技大学和月之暗面联合推出的扩散变换器模型,能够从文本、视频、图像等多种模态生成高质量音频和音乐,具备强大的跨模态学习能力和泛化能力。
481 36
AudioX:颠覆创作!多模态AI一键生成电影级音效+配乐,耳朵的终极盛宴
|
11月前
|
JSON 缓存 Java
优雅至极!Spring Boot 3.3 中 ObjectMapper 的最佳实践
【10月更文挑战第5天】在Spring Boot的开发中,ObjectMapper作为Jackson框架的核心组件,扮演着处理JSON格式数据的核心角色。它不仅能够将Java对象与JSON字符串进行相互转换,还支持复杂的Java类型,如泛型、嵌套对象、集合等。在Spring Boot 3.3中,通过优雅地配置和使用ObjectMapper,我们可以更加高效地处理JSON数据,提升开发效率和代码质量。本文将从ObjectMapper的基本功能、配置方法、最佳实践以及性能优化等方面进行详细探讨。
867 2
|
11月前
|
运维 网络安全 持续交付
IDEA+Docker 远程一键部署项目:技术干货分享
【10月更文挑战第4天】在现代软件开发中,快速、可靠、自动化的部署流程是提升开发效率和运维质量的关键。IDEA(IntelliJ IDEA)作为Java开发者首选的IDE,结合Docker这一轻量级容器化技术,能够实现远程一键部署项目,极大地简化了开发到生产的流程。今天,我将和大家分享这一组合在工作学习中的实际应用和技术干货。
903 3
|
Java Windows
Win环境安装Protobuf 2.0 版本
Win环境安装Protobuf 2.0 版本
301 1
|
11月前
|
传感器 人工智能 物联网
探索智能家居技术:现状与未来
本文深入探讨了智能家居技术的发展历程、当前主要技术和应用,并展望了其未来的发展趋势。通过对现有技术的详细解析和案例分析,揭示了智能家居在提升生活品质、节能减排等方面的潜力,同时指出了目前面临的挑战和可能的解决方案。
|
前端开发 Java 数据库
SpringBoot返回枚举对象中的所有属性以对象的形式返回(一个@JSONType解决)
SpringBoot返回枚举对象中的所有属性以对象的形式返回(一个@JSONType解决)
906 0
|
存储 Python
`tempfile`模块在Python中用于创建临时文件和目录。
`tempfile`模块在Python中用于创建临时文件和目录。
|
数据采集 搜索推荐 算法
fuzzywuzzy,一个好用的 Python 库!
fuzzywuzzy,一个好用的 Python 库!
338 0
|
Web App开发 自然语言处理 API
5000字详说Elasticsearch入门(一)
本文主要介绍快速入门Elasticsearch,从安装、基本概念、分词器、文档基本操作这4个方面快速入门。本篇是ES入门系列的第一篇,后续还有springboot项目集成ES、ES高级查询用法、数据库同步到ES的方案等。
5000字详说Elasticsearch入门(一)
向量的内积外积与其几何意义
向量的内积外积与其几何意义
655 0

热门文章

最新文章