SpringBoot实现Java高并发秒杀系统之并发优化

秒杀系统架构的设计和优化分析,以我一个小菜鸡,目前是说不出来的o(╥﹏╥)o。

因此呢,我这里仅从本项目已经实现的优化来介绍一下:

本项目中做到了以下优化:

秒杀接口采用md5加密方式防刷。

订单表使用联合主键方式,限制一个用户只能购买该商品一次。

配合Spring事务控制实现简单的优化。

使用redis缓存优化。

Spring的事务控制

Spring的声明式事务通过:传播行为、隔离级别、只读提示、事务超时、回滚规则来进行定义。

传播行为

事务的第一个方面就是传播行为。传播行为定义了客户端与被调用方法之间的事务边界。Spring定义了7中不同的传播行为,传播规则规定了何时要创建一个事务或何时使用已有的事务:

传播行为 含义

PROPAGATION_MANDATORY 表示该方法必须在事务中运行。如果当前事务不存在,则会抛出一个异常

PROPAGATION_NESTED 表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立与当前事务进行单独的提交或回滚

PROPAGATION_NEVER 表示当前方法不应该运行在事务上下文中,如果当前正在有一个事务运行,则会抛出异常

PROPAGATION_NOT_SUPPORTED 表示该方法不应该运行在事务中。

PROPAGATION_REQUIRED 表示当前方法必须运行在事务中。如果当前事务存在,方法将会在该事务中运行。否者,会启动一个新的事务

PROPAGATION_REQUIRES_NEW 表示当前方法必须运行在它自己的事务中。一个新的事务将被启动

PROPAGATION_SUPPORTS 表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行

隔离级别

声明式事务的第二个维度就是隔离级别。隔离级别定义了一个事务可能受其他并发事务影响的程度。多个事务并发运行,经常会操作相同的数据来完成各自的任务,但是可以回导致以下问题:

更新丢失:当多个事务选择同一行操作,并且都是基于最初的选定的值,由于每个事务都不知道其他事务的存在,就会发生更新覆盖的问题。

脏读:事务A读取了事务B已经修改但为提交的数据。若事务B回滚数据,事务A的数据存在不一致的问题。

不可重复读:书屋A第一次读取最初数据,第二次读取事务B已经提交的修改或删除的数据。导致两次数据读取不一致。不符合事务的隔离性。

幻读:事务A根据相同条件第二次查询到的事务B提交的新增数据,两次数据结果不一致,不符合事务的隔离性。

理想情况下,事务之间是完全隔离的,从而可以防止这些问题的发生。但是完全的隔离会导致性能问题,因为它通常会涉及锁定数据库中的记录。侵占性的锁定会阻碍并发性,要求事务互相等待以完成各自的工作。

因此为了实现在事务隔离上有一定的灵活性。因此,就会有多重隔离级别:

隔离级别 含义

ISOLATION_DEFAULT 使用后端数据库默认的隔离级别

SIOLATION_READ_UNCOMMITTED 允许读取尚未提交的数据变更。可能会导致脏读、幻读或不可重复读

ISOLATION_READ_COMMITTED 允许读取并发事务提交的数据。可以阻止脏读,但是幻读或不可重复读仍可能发生

ISOLATION_REPEATABLE_READ 对同一字段的多次读取结果是一致的,除非数据是被本事务自己所修改,可以阻止脏读和不可重复读,但幻读仍可能发生

ISOLATION_SERIALIZABLE 完全服从ACID的事务隔离级别,确保阻止脏读、不可重复读、幻读。这是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库来实现的

回滚规则

Spring的事务管理器默认是针对unchecked exception回滚,也就是默认对Error异常和RuntimeException异常以及其子类进行事务回滚。

也就是说事务只有在遇到运行期异常才会回滚,而在遇到检查型异常时不会回滚。

这也就是我们之前设计Service业务层逻辑的时候一再强调捕获try catch异常,且将编译期异常转换为运行期异常。

简单优化

