Java高并发秒杀业务Api-Service层构建过程
章节目录
- 秒杀Service 接口开发工作
- 秒杀业务逻辑编写
- spring-IOC 管理 service 组件
- context:component-scan
- Spring 声明式事务
- junit测试
创建基本的代码包层
1.创建DTO - 数据传输层对象
网络数据到达Controller 层后会使用框架自带的数据绑定 以及反序列化为dto对 象,并作为参数传递至service层进行处理。
2.业务接口实现
注意:业务接口的实现需要站在使用者的角度去设计接口
- 方法定义粒度-非常明确,参数简练,直接,不要一个大map对象去传递,return 还可以抛出异常。
代码如下:
业务逻辑接口声明类 SecKillService.java
package org.seckill.service; import org.seckill.domain.SecKill; import org.seckill.dto.Exposer; import org.seckill.dto.SecKillExcution; import org.seckill.exception.RepeatKillException; import org.seckill.exception.SecKillCloseException; import org.seckill.exception.SecKillException; import java.util.List; /** * 业务接口的实现需要站在使用者的角度去设计接口 * 三个方面:方法定义粒度、参数、返回类型 dto就可以 */ public interface SecKillService { /** * 返回秒杀商品列表 * * @return */ List<SecKill> getSecKillList(); /** * 查询秒杀商品单条记录 * * @param secKillId * @return */ SecKill getSecKillById(long secKillId); /** * 秒杀开启时,输出秒杀接口的地址,否则输出系统时间,和秒杀时间 * * @param secKillId * @return */ Exposer exportSecKillUrl(long secKillId); /** * 执行秒杀操作 * 验证当前的excuteSecKill id 与 传递过来的md5是否相同 * * @param secKillId * @param userPhone * @param md5 */ SecKillExcution excuteSecKill(long secKillId, String userPhone, String md5) throws SecKillException, RepeatKillException, SecKillCloseException; }
业务逻辑接口实现类-SecKillServiceImpl
package org.seckill.service.impl; import org.seckill.dao.SecKillDao; import org.seckill.dao.SuccessKilledDao; import org.seckill.domain.SecKill; import org.seckill.domain.SuccessKilled; import org.seckill.dto.Exposer; import org.seckill.dto.SecKillExcution; import org.seckill.enums.SecKillStateEnum; import org.seckill.exception.RepeatKillException; import org.seckill.exception.SecKillCloseException; import org.seckill.exception.SecKillException; import org.seckill.service.SecKillService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.DigestUtils; import java.util.Date; import java.util.List; @Service public class SecKillServiceImpl implements SecKillService { //在业务逻辑层打日志 private Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowire private SecKillDao secKillDao; @Autowire private SuccessKilledDao successKilledDao; //md5加盐,混淆加密 private final String salt = "asdasd8Zy*&ZCY87ywer7t678tzt67wer"; public List<SecKill> getSecKillList() { return secKillDao.queryAll(0, 4); } public SecKill getSecKillById(long secKillId) { return secKillDao.queryById(secKillId); } public Exposer exportSecKillUrl(long secKillId) { SecKill secKill = secKillDao.queryById(secKillId); if (secKill == null) { return new Exposer(false, secKillId);//没有相关产品的秒杀活动 } Date startTime = secKill.getStartTime(); Date endTime = secKill.getEndTime(); Date nowTime = new Date(); if (nowTime.getTime() < startTime.getTime() || nowTime.getTime() > endTime.getTime()) { return new Exposer(false, secKillId, nowTime.getTime(), startTime.getTime(), endTime.getTime());//没有相关产品的秒杀活动 } //转化特定字符串的过程,不可逆 String md5 = null; return new Exposer(secKillId, md5, true); } /** * 生成对应秒杀商品的md5值,做参数校验 * 保证可重用 * * @param secKillId * @return */ private String getMD5(long secKillId) { String base = secKillId + "/" + salt;//用户不知道salt String md5 = DigestUtils.md5DigestAsHex(base.getBytes()); return md5; } /** * 执行秒杀 * * @param secKillId * @param userPhone * @param md5 * @return * @throws SecKillException * @throws RepeatKillException * @throws SecKillCloseException */ public SecKillExcution excuteSecKill(long secKillId, String userPhone, String md5) throws SecKillException, RepeatKillException, SecKillCloseException { try { //1.用户输入的md5值验证 if (md5 == null || !md5.equals(getMD5(secKillId))) { throw new SecKillException("seckill data rewrite"); } //2.减库存、执行秒杀逻辑+记录购买行为,执行秒杀时间 Date nowTime = new Date(); int updateCount = secKillDao.reduceStock(secKillId, nowTime); if (updateCount <= 0) { //没有更新到记录,秒杀结束 throw new SecKillCloseException("seckill is closed"); } else { //3.记录购买行为 insertSuccessKilled 可能出现数据库连接超时等问题,所以需要外层try int insertCount = successKilledDao.insertSuccessKilled(secKillId, userPhone); //唯一验证,secKillId + userPhone if (insertCount <= 0) {//数据库联合主键冲突 throw new RepeatKillException("seckill repeated"); } else {//秒杀成功,返回秒杀成功的实体 SuccessKilled successKilled = successKilledDao.queryByIdWithSecKill(secKillId, userPhone); //不优雅的实现方式,在多处需要用到提示信息时,我们可以采用统一的常量去返回,这样待客户端提示语改变时,我们可以统一进行更改。 // return new SecKillExcution(secKillId, 1, "秒杀成功", successKilled); return new SecKillExcution(secKillId, SecKillStateEnum.SUCCESS, successKilled); } } } catch (SecKillCloseException e1) { throw e1;//还是要回滚 } catch (RepeatKillException e2) { throw e2;//还是要回滚 } catch (Exception e) { logger.error(e.getMessage()); //所有异常最终会转化为 运行时异常,spring 的声明式事务会帮我们做rollback。 throw new SecKillException("seckill inner error" + e.getMessage()); } } }
数据传输层类- Exposer
package org.seckill.dto; /** * 暴露秒杀接口 */ public class Exposer { private boolean exposed;//秒杀是否开启标志位 private String md5; //加密措施,暴露地址包括一个md5值 private long now; //系统当前时间(毫秒),方便浏览器计算距离服务器秒杀开启时间 private long secKillId; //秒杀商品的id private long start; private long end; //秒杀正在进行 public Exposer(long secKillId, String md5, boolean exposed) { this.secKillId = secKillId; this.md5 = md5; this.exposed = exposed; } //秒杀结束或还没开启 public Exposer(boolean exposed, long secKillId, long now, long start, long end) { this.exposed = exposed; this.secKillId = secKillId; this.now = now; this.start = start; this.end = end; } //没有相关商品秒杀活动 public Exposer(boolean exposed, long secKillId) { this.exposed = exposed; this.secKillId = secKillId; } public boolean isExposed() { return exposed; } public void setExposed(boolean exposed) { this.exposed = exposed; } public String getMd5() { return md5; } public void setMd5(String md5) { this.md5 = md5; } public long getNow() { return now; } public void setNow(long now) { this.now = now; } public long getStart() { return start; } public void setStart(long start) { this.start = start; } public long getEnd() { return end; } public void setEnd(long end) { this.end = end; } public long getSecKillId() { return secKillId; } public void setSecKillId(long secKillId) { this.secKillId = secKillId; } @Override public String toString() { return "Exposer{" + "exposed=" + exposed + ", md5='" + md5 + '\'' + ", now=" + now + ", secKillId=" + secKillId + ", start=" + start + ", end=" + end + '}'; } }
数据传输层-SecKillExcution
package org.seckill.dto; import org.seckill.domain.SuccessKilled; import org.seckill.enums.SecKillStateEnum; /** * 执行秒杀之后的结果 */ public class SecKillExcution { private long secKillId; private int state;//状态的标识 private String stateInfo;//状态表示 private SuccessKilled successSecKilled;//秒杀成功对象 //成功 jakson 在转化枚举的时候会出现问题,不支持枚举序列化 public SecKillExcution(long secKillId, SecKillStateEnum secKillStateEnum, SuccessKilled successSecKilled) { this.secKillId = secKillId; this.state = secKillStateEnum.getState(); this.stateInfo = secKillStateEnum.getStateInfo(); this.successSecKilled = successSecKilled; } //失败,使用到枚举 public SecKillExcution(long secKillId, SecKillStateEnum secKillStateEnum) { this.secKillId = secKillId; this.state = secKillStateEnum.getState(); this.stateInfo = secKillStateEnum.getStateInfo(); } public long getSecKillId() { return secKillId; } public void setSecKillId(long secKillId) { this.secKillId = secKillId; } public int getState() { return state; } public void setState(int state) { this.state = state; } public String getStateInfo() { return stateInfo; } public void setStateInfo(String stateInfo) { this.stateInfo = stateInfo; } public SuccessKilled getSuccessSecKilled() { return successSecKilled; } public void setSuccessSecKilled(SuccessKilled successSecKilled) { this.successSecKilled = successSecKilled; } @Override public String toString() { return "SecKillExcution{" + "secKillId=" + secKillId + ", state=" + state + ", stateInfo='" + stateInfo + '\'' + ", successSecKilled=" + successSecKilled + '}'; } }
业务运行时异常类-SecKillException、RepeatKillException、SecKillCloseException
package org.seckill.exception; /** * 秒杀相关业务异常 */ public class SecKillException extends RuntimeException { public SecKillException(String message) { super(message); } public SecKillException(String message, Throwable cause) { super(message, cause); } }
package org.seckill.exception; /** * 重复秒杀的异常(运行时异常) * 声明式事务,只接收运行时异常 */ public class RepeatKillException extends SecKillException { public RepeatKillException(String message) { super(message); } public RepeatKillException(String message, Throwable cause) { super(message, cause); } }
package org.seckill.exception; /** * 秒杀关闭异常-友好响应给用户 */ public class SecKillCloseException extends SecKillException { public SecKillCloseException(String message) { super(message); } public SecKillCloseException(String message, Throwable cause) { super(message, cause); } }
2.spring-IOC管理 service 组件
spring-service.xml 配置
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <!-- 扫描service包下所有使用注解的类型--> <context:component-scan base-package="org.seckill.service" /> <!-- 配置事务管理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!-- 注入数据库连接池--> <property name="dataSource" ref="dataSource" /> </bean> <!-- 配置基于注解的声明式事务 默认使用注解来管理事务行为 --> <tx:annotation-driven transaction-manager="transactionManager" /> </beans>
Spring 声明式事务
什么是声明式事务
底层原理:采用动态代理的方式为我们的事务核心逻辑添加开启事务、回滚提交事务的控制操作
声明式事务使用方式
推荐使用@Transactional
声明式事务的传播行为
定义:即事务方法的嵌套
一个业务需要调用多个声明了事务控制的方法,那么最新组合的事务是重新启动一个事务,还是说沿用老的事务呢?
默认的事务传播行为: propagation_required 浅析如下: ServiceA { void methodA() { ServiceB.methodB(); } } ServiceB { void methodB() { } }
比如说ServiceB.methodB 事务传播行为定义为PROPAGATION_REQUIRED,
- 那么当ServiceA.methodA 调用 ServiceB.methodB 时,methodA起了新的事
务,那么ServiceB.methodB看到自己已经运行在ServiceA.methodA的事务内
部,就不再起新的事务。 - 而假如ServiceA.methodA运行的时候发现自己没有在事务中,他就会为自己
分配一个事务。这样,在ServiceA.methodA或者在ServiceB.methodB内的任何
地方出现异常,事务都会被回滚。即使ServiceB.methodB的事务已经被提交,
但是ServiceA.methodA在接下来fail要回滚,ServiceB.methodB也要回滚
相当于 methodB 通过自己的事务传播行为告诉methodA 自己使用事务的原
则,告诉methodA 你要有事务我methodB就用你的,如果methodA没有事务,
那你methodA就需要创建一个事务。
什么时候回滚事务
当业务方法抛出运行时异常(RuntimeException)的时候spring 事务管理器会进行commit
配置事务管理器
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"> </bean>
配置基于注解的声明式事务
<tx:annotation-driven transaction-manager="transactionManager"/>
使用注解控制事务方法的优点
- 开发团队达成一致约定,约定明确标注事务方法的编程风格
- 保证事务方法的执行时间尽可能的短,不要穿插其他的网络操作,RPC/HTTP请求,如果必须需要的话,那么将这些请求剥离出来,形成一个干净的方法调用。不要混合编写和外部系统进行网络通信的代码。
- 不是所有的方法都需要事务,select、insert操作,单条语句的insert、update都不需要事务操作、不需要并发控制&多个操作联合形成一个事务时,不需要设置事务,因为mysql有autocommit=1的设置。
上述是性能杀手啊 ,注意再注意。
单元测试
package org.seckill.service; import org.junit.Test; import org.junit.runner.RunWith; import org.seckill.domain.SecKill; import org.seckill.dto.Exposer; import org.seckill.dto.SecKillExcution; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import java.util.List; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration({"classpath:spring/spring-dao.xml", "classpath:spring/spring-service.xml"}) public class SecKillServiceTest { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private SecKillService secKillService; @Test public void getSecKillList() throws Exception { List<SecKill> secKillList = secKillService.getSecKillList(); logger.info("list{}", secKillList); } @Test public void getSecKillById() throws Exception { long secKillId = 1000L; SecKill secKill = secKillService.getSecKillById(secKillId); logger.info("seckill{}", secKill); } @Test public void exportSecKillUrl() throws Exception { long secKillId = 1000L; Exposer exposer = secKillService.exportSecKillUrl(secKillId); logger.info("exposer{}", exposer); } @Test public void excuteSecKill() throws Exception { long secKillId = 1000L; String userPhone = "15300815981"; String md5 = "6e3cc65f3b42e656bdbc55a6a381f5d0"; SecKillExcution secKillExcution = secKillService.excuteSecKill(secKillId, userPhone, md5); logger.info("secKillExcution{}", secKillExcution); } }
intellj idea 下单元测试快捷键:ctrl+shift+t
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Android Kotlin/Java 动态设置 shape/drawable 等状态效果
小菜最近遇到一个小需求,程序里面有个别页面,需要动态的调整某个页面的样式,包括一键变灰等效果。 以前页面是用 shape 和 drawable 之类实现的效果。现在需要用 Kotlin/Java 代码实现动态修改。由于小菜技术浅浅,仅整理一下遇到一些坑。 日常应用的样式: 1. 圆角边框 默认 shape.xml 方式: <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"> <stroke android:width="0.5dp" android:color="@color/colorAccent" /> <corners android:radius="15dp" /> </shape> 现 Kotlin/Java 方式动态修改边框颜色: var myGrad = tv2!!.getBackground() as GradientDrawable myGrad.set...
- 下一篇
(Java)程序员应打破牢笼,展望更高层次的世界
回顾过去,我认为每个程序猿都关在一个透明的牢笼中,限制了思维、蒙蔽了眼界、蹉跎了岁月而不自知,如果不尝试走出去是一辈子都不能感知到牢笼的存在。这个牢笼就是技术本身。 一些程序员就要说,我们就是靠技术吃饭的,天天考虑各种编程技巧,技术怎么成为束缚我们的牢笼呢?那是因为很多人只是看到软件技术的表象而没看到本质。 孙子兵法说:不知兵之害者不能尽用兵之利也。套过来说,不知技术之害者不能尽用技术之利也。技术也存在有害的一面,它是程序猿谋生的工具,同时也是关着程序猿的牢笼。为什么是牢笼呢,这就涉及到技术的两个本质:社会本质和价值本质。 现在信息化社会是分裂的,一边是普通的自然人,一边是计算机,也就是机器。普通人类和机器之间存在着巨大的壁垒;人类擅长思考、创新、情感;机器擅长记忆和精确计算。人类不能理解机器,机器不能理解人类。而我们程序猿就是帮助沟通人类和机器,各种软件就是人类和机器中间挖掘出来的管道。因此在人类社会中,技术的社会本质就是挖掘管道。只不过有的管道宽敞笔直,有的像老鼠洞一样窄小曲折。 那么如何挖掘宽敞笔直的管道呢?这就涉及到技术的价值本质了。 马克思的经济学中,价值决定价格。程序猿的...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS8编译安装MySQL8.0.19
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- SpringBoot2整合Redis,开启缓存,提高访问速度
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Hadoop3单机部署,实现最简伪集群
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果