ITPub博客

首页 > > > JAVA构建高并发商城秒杀系统——操作实践

JAVA构建高并发商城秒杀系统——操作实践

作者:孤独键客 时间:2020-05-13 10:46:35 0 删除 编辑


点击上方“程序员的成长之路”,选择“置顶公众号”

技术文章第一时间送达!



JAVA构建高并发商城秒杀系统——架构分析:


https://blog.csdn.net/lkp1603645756/article/details/81744558


未看理论知识的可以点击上方链接查看。


 


前面说了那么多理论,接下来自己写代码:


 


不清楚如何用IDEA创建Spring Boot项目的童鞋,可以点击该链接查看:


https://blog.csdn.net/lkp1603645756/article/details/81872249


 


首先,创建数据库,建立seckill_goods和seckill_order表



 


配置项目application.properties文件,设置数据库连接


spring.datasource.url = jdbc:mysql://localhost:3306/databaseset?useUnicode=true&characterEncoding=utf-8

spring.datasource.username = root

spring.datasource.password =  123456

spring.datasource.driverClassName = com.mysql.jdbc.Driver

 

#redis

spring.redis.hostName=127.0.0.1

spring.redis.password=

spring.redis.port=6379

spring.redis.jedis.pool.max-active=9

spring.redis.jedis.pool.max-wait=-1ms

spring.redis.jedis.pool.max-idle=8

spring.redis.jedis.pool.min-idle=0

spring.redis.timeout=10000ms

 

 


创建一个Spring Boot项目


分别创建两张表的实体类


/**

 * 作者:LKP

 * 时间:2018/8/20

 */

public class Goods {

    private int id;

    private String name;

    private int count;

    private int sale;

    private int version;

 

    public int getId() {

        return id;

    }

 

    public void setId(int id) {

        this.id = id;

    }

 

    public String getName() {

        return name;

    }

 

    public void setName(String name) {

        this.name = name;

    }

 

    public int getCount() {

        return count;

    }

 

    public void setCount(int count) {

        this.count = count;

    }

 

    public int getSale() {

        return sale;

    }

 

    public void setSale(int sale) {

        this.sale = sale;

    }

 

    public int getVersion() {

        return version;

    }

 

    public void setVersion(int version) {

        this.version = version;

    }

}

/**

 * 作者:LKP

 * 时间:2018/8/20

 */

public class Order {

    private int id;

    private String custname;

    private String createTime;

 

    public int getId() {

        return id;

    }

 

    public void setId(int id) {

        this.id = id;

    }

 

    public String getCustname() {

        return custname;

    }

 

    public void setCustname(String custname) {

        this.custname = custname;

    }

 

    public String getCreateTime() {

        return createTime;

    }

 

    public void setCreateTime(String createTime) {

        this.createTime = createTime;

    }

}

 


分别创建两张表的mapper映射文件,这里我们采取纯注解的方法配置,不太了解的童鞋自行百度一下。


 


商品mapper创建三个方法,分别是构建扣减库存的两个方法,一个是悲观锁,一个是乐观锁调用的,在构建一个查询商品的方法。


/**

 * 作者:LKP

 * 时间:2018/8/2

 */

public interface GoodsMapper {

    /**

     * 减掉商品库存——悲观锁

     * @return

     */

    @Update("UPDATE `databaseset`.`seckill_goods` SET `name` = 'iphone X', `count` = #{goods.count}, `sale` = #{goods.sale}, `version` = 0 WHERE `id` = 1 ;")   //for update

    int updateGoodsCount(@Param("goods")Goods goods);

 

    /**

     * 减掉商品库存——乐观锁

     * @return

     */

    @Update("UPDATE `databaseset`.`seckill_goods` SET `name` = 'iphone X', `count` = #{goods.count}, `sale` = #{goods.sale}, `version` = #{goods.version}+1 WHERE `id` = #{goods.id} and version = #{updateVersion};")

    int updateGoodsCountOptimisticLock(@Param("goods")Goods goods, @Param("updateVersion")int version);

 

    /**

     * 查询商品

     * @return

     */

    @Select("select `id`, `name`, `count`, `sale`, `version` from seckill_goods where id = 1 for update;")

    Goods getGoods();

}

 


