所有栏目 | 云社区 美国云服务器[国内云主机商]
你的位置:首页 > 云社区 » 正文

如何解决秒杀编程高并发问题?

发布时间:2020-04-12 09:02:19

资讯分类:编程  秒杀  并发  解决  请求  队列  服务器
如何解决秒杀编程高并发问题?

“双11”、“618”现在已经成为全民“剁手”的节日了,每年淘宝都会公布当晚的总成交额,而在这些成交额的背后,我们不得不提到秒杀技术。秒杀必然会涉及到高并发的问题,如何保障高并发下业务稳定运行也成了重中之重,下面我们具体探讨下。

秒杀、高并发场景下会导致哪些问题?

秒杀系统在一定时间周期内访问频率很高,所以会导致数据库及应用服务器负载过高,严重时甚至会导致服务宕机不可用。给用户的感知就是:慢、打不开、没响应。另一方面,哪怕服务器性能能抗得住高并发,但业务逻辑也可能会出现问题,比如商品出现超卖现象(成功下单的人数比商品库存要多得多)。

如何优化秒杀系统?

1、尽量将请求拦截在系统上游进行处理,避免后端服务器压力过大

  • 所有的前端资源一律走CDN加速,通过CDN自身优势使用户能就近读取资源,也避免了请求回源导致源服带宽压力过大;

  • 产品体验层面,当用户点击秒杀相关按钮后置灰,防止用户重复提交请求;

  • 前端限制用户特定时间段内的请求数。

2、合理利用缓存机制消息队列异步处理

上面第1点过滤的是用户重复请求带来的压力,但不能防制某些用户(机器手段)重复请求,所以后端程序需要做一些优化:

  • 针对单个用户,特定时间段内只允许一个有效请求,其它重复性请求一律拒绝;

  • 利用缓存技术缓存热点数据,减少数据库查询频率;

  • 通过消息队列的方式将业务解耦异步并行处理多种逻辑;

  • 另外可以利用消息队列来实现“队列”功能,请求逐一从队列中取出执行,缓解后台服务器的瞬时压力。

3、不重要的业务做降级处理

将一些不重要的业务降级(即:停止服务),减少服务器性能开销。

4、后端服务器的扩容

采用分布式集群方式部署应用,在流量洪峰来临时弹性扩容,让更多的服务器分担压力。

5、数据库分库分表、读写分离

不管如何利用缓存来缓存热点数据,最终数据还是要落地到数据库的。所以数据库在前期设计时就要考虑分库分表、读写分离的实施,提升查询写入性能。


高并发是互联网架构设计中必须要考虑的重要因素之一。虽然我们解决问题的具体技术方案可能千差万别,但是核心目标是一致的,就是快速响应请求。以上分别从前端,服务层,数据库层面分别做了优化,具体优化需要结合业务实际场景以及公司产品网络架构。


以上就是我的观点,对于这个问题大家是怎么看待的呢?欢迎在下方评论区交流 ~ 我是科技领域创作者,十年互联网从业经验,欢迎关注我了解更多科技知识!

如何解决秒杀编程高并发问题?

高并发问题

  就是指在同一个时间点,有大量用户同时访问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
留言与评论(共有 0 条评论)
   
验证码:
Top