秒杀系统实现高并发的优化

Posted nullering

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了秒杀系统实现高并发的优化相关的知识,希望对你有一定的参考价值。

一:先上代码,看着代码学习效率更好: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

 

 至此,此系统的代码优化工作基本完成。但是在部署时可以将其更加优化,我们一般会使用如下架构

技术分享图片




以上是关于秒杀系统实现高并发的优化的主要内容,如果未能解决你的问题,请参考以下文章

SSM框架学习之高并发秒杀业务--笔记5-- 并发优化

SSM框架学习之高并发秒杀业务--笔记5-- 并发优化

Java高并发秒杀API之高并发优化

秒杀系统-高并发的优化

高并发秒杀系统的优化

SSM实战——秒杀系统之高并发优化