订单mapper,创建一个生成订单的方法


/**

 * 作者:LKP

 * 时间:2018/8/2

 */

public interface OrderMapper {

 

    /**

     * 生成订单

     * @param name

     * @param createTime

     * @return

     */

    @Insert("INSERT INTO `databaseset`.`seckill_order`(`custname`, `create_time`) VALUES (#{name}, #{createTime});")

    int insertOrder(@Param("name") String name, @Param("createTime") String createTime);

 

}

 


创建商品Service接口


/**

 * 作者:LKP

 * 时间:2018/8/2

 */

public interface GoodsService {

    /**

     * 减掉商品库存——悲观锁

     * @return

     */

    int updateGoodsCount(Goods goods);

 

    /**

     * 减掉商品库存——乐观锁

     * @return

     */

    int updateGoodsCountOptimisticLock(Goods goods,int version);

 

    /**

     * 查询商品

     * @return

     */

    Goods getGoods();

}

实现它


/**

 * 作者:LKP

 * 时间:2018/8/2

 */

@Service

public class GoodsServiceImpl implements GoodsService {

 

    @Autowired

    private GoodsMapper userMapper;

 

    @Override

    public int updateGoodsCount(Goods goods) {

        return userMapper.updateGoodsCount(goods);

    }

 

    @Override

    public int updateGoodsCountOptimisticLock(Goods goods,int version) {

        return userMapper.updateGoodsCountOptimisticLock(goods,version);

    }

 

    @Override

    public Goods getGoods() {

        return userMapper.getGoods();

    }

}

 


 


创建订单Service接口


/**

 * 作者:LKP

 * 时间:2018/8/21

 */

public interface OrderService {

    /**

     * 生成订单

     * @param name

     * @param createTime

     * @return

     */

    int insertOrder(String name, String createTime);

 

    /**

     * 悲观锁

     * @return

     */

    void seckillPessimism() throws Exception;

 

    /**

     * 不重试乐观锁

     * @return

     */

    void seckillOptimistic();

 

    /**

     * 会重试的乐观锁

     * @return

     */

    int seckillWithOptimistic();

 

    /**

     * 无锁

     */

    void seckill();

 

    /**

     * 使用redis原子操作保障原子性

     */

    void seckillwithRedis();

}

实现它


/**

 * 作者:LKP

 * 时间:2018/8/21

 */

@Service

public class OrderServiceImpl implements OrderService {

 

    @Autowired

    private OrderMapper orderMapper;

 

    @Override

    public int insertOrder(String name, String createTime) {

        return orderMapper.insertOrder(name,createTime);

    }

 

    @Autowired

    private GoodsService goodsService;

 

    @Resource

    private SqlSessionFactory sqlSessionFactory;

 

    @Autowired

    private StringRedisTemplate stringRedisTemplate;

 

    /**

     * 悲观锁

     * @return

     */

    @Override

    public void seckillPessimism() throws Exception {

        //悲观锁begin

        SqlSession sqlSession = sqlSessionFactory.openSession(false);

        sqlSession.getConnection().setAutoCommit(false);

 

        //查询库存,如果库存大于0,则继续秒杀逻辑

        Goods goods = goodsService.getGoods();

        if (null != goods && goods.getCount() <= 0) {

            System.out.println(Thread.currentThread().getName() + "悲观锁方式商品卖光了!!!当前时间:" + System.currentTimeMillis());

            return;

        }

 

        //库存-1,销量+1

        Goods goodsForUpdate = new Goods();

        goodsForUpdate.setCount(goods.getCount()-1);

        goodsForUpdate.setSale(goods.getSale()+1);

        goodsForUpdate.setId(1);

        int i = goodsService.updateGoodsCount(goodsForUpdate);

 

        //当库存更新成功后创建订单

        if(1>0){

            //创建订单

            String time = System.currentTimeMillis()+"";

            String custname = "zhangsan"+time.substring(8,time.length());

            String createTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());

            insertOrder(custname,createTime);

        }

 

