前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JAVA构建高并发商城秒杀系统——操作实践

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

作者头像
良月柒
发布2019-03-19 16:27:20
1.1K0
发布2019-03-19 16:27:20
举报

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-09-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员的成长之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
代码托管
CODING 代码托管(CODING Code Repositories,CODING-CR)是为开发者打造的云端便捷代码管理工具,旨在为更多的开发者带去便捷、高效的开发体验,全面支持 Git/SVN 代码托管,包括代码评审、分支管理、超大仓库等功能。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档