JAVA构建高并发商城秒杀系统——操作实践 ゞ 浴缸里的玫瑰 2022-05-16 11:25 375阅读 0赞 JAVA构建高并发商城秒杀系统——架构分析: [https://blog.csdn.net/lkp1603645756/article/details/81744558][https_blog.csdn.net_lkp1603645756_article_details_81744558] 未看理论知识的可以点击上方链接查看。 前面说了那么多理论,接下来自己写代码: 不清楚如何用IDEA创建Spring Boot项目的童鞋,可以点击该链接查看: [https://blog.csdn.net/lkp1603645756/article/details/81872249][https_blog.csdn.net_lkp1603645756_article_details_81872249] 首先,创建数据库,建立seckill\_goods和seckill\_order表 ![70][] ![70 1][] 配置项目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,配置好并能正确访问。 ![70 2][] 首先我们来测试一下我们的nginx无限流,我们用20个并发来请求nginx ![70 3][] ![70 4][] 启动本机的nginx 启动我们的测试用例 输出结果如下: ![70 5][] 打印了20success,证明20个请求都通过了。 接下来我们这是nginx的漏桶限流,来限制通过的流量。 找到你nginx的安装目录,进入config目录下面的nginx.conf文件 ![70 6][] 增加一行该配置,$binary\_remote\_addr,限流维度,表示对每一个ip进行限流,1r/s表示1秒一个 ![70 7][] 在这里引用它 记得重启nginx哦,不然修改不会生效 ![70 8][] 接下来我们在运行代码 童鞋们猜猜看能通过多少请求呢,也就是打印多少个success? ![70 9][] 总共只有一个请求通过了拦截。 因为设置1秒钟一个请求的限流,当20请求同时过来的时候,只有一个请求能成功通过拦截,剩下的都被拦截掉了。 PS:限流运行的时候,会有报错,不过这是正常现象,因为剩下的请求被限流了,没有被处理。 ![70 10][] 前面我们所了,限流桶,刚刚我们只是设置了限流,但是没有用上桶,现在我们设置一个漏桶为5的容量,它会慢慢处理掉桶里的请求。这里童鞋们猜猜会正常多少并发请求呢? ![70 11][] 修改完配置记得重启nginx 运行结果: ![70 12][] 桶的容量只有5个,为什么处理了6个请求呢? 因为当第一个请求过来的时候,它直接被处理掉了,之后过来的请求,就被装在了漏桶里面,直到5个空间被装满,之后会就被慢慢处理掉,所以加上第一次处理的请求,和漏桶里面的请求,总共就处理了6个请求。 ![70 13][] 还可以在集群的tomcat去限流,总共接受500,超过的请求就掉请求,nginx还有很多其他的限流方式,感兴趣的小伙伴们可以去试试。 Java还可以引用 guawa 做令牌桶限流,这里不演示了,很简单,自己可以去百度查查 其他前端限流,nginx限流,java限流,分布式限流之后,到达数据库的流量已经很小了,就相当于100个并发抢100个商品,这里我们在用乐观锁和悲观锁进行控制既可以了。 首先我们演示一下无锁的情况下,200个并发抢购100个商品,看看会出现什么情况。 ![70 14][] ![70 15][] 设置好两个表的值,然后注释掉上一条代码,设置url\_nolock ![70 16][] 修改并发为200个,然后点击运行 ![70 17][] 程序运行完 ![70 18][] 我们去看一下数据库的数据 ![70 19][] 订单表有200个订单 ![70 20][] 商品表,还剩24个商品,是不是很神奇,凭空卖出了那么多订单。 这样肯定是不行的,怎么预防这种情况呢?这时候乐观锁和悲观锁就登场了。 把商品表数据恢复,清空订单表的数据 接下来测试悲观锁 ![70 21][] 运行程序,查看运行结果: ![70 22][] 我们再去看看数据库的数据 商品表: ![70 23][] 订单表: ![70 24][] 我们通过sql统计来更直观来查看 ![70 25][] 获得执行时间 ![70 26][] 下面我们用不重试的乐观锁来测试 ![70 27][] 启动程序,查看运行结果: ![70 28][] 查看数据库的数据: 计算出不重试乐观锁的时间: ![70 29][] 这里可能出现200个并发秒杀商品,抢购不完的,可以加到并发。 下面我们在测试一下重试乐观锁 计算出它的实际 ![70 30][] 虽然三种情况测试出来的时间与前面讲的不符 但是,高并发情况下两个锁的结论:悲观锁速度更快!!!有时乐观锁偶然会比悲观锁低,但是在大数据的情况下,悲观锁会比乐观锁低! 有兴趣的童鞋可以自己去操作一边,如果是不一样的时间,可以在下放评论。 接下来通过redis的原子性来实现,因为redis是单线程的 修改访问url ![70 31][] 在redis里面预先存入一个key为seckill,值为100的数据 然后启动程序: ![70 32][] 200个线程,只有100个得到了处理。 到这里就结束了,有兴趣的童鞋可以自己动手试试。 秒杀系统代码托管在GitHub:[https://github.com/gdjkmax/SpeedKillSystem][https_github.com_gdjkmax_SpeedKillSystem] 有需要的童鞋可自行下载。 [https_blog.csdn.net_lkp1603645756_article_details_81744558]: https://blog.csdn.net/lkp1603645756/article/details/81744558 [https_blog.csdn.net_lkp1603645756_article_details_81872249]: https://blog.csdn.net/lkp1603645756/article/details/81872249 [70]: /images/20220516/7b6ab3d1f3bd4e2aa3cfda1e268b20cc.png [70 1]: /images/20220516/bddcf8229f614b9cb1ddbccc7f309621.png [70 2]: /images/20220516/24cc0c2b99a44147b6e64b85cadc00cc.png [70 3]: /images/20220516/5e3bf0b0ef9c4128932d3b0da698dda1.png [70 4]: /images/20220516/e5f863b7e07a4bf2b8884c6f1ae8ecda.png [70 5]: /images/20220516/28e34dee18d9452fbda3bf806bbc5646.png [70 6]: /images/20220516/10353f47a2ea4d8ba762973d0bf5f988.png [70 7]: /images/20220516/f1b6c5d3250848eabd8a85047f849865.png [70 8]: /images/20220516/e5f5b61fea6c4b2fa79538987c3cdfae.png [70 9]: /images/20220516/400b0646fdb24e6db2cc8bdccbec2e2f.png [70 10]: /images/20220516/d5ee27192f124d42bb76992a495a40d9.png [70 11]: /images/20220516/f5747bf0c7074ff6b3c0511738a9bfc1.png [70 12]: /images/20220516/0c9ebc99d36c4a55a6099c66cb79ca82.png [70 13]: /images/20220516/c79449b6e79644699c22fe316d2932d9.png [70 14]: /images/20220516/bb15784b8dca44d49d0a4f94d79b7a73.png [70 15]: /images/20220516/93d65a5c11514761aa1309057ca1af08.png [70 16]: /images/20220516/71e4eda8338540979ed41f474156e316.png [70 17]: /images/20220516/21528b016bc9461d8516361376a23ff1.png [70 18]: /images/20220516/67f0c3e8f822491d93ee4583709f3109.png [70 19]: /images/20220516/9a075d1d54804d829679a715a6007fb6.png [70 20]: /images/20220516/707680284a374769b9f6993ea5b9c38b.png [70 21]: /images/20220516/b303fc8b78aa471bb479dd80cd721509.png [70 22]: /images/20220516/505e96de39d547ef96a58dee3624fc95.png [70 23]: /images/20220516/9744c57f6bf245a495a368aa9df13572.png [70 24]: /images/20220516/be1575f382104f56afde165e5e19e161.png [70 25]: /images/20220516/929ec80a33dd4ccaa2f8a39eb75ef266.png [70 26]: /images/20220516/9c42b4ba397b4e128f2dfe1c619ad34c.png [70 27]: /images/20220516/4d967c1c9cd14600a4c8c5b709b1b8b6.png [70 28]: /images/20220516/d9afa7e1f32b4acc91a77cc4d38eff46.png [70 29]: /images/20220516/43e09747e8e84c0d80cb66ab8c27b9c6.png [70 30]: /images/20220516/fb66f5d55cfc4441bf4aa3893b5bf202.png [70 31]: /images/20220516/894ee2d13a73485aa34a70c607826cf8.png [70 32]: /images/20220516/4844b1ed3e0e45968d694780e4d8b065.png [https_github.com_gdjkmax_SpeedKillSystem]: https://github.com/gdjkmax/SpeedKillSystem
还没有评论,来说两句吧...