        sqlSession.getConnection().commit();

    }

 

    @Override

    public void seckillOptimistic() {

        //查询库存,如果库存大于0,则继续秒杀逻辑

        Goods goods = goodsService.getGoods();

        if (null != goods && goods.getCount() <= 0) {

            System.out.println(Thread.currentThread().getName() + "乐观锁方式商品卖光了!!!当前时间:" + System.currentTimeMillis());

            return;

        }

        int currentVersion = goods.getVersion();

        Goods goodsForUpdate = new Goods();

        goodsForUpdate.setVersion(currentVersion);

        goodsForUpdate.setCount(goods.getCount()-1);

        goodsForUpdate.setSale(goods.getSale()+1);

        goodsForUpdate.setId(1);

        int i = goodsService.updateGoodsCountOptimisticLock(goodsForUpdate,currentVersion);

 

        //当库存更新成功后创建订单

        if(1>0){

            String time = System.currentTimeMillis()+"";

            String custname = "zhangsan"+time.substring(8,time.length());

            String createTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());

            insertOrder(custname,createTime);

        }

    }

 

 

    /**

     * 会重试的乐观锁

     * @return

     */

    @Override

    public int seckillWithOptimistic() {

 

        //查询库存,如果库存大于0,则继续秒杀逻辑

        Goods goods = goodsService.getGoods();

        if (null != goods && goods.getCount() <= 0) {

            System.out.println(Thread.currentThread().getName() + "乐观锁方式商品卖光了!!!当前时间:" + System.currentTimeMillis());

            return -1;

        }

        int currentVersion = goods.getVersion();

        Goods goodsForUpdate = new Goods();

        goodsForUpdate.setVersion(currentVersion);

        goodsForUpdate.setCount(goods.getCount()-1);

        goodsForUpdate.setSale(goods.getSale()+1);

        goodsForUpdate.setId(1);

        int i = goodsService.updateGoodsCountOptimisticLock(goodsForUpdate,currentVersion);

 

        //当库存更新成功后创建订单

        if(1>0){

            String time = System.currentTimeMillis()+"";

            String custname = "zhangsan"+time.substring(8,time.length());

            String createTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());

            insertOrder(custname,createTime);

            return 1;

        }else{      //乐观锁如何重试呢?

            return 0;

        }

    }

 

    /**

     * 无锁

     */

    @Override

    public void seckill() {

        //查询库存,如果库存大于0,则继续秒杀逻辑

        Goods goods = goodsService.getGoods();

        if (null != goods && goods.getCount() <= 0) {

            System.out.println(Thread.currentThread().getName() + "无锁方式商品卖光了!!!当前时间:" + System.currentTimeMillis());

            return;

        }

 

        //库存-1,销量+1

        Goods goodsForUpdate = new Goods();

        goodsForUpdate.setCount(goods.getCount()-1);

        goodsForUpdate.setSale(goods.getSale()+1);

        goodsForUpdate.setId(1);

        int i = goodsService.updateGoodsCount(goodsForUpdate);

 

        //当库存更新成功后创建订单

        if(1>0){

            //创建订单

            String time = System.currentTimeMillis()+"";

            String name = "zhangsan"+time.substring(8,time.length());

            String createTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());

            insertOrder(name,createTime);

        }

 

    }

 

    @Override

    public void seckillwithRedis() {

        String key = "seckill";     //定义一个key,key的值就是商品的数量

        long count = stringRedisTemplate.opsForValue().increment(key,-1l);

        if(count >=0 ){

            //创建订单

            String time = System.currentTimeMillis()+"";

            String name = "zhangsan"+time.substring(8,time.length());

            String createTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());

            insertOrder(name,createTime);

        }else{

            System.out.println("卖光了"+System.currentTimeMillis());

        }

    }

 

}

接下来创建我们的Controller


/**

 * 作者:LKP

 * 时间:2018/8/2

 */

@Controller

@EnableAutoConfiguration

public class DemoController {

 

    @Autowired

    private OrderService orderService;

 

    /**

     * 访问nginx

     */

    @RequestMapping("/nginx")

    @ResponseBody

    public String nginx(){

        RestTemplate restTemplate = new RestTemplate();

        String conent = restTemplate.getForObject("http://127.0.0.1/",String.class);

        if(conent.contains("Welcome to nginx!")){

            return "success";

        }

        return null;

    }

 

    /**

     * 无锁

     * @return

     */

    @RequestMapping(value = "/seckill")

    @ResponseBody

