一:先上代码,看着代码学习效率更好:https://github.com/3218870799/Seckill
二:高并发问题就是指在同一个时间点,有大量用户同时访问URL地址,比如淘宝双11都会产生高并发。
三:高并发带来的后果
- 服务端
??导致站点服务器、DB服务器资源被占满崩溃。
??数据的存储和更新结果和理想的设计不一致。 - 用户角度
??尼玛,网站这么卡,刷新了还这样,垃圾网站,不玩了
四:阻碍服务速度的原因
1:事物行级锁的等待:java的事务管理机制会限制在一次commit之前,下一个用户线程是无法获得锁的,只能等待
2:网络延迟
3:JAVA的自动回收机制(GC)
四:处理高并发的常见方法
1:首先可以将静态资源放入CDN中,减少后端服务器的访问
2:访问数据使用Redis进行缓存
3:使用Negix实现负载均衡
4:数据库集群与库表散列
五:对于这个秒杀系统来说我们从以下方面进行优化
1:首先先分析,当用户在想秒杀时,秒杀时间未到,用户可能会一直刷新页面,获取系统时间和资源(A:此时会一直访问服务器),当时间到了,大量用户同时获取秒杀接口API(B),获取API之后执行秒杀(C),指令传输到各地服务器,服务器执行再将传递到中央数据库执行(D),服务器启用事务执行减库存操作,在服务器端JAVA执行过程中,可能因为JAVA的自动回收机制,还需要一部分时间回收内存(E)。
2:优化思路:面对上面分析可能会影响的过程,我们可以进行如下优化
A:我们可以将一些静态的资源放到CDN上,这样可以减少对系统服务器的请求
B:对于暴露秒杀接口,这种动态的无法放到CDN上,我们可以采用Redis进行缓存
request——>Redis——>MySQL
C:数据库操作,对于MYSQL的执行速度大约可以达到1秒钟40000次,影响速度的还是因为行级锁,我们应尽可能减少行级锁持有时间。
DE:对于数据库来说操作可以说是相当快了,我们可以将指令放到MYSQL数据库上去执行,减少网络延迟以及服务器GC的时间。
3:具体实现
3.1:使用Redis进行缓存(Redis的操作可以参考我以前的博客https://www.cnblogs.com/nullering/p/9332589.html)
引入redis访问客户端Jedis
1 <!-- redis客户端:Jedis --> 2 <dependency> 3 <groupId>redis.clients</groupId> 4 <artifactId>jedis</artifactId> 5 <version>2.7.3</version> 6 </dependency>
pom.xml
优化暴露秒杀接口:对于SecviceImpl 中 exportSeckillUrl 方法的优化,伪代码如下
get from cache //首先我们要从Redis中获取需要暴露的URL
if null //如果从Redis中获取的为空
get db //那么我们就访问MYSQL数据库进行获取
put cache //获取到后放入Redis中
else locgoin //否则,则直接执行
我们一般不能直接访问Redis数据库,首先先建立数据访问层RedisDao,RedisDao中需要提供两个方法,一个是 getSeckill 和 putSeckill
在编写这两个方法时还需要注意一个问题,那就是序列化的问题,Redis并没有提供序列化和反序列化,我们需要自定义序列化,我们使用 protostuff 进行序列化与反序列化操作
引入 protostuff 依赖包
1 <!-- protostuff序列化依赖 --> 2 <dependency> 3 <groupId>com.dyuproject.protostuff</groupId> 4 <artifactId>protostuff-core</artifactId> 5 <version>1.0.8</version> 6 </dependency> 7 <dependency> 8 <groupId>com.dyuproject.protostuff</groupId> 9 <artifactId>protostuff-runtime</artifactId> 10 <version>1.0.8</version> 11 </dependency>
pom.xml
编写数据访问层RedisDao
1 package com.xqc.seckill.dao.cache; 2 3 import org.slf4j.Logger; 4 import org.slf4j.LoggerFactory; 5 6 import com.dyuproject.protostuff.LinkedBuffer; 7 import com.dyuproject.protostuff.ProtostuffIOUtil; 8 import com.dyuproject.protostuff.runtime.RuntimeSchema; 9 import com.xqc.seckill.entity.Seckill; 10 11 import redis.clients.jedis.Jedis; 12 import redis.clients.jedis.JedisPool; 13 14 /** 15 * Redis缓存优化 16 * 17 * @author A Cang(xqc) 18 * 19 */ 20 public class RedisDao { 21 private final Logger logger = LoggerFactory.getLogger(this.getClass()); 22 23 private final JedisPool jedisPool; 24 25 public RedisDao(String ip, int port) { 26 jedisPool = new JedisPool(ip, port); 27 } 28 29 private RuntimeSchema<Seckill> schema = RuntimeSchema.createFrom(Seckill.class); 30 31 public Seckill getSeckill(long seckillId) { 32 //redis操作逻辑 33 try { 34 Jedis jedis = jedisPool.getResource(); 35 try { 36 String key = "seckill:" + seckillId; 37 //并没有实现内部序列化操作 38 // get-> byte[] -> 反序列化 ->Object(Seckill) 39 // 采用自定义序列化 40 //protostuff : pojo. 41 byte[] bytes = jedis.get(key.getBytes()); 42 //缓存中获取到bytes 43 if (bytes != null) { 44 //空对象 45 Seckill seckill = schema.newMessage(); 46 ProtostuffIOUtil.mergeFrom(bytes, seckill, schema); 47 //seckill 被反序列化 48 return seckill; 49 } 50 } finally { 51 jedis.close(); 52 } 53 } catch (Exception e) { 54 logger.error(e.getMessage(), e); 55 } 56 return null; 57 } 58 59 public String putSeckill(Seckill seckill) { 60 // set Object(Seckill) -> 序列化 -> byte[] 61 try { 62 Jedis jedis = jedisPool.getResource(); 63 try { 64 String key = "seckill:" + seckill.getSeckillId(); 65 byte[] bytes = ProtostuffIOUtil.toByteArray(seckill, schema, 66 LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE)); 67 //超时缓存 68 int timeout = 60 * 60;//1小时 69 String result = jedis.setex(key.getBytes(), timeout, bytes); 70 return result; 71 } finally { 72 jedis.close(); 73 } 74 } catch (Exception e) { 75 logger.error(e.getMessage(), e); 76 } 77 78 return null; 79 } 80 81 82 }
RedisDao.java
优化ServiceImpl的 exportSeckillUrl 的方法
1 public Exposer exportSeckillUrl(long seckillId) { 2 // 优化点:缓存优化:超时的基础上维护一致性 3 //1:访问redis 4 Seckill seckill = redisDao.getSeckill(seckillId); 5 if (seckill == null) { 6 //2:访问数据库 7 seckill = seckillDao.queryById(seckillId); 8 if (seckill == null) { 9 return new Exposer(false, seckillId); 10 } else { 11 //3:放入redis 12 redisDao.putSeckill(seckill); 13 } 14 } 15 16 Date startTime = seckill.getStartTime(); 17 Date endTime = seckill.getEndTime(); 18 //系统当前时间 19 Date nowTime = new Date(); 20 if (nowTime.getTime() < startTime.getTime() 21 || nowTime.getTime() > endTime.getTime()) { 22 return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(), 23 endTime.getTime()); 24 } 25 //转化特定字符串的过程,不可逆 26 String md5 = getMD5(seckillId); 27 return new Exposer(true, md5, seckillId); 28 } 29 30 private String getMD5(long seckillId) { 31 String base = seckillId + "/" + salt; 32 String md5 = DigestUtils.md5DigestAsHex(base.getBytes()); 33 return md5; 34 }
ServiceImpl的exportSeckillUrl方法
3.2 并发优化:
在执行秒杀操作死,正常的执行应该如下:先减库存,并且得到行级锁,再执行插入购买明细,然后再提交释放行级锁,这个时候行级锁锁住了其他一些操作,我们可以进行如下优化,这时只需要延迟一倍。
修改executeSeckill方法如下:
1 @Transactional 2 /** 3 * 使用注解控制事务方法的优点: 4 * 1:开发团队达成一致约定,明确标注事务方法的编程风格。 5 * 2:保证事务方法的执行时间尽可能短,不要穿插其他网络操作RPC/HTTP请求或者剥离到事务方法外部. 6 * 3:不是所有的方法都需要事务,如只有一条修改操作,只读操作不需要事务控制. 7 */ 8 public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) 9 throws SeckillException, RepeatKillException, SeckillCloseException { 10 if (md5 == null || !md5.equals(getMD5(seckillId))) { 11 throw new SeckillException("seckill data rewrite"); 12 } 13 //执行秒杀逻辑:减库存 + 记录购买行为 14 Date nowTime = new Date(); 15 16 try { 17 //记录购买行为 18 int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone); 19 //唯一:seckillId,userPhone 20 if (insertCount <= 0) { 21 //重复秒杀 22 throw new RepeatKillException("seckill repeated"); 23 } else { 24 //减库存,热点商品竞争 25 int updateCount = seckillDao.reduceNumber(seckillId, nowTime); 26 if (updateCount <= 0) { 27 //没有更新到记录,秒杀结束,rollback 28 throw new SeckillCloseException("seckill is closed"); 29 } else { 30 //秒杀成功 commit 31 SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone); 32 return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled); 33 } 34 } 35 } catch (SeckillCloseException e1) { 36 throw e1; 37 } catch (RepeatKillException e2) { 38 throw e2; 39 } catch (Exception e) { 40 logger.error(e.getMessage(), e); 41 //所有编译期异常 转化为运行期异常 42 throw new SeckillException("seckill inner error:" + e.getMessage()); 43 } 44 }
ServiceImpl的executeSeckill方法
3.3深度优化:(存储过程)
定义一个新的接口,使用存储过程执行秒杀操作
1 /** 2 * 执行秒杀操作by 存储过程 3 * @param seckillId 4 * @param userPhone 5 * @param md5 6 */ 7 SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5);
executeSeckillProcedure接口
实现executeSeckillProcedure方法
1 public SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5) { 2 if (md5 == null || !md5.equals(getMD5(seckillId))) { 3 return new SeckillExecution(seckillId, SeckillStatEnum.DATA_REWRITE); 4 } 5 Date killTime = new Date(); 6 Map<String, Object> map = new HashMap<String, Object>(); 7 map.put("seckillId", seckillId); 8 map.put("phone", userPhone); 9 map.put("killTime", killTime); 10 map.put("result", null); 11 //执行存储过程,result被复制 12 try { 13 seckillDao.killByProcedure(map); 14 //获取result 15 int result = MapUtils.getInteger(map, "result", -2); 16 if (result == 1) { 17 SuccessKilled sk = successKilledDao. 18 queryByIdWithSeckill(seckillId, userPhone); 19 return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, sk); 20 } else { 21 return new SeckillExecution(seckillId, SeckillStatEnum.stateOf(result)); 22 } 23 } catch (Exception e) { 24 logger.error(e.getMessage(), e); 25 return new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR); 26 27 } 28 29 }
executeSeckillProcedure实现
编写SeckillDao实现有存储过程执行秒杀的逻辑
1 /** 2 * 使用存储过程执行秒杀 3 * @param paramMap 4 */ 5 void killByProcedure(Map<String,Object> paramMap);
SeckillDao.java
在Mybatis中使用
1 <!-- mybatis调用存储过程 --> 2 <select id="killByProcedure" statementType="CALLABLE"> 3 call execute_seckill( 4 #{seckillId,jdbcType=BIGINT,mode=IN}, 5 #{phone,jdbcType=BIGINT,mode=IN}, 6 #{killTime,jdbcType=TIMESTAMP,mode=IN}, 7 #{result,jdbcType=INTEGER,mode=OUT} 8 ) 9 </select>
seclillDao.xml
在Controller层使用
1 @ResponseBody 2 public SeckillResult<SeckillExecution> execute(@PathVariable("seckillId") Long seckillId, 3 @PathVariable("md5") String md5, 4 @CookieValue(value = "killPhone", required = false) Long phone) { 5 //springmvc valid 6 if (phone == null) { 7 return new SeckillResult<SeckillExecution>(false, "未注册"); 8 } 9 SeckillResult<SeckillExecution> result; 10 try { 11 //存储过程调用. 12 SeckillExecution execution = seckillService.executeSeckillProcedure(seckillId, phone, md5); 13 return new SeckillResult<SeckillExecution>(true,execution); 14 } catch (RepeatKillException e) { 15 SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL); 16 return new SeckillResult<SeckillExecution>(true,execution); 17 } catch (SeckillCloseException e) { 18 SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.END); 19 return new SeckillResult<SeckillExecution>(true,execution); 20 } catch (Exception e) { 21 logger.error(e.getMessage(), e); 22 SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR); 23 return new SeckillResult<SeckillExecution>(true,execution); 24 } 25 }
SeckillResult
至此,此系统的代码优化工作基本完成。但是在部署时可以将其更加优化,我们一般会使用如下架构
原文地址:https://www.cnblogs.com/nullering/p/9533795.html