这里我们还是要关注一些项目中的两个核心的业务:1.减库存;2.插入购买明细。我们以一张图来看一下这两个操作的事务执行流程:

可以看到我们的秒杀操作主要是基于Mysql的事务进行的,而基于MySQL事务的秒杀操作主要瓶颈是网络延迟和GC(Java垃圾回收机制)。执行一条update语句首先要拿到MySQL的行级锁rowLock,而我们要解决的就是如何降低update对rowLock的持有时间。

我们先了解一下MySQL的InnoDB储存引擎的行级锁(rowLock):

行锁的劣势:开销大;加锁慢;会出现死锁

行锁的优势:锁的粒度小,发生锁冲突的概率低;处理并发的能力强

加锁的方式:自动加锁。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁;对于普通SELECT语句,InnoDB不会加任何锁;当然我们也可以显示的加锁:

共享锁:select * from tableName where … + lock in share more

排他锁:select * from tableName where … + for update

InnoDB和MyISAM的最大不同点有两个:一,InnoDB支持事务(transaction);二,默认采用行级锁。加锁可以保证事务的一致性,可谓是有人(锁)的地方,就有江湖(事务)。

详细的介绍请看博文:MySQL 表锁和行锁机制

所以在此基础上我们可以进行简单的优化:

很简单,就是调整update和insert操作的执行顺序。目的就是为了缩短update对rowLock的持有时间提高性能,因为我们的查询语句使用了insert ignore into xx的方式来避免重复秒杀,那么闲执行insert语句可以在插入时就排除可能存在重复秒杀的操作,这样就不用再向下执行更新操作了。在一定程度上降低了一倍的rowLock持有时间。

下面是源码:

@Override

@Transactional

public SeckillExecution executeSeckill(long seckillId, BigDecimal money, long userPhone, String md5)

throws SeckillException, RepeatKillException, SeckillCloseException {

if (md5 == null || !md5.equals(getMD5(seckillId))) {

throw new SeckillException(“seckill data rewrite”);

}

//执行秒杀逻辑:1.减库存;2.储存秒杀订单

Date nowTime = new Date();

try {

//记录秒杀订单信息

int insertCount = seckillOrderMapper.insertOrder(seckillId, money, userPhone);

//唯一性:seckillId,userPhone,保证一个用户只能秒杀一件商品

if (insertCount <= 0) {

//重复秒杀

throw new RepeatKillException(“seckill repeated”);

} else {

//减库存

int updateCount = seckillMapper.reduceStock(seckillId, nowTime);

if (updateCount <= 0) {

//没有更新记录,秒杀结束

throw new SeckillCloseException(“seckill is closed”);

} else {

//秒杀成功

SeckillOrder seckillOrder = seckillOrderMapper.findById(seckillId);

return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, seckillOrder);

}

}

} catch (SeckillCloseException e) {

throw e;

} catch (RepeatKillException e) {

throw e;

} catch (Exception e) {

logger.error(e.getMessage(), e);

//所有编译期异常,转换为运行期异常

throw new SeckillException(“seckill inner error:” + e.getMessage());

}

}

Redis缓存优化

准备

如果想使用Redis缓存进行优化,首先你需要连接什么是Redis缓存,以及Spring提供的一种操作Redis缓存的框架:Spring-data-redis。最终要的是:你需要在本地电脑上安装好Redis缓存服务器:

所以呢,我推荐你看一下我的几篇文章:

Redis即Spring-data-redis入门学习

优雅的整合SSM+Shiro+Redis+Solr框架

在看了上面的文章后相信你已经初步了解了使用Spring-data-redis操作Redis缓存服务器,下面讲解针对本项目的缓存优化实现:

启动安装好的Redis缓存服务器,修改项目中的 resources/application.yml 关于Redis和Jedis的配置,

例中我使用的本地Redis服务器:host:127.0.0.1;port:6379

沈阳妇科医院哪家好:http://iask.sina.com.cn/h-fk

沈阳性病医院哪家好:http://xb.029nk.com/

猜你喜欢

转载自blog.csdn.net/a13804947436/article/details/83817431