    public void seckill(){

        orderService.seckill();

    }

 

    /**

     * 悲观锁

     * @return

     */

    @RequestMapping(value = "/seckillPessimisticLock")

    @ResponseBody

    public void seckillPessimisticLock(){

        try {

            orderService.seckillPessimism();

        } catch (Exception e) {

            e.printStackTrace();

        }

    }

 

    /**

     * 乐观锁

     * @return

     */

    @RequestMapping(value = "/seckillOptimisticLock")

    @ResponseBody

    public void OptimisticLock(){

        orderService.seckillOptimistic();

    }

 

    /**

     * 失败会重试乐观锁

     * @return

     */

    @RequestMapping(value = "/seckillOptimisticLockretry")

    @ResponseBody

    public void OptimisticLockRetry(){

 

        while (true){

            int i = orderService.seckillWithOptimistic();

            //如果卖光了 或者卖出成功跳出循环,否者一直循环,直到卖出去位置

            if(i==-1 || i>0){

                break;

            }

        }

    }

 

    /**

     * 使用redis原子操作保障原子性

     */

    @RequestMapping(value = "/seckillRedis")

    @ResponseBody

    public void seckillRedis(){

        orderService.seckillwithRedis();

    }

}

到这里所有的功能都已经写好了,接下来我们就来测试一下我们的秒杀系统。


新建测试用例


/**

 * 作者:LKP

 * 时间:2018/8/21

 */

@RunWith(SpringRunner.class)

@Component

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

public class DemoApplicationTests {

 

    RestTemplate restTemplate = new RestTemplate();

 

    /**

     * @LocalServerPort 提供了 @Value("${local.server.port}") 的代替

     */

    @LocalServerPort

    private int port;

 

    private URL base;

 

    @Before

    public void setUp() throws Exception {

        String url = String.format("http://localhost:%d/", port);

        System.out.println(String.format("port is : [%d]", port));

        this.base = new URL(url);

 

        //测试nginx的正常请求和限流请求

        url_nginx = "http://127.0.0.1:"+port+"/nginx";

        //测试数据库-无锁

        url_nolock = "http://127.0.0.1:"+port+"/seckill";

        //测试乐观锁

        url_optimistic = "http://127.0.0.1:"+port+"/seckillOptimisticLock";

        //测试带重试的乐观锁

        url_optimisticWithRetry = "http://127.0.0.1:"+port+"/seckillOptimisticLockretry";

        //测试悲观锁

        url_pessimistic = "http://127.0.0.1:"+port+"/seckillPessimisticLock";

        //使用redis原子操作保障原子性

        url_redis = "http://127.0.0.1:"+port+"/seckillRedis";

    }

 

    //测试nginx的正常请求和限流请求

    String url_nginx = "http://127.0.0.1:8080/nginx";

    //测试数据库-无锁

    String url_nolock = "http://127.0.0.1:8080/seckill";

    //测试乐观锁

    String url_optimistic = "http://127.0.0.1:8080/seckillOptimisticLock";

    //测试带重试的乐观锁

    String url_optimisticWithRetry = "http://127.0.0.1:8080/seckillOptimisticLockretry";

    //测试悲观锁

    String url_pessimistic = "http://127.0.0.1:8080/seckillPessimisticLock";

    //使用redis原子操作保障原子性

    String url_redis = "http://127.0.0.1:8080/seckillRedis";

 

 

    //测试nginx 使用20个并发,测试购买商品使用200个并发

    private static final int amount = 200;

    //发令枪,目的是模拟真正的并发,等所有线程都准备好一起请求

    private CountDownLatch countDownLatch = new CountDownLatch(amount);

 

    @Test

    public void contextLoads() throws InterruptedException {

        System.out.println("开始卖:"+System.currentTimeMillis());

        for (int i = 0; i < amount; i++) {

            new Thread(new Request()).start();

            countDownLatch.countDown();

        }

        Thread.currentThread().sleep(100000);

    }

 

    public class Request implements Runnable{

 

        @Override

