SSM框架学习之高并发秒杀业务--笔记5-- 并发优化
Posted f91og
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SSM框架学习之高并发秒杀业务--笔记5-- 并发优化相关的知识,希望对你有一定的参考价值。
前几节终于实现了这个高并发秒杀业务,现在问题是如何优化这个业务使其能扛住一定程度的并发量。
一. 优化分析
对于整个业务来说,首先是分析哪些地方会出现高并发,以及哪些地方会影响到了业务的性能。可能会出现高并发的地方:详情页,获取系统时间,地址暴露接口,执行秒杀操作。
这个业务为什么要单独获取时间呢?用户会在详情页大量刷新,为了优化这里,将detal.jsp详情页和一些静态资源(css,js等)部署在CDN的节点上(至于这个CDN是什么,下面会说),也就是说用户访问详情页是不需要访问我们的系统的,这样就降低了服务器的负荷,但这个时候就拿不到系统的时间了,所以要单独做一个请求来获取当前的系统时间。
那么什么是CDN呢,content distribute network 内容分发网络,本质上是一种加速用户获取数据的系统,把一些用户频繁访问的静态资源部署在离用户最近的网络节点上,关于CDN的具体解释可以见这篇博文:http://blog.csdn.net/coolmeme/article/details/9468743。对于获取系统时间这个操作,因为java访问一次内存大约10ns,所以不需要优化。
对于秒杀地址接口,因为是经常变化的,所以不适合部署在CDN上,要部署在我们服务器的系统上,这里要用到服务器端缓存如:redis缓存服务器来进行优化。
redis缓存服务器:Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。关于Redis,他是一个内存数据库,即将硬盘上的部分数据缓存在内存中。对内度的读取的速度要远快于对硬盘的读取,记得我们数据库老师以前和我们说过,对于数据库的设计而言,优化的最核心的部分是如何减少对磁盘的IO操作,因为磁盘的IO是其实就是硬盘的磁头在读磁片上的磁道,这是个机械运动,速度要远慢于内存的读写。关于redis的一些知识,还要深入学习才行。
对于秒杀操作,这个是整个业务最核心的东西,不可能部署在CDN上,也不能使用redis缓存服务器,因为不可能在缓存中取减库存,要在mysql中操作,否则会产生数据不一致的情况。老师也说了一些其他的优化方案,不过我听不懂就是了,什么原子计数器,分布式MQ,消费消息并落地之类的。貌似和分布式系统有关?不明白啊,还得好好去学,先知道有这个东西先。
对于并发程序来说,拖慢速度的关键是事务控制,涉及到数据库中的行级锁,优化方向是:如何减少行级锁的持有时间。那么优化思路是:将客户端逻辑放到MySql服务端,同时避免网络延迟和GC(垃圾回收)的影响。具体说就是把在客户端中的事务控制放在MySql服务端。具体方式就是使用存储过程,使整个事务在到MySql端完成。什么是存储过程:在大型数据库系统中,一组为了完成特定功能的SQL 语句集,存储在数据库中,经过第一次编译后再次调用不需要再次编译,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。具体见:存储过程简介。
二. 具体优化
1.redis后端缓存优化编码
redis的下载和安装,以及如何使用Redis的官方首选Java开发包Jedis:Windows下Redis的安装使用。
在dao包下新建cache目录,新建RedisDao类,用于访问我们的redis。
RedisDao.java
1 package org.seckill.dao.cache; 2 3 import com.dyuproject.protostuff.LinkedBuffer; 4 import com.dyuproject.protostuff.ProtobufIOUtil; 5 import com.dyuproject.protostuff.ProtostuffIOUtil; 6 import com.dyuproject.protostuff.runtime.RuntimeSchema; 7 import org.seckill.entity.Seckill; 8 import org.slf4j.Logger; 9 import org.slf4j.LoggerFactory; 10 import redis.clients.jedis.Jedis; 11 import redis.clients.jedis.JedisPool; 12 13 /** 14 * Created by yuxue on 2016/10/22. 15 */ 16 public class RedisDao { 17 private final Logger logger= LoggerFactory.getLogger(this.getClass()); 18 private final JedisPool jedisPool; 19 20 private RuntimeSchema<Seckill> schema=RuntimeSchema.createFrom(Seckill.class); 21 22 public RedisDao(String ip, int port ){ 23 jedisPool=new JedisPool(ip,port); 24 } 25 26 public Seckill getSeckill(long seckillId) { 27 //redis操作逻辑 28 try{ 29 Jedis jedis=jedisPool.getResource(); 30 try { 31 String key="seckill:"+seckillId; 32 //并没有实现序列化机制 33 //get->byte[]->反序列化->Object(Seckill) 34 //采用自定义序列化 35 //protostuff : pojo. 36 byte[] bytes=jedis.get(key.getBytes()); 37 //缓存获取到 38 if(bytes!=null){ 39 //空对象 40 Seckill seckill=schema.newMessage(); 41 ProtostuffIOUtil.mergeFrom(bytes,seckill,schema); 42 //seckill被反序列化 43 return seckill; 44 } 45 }finally { 46 jedis.close(); 47 } 48 }catch (Exception e){ 49 logger.error(e.getMessage(),e); 50 } 51 return null; 52 } 53 54 public String putSeckill(Seckill seckill){ 55 // set Object(Seckill) -> 序列化 ->发送给redis 56 try{ 57 Jedis jedis=jedisPool.getResource(); 58 try{ 59 String key="seckill:"+seckill.getSeckillId(); 60 byte[] bytes=ProtostuffIOUtil.toByteArray(seckill,schema, 61 LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE)); 62 //超时缓存 63 int timeout=60*60;//1小时 64 String result=jedis.setex(key.getBytes(),timeout,bytes); 65 return result; 66 }finally{ 67 jedis.close(); 68 } 69 }catch (Exception e){ 70 logger.error(e.getMessage(),e); 71 } 72 return null; 73 } 74 75 }
这里有个优化点是:redis并没有实现对象的序列化,需要我们自己手动去序列化对象,当然这里可以让对象实现Serializable接口,也就是用jdk提供的对象序列化机制。但是这里为了优化这个目的,我们需要一个速度更快得序列化机制,所以老师这里用的是基于谷歌Protobuff的ProtoStuff序列化机制。
ProtoStuff的依赖
<!--protostuff序列化依赖--> <dependency> <groupId>com.dyuproject.protostuff</groupId> <artifactId>protostuff-core</artifactId> <version>1.0.8</version> </dependency> <dependency> <groupId>com.dyuproject.protostuff</groupId> <artifactId>protostuff-runtime</artifactId> <version>1.0.8</version> </dependency>
在spring-dao.xml配置RedisDao
1 <!--RedisDao--> 2 <bean id="redisDao" class="org.seckill.dao.cache.RedisDao"> 3 <constructor-arg index="0" value="localhost"/> 4 <constructor-arg index="1" value="6379"/> 5 </bean>
修改SeckillServiceImpl.java为
1 package org.seckill.service.impl; 2 3 import org.apache.commons.collections.MapUtils; 4 import org.seckill.dao.SeckillDao; 5 import org.seckill.dao.SuccesskilledDao; 6 import org.seckill.dao.cache.RedisDao; 7 import org.seckill.dto.Exposer; 8 import org.seckill.dto.SeckillExecution; 9 import org.seckill.entity.Seckill; 10 import org.seckill.entity.SuccessKilled; 11 import org.seckill.enums.SeckillStatEnum; 12 import org.seckill.exception.RepeatKillException; 13 import org.seckill.exception.SeckillCloseException; 14 import org.seckill.exception.SeckillException; 15 import org.seckill.service.SeckillService; 16 import org.slf4j.Logger; 17 import org.slf4j.LoggerFactory; 18 import org.springframework.beans.factory.annotation.Autowired; 19 import org.springframework.stereotype.Service; 20 import org.springframework.transaction.annotation.Transactional; 21 import org.springframework.util.DigestUtils; 22 23 import java.util.Date; 24 import java.util.HashMap; 25 import java.util.List; 26 import java.util.Map; 27 28 /** 29 * Created by yuxue on 2016/10/15. 30 */ 31 @Service 32 public class SeckillServiceImpl implements SeckillService { 33 private Logger logger = LoggerFactory.getLogger(this.getClass()); 34 35 @Autowired 36 private SeckillDao seckillDao; 37 38 @Autowired 39 private SuccesskilledDao successkilledDao; 40 41 @Autowired 42 private RedisDao redisDao; 43 44 //md5盐值字符串,用于混淆MD5 45 private final String salt = "fsladfjsdklf2jh34orth43hth43lth3"; 46 47 public List<Seckill> getSeckillList() { 48 return seckillDao.queryAll(0, 4); 49 } 50 51 public Seckill getById(long seckillId) { 52 return seckillDao.queryById(seckillId); 53 } 54 55 public Exposer exportSeckillUrl(long seckillId) { 56 //优化点:缓存优化,超时的基础上维护一致性 57 //1.访问redis 58 Seckill seckill = redisDao.getSeckill(seckillId); 59 if (seckill == null) { 60 //2.若缓存中没有则访问数据库 61 seckill = seckillDao.queryById(seckillId); 62 if (seckill == null) { 63 return new Exposer(false, seckillId); 64 } else { 65 //3.放入redis 66 redisDao.putSeckill(seckill); 67 } 68 } 69 Date startTime = seckill.getStartTime(); 70 Date endTime = seckill.getEndTime(); 71 Date nowTime = new Date(); 72 if (nowTime.getTime() < startTime.getTime() || 73 nowTime.getTime() > endTime.getTime()) { 74 return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime()); 75 } 76 //转化特定字符串的过程,不可逆 77 String md5 = getMD5(seckillId); 78 return new Exposer(true, md5, seckillId); 79 } 80 81 private String getMD5(long seckillId) { 82 String base = seckillId + "/" + salt; 83 String md5 = DigestUtils.md5DigestAsHex(base.getBytes()); 84 return md5; 85 } 86 87 @Transactional 88 /* 89 * 使用注解控制事务方法的优点: 90 * 1:开发团队一致的约定 91 * 2:保证事务方法的执行时间经可能的短,不要穿插其他网络操作RPC/HTTP请求或者剥离到事务方法外部,使得 92 * 这个事务方法是个比较干净的对数据库的操作 93 * 3:不是所有的方法都需要事务,如只有一条修改操作,只读操作不需要事务控制 94 * */ 95 public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, 96 RepeatKillException, SeckillCloseException { 97 if (md5 == null || !md5.equals(getMD5(seckillId))) { 98 throw new SeckillException("seckill data rewrite"); 99 } 100 //执行秒杀逻辑:减库存+记录购买行为 101 Date nowTime = new Date(); 102 103 try { 104 //记录购买行为 105 int insertCount = successkilledDao.insertSucessSeckilled(seckillId, userPhone); 106 //唯一:seckillId,userPhone 107 if (insertCount <= 0) { 108 //重复秒杀 109 throw new RepeatKillException("seckill repeated"); 110 } else { 111 //减库存,热点商品竞争 112 int updateCount = seckillDao.reduceNumber(seckillId, nowTime); 113 if (updateCount <= 0) { 114 //没有更新到记录,秒杀结束,rollback 115 throw new SeckillCloseException("seckill is close"); 116 } else { 117 //秒杀成功,commit 118 SuccessKilled successKilled = successkilledDao.queryByIdWithSeckill(seckillId, userPhone); 119 return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled); 120 } 121 } 122 } catch (SeckillCloseException e1) { 123 throw e1; 124 } catch (RepeatKillException e2) { 125 throw e2; 126 } catch (Exception e) { 127 logger.error(e.getMessage()); 128 throw new SeckillException("seckill inner error" + e.getMessage()); 129 } 130 } 131 132 public SeckillExecution executeSeckillProdure(long seckillId, long userPhone, String md5){ 133 if (md5 == null || !md5.equals(getMD5(seckillId))) { 134 return new SeckillExecution(seckillId, SeckillStatEnum.DATA_REWRITE); 135 } 136 Date killTime=new Date(); 137 Map<String,Object> map=new HashMap<String, Object>(); 138 map.put("seckillId",seckillId); 139 map.put("phone",userPhone); 140 map.put("killTime",killTime); 141 map.put("result",null); 142 try{ 143 seckillDao.killByProcedure(map); 144 //获取result 145 int result= MapUtils.getInteger(map,"result",-2); 146 if(result==1){ 147 SuccessKilled sk=successkilledDao. 148 queryByIdWithSeckill(seckillId,userPhone); 149 return new SeckillExecution(seckillId,SeckillStatEnum.SUCCESS,sk); 150 }else{ 151 return new SeckillExecution(seckillId,SeckillStatEnum.stateOf(result)); 152 } 153 }catch (Exception e){ 154 logger.error(e.getMessage(),e); 155 return new SeckillExecution(seckillId,SeckillStatEnum.INNER_ERROR); 156 } 157 } 158 }
对于暴露秒杀接口exportSeckillUrl这个方法,原本是直接从数据库中取Seckill对象的,现在优化为先在Redis缓存服务器中取,如果没有则去数据库中取,并将其放入Redis缓存中。这里还有个优化点就是在执行秒杀executeSeckill方法中
将insert操作放到了update之前。
2. 利用存储过程
对于现在的update操作,还是在客户端控制事务的,为了进一步优化,现在将update的操作逻辑放在Mysql端来执行,也就是利用存储过程来完成商品更新操作,减少行级锁的持有时间。
在src/main/sql目录下新建seckill.sql, 编写存储过程
1 -- 秒杀执行存储过程 2 DELIMITER $$ -- console ; 转换为 $$ 3 -- 定义存储过程 4 -- 参数:in 输入参数;out 输出参数 5 -- row_count():返回上一条修改类型sql(delete, insert,update)的影响行数 6 -- row_count: 0:未修改;>0:表示修改的行数;<0:sql错误/未执行 7 CREATE PROCEDURE `seckill`.`execute_seckill` 8 (in v_seckill_id bigint, in v_phone bigint, 9 in v_kill_time timestamp,out r_result int) 10 BEGIN 11 DECLARE insert_count int DEFAULT 0; 12 START TRANSACTION ; 13 insert ignore into seccess_killed 14 (seckill_id,user_phone,create_time) 15 values (v_seckill_id,v_phone,v_kill_time); 16 select row_count() into insert_count; 17 IF (insert_count=0) THEN 18 ROLLBACK ; 19 set r_result=-1; 20 ELSEIF (insert_count<0) THEN 21 ROLLBACK ; 22 set r_result=-2; 23 ELSE 24 update seckill 25 set number=number-1 26 where seckill_id=v_seckill_id 27 and end_time>v_kill_time 28 and start_time<v_kill_time 29 and number>0; 30 select row_count() into insert_count; 31 IF (insert_count=0) THEN 32 ROLLBACK ; 33 set r_result=0; 34 ELSEIF (insert_count<0) THEN 35 ROLLBACK ; 36 set r_result=-2; 37 ELSE 38 COMMIT; 39 set r_result=1; 40 END IF; 41 END IF; 42 END; 43 $$ 44 -- 存储过程定义结束 45 46 DELIMITER ; 47 -- 48 set @r_result=-3; 49 -- 执行存储过程 50 call execute_seckill(1004,13225534035,now(),@r_result); 51 -- 获取结果 52 select @r_result; 53 54 -- 存储过程 55 -- 1:存储过程优化:事务行级锁持有时间 56 -- 2:不要过度依赖存储过程 57 -- 3:简单的逻辑可以应用存储过程 58 -- 4:QPS:一个秒杀单6000/qps
在Service层和dao层分别定义调用存储过程的接口,然后在Mybatis中配置调用存储过程
1 package org.seckill.service; 2 3 import org.seckill.dto.Exposer; 4 import org.seckill.dto.SeckillExecution; 5 import org.seckill.entity.Seckill; 6 import org.seckill.exception.RepeatKillException; 7 import org.seckill.exception.SeckillCloseException; 8 import org.seckill.exception.SeckillException; 9 10 import java.util.List; 11 12 /** 13 * 业务接口:站在"使用者"角度设计接口 14 * 三个方面:方法定一粒度,参数,返回类型/异常 15 * Created by yuxue on 2016/10/15. 16 */ 17 public interface SeckillService { 18 19 /** 20 * 查询所有秒杀记录 21 * @return 22 */ 23 List<Seckill> getSeckillList( ); 24 25 /** 26 * 查询单个秒杀记录 27 * @param seckillId 28 * @return 29 */ 30 Seckill getById(以上是关于SSM框架学习之高并发秒杀业务--笔记5-- 并发优化的主要内容,如果未能解决你的问题,请参考以下文章
01 整合IDEA+Maven+SSM框架的高并发的商品秒杀项目之业务分析与DAO层
Java高并发秒杀系统API之SSM框架集成swagger与AdminLTE
02 整合IDEA+Maven+SSM框架的高并发的商品秒杀项目之Service层