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

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

时间: 2024-10-21 19:06:02

秒杀系统实现高并发的优化的相关文章

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

一:高并发点 高并发出现在秒杀详情页,主要可能出现高并发问题的地方有:秒杀地址暴露.执行秒杀操作. 二:静态资源(页面)访问优化--CDN CDN,内容分发网络.我们把静态的资源(html/css/js)放在CDN上,以加快用户获取数据的速度. 用户访问页面时,优先从最近的CDN服务器上获取页面资源,而不是从单一的网站服务器上获取.只有CDN获取不到才会访问后端服务器. 因此,我们可以使用CDN进行网站的加速优化,把静态资源(或某些动态资源)推送到CDN站点上.(大公司自己搭建CDN网络,小公司

GNU Linux高并发性能优化方案

/*********************************************************** * Author : Samson * Date : 07/14/2015 * Test platform: * gcc (Ubuntu 4.8.2-19ubuntu1) 4.8.2 * GNU bash, 4.3.11(1)-release (x86_64-pc-linux-gnu) * Nginx version: * Nginx 1.6.2 * Nginx 1.8.0

【MySQL优化】MySQL 高并发配置优化基础知识

[MySQL优化]MySQL 高并发配置优化基础知识 MySQL的优化分为两个部分,一是服务器物理硬件的优化,二是MySQL自身(my.cnf)的优化. 一.服务器硬件对MySQL性能的影响 ① 磁盘寻道能力(磁盘I/O),以目前高转速SCSI硬盘(7200转/秒)为例,这种硬盘理论上每秒寻道7200次,这是物理特性决定的,没有办法改变. MySQL每秒钟都在进行大量.复杂的查询操作,对磁盘的读写量可想而知.所以,通常认为磁盘I/O是制约MySQL性能的最大因素之一,对于日均访问量 在100万P

系统架构~高并发日志系统设计

对于一个项目来说,日志是必须的,一般日志的持久化方式有文件和数据库,而在多数情况下,我们都采用文件系统来实现,而对于高并发的情况下,频繁进行I/O操作,对系统的性能肯定是有影响的,这个毋庸置疑!针对这种高并发的场合,我们采用一种缓存队列的方式来处理这个Case是比较明智的,本文主要是向各位展现一下,我所设计的<高并发日志系统设计>,如在功能上有什么需要改进的地方,欢迎各位来回复. 一 项目结构图 二 项目实现代码 /// <summary> /// 工作任务基类 /// </

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

---恢复内容开始--- 第1章 秒杀系统高并发优化分析   1.为什么要单独获得系统时间 访问cdn这些静态资源不用请求系统服务器 而CDN上没有系统时间,需要单独获取,获取系统时间不用优化,只是new了一个日期对象返回,java访问一次内存(cacheline)的时间大概为10ns,即一秒可可访问一亿次 倒计时放在js端,在浏览器中,不会对服务器端造成影响,也不用优化 2.秒杀地址接口分析 秒杀未开启,秒杀开启,秒杀结束,秒杀地址返回的数据不同,不是静态的,无法使用CDN缓存 但它适合使用r

Linux高并发内核优化-TougheRadius

linux 内核优化 默认情况下,linux系统有一些限制,并不能直接支持高并发性能,需要做一些内核优化. 1.把以下内容加入 /etc/sysctl.conf 1 net.ipv4.ip_forward=1 2 net.ipv4.tcp_syncookies = 1 3 net.ipv4.tcp_tw_reuse = 1 4 net.ipv4.tcp_tw_recycle = 1 5 net.ipv4.tcp_fin_timeout = 30 6 net.ipv4.tcp_keepalive_

【高并发】优化加锁方式时竟然死锁了!!

写在前面 今天,在优化程序的加锁方式时,竟然出现了死锁!!到底是为什么呢?!经过仔细的分析之后,终于找到了原因. 为何需要优化加锁方式? 在<[高并发]高并发环境下诡异的加锁问题(你加的锁未必安全)>一文中,我们在转账类TansferAccount中使用TansferAccount.class对象对程序加锁,如下所示. public class TansferAccount{ private Integer balance; public void transfer(TansferAccoun

SpringCloud注册中心集群化及如何抗住大型系统的高并发访问

一.场景引入 本人所在的项目由于直接面向消费者,迭代周期迅速,所以服务端框架一直采用Springboot+dubbo的组合模式,每个服务由service模块+web模块构成,service模块通过公司API网关向安卓端暴 露restful接口,web模块通过dubbo服务向service模块获取数据渲染页面.测试环境dubbo的注册中心采用的单实例的zookeeper,随着时间的发现注册在zookeeper上的生产者和消费者越来越多,测试 人员经常在大规模的压测后发现zookeeper挂掉的现象

Tomcat高并发配置优化

用的JMeter在自己电脑上测试的.Ubuntu10.04(x64)内存2G,cpu E5400 主频2.7.jdk1.6.0_27(x64) , tomcat6.0.33(x64) , oracle 10g测试一个条件分页查询,数据库响应时间在0.5秒左右.之前测试第秒100个并发查询,持续1小时,响应时间先是保持在9秒左右,到后来越来越长.以这个配置测试时,200个并发,响应先是在4秒左右,之后越来越快,在1到2秒的时间内可以响应果.我的那项目并发最多也就100个,所以之后的就没有测试了,达