秒杀系统-springboot,mysql,redis

一步步完善的过程

1. 环境搭建

1. 1 目录结构

在这里插入图片描述

1.2 sql

create table product(
    id            int(12)        not null auto_increment comment 'id',
    product_name  varchar(60)    not null comment '商品名称',
    stock         int(10)        not null comment '库存',
    price         decimal(16,2)  not null comment '单价',
    version       int(10)        not null default 0 comment '版本号,乐观锁',
    primary key (id)
);


-- 购买记录表
create table purchase_record(
    id             int(12)        not null auto_increment comment ,
    user_id        int(12)        not null comment '用户编号',
    product_id     int(12)        not null comment '商品编号',
    price          decimal(16,2)  not null comment '价格',
    quantity       int(12)        not null comment '数量',
    sum            decimal(16,2)  not null comment '总价',
    purchase_date  timestamp      not null default now() comment '购买日期',
    primary key (id)
);

service:

  @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void seckillbyId(Integer id) {
        Product product = productMapper.getById(id);
        if (product .getStock() > 0) {
            productMapper.decreaseStock(id);
        } else {
            System.out.println("库存不足");
        }
    }

mapper:

@Update("update product set stock=stock-1 where id = #{id}")
public int decreaseStock(@Param("id") int id);

2.理论基础

在这里插入图片描述
图片来自:https://www.cnblogs.com/SteadyJack/p/11228391.html
本文实现精简版

1.乐悲观锁原理了解
参见
2.压力测试JMeter基本使用
参见
3.redisTemplate基本使用
参见
4.秒杀系统设计概念
秒杀系统架构优化思路
秒杀架构参考

3.基于Mysql+Mybatis

3.1 基本情况

测试了好几组,一直没有出现超发情况,只能一直不停的修改增加线程数之类的,最后感觉一次减少一个不太行,就把代码改成了以下这样:

  @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void seckillbyId(Integer id) {
        Product product = productMapper.getById(id);
        //增加随机数
        int i = new Random().nextInt(10);
        if (product .getStock() > 0) {
            productMapper.decreaseStock(id, i);
        } else {
            System.out.println("库存不足");
        }

    }
  @Update("update product set stock=stock-#{decrease} where id = #{id}")
    public int decreaseStock(@Param("id") int id, @Param("decrease") int quantity);

原数据库数据:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
超发第一次:
在这里插入图片描述
超发第二次:
在这里插入图片描述
其实都知道,直接查出来是否为0再减库存的方法肯定是不行,这样做的目的也就是测试一下测试是否可行,以及能否出现超发情况,不然一个错误的方法不能出现超发,后面也不用玩了!

3.2 基于乐观锁

乐观锁就是再修改之前看看,当前值是否和我拿到的值是否一样,如果不一样就是说明了,被别人修改了,那么我再去修改这个值可能就会出现并发修改的错误.
所以每次我要减库存了,我就来看看我改之前拿到的版本号是否被人修改了,即version
下面是修改过后的关键代码:

 @Update("update product set stock=stock-#{decrease} , version = version +1 where id = #{id} and version=#{version}")
    public int decreaseStockByOptimisticLock(@Param("id") int id, @Param("decrease") int quantity, @Param("version") int version);

    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void seckillbyId(Integer id) {
        Product byId = productMapper.getById(id);
        int i = new Random().nextInt(10);
        if (byId.getStock() > 0) {
//            productMapper.decreaseStock(id, i);
            productMapper.decreaseStockByOptimisticLock(id,i,byId.getVersion());
        } else {
            System.out.println("库存不足");
        }

    }

然后进行测试:
然而:
在这里插入图片描述
没错,超发了
问题在哪?
… 认真看了的应该能够发现,没发现也可以思考一下
我就不说错误原因了:
修改后的代码如下:

 @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void seckillbyId(Integer id) {
        Product byId = productMapper.getById(id);
        int i = new Random().nextInt(10);
        if (byId.getStock() > 0 && byId.getStock() >= i) {
//            productMapper.decreaseStock(id, i);
            productMapper.decreaseStockByOptimisticLock(id, i, byId.getVersion());
        } else {
            System.out.println("库存不足");
        }
    }

再来进行测试:
在这里插入图片描述
哦nice!
但是这只是第一次,并发是有偶然性的哦!我这样提醒自己
我把每次的数据库的前后的值都贴出来
在这里插入图片描述
在这里插入图片描述
再次修改:
在这里插入图片描述
在这里插入图片描述
奥里给!!!还没出现超发
再来给个大的:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
???
我不是设置的5000次吗? 没多大意义的一次测试

3.2 基于悲观锁

如何实现悲观锁呢?
最简单的方法就是加一个synchronized,在一个线程持有锁的时候,其他线程是无法获取到锁的
代码如下:

   @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public synchronized void  seckillbyId(Integer id) {
        Product byId = productMapper.getById(id);
        int i = new Random().nextInt(10);
        if (byId.getStock() > 0 && byId.getStock() >= i) {
            productMapper.decreaseStock(id, i);
//            productMapper.decreaseStockByOptimisticLock(id, i, byId.getVersion());
        } else {
            System.out.println("库存不足");
        }
    }

jconsole表示如下:
第一次:
堆内存:

在这里插入图片描述
线程:
在这里插入图片描述

跑回去测试了下乐观锁,乐观锁的jconsole表现如下:
在这里插入图片描述

4.基于Redis

由上面实现的乐观锁,悲观锁的方式,我们可以看出:
1.乐观锁其实把压力还是给到了数据库,一查一尝试写
2.悲观锁对数据库的压力只有一查一准确写
可见,使用悲观锁对数据库的压力是要小于乐观锁的,但是使用悲观锁,其实就是使减库存操作变成了串行,在性能方面其实是是否堪忧的,而在秒杀环境下,一下子进来那么多请求,完成一次秒杀应该是在很短的时间内的,这样了,我们才想到了,如何能够分摊数据库压力,提高响应速度,增加吞吐量?

当然这一切都是建立在绝对不能超发的情况下的;

众所周知,内存的读写速度是要远高于磁盘速度的
那就用redis做一层缓存呗
如何优化?

  • 读数据库的剩余库存是否可以转移到redis
  • 版本号是否可以转移到redis

很明显,全部全部全部都可以放到redis缓存里;
其实就是把对mysql的操作放到redis里面,等最后结束秒杀了,就同步回数据库

当然以上以上都是我个人的想法;
等过一段时间我再来看看,更加完善这一部分的代码书写;

发布了37 篇原创文章 · 获赞 6 · 访问量 4643

猜你喜欢

转载自blog.csdn.net/littlewhitevg/article/details/105490867