Java高并发秒杀API之业务分析与DAO层,Service层,Web层,高并发优化 过程记录 本项目是Java EE完成(jee只处理了项目创建过程,后期开发的主要过程都是在IntelliJ IDEA 15.0.2中完成) 第一期:Java高并发秒杀API之业务分析与DAO层 20170425左右开始 第二期:Java高并发秒杀API之Service层 2017-05-20开始 第三期:Java高并发秒杀API之Web层 2017-05-20开始 第四期:Java高并发秒杀API之高并发优化 2017-06-01号开始 注意:自动导包输入是大小写敏感的. 1、秒杀具备典型的"事务"特性、秒杀/红包类的需求越来越多、面试常见问题 2、学习框架的使用和整合技巧、秒杀的分析过程以及优化 3、技术介绍 MySQL: 表设计 SQL技巧 事务和行级锁 MyBatis DAO层设计与开发 MyBatis合理利用 MyBatis与Spring整合 Spring:Spring IOC整合Service 声明式事务运用 SpringMVC Restful接口设计和使用 框架运行流程 Controller开发技巧 前端:交互设计 Bootstrap jQuery 高并发:高并发点和高并发分析 优化思路并实现 4、基于Maven创建项目 从0开始创建项目 从官网获取相关配置 使用Maven创建项目 为什么从官网获取资源:文档更全民权威 避免过时或错误 官网地址:logback配置:http://logback.qos.ch/manual/configuration.html Spring配置:http://docs.spring.io/spring/docs/ MyBatis配置:http://mybatis.github.io/mybatis-3/zh/index.html 5、先安装Maven http://jingyan.baidu.com/article/1709ad808ad49f4634c4f00d.html 下载 -> 安装 -> 直接放在C盘 添加环境变量-> 生成本地仓库-> mvn help:system 命令然后回车 会在C:\Users\Administrator\.m2\repository生成文件(Administrator路径可能不能系统也会不同) 成功打印详细信息,最后一条显示MAVEN_PROJECTBASEDIR=C:\Users\Administrator 自定义用户配置 -> C:\Users\Administrator\.m2 \settings.xml (来源C盘解压目录) 6、开始创建项目 maven命令创建web骨架项目 mvn archetype:create -DgroupId=org.seckill -DartifactId=seckill - DarchetypeArtifactId=maven-archetype-webapp 使用这句创建 mvn archetype:generate -DgroupId=org.seckill -DartifactId=seckill -DarchetypeArtifactId=maven-archetype-webapp -DarchetypeCatalog=internal (高版本中使用 archetype:generate而不是 archetype:create ,参数 -DarchetypeCatalog=internal 不让执行过程卡主等待很久) 注意: 注意先不使用IDE工具创建项目,使用mvn创建最基本的项目结构后, 使用eclipse import -> Maven -> Existing Maven Projects->选择创建项目根文件夹->自动勾选pom.xml 使用IntelliJ IDEA,File —> Open 复制 目录的路径后直接打开 D:\workspace\idea\seckill (使用eclipse可能导致dependency不能提示自动补全) 依赖敲完后clean一下项目开始自动更新或者下载jar 7、处理默认创建的项目问题 切换servlet更高的版本:直接在别的项目中复制WEB-INF/web.xml 8、main文件夹下新建Java,放置Java源文件,在src目录下新建一个test文件夹并新建java、resources配置 9、查看pom.xml 把默认3.8.1的junit改成4.11版本(通过注释的方式运行junit) 继续补全依赖 日志 Java日志 slf4j,log4j,logback,common-logging slf4j: 是规范/接口 日志实现:log4j,logback,commont-logging 使用:slf4j + logback 注意:从视频中看只要添加artifactId,其他两个值可以自动提示 <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.3.0</version> </dependency> <!--2、 数据库相关依赖 --> <scope>runtime</scope> <!-- 数据库的连接池 (优化数据库操作)--> <!-- DAO框架:MYbatis依赖 (数据库持久化) --> <!-- mybatis自身实现的整合依赖 --> <!--3、 Servlet web相关依赖 --> <!-- 4、Spring依赖 --> <!-- 1)spring核心依赖 --> <!-- 2)spring dao层依赖 --> <!-- 3)spring web依赖 --> <!-- 4)spring test依赖 --> 添加完依赖后自行Ctrl B,Build ALL 10、秒杀业务分析 针对库存做处理 针对用户:减库存、记录购买明细=>完整事务=>数据落地 关于数据落地:MySQL VS NoSQL(可以理解为所有非关系型数据库 追求性能、高可用、分布式,对事务支持不是很好) 事务机制依然是目前最可靠的落地方案 11、MySQL实现秒杀的难点分析 "竞争":事务+行级锁(等待:如果高效的处理竞争) 12、秒杀功能 秒杀接口暴露(防止提前暴露秒杀地址) 执行秒杀 相关查询 13、代码开发阶段 DAO设计编码 Service设计编码 Web设计编码 14、数据库编码 --MySQL数据库使用5.7版本-- 使用之前启动服务 net start MYSQL57 (MYSQL57 是安装MySQL是设置的) 新建sql包并新建schema.sql 手写代码 注意对表属性设置的方法 ENGINE=InnoDB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COMMENT="秒杀库存表"; 创建索引 PRIMARY KEY (seckill_id), key idx_start_time(start_time), key idx_create_time(create_time), 初始化数据(values可以连续写) insert into seckill(name,number,start_time,end_time) values ('1000元秒杀iPhone6',100,'2015-11-01 00:00:00','2015-11-02 00:00:00'), ('500元秒杀iPad2',100,'2015-11-01 00:00:00','2015-11-02 00:00:00'), ('300元秒杀小米4',100,'2015-11-01 00:00:00','2015-11-02 00:00:00'), ('200元秒杀红米note',100,'2015-11-01 00:00:00','2015-11-02 00:00:00'), /*联合主键*/ PRIMARY KEY(seckill_id,user_phone), 查看创建表的语句包括语句 show create table seckill\G 15、开始DAO层实体和接口编码 Table => Entity 在java包下新建org.seckill.entity org.seckill.dao 继续创建entity下的实体Seckill SuccessKilled 继续新建dao包下的接口 SeckillDao SuccessKilledDao 16、基于MyBatis实现DAO 数据库 <=> 映射 <=> 对象 MyBatis HIBERNATE 都是工作在映射这一层 ORM(Object Relational Mapping 对象关系映射) 特点:参数 + SQL = Entity/List SQL写在哪? XML提供SQL(推荐) 注解提供SQL 如何DAO接口? Mapper自动实现DAO接口(推荐) API编程方式实现DAO接口 17、使用MyBatis编码 首先在resource文件夹下新建mybatis全局配置文件 mybatis-config.xml 再创建一个目录 mapper,放mybatis映射的文件 http://www.mybatis.org/mybatis-3/zh/getting-started.html(中文) http://www.mybatis.org/mybatis-3/getting-started.html <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> 复制这么多 <configuration> <!-- 配置全局属性 --> <settings> <setting name="" value="" /> </settings> </configuration> 18、在mapper在新建SeckillDao.xml 注意命名规范 添加 <mapper namespace="org.seckill.dao.SeckillDao"> ---指定命名空间,就是指定具体位置 <!-- 目的:为dao接口方法提供sql语句配置 --> <update id="reduceNumber"> update seckill set number = number-1 where seckill_id = #{seckillId} and start_time <![CDATA[ <= ]]> #{killTime}---<= 可能和原生语言混淆 and end_time >= #{killTime} and number>0; </update> </mapper> 这里的参数都是直接来源于org.seckill.dao.SeckillDao中的接口方法 resultType="Seckill" parameterType="long" 多个参数的话就不要指定? 一般情况下,指定类型需要指定包名+类名,这里直接使用类名,还需要配置一下 19、 <select id="queryByIdWithSeckill" resultType="SuccessKilled"> <!-- 根据id查询SuccessKilled并携带秒杀产品对象 --> <!-- 如果告诉MyBatis把结果映射到SuccessKilled同时映射seckill属性 --> <!-- 可以自由控制SQL --> select sk.seckill_id, sk.user_phone, sk.create_time, sk.state, s.seckill_id "seckill.seckill_id",---Mybatis可以省略as,加""为了防止执行混淆 s.name "seckill.name", s.number "seckill.number", s.start_time "seckill.start_time", s.end_time "seckill.end_time", s.create_time "seckill.create_time" from success_killed sk inner join seckill s on sk.seckill_id=s.seckill_id where sk.seckill_id=#{seckillId}; </select> 20、MyBatis整合Spring 整合目标 --更少的编码: 只写接口,不写实现. 接口本身能说明很多事(参数 方法名(行为) 返回类型) => SQL --更少的配置 别名:org.seckill.entity.Seckill => Seckill (package scan => Seckill,Killed....) 配置扫描: <mapper resource="mapper/SeckillDao.xml"/> => 自动扫描配置文件 dao实现: <bean id="ClubDao" class="...ClubDao"/> => 自动实现DAO接口/自动注入Spring容器 --足够的灵活性 自己定制SQL 只有传参 结果集自动赋值 XML提供SQL DAO接口Mapper 21、开始配置整合mybatis过程 在resource下新建spring,并生成spring-dao.xml http://docs.spring.io/spring/docs/current/spring-framework-reference/ 下载pdf后搜 7.2 Container overview 这个头部信息不够完整,实测可运行的如下 <?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" 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"> </beans> 22、开始单元测试 (先确认数据库服务已经启动) 新建test.java.org.seckill.dao 并新建SeckillDaoTest.java 23、注意 // org.apache.ibatis.binding.BindingException: Parameter 'offset' not found. Available parameters are [0, 1, param1, param2] // List<Seckill> queryAll(int offset,int limit); // java 没有保存新参的记录queryAll(int offset,int limit)->queryAll(arg0,arg1) //改成 List<Seckill> queryAll(@Param("offset") int offset, @Param("limit") int limit); // (多个参数时需要配置) 第一期结束 24、Java高并发秒杀API之Service层开始 -2017-05-20 DAO编码之后的思考:DAO层工作演变为:接口设计+SQL编写 DAO(Data Access Object) 数据访问对象是一个面向对象的数据库接口 代码和SQL的分离,方便Review DAO拼接等逻辑在Service层完成 25、在org.seckill中新建service和exception包 再创建dto包:数据传输层 (web和service之间的数据传递,entity是业务相关) 26、在service下新建SeckillService的接口 业务接口:站在"使用者"的角度设计接口 1、方法定义粒度 2、参数 3、返回类型(return 类型/异常) 在dto中新建Exposer类 -> 暴露秒杀地址DTO 在dto中新建SeckillExecution类 -> 封装秒杀执行后结果 在exception中新建RepeatKillException类 extends RuntimeException -> 重复秒杀异常 在exception中新建SeckillCloseException类 extends RuntimeException -> 秒杀关闭异常 在exception中新建SeckillException类 extends RuntimeException -> 秒杀相关业务的异常 将前两个异常继承改成SeckillException 27、写接口的实现 在service下新建impl,并新建SeckillServiceImpl 实现SeckillService 并具体实现对应的方法 生成md5 private String getMD5(long seckillId) { String base = seckillId + "/" + salt; String md5 = DigestUtils.md5DigestAsHex(base.getBytes());//DigestUtils有Spring提供 return md5; } //所有编译期异常转换成运行期异常? //运行期异常会rollback } catch (SeckillCloseException e1) {//先catch一些异常 throw e1; } catch (RepeatKillException e2) { throw e2; } catch (Exception e) { logger.error(e.getMessage(), e); //所有编译期异常转换成运行期异常 运行期异常会rollback throw new SeckillException("seckill inner error" + e.getMessage()); } 28、新建enums包存放常量 SeckillStateEnum 注意枚举中可是使用 values()得到所有的枚举常量 29、基于Spring管理Service依赖 Spring IOC功能理解:依赖注入 控制反转(Inversion of Control,英文缩写为IoC) 对象工厂 + 依赖管理 -> 一致的访问接口 业务对象依赖图 ↗ SeckillDao ↘ //体现在SeckillServiceImpl.java SeckillService < > SqlSessionFactory ↘ SuccessKilledDao ↗ ↓ ↓ DataSource... 为什么使用IOC 对象创建统一托管 规范的生命周期管理 灵活的依赖注入 一致的获取对象(默认都是单例singleton) Spring-IOC注入方式和场景 XML:1、Bean实现来自第三方类库,如:DataSource等;2,需要命名空间配置,如:context,aop,mvc等 注解:项目中自身开发使用的类,可直接在代码中使用注解如:@Service,@Controller等 Java配置类:需要通过代码控制对象创建逻辑的场景 如:自定义修改依赖类库 本项目IOC使用 XML配置 -> package-scan -> Annotation注解 30、在spring包下新建spring-service.xml new -> XML Configuration File -> Spring Config -> 创建并自动生成头部信息 但是头部信息好像不全,所以最好还是复制。 <context:component-scan base-package="org.seckill.service"/> <!--扫描service包下所有使用注解的类型--> 再到service实现类中添加注解 SeckillServiceImpl.java //@Component(统称) @Service @Dao @Controller @Autowired // @Resource @Inject 31、Spring声明式事务 什么是生命式事务(解脱事务代码,交给第三方框架) 开始事务 -> 修改SQL1 -> 修改SQL2 -> 修改SQL3 -> 修改SQLn -> 提交/回滚事务 声明式事务使用方式 ProxyFactoryBean + XML --> 早期使用方式(2.0) tx:advice+aop命名空间 --> 一次配置永久生效 注解 @Transactional --> 注解控制(推荐) 事务方法嵌套 声明式事务独有的概念 传播行为 -> propagation_required(新事务加入到原有事务中,没有就创建新事务) ... 什么时候回滚事务 抛出运行期异常(RuntimeException) 小心不当的try-catch(Spring没有感觉到异常) 32、配置Spring声明式事务 在spring-service.xml中配置 然后再SeckillServiceImpl.java中的executeSeckill()方法添加@Transactional /** *使用注解控制事务方法的优点 *1:开发团队一致约定,明确标注事务方法的编码风格 *2:保证事务方法的执行时间尽可能短,不要穿插其他的网络操作,RPC/HTTP请求或者剥离到事务方法外部 *3:不是所有的方法都需要事务,如只有一条修改操作,只读操作不需要事务控制(注:这个说法跟其他的视频介绍的优点不同) */ 33、单元测试 配置Ctrl + Shift + 创建单元测试类 http://blog.csdn.net/jiaotuwoaini/article/details/52767007 在SeckillService.java的类名后Ctrl+Shift+T,创建单元测试类(未生效) 手动创建 org.seckill.service包和SeckillServiceTest类 给Test类添加注解 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration({ "classpath:/spring/spring-dao.xml", "classpath:/spring/spring-service.xml"}) 注意多个xml实验数组(Java的数据初始化是{}) slf4j的日志工具 import org.slf4j.Logger; import org.slf4j.LoggerFactory; private Logger logger = LoggerFactory.getLogger(this.getClass()); 相关配置 https://logback.qos.ch/manual/configuration.html 在resources下新建logback.xml 复制(并加入XML头) <configuration debug="true"> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <!-- encoders are by default assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder --> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="debug"> <appender-ref ref="STDOUT" /> </root> </configuration> 34、执行测试 testExportSeckillUrl()注意seckill表确定数据中数据(开始时间、结束时间) logger.info("seckillExecution----------------{}", seckillExecution); //返回的一直是null 和 {},具体问题原因未知,toSting()都写了 新增一个单元测试方法合并 testExportSeckillUrl() testExecuteSeckill() 为testSeckillLogic() Java高并发秒杀API之Service层 2017-05-20结束 35、Java高并发秒杀API之Web层 2017-05-20开始 前端交互设计 Restful SpringMVC Bootstrap+jQuery 前端页面流程: 列表页 → 详情页 ↓ 登录操作 ← login ↓ ↓ 写入Cookie → 展示逻辑 详情页流程逻辑: 1:获取标准系统时间 ↓ 4: 3: ← 倒计时 ← 2:时间判断 执行 ← 秒杀 开始时间 秒杀 秒杀 地址 ← 结束时间 → 结束 ↓ 结束 36、什么是Restful 兴起于Rails 一种优雅的URI表述方式 资源的状态和状态转移 GET /seckill/list POST /seckill/execute/{seckillId} ╳ /seckill/{seckillId}/execution √(资源的状态) DELETE /seckill/{id}/delete GET -> 查询操作 POST -> 添加/修改操作 PUT -> 修改操作 DELETE -> 删除操作 URL设计 /模块/资源/{标示}/集合1/... /user/{uid}/friends -> 好友列表 /user/{uid}/followers -> 关注着列表 秒杀API的URL设计 GET /seckill/list 秒杀列表 GET /seckill/{id}/detail 详情页 GET /seckill/time/now 系统时间 POST /seckill/{id}/exposer 暴露秒杀 POST /seckill/{id}/{md5}/execution 执行秒杀 37、使用SpringMVC框架理论 围绕Handlder开发 数据Model Handler → 页面View 38、HTTP请求地址映射原理 Servlet容器(Tomcat,Jetty等) Http请求 → SpringMVC Handler Mapping注解,XML编程等 → Handler处理方法 注解映射技巧 @RequestMapping注解: (1)支持标准的URL (2)Ant风格URL (?和*和**等字符) (3)带(xxx)占位符的URL. 例如 /user/*/creation 匹配/user/aaa/creation、/user/bbb/creation等URL /user/**/creation 匹配/user/creation、/user/aaa/bbb/creation等URL /user/{userId} 匹配/user/123、/user/abc等URL /company/{companyId}/user/{userId}/detail 匹配/company/123/user/456/detail等URL 请求方法细节处理 1、请求参数绑定 2、请求方式限制 3、请求转发和重定向 4、数据模型赋值 5、返回json数据 6、cookie访问 例 子 @RequestMapping(value = "/{seckill}/detail",method=RequestMethod.GET) public String detail(@PathVariable("seckill") Long seckillId,Model model){ if(seckillId==null){ return "redirect:/seckill/list"; } Seckill seckill = seckillService.getById(seckill); if(seckill==null){ return "forward:/seckill/list"; } model.addAttribute("seckill",seckill);//model return "detail";//view } 返回json数据 @RequestMapping(value = "/{seckill}/detail",method=RequestMethod.POST,produces={ "application/json;charset=UTF-8" }) @ResponseBody public SeckillResult<Exposer> exportSeckillURL(@PathVariable("id") long id){ SeckillResult<Exposer> result; try{ Exposer exposer=... }catch(Exception e){ logger.error(e.getMessage(),e); result = new SeckillResult<Exposer>(false,e.getMessage()); } return result; } Cookie访问 @RequestMapping(value="/{seckillId}/{md5}/execution",method=RequestMethod.POST) public SeckillResult<SeckillExecution> execute( @PathVariable("seckillId") long seckillId, @PathVariable("md5") String md5, @CookieValue(value="killPhone",required=false) Long phone){ if(phone == null){ return new SeckillResult<SeckillExecution>(false,"未注册电话"); } SeckillExecution execution = seckillService.executeSeckillKillByProcedure(seckillId,phone,md5); SeckillResult<SeckillExecution> result = new SeckillResult<SeckillExecution>(true,execution); return result; } 39、整合配置SpringMVC框架 首先打开 webapp/WEB_INF/web.xml 整合顺序 MyBatis -> spring -> springMVC 新建spring-web.xml 注意这条命名空间不要选错 xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context" 40、实现Restful的API 新建org.seckill.web包并新建SeckillController包 @Controller // @Service @Component @RequestMapping("/seckill")// URL:/模块/资源/{id}/细分 /seckill/list @RequestMapping(value = "/list", method = RequestMethod.GET) public String list(Model model) { //list.jsp+model=ModelAndView return null; } 并写好详情页的接口 然后写返回秒杀地址的ajax接口,返回json 在dto中新建SeckillResult<T> 泛型 用于封装json结果 //重点理解如何转换成json的 ---暴露秒杀地址--- //ajax 返回json @RequestMapping(value = "/{seckillId}/exposer", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"}) @ResponseBody public SeckillResult<Exposer> exposer(Long seckillId) { SeckillResult<Exposer> result; try { Exposer exposer = seckillService.exportSeckillUrl(seckillId); result = new SeckillResult<Exposer>(true, exposer); } catch (Exception e) { logger.error(e.getMessage(), e); result = new SeckillResult<Exposer>(false, e.getMessage()); } return result; } ---执行秒杀接口--- @RequestMapping(value = "/{seckillId}/{md5}/execution", method = RequestMethod.POST, produces = {"application/json;charset:UTF-8"}) @ResponseBody public SeckillResult<SeckillExecution> execute( @PathVariable("seckillId") Long seckillId, @PathVariable("md5") String md5, @CookieValue(value = "killPhone", required = false) Long phone) { //这里设置 required = false防止报错,验证放在程序中 //也可使用springMVC valid来验证 适合很多参数 if (phone == null) { return new SeckillResult<SeckillExecution>(false, "未注册"); } SeckillResult<SeckillExecution> result; try { SeckillExecution seckillExecution = seckillService.executeSeckill(seckillId, phone, md5); return new SeckillResult<SeckillExecution>(true, seckillExecution); } catch (RepeatKillException e) {//注意处理异常的优先级 SeckillExecution execution = new SeckillExecution(seckillId, SeckillStateEnum.REPEAT_KILL); return new SeckillResult<SeckillExecution>(false, execution); } catch (SeckillCloseException e) { SeckillExecution execution = new SeckillExecution(seckillId, SeckillStateEnum.END); return new SeckillResult<SeckillExecution>(false, execution); } catch (Exception e) { logger.error(e.getMessage(), e); SeckillExecution execution = new SeckillExecution(seckillId, SeckillStateEnum.INNER_ERROR); return new SeckillResult<SeckillExecution>(false, execution); } } ---获取系统时间--- 完成public SeckillResult<Long> time() now.getTime());//注意这里需要getTime() 41、 使用bootstrap开发前端页面 环境安装http://www.runoob.com/bootstrap/bootstrap-environment-setup.html 拷贝HTML模板,新建jsp/list.jsp jsp/detail.jsp 将要CDN改成推荐版本(链接) 去掉css和主题,并将引用改到</body>后面 新建common文件夹存放通用的jsp, /common/head.jsp ---静态包含---(动态包含会有多个servlet) 新建common文件夹存放通用的jsp, /common/tag.jsp 完成list.jsp 42、 运行测试 不能连接数据库要注意执行 net start mysql启动数据库服务 http://localhost:8080/seckill/list 修改网页代码后也需要重新编译 Build->Build Artifacts->选择一个war, 不重启服务器 43、 在detail.jsp中实现cookie登录交互,首先完成相应的布局 从http://www.bootcdn.cn/获取jQuery cookie操作插件和countDown倒计时插件 开始编写交互逻辑 在webapp下新建resources/script/seckill.js $.post(killUrl, {}, function (result) { } );//post请求,但是把参数都放在url上面了. 针对重复秒杀的情况 出现系统异常的恢复 json不显示 44、 第三期:Java高并发秒杀API之Web层 2017-05-31号完成 第四期:Java高并发秒杀API之高并发优化 2017-06-01号开始 可能出现高并发的点 详情页 系统时间 地址暴露接口 执行秒杀操作 为什么要单独获取系统时间 用户大量刷新->CDN(detail页静态化 静态资源css,js等)->秒杀系统 获取系统时间不用优化 访问一次内存(Cacheline)大约10ns(1秒钟可执行1亿次) 秒杀地址接口分析 无法使用CDN,适合服务端缓存:redis等,一致性维护成本低 秒杀地址接口优化 请求地址->redis->Mysql(两者一致性维护:超时穿透/主动更新) 秒杀操作优化分析 无法使用cdn,后端缓存困难:库存问题,一行数据竞争:热点商品 其他方案分析 执行秒杀 原子计数器 -> 技术 -> redis/NoSql | 记录行为消息 -> 技术 -> 分布式MQ | 消费消息并落地 -> 技术 -> MySql 成本分析:运维成本和稳定性:NoSQL,MQ等 开发成本:数据一致性,回滚方案等,幂等性难保证:重复秒杀问题,不适合新手 为什么不用MySQL解决?认为MySql低效。MySql真的低效? 测试:一条update压力测试(约4wQPS) Java控制事务行为分析 sql->insert购买明细->等待行锁...... 瓶颈分析: update减库存-> 网络延迟、GC -> insert购买明细 -> 网络延迟、GC -> commit/rollback 优化分析: 行级锁是在Commit之后释放 -> 优化方向减小行级锁持有时间 -> 延迟分析 同城机房: 异地机房:北京到上海总距离1300公里,真空中30w公里/s,光在玻璃的传播速度是真空中的2/3, 往返距离1300*2=2600公里,1300* 2/(300000 * 2 / 3)=13毫秒 实际在20毫秒左右,QPS最好只有50(只考虑一次网络连接时间) 如何判断Update更新库存成功 两个条件:1、Update自身没有错 2、客户端确认Update影响记录数 优化思路:把客户端逻辑放到MySql服务器,避免网络延迟和GC影响 如何放在Mysql服务器? 两种解决方案: -定制SQL方案:update/*+[auto_commit]*/ 需要修改MySql源码 -使用存储过程:整个事务在MySql端完成 优化总结 前端控制:暴露接口、按钮防重复 动静态数据分离:CDN缓存(好像有API推送),后端缓存(redis、memcache) 事务竞争优化:减少事务锁时间(好像是把几条语句放在一起执行,相当于一条语句执行,(一秒钟可以执行几万次) 这样同时减少了网络连接过程中的延迟时间) 45、开始秒杀优化 redis后端缓存优化 使用redis优化"地址暴露接口" redis官方不支持windows,但是window开放技术团队开发支持了redis的应用 本次测试在linux下配置redis服务 下载->解压->进入->make->make install->redis-server->redis-cli -p 6379(默认端口可省略)->info 查看运行情况 打开pom.xml,引入Java访问redis客户端的jar包https://redis.io/clients 注意redis-server 和 redis-cli 没有空格 打开SeckillServiceImpl.java 开始优化exportSeckillUrl() 在dao中新建cache包和RedisDao类 配置protostuff序列化依赖(空间、速度、cpu) Java序列化性能对比https://github.com/eishay/jvm-serializers/wiki protostuff的简单实用http://blog.csdn.net/canot/article/details/53750443 RedisDaoTest的单元测试 spring-dao.xml中配置 <!--RedisDao--> <bean id="redisDao" class="org.seckill.dao.cache.RedisDao"> <constructor-arg index="0" value="192.168.0.107"/><!--ifconfig localhost 192.168.0.107--> <constructor-arg index="1" value="6379"/> <!--JedisDataException: DENIED Redis is running in protected mode because protected mode is.... http://blog.csdn.net/u010003835/article/details/52853574 先redis-cli ,然后执行CONFIG SET protected-mode no--> </bean> 执行dbsize查看已经写入的缓存条数 执行keys * 查看所有的key 执行get xxxkey 查看被序列化的值 单元测试通过: 开始优化exportSeckillUrl()方法 46、 简单优化 调整减库存和插入购买明细操作的顺序 insert购买明细 -> 网络延迟/GC -> update减库存rowLock -> 网络延迟/GC -> commit/rollback freeLock 进入SeckillServiceImpl.java 中的executeSeckill() //先屏蔽一部分重复秒杀的请求 深度优化 事务SQL在MySQL端执行(存储过程) 减少网络延迟的影响,提高TPS 在sql包中创建sckill.sql 连接数据库mysql -uadmin -padmin -Dseckill 分别执行 DELIMITER $$ 和 定义存储过程 部分 再执行show create procedure execute_seckill\G 执行DELIMITER ; --还原 执行set @r_result=-3; 并查看一下数据 select @r_result; 执行存储过程前查看数据 select * from seckill where seckill_id=1001\G mysql> select * from seckill where seckill_id=1001\G *************************** 1. row *************************** seckill_id: 1001 name: 500元秒杀iPad2 number: 198 start_time: 2017-06-01 23:33:35 end_time: 2017-07-19 00:00:00 create_time: 2017-05-02 22:27:23 1 row in set (0.00 sec) 查看秒杀成功表中的1003的数据或者直接删除所有1003的数据 select * from success_killed; delete from success_killed where seckill_id = 1001; 执行存储过程 call execute_seckill(1001,13502172008,now(),@r_result); 查看输出 select @r_result; 返回 1 查看成功记录表 select * from success_killed; 继续执行存储过程 并查看返回值,返回 -1,并确定成功记录表和商品数量表中数据没有变化 总结 -- 存储过程 -- 1:存储构成优化:事务行级锁私持有时间 -- 2:不要过度依赖存储过程 -- 3: 简单的逻辑可以应用存储过程(银行业使用,一般的互联网应用不用) -- 4:QPS:一个秒杀单6000/qps 47、seckill项目调用存储过程代码实现 SeckillService.java中定义一个新接口executeSeckillProcedure(),不需要抛出异常 并在SeckillServiceImpl.java中实现这个接口 会在SeckillDao.java中新建void killByProcedure(Map<String,Object> paramMap); 并在SeckillDao.xml中写sql 继续实现接口(其中会添加依赖 commons-collections MapUtils获得工具) 写单元测试testExecuteSeckillProcedure() 注意SeckillDao.xml中的call execute_seckill, 这个"存储过程"就是seckill.sql中创建并在命令行中执行的,而不是后期在代码中实现!!! 通过Navicat for MySQL工具连接数据库后,数据库中并没有execute_seckill表,而是有一个execute_seckill函数 单元测试通过后修改SeckillController中的execute()方式 调用执行秒杀调用executeSeckillProcedure() 重新编译后启动web测试效果 48、大型系统部署架构 系统会用到哪些服务? CDN WebServer:Nginx+Jetty Redis MySQL 系统部署的架构应该是什么样的? 智能DNS解析 ← 流量 Nginx Nginx ↓ ↓ CDN缓存 逻辑集群 Jetty Jetty Jetty → 缓存集群 ↓ Redis Redis ↓mod(seckillId) 分库分表 → 统计分析 DB-1 DB-2 DB-3 分库分表框架:阿里巴巴阿里巴巴中间件TDDL 可能参与的角色? 开发:前端+后端 测试 DBA 运维:机器、监控、初始化脚本、Nginx之间配置 49、课程总结 ---数据库设计和实现 ---MyBatis理解和使用技巧 ---MyBatis整合Spring技巧(自动包扫描、sql文件扫描、别名识别,做到一次配置之后不需要修改) 业务层技术回顾 ---业务接口设计和封装(站在使用者的角度去设计) ---SpringIOC配置技巧:声明式事务:xml配置,自己开发的dao、service、controller:使用注解 ---Spring声明式事务使用和理解:什么情况下回滚,什么情况下提交 Web技术回顾 ---Restful接口运用(规范和理解:描述一个资源,通过不同的提交方式来达到描述行为的目的,写通过POST,读通过GET) ---SpringMVC使用技巧 (如何配置 参数如何映射 如何打包成json返回给浏览器 (返回json是通过Spring框架(包package org.springframework.web.bind.annotation;),而不是Restful,Resultful只是一种设计规范)) ---前端交互分析过程 ---Bootstrap和JS使用 ---系统瓶颈点分析 ---事务,锁,网络延迟理解(还有GC), ---前端,CDN,缓存等理解使用 ---集群化部署 2017-06-06 21:11 学习结束 https://tech.jiangjiesheng.cn/dev/study/java/seckill/study-record.html
有任何疑问或技术合作都可联系我
微信:yanfahezuo 【推荐】
QQ:596957738