        public void run() {

            try {

                countDownLatch.await();

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

 

            //System.out.println(restTemplate.getForObject(url_nginx,String.class));

            restTemplate.getForObject(url_redis,String.class);

        }

    }

 

}

 


记得预先在本机上安装nginx,配置好并能正确访问。

 


首先我们来测试一下我们的nginx无限流,我们用20个并发来请求nginx





启动本机的nginx


启动我们的测试用例


输出结果如下:



打印了20success,证明20个请求都通过了。


接下来我们这是nginx的漏桶限流,来限制通过的流量。


找到你nginx的安装目录,进入config目录下面的nginx.conf文件



增加一行该配置,$binary_remote_addr,限流维度,表示对每一个ip进行限流,1r/s表示1秒一个



在这里引用它


记得重启nginx哦,不然修改不会生效



接下来我们在运行代码


童鞋们猜猜看能通过多少请求呢,也就是打印多少个success?



总共只有一个请求通过了拦截。


因为设置1秒钟一个请求的限流,当20请求同时过来的时候,只有一个请求能成功通过拦截,剩下的都被拦截掉了。


PS:限流运行的时候,会有报错,不过这是正常现象,因为剩下的请求被限流了,没有被处理。



 


前面我们所了,限流桶,刚刚我们只是设置了限流,但是没有用上桶,现在我们设置一个漏桶为5的容量,它会慢慢处理掉桶里的请求。这里童鞋们猜猜会正常多少并发请求呢?



修改完配置记得重启nginx


运行结果:



桶的容量只有5个,为什么处理了6个请求呢?


因为当第一个请求过来的时候,它直接被处理掉了,之后过来的请求,就被装在了漏桶里面,直到5个空间被装满,之后会就被慢慢处理掉,所以加上第一次处理的请求,和漏桶里面的请求,总共就处理了6个请求。



还可以在集群的tomcat去限流,总共接受500,超过的请求就掉请求,nginx还有很多其他的限流方式,感兴趣的小伙伴们可以去试试。


 


Java还可以引用 guawa 做令牌桶限流,这里不演示了,很简单,自己可以去百度查查


 


其他前端限流,nginx限流,java限流,分布式限流之后,到达数据库的流量已经很小了,就相当于100个并发抢100个商品,这里我们在用乐观锁和悲观锁进行控制既可以了。


 


首先我们演示一下无锁的情况下,200个并发抢购100个商品,看看会出现什么情况。





设置好两个表的值,然后注释掉上一条代码,设置url_nolock



修改并发为200个,然后点击运行



程序运行完



我们去看一下数据库的数据


订单表有200个订单




商品表,还剩24个商品,是不是很神奇,凭空卖出了那么多订单。


这样肯定是不行的,怎么预防这种情况呢?这时候乐观锁和悲观锁就登场了。


 


把商品表数据恢复,清空订单表的数据


接下来测试悲观锁



运行程序,查看运行结果:



我们再去看看数据库的数据


商品表:



订单表:

我们通过sql统计来更直观来查看



获得执行时间



 


下面我们用不重试的乐观锁来测试



启动程序,查看运行结果:



查看数据库的数据:


 


计算出不重试乐观锁的时间:



这里可能出现200个并发秒杀商品,抢购不完的,可以加到并发。


 


下面我们在测试一下重试乐观锁


计算出它的实际



 


虽然三种情况测试出来的时间与前面讲的不符


但是,高并发情况下两个锁的结论:悲观锁速度更快!!!有时乐观锁偶然会比悲观锁低,但是在大数据的情况下,悲观锁会比乐观锁低!


有兴趣的童鞋可以自己去操作一边,如果是不一样的时间,可以在下放评论。


 


接下来通过redis的原子性来实现,因为redis是单线程的


修改访问url



在redis里面预先存入一个key为seckill,值为100的数据


然后启动程序:



200个线程,只有100个得到了处理。


 


到这里就结束了,有兴趣的童鞋可以自己动手试试。


 


 


秒杀系统代码托管在GitHub:https://github.com/gdjkmax/SpeedKillSystem  有需要的童鞋可自行下载。



END


扫一扫

关注我们



来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69902700/viewspace-3129736/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论
一个不羁的码农,终身学习者。公众号「程序员的成长之路」。目前就职于一家游戏公司,主要负责服务器端,以及web系统的开发工作,对Java相关知识比较擅长。

注册时间:2018-12-14

  • 博文量
    793
  • 访问量
    119848