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

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lkp1603645756/article/details/81871966

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  有需要的童鞋可自行下载。

猜你喜欢

转载自blog.csdn.net/lkp1603645756/article/details/81871966