高并发问题
就是指在同一个时间点,有大量用户同时访问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访问客户端Jedis
1 <!-- redis客户端:Jedis -->2 <dependency>3 <groupId>redis.clients</groupId>4 <artifactId>jedis</artifactId>5 <version>2.7.3</version>6 </dependency>优化暴露秒杀接口:对于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>编写数据访问层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 //缓存中获取到bytes43 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 }优化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:放入redis12 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 }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,userPhone20 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 //没有更新到记录,秒杀结束,rollback28 throw new SeckillCloseException("seckill is closed");29 } else {30 //秒杀成功 commit31 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 }3.3深度优化:(存储过程)
定义一个新的接口,使用存储过程执行秒杀操作
1 /**2 * 执行秒杀操作by 存储过程3 * @param seckillId4 * @param userPhone5 * @param md56 */7 SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5);实现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 //获取result15 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 }编写SeckillDao实现有存储过程执行秒杀的逻辑
1 /**2 * 使用存储过程执行秒杀3 * @param paramMap4 */5 void killByProcedure(Map<String,Object> paramMap);在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>在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 }至此,此系统的代码优化工作基本完成。但是在部署时可以将其更加优化,我们一般会使用如下架构
Copyright © 广州京杭网络科技有限公司 2005-2025 版权所有 粤ICP备16019765号
广州京杭网络科技有限公司 版权所有