最近做了一个点餐的平台,其中涉及到一个很重要的问题,活动期间的秒杀系统的实现。
抢购/秒杀是如今很常见的一个应用场景,是高并发编程的一个挑战,在网上也找了一些资料,大部分都是理论,关于java的实现也是很少,就算有也是很简单的demo,为此,决定将此次实现的秒杀系统整理一番,发布出来。
架构思路
Question1: 由于要承受高并发,mysql在高并发情况下的性能下降尤其严重,下图为Mysql性能瓶颈测试。
而且硬盘持久化的io操作将耗费大量资源。所以决定采用基于内存操作的redis,redis的密集型io
Question2: 秒杀系统必然是一个集群系统,在硬件不提升的情况下利用nginx做负载均衡也是不错的选择。
实现难点
- 超买超卖问题的解决。
- 订单持久化,多线程将订单信息写入数据库
解决方案
- 采用redis的分布式乐观锁,解决高并发下的超买超卖问题.
- 使用countDownLatch作为计数器,将数据四线程写入数据库,订单的持久化过程在我的机器上效率提升了1000倍。
进阶方案
1.访问量还是大。系统还是撑不住。
2.防止用户刷新页面导致重复提交。
3.脚本攻击
解决思路:
1.访问量还是过大的话,要看性能瓶颈在哪里,一般来说首先撑不住的是tomcat,考虑优化tomcat,单个tomcat经过实践并发量撑住1000是没有问题的。先搭建tomcat集群,如果瓶颈出现在redis上的话考虑集群redis,这时候消息队列也是必须的,至于采用哪种消息队列框架还是根据实际情况。
2.问题2和问题3其实属于同一个问题。这个问题其实属于网络问题的范畴,和我们的秒杀系统不在一个层面上。因此不应该由我们来解决。很多交换机都有防止一个源IP发起过多请求的功能。开源软件也有不少能实现这点。如linux上的TC可以控制。流行的Web服务器Nginx(它也可以看做是一个七层软交换机)也可以通过配置做到这一点。一个IP,一秒钟我就允许你访问我2次,其他软件包直接给你丢了,你还能压垮我吗?
交换机也不行了呢?
可能你们的客户并发访问量实在太大了,交换机都撑不住了。 这也有办法。我们可以用多个交换机为我们的秒杀系统服务。 原理就是DNS可以对一个域名返回多个IP,并且对不同的源IP,同一个域名返回不同的IP。如网通用户访问,就返回一个网通机房的IP;电信用户访问,就返回一个电信机房的IP。也就是用CDN了! 我们可以部署多台交换机为不同的用户服务。 用户通过这些交换机访问后面数据中心的Redis Cluster进行秒杀作业。
我是在springboot + SpringData JPA的环境下实现的系统。引入了spring-data-redis的依赖
org.springframework.boot
spring-boot-starter-data-redis
config包下有两个类
public interface SecKillConfig {
String productId = “1234568”; //这是我数据库中的要秒杀的商品id
}
这个类的作用主要是配置RedisTemplate,否则使用默认的RedisTemplate会使key和value乱码。
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public CacheManager cacheManager(RedisTemplate<?, ?> redisTemplate) {
CacheManager cacheManager = new RedisCacheManager(redisTemplate);
return cacheManager;
}
// 以下两种redisTemplate自由根据场景选择
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(mapper);
template.setValueSerializer(serializer);
//使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());//这两句是关键
template.setHashKeySerializer(new StringRedisSerializer());//这两句是关键
template.afterPropertiesSet();
return template;
}
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
stringRedisTemplate.setConnectionFactory(factory);
return stringRedisTemplate;
}
}
下面是util包
public class KeyUtil {
public static synchronized String getUniqueKey(){
Random random = new Random();
Integer num = random.nextInt(100000);
return num.toString()+System.currentTimeMillis();
}
}
public class SecUtils {
/*
创建虚拟订单
*/
public static SecOrder createDummyOrder(ProductInfo productInfo){
String key = KeyUtil.getUniqueKey();
SecOrder secOrder = new SecOrder();
secOrder.setId(key);
secOrder.setUserId("userId="+key);
secOrder.setProductId(productInfo.getProductId());
secOrder.setProductPrice(productInfo.getProductPrice());
secOrder.setAmount(productInfo.getProductPrice());
return secOrder;
}
/*
伪支付
/
public static boolean dummyPay(){
Random random = new Random();
int result = random.nextInt(1000) % 2;
if (result == 0){
return true;
}
return false;
}
}
下面是重点,分布式锁的解决
/*
-
分布式乐观锁
*/
@Component
@Slf4j
public class RedisLock {@Autowired
private StringRedisTemplate redisTemplate;@Autowired
private ProductService productService;/*
加锁
*/
public boolean lock(String key,String value){//setIfAbsent对应redis中的setnx,key存在的话返回false,不存在返回true if ( redisTemplate.opsForValue().setIfAbsent(key,value)){ return true; } //两个问题,Q1超时时间 String currentValue = redisTemplate.opsForValue().get(key); if (!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()){ //Q2 在线程超时的时候,多个线程争抢锁的问题 String oldValue = redisTemplate.opsForValue().getAndSet(key, value); if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)){ return true; } } return false;
}
public void unlock(String key ,String value){
try{
String currentValue = redisTemplate.opsForValue().get(key);
if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)){
redisTemplate.opsForValue().getOperations().delete(key);
}
}catch(Exception e){
log.error(“redis分布上锁解锁异常, {}”,e);
}}
public SecProductInfo refreshStock(String productId){
SecProductInfo secProductInfo = new SecProductInfo();
ProductInfo productInfo = productService.findOne(productId);
if (productId == null){
throw new SellException(203,“秒杀商品不存在”);
}
try{
redisTemplate.opsForValue().set(“stock”+productInfo.getProductId(),String.valueOf(productInfo.getProductStock()));
String value = redisTemplate.opsForValue().get(“stock”+productInfo.getProductId());
secProductInfo.setProductId(productId);
secProductInfo.setStock(value);
}catch(Exception e){
log.error(e.getMessage());
}
return secProductInfo;}
}
分布式锁的实现思路
线程进来之后先执行redis的setnx,若是key存在就返回0,否则返回1.返回1即代表拿到锁,开始执行代码,执行完毕之后将key删除即为解锁。
存在两个问题,有可能存在死锁,就是一个线程执行拿到锁之后,解锁之前的代码时出现bug,导致锁释放不出来,下一个线程进来之后一直等待上一个线程释放锁。解决方案就是加上超时时间,超时过后自行无论执行是否成功都将锁释放出来。但是又会出现第二个问题,在超时的情况下,多个线程同时等待锁释放出来,然后竞争拿到锁,此时又会出现线程不安全现象,解决方案是使用redis的getandset方法,其中一个线程拿到锁之后立即将value值改变,同时将oldvalue与原来的value值比较,这样就保证了多线程竞争锁的安全性。
下面是业务逻辑部分的代码。
先是controller
@RestController
@Slf4j
@RequestMapping("/skill")
public class SecKillController {
@Autowired
private SecKillService secKillService;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Resource
private RedisTemplate<String,SecOrder> redisTemplate;
/*
下单,同时将订单信息保存在redis中,随后将数据持久化
*/
@GetMapping("/order/{productId}")
public String skill(@PathVariable String productId) throws Exception{
//判断是否抢光
int amount = Integer.valueOf(stringRedisTemplate.opsForValue().get("stock"+productId));
if (amount >= 2000){
return "不好意思,活动结束啦";
}
//初始化抢购商品信息,创建虚拟订单。
ProductInfo productInfo = new ProductInfo(productId);
SecOrder secOrder = SecUtils.createDummyOrder(productInfo);
//付款,付款时时校验库存,如果成功redis存储订单信息,库存加1
if (!SecUtils.dummyPay()){
log.error("付款慢啦抢购失败,再接再厉哦");
return "抢购失败,再接再厉哦";
}
log.info("抢购成功 商品id=:"+ productId);
//订单信息保存在redis中
secKillService.orderProductMockDiffUser(productId,secOrder);
return "订单数量: "+redisTemplate.opsForSet().size("order"+productId)+
" 剩余数量:"+(2000 - Integer.valueOf(stringRedisTemplate.opsForValue().get("stock"+productId)));
}
/*
在redis中刷新库存
*/
@GetMapping("/refresh/{productId}")
public String refreshStock(@PathVariable String productId) throws Exception{
SecProductInfo secProductInfo = secKillService.refreshStock(productId);
return "库存id为 "+productId +" <br> 库存总量为 "+secProductInfo.getStock();
}
}
Service
@Service
public interface SecKillService {
long orderProductMockDiffUser(String productId,SecOrder secOrder);
SecProductInfo refreshStock(String productId);
}
Impl
@Service
@Slf4j
public class SecKillServiceImpl implements SecKillService {
@Autowired
private RedisLock redisLock;
@Autowired
private SecOrderService secOrderService;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Resource
private RedisTemplate<String,SecOrder> redisTemplate;
private static final int TIMEOUT = 10 * 1000;
@Override
public long orderProductMockDiffUser(String productId,SecOrder secOrder) {
//加锁 setnx
long orderSize = 0;
long time = System.currentTimeMillis()+ TIMEOUT;
boolean lock = redisLock.lock(productId, String.valueOf(time));
if (!lock){
throw new SellException(200,"哎呦喂,人太多了");
}
//获得库存数量
int stockNum = Integer.valueOf(stringRedisTemplate.opsForValue().get("stock"+productId));
if (stockNum >= 2000) {
throw new SellException(150, "活动结束");
} else {
//仓库数量减一
stringRedisTemplate.opsForValue().increment("stock"+productId,1);
//redis中加入订单
redisTemplate.opsForSet().add("order"+productId,secOrder);
orderSize = redisTemplate.opsForSet().size("order"+productId);
if (orderSize >= 1000){
//订单信息持久化,多线程写入数据库(效率从单线程的9000s提升到了9ms)
Set<SecOrder> members = redisTemplate.opsForSet().members("order"+productId);
List<SecOrder> memberList = new ArrayList<>(members);
CountDownLatch countDownLatch = new CountDownLatch(4);
new Thread(() -> {
for (int i = 0; i <memberList.size() /4 ; i++) {
secOrderService.save(memberList.get(i));
countDownLatch.countDown();
}
//CountDownLatch的作用是,线程进入等待后,需要计数器达到0才能通行。
}, “therad1”).start();
new Thread(() -> {
for (int i = memberList.size() /4; i <memberList.size() /2 ; i++) {
secOrderService.save(memberList.get(i));
countDownLatch.countDown();
}
}, “therad2”).start();
new Thread(() -> {
for (int i = memberList.size() /2; i <memberList.size() * 3 / 4 ; i++) {
secOrderService.save(memberList.get(i));
countDownLatch.countDown();
}
}, “therad3”).start();
new Thread(() -> {
for (int i = memberList.size() * 3 / 4; i <memberList.size(); i++) {
secOrderService.save(memberList.get(i));
countDownLatch.countDown();
}
}, “therad4”).start();
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("订单持久化完成");
}
}
//解锁
redisLock.unlock(productId,String.valueOf(time));
return orderSize;
}
@Override
public SecProductInfo refreshStock(String productId) {
return redisLock.refreshStock(productId);
}
}
还有一些辅助的service,和实体类,不过多解释,一起贴出来吧,方便大家测试
public interface SecOrderService {
List<SecOrder> findByProductId(String productId);
SecOrder save(SecOrder secOrder);
}
@Service
public class SecOrderServiceImpl implements SecOrderService {
@Autowired
private SecOrderRepository secOrderRepository;
@Override
public List<SecOrder> findByProductId(String productId) {
return secOrderRepository.findByProductId(productId);
}
public SecOrder save(SecOrder secOrder){
return secOrderRepository.save(secOrder);
}
}
public interface SecOrderRepository extends JpaRepository<SecOrder,String> {
List<SecOrder> findByProductId(String productId);
SecOrder save(SecOrder secOrder);
}
@Entity
@Data
public class ProductInfo {
@Id
private String productId;
/**
* 产品名
*/
private String productName;
/**
* 单价
*/
private BigDecimal productPrice;
/**
* 库存
*/
private Integer productStock;
/**
* 产品描述
*/
private String productDescription;
/**
* 小图
*/
private String productIcon;
/**
* 商品状态 0正常 1下架
*/
private Integer productStatus = ProductStatusEnum.Up.getCode();
/**
* 类目编号
*/
private Integer categoryType;
/** 创建日期*/
@JsonSerialize(using = Date2LongSerializer.class)
private Date createTime;
/**更新时间 */
@JsonSerialize(using = Date2LongSerializer.class)
private Date updateTime;
@JsonIgnore
public ProductStatusEnum getProductStatusEnum(){
return EnumUtil.getBycode(productStatus,ProductStatusEnum.class);
}
public ProductInfo(String productId) {
this.productId = productId;
this.productPrice = new BigDecimal(3.2);
}
public ProductInfo() {
}
}
@Data
@Entity
public class SecOrder implements Serializable{
private static final long serialVersionUID = 1724254862421035876L;
@Id
private String id;
private String userId;
private String productId;
private BigDecimal productPrice;
private BigDecimal amount;
public SecOrder(String productId) {
String utilId = KeyUtil.getUniqueKey();
this.id = utilId;
this.userId = "userId"+utilId;
this.productId = productId;
}
public SecOrder() {
}
@Override
public String toString() {
return "SecOrder{" +
"id='" + id + '\'' +
", userId='" + userId + '\'' +
", productId='" + productId + '\'' +
", productPrice=" + productPrice +
", amount=" + amount +
'}';
}
}
@Data
public class SecProductInfo {
private String productId;
private String stock;
}
曾经被问过好多次怎样实现秒杀系统的问题。昨天又在优快云架构师微信群被问到了。因此这里把我设想的实现秒杀系统的价格设计分享出来。供大家参考。
秒杀系统的架构设计
秒杀系统,是典型的短时大量突发访问类问题。对这类问题,有三种优化性能的思路:
写入内存而不是写入硬盘
异步处理而不是同步处理
分布式处理
用上这三招,不论秒杀时负载多大,都能轻松应对。更好的是,Redis能够满足上述三点。因此,用Redis就能轻松实现秒杀系统。
用我这个方案,无论是电商平台特价秒杀,12306火车票秒杀,都不是事:)
下面介绍一下为什么上述三种性能优化思路能够解决秒杀系统的性能问题:
写入内存而不是写入硬盘
传统硬盘的读写性能是相当差的。SSD硬盘比传统硬盘快100倍。而内存又比SSD硬盘快10倍以上。因此,写入内存而不是写入硬盘,就能使系统的能力提升上千倍。也就是说,原来你的秒杀系统可能需要1000台服务器支撑,现在1台服务器就可以扛住了。
你可能会有这样的疑问:写入内存而不是持久化,那么如果此时计算机宕机了,那么写入的数据不就全部丢失了吗?如果你就这么倒霉碰到服务器宕机,那你就没秒到了,有什么大不了?
最后,后面真正处理秒杀订单时,我们会把信息持久化到硬盘中。因此不会丢失关键数据。
Redis是一个缓存系统,数据写入内存后就返回给客户端了,能够支持这个特性。
异步处理而不是同步处理
像秒杀这样短时大并发的系统,在性能负载上有一个明显的波峰和长期的波谷。为了应对相当短时间的大并发而准备大量服务器来应对,在经济上是相当不合算的。
因此,对付秒杀类需求,就应该化同步为异步。用户请求写入内存后立刻返回。后台启动多个线程从内存池中异步读取数据,进行处理。如用户请求可能是1秒钟内进入的,系统实际处理完成可能花30分钟。那么一台服务器在异步情况下其处理能力大于同步情况下1800多倍!
异步处理,通常用MQ(消息队列)来实现。Redis可以看作是一个高性能的MQ。因为它的数据读写都发生在内存中。
分布式处理
好吧。也许你的客户很多,秒杀系统即使用了上面两招,还是捉襟见肘。没关系,我们还有大招:分布式处理。如果一台服务器撑不住秒杀系统,那么就多用几台服务器。10台不行,就上100台。分布式处理,就是把海量用户的请求分散到多个服务器上。一般使用hash实现均匀分布。
这类系统在大数据云计算时代的今天已经有很多了。无非是用Paxos算法和Hash Ring实现的。
Redis Cluster正是这样一个分布式的产品。
使用Redis实现秒杀系统
Redis和Redis Cluster(分布式版本),是一个分布式缓存系统。其支持多种数据结构,也支持MQ。Redis在性能上做了大量优化。因此使用Redis或者Redis Cluster就可以轻松实现一个强大的秒杀系统。
基本上,你用Redis的这些命令就可以了。
RPUSH key value
插入秒杀请求
当插入的秒杀请求数达到上限时,停止所有后续插入。
后台启动多个工作线程,使用
LPOP key
读取秒杀成功者的用户id,进行后续处理。
或者使用LRANGE key start end命令读取秒杀成功者的用户id,进行后续处理。
每完成一条秒杀记录的处理,就执行INCR key_num。一旦所有库存处理完毕,就结束该商品的本次秒杀,关闭工作线程,也不再接收秒杀请求。
要是还撑不住,该怎么办
也许你会说,我们的客户很多。即使部署了Redis Cluster,仍然撑不住。那该怎么办呢?
记得某个伟人曾经说过:办法总比困难多!
下面,我们具体分析下,还有哪些情况会压垮我们架构在Redis(Cluster)上的秒杀系统。
脚本攻击
如现在有很多抢火车票的软件。它们会自动发起http请求。一个客户端一秒会发起很多次请求。如果有很多用户使用了这样的软件,就可能会直接把我们的交换机给压垮了。
这个问题其实属于网络问题的范畴,和我们的秒杀系统不在一个层面上。因此不应该由我们来解决。很多交换机都有防止一个源IP发起过多请求的功能。开源软件也有不少能实现这点。如linux上的TC可以控制。流行的Web服务器Nginx(它也可以看做是一个七层软交换机)也可以通过配置做到这一点。一个IP,一秒钟我就允许你访问我2次,其他软件包直接给你丢了,你还能压垮我吗?
交换机撑不住了
可能你们的客户并发访问量实在太大了,交换机都撑不住了。
这也有办法。我们可以用多个交换机为我们的秒杀系统服务。
原理就是DNS可以对一个域名返回多个IP,并且对不同的源IP,同一个域名返回不同的IP。如网通用户访问,就返回一个网通机房的IP;电信用户访问,就返回一个电信机房的IP。也就是用CDN了!
我们可以部署多台交换机为不同的用户服务。 用户通过这些交换机访问后面数据中心的Redis Cluster进行秒杀作业。
总结
有了Redis Cluster的帮助,做个支持海量用户的秒杀系统其实So Easy!
这里介绍的方案虽然是针对秒杀系统的,但其背后的原理对其他高并发系统一样有效。
最后,我们再重温一下高性能系统的优化原则:
写入内存而不是写入硬盘
异步处理而不是同步处理
分布式处理
秒杀系统设计思路笔记
设计原因:
为什么要针对秒杀设计一个完善的方案?因为系统可能会因为1%的秒杀业务影响其余99%正常业务的运行,所以需要将秒杀系统独立出来。
待解决问题:
主要解决两个问题:并发读、并发写。
整体架构要求:
概括为:“稳、准、快”,即对应"高可用、一致性、高性能",其介绍分别如下:
高可用:保证系统的高可用和正确性,设计PlanB进行兜底。
一致性:保证秒杀减库存中的数据一致性。
高性能:涉及大量并发读写,所以需要支持高并发,从动静分离、热点发现与隔离、请求削峰与分层过滤、服务端极致优化来介绍。
五个架构原则(4要1不要):
数据尽量少:包含请求和响应的数据,因为网络传输需要时间。可简化秒杀页面、减少数据库打交道。
请求数尽量少:浏览器渲染页面包含额外请求,比如以来了css、js、图片等定义为额外请求,这些请求尽可能少,若请求域名不一致,还涉及DNS解析,耗时更久,可采用合并方法如:http://xxx.com/tm/modes??module-preview/index.xtpl.js,module-jhs/index.xtpl.js ,这个在服务器依旧是单独存放,只是提供了一个组件解析这个url。比如以下是淘宝的一个静态资源链接:
路径尽量短:这里的路径表示节点。每个节点都会产生新的socket连接,一个节点的可用性是99.9%,经过五个节点后就是五次方为99.5%。方法可以是服务内部RPC调用变为jvm内部调用。
依赖尽量少:将服务分级,比如将支付服务作为0级,优惠券是1级,要避免0级系统被1级系统拖垮。
不要有单点:单点意味着没有备份。将服务无状态化,即避免服务与机器绑定。可设置配置中心映射。
注:数据、请求数、路径、依赖、单点。但是其实这些原则会产生冲突,如请求数量少中的合并请求会使单次请求的数据量变大,这与数据尽量少违背。所以需要我们做一个平衡。
秒杀系统的大体涉及结构:
1 页面彻底动静分离,使得用户秒杀时不需要刷新整个页面,降低刷新请求数。
2 服务器缓存秒杀商品,直接调用缓存层,无需穿透到数据库层找数据。
3 增加流量限流保护,防止最g坏情况。
如何动静分离:
把用户请求数据(如HTML)分为"动态数据"和"静态数据"。
动态数据与静态数据区别:确定输出的数据是否含访问者个性化数据如 个人信息、cookie等私密数据。
区别动静数据的作用:区分了动静数据,就可以将静态数据缓存,提高效率。
如何对静态数据做缓存:
-
将静态数据放在离用户最近的地方,如CDN、用户浏览器、服务端的Cache。
-
做静态化改造,直接缓存HTTP连接。Web服务器根据请求URL直接取出HTTP响应头、响应体直接返回。
-
选择谁来缓存静态数据,如可在web服务器层做缓存,屏蔽java层的弱点,不在java层做缓存。
如何做动静分离改造:
-
URL唯一化。如每个商品链接为:http://item.xxx/xxxx?id=xxx 来作为缓存的key,用于缓存整个HTTP连接。
-
分离请求,含用户信息相关、时间、地狱相关,这些都可以通过异步请求独立获取。
-
服务器返回的信息可去掉cookie。如Vanish可用unset req.http.cookie去掉cookie。
动态内容处理方案:
ESI:web代理服务器做动态请求时,将动态内容插入静态页面,然后全部返回。
CSI:异步js请求,性能最佳,但是有一定时延,可能对用户体验不好。
动静分离的几种架构方案:
- 实体机单机部署:将虚拟机运行的Java应用换成实体机
优点:无网络瓶颈、可使用大内存。
提升命中率,减少Gzip压缩。
减少cache失效压力,因为采用定时失效,如3分钟失效。
2 统一Cache层:即抽离cache出来作为一个独立的集群。可设置二级Cache,放置回原(原服务器)
- 上CDN:可增加二级Cache防止回原(原服务器)。
二八原则(针对性处理热点数据):
为什么要针对性处理:热点数据会大量占用服务资源,0.1%业务会抢占系统90%以上的资源。
什么是热点:
热点分为热点操作和热点数据,其中热点操作是一种优化的方式,将会在秒杀的操作优化讲解。
热点数据分为动态、静态热点数据如下:
-静态热点数据即可提前预测数据如数据分析出哪种更热门、一个商品做活动等。
-动态热点数据不可预测,如抖音广告然后突然火了。
如何发现热点数据:
发现静态热点数据:强制让卖家通过报名方式提前把热点数据筛选出来缓存,但是增加了卖家的工作量,也不够实时。也可以根据每日访问数进行统计,然后缓存TOP N的商品。
发现动态热点数据:抽离出一个中间件用于收集搜索、商品详情、购物车等关键热点业务的点击数据,然后异步记录到日志,然后根据规则判断是否热点数据后缓存在队列中(因为热点数据一般是临时的,所以可采用LRU算法淘汰)。
流量削峰怎么做:
为什么要削峰:稳定服务端,节省资源,本质是延缓用户请求发出,减少和过滤无用请求(遵循请求书尽量少原则)。
削峰思路:排队、答题、分层过滤
排队:消息队列缓存大量并发,把原来的一步操作变成两步。虽然违背了增加访问路径原则,但是防止了系统崩溃。
答题:可防止爬虫等的自动抢购的脚本。延缓请求从之前的1s内延缓到2-10s,对事件进行了分片,减缓服务器压力,如微信的摇一摇、支付宝休一休。也可限制答题时间间隔。
分层过滤:分层为:CDN->前台读系统(商品详情系统)->后台写系统(交易系统)->DB
大部分数据和流量都在CDN获取,拦截了大部分读的数据。
经过第二层(前台读系统)尽量走Cache。
到第三层(后台写系统),做数据校验、限流,进一步减少数据量和请求。
最后在数据层完成强一致性校验。
分层过滤核心思想:各层过滤无效请求,所以必须对数据做分层校验,其校验原则如下:
将动态请求的读数据缓存在浏览器本地,过滤无效数据读。
对读数据不做强一致性校验,较少一致性校验带来的性能问题。
对写数据基于时间的合理分片,过滤过期失效请求。
对写数据做强一致性校验,只保留有效数据。
影响性能的因素及其可优化处:
影响服务端性能的因素:QPS、响应时间(RT)
计算公式:QPS = (1000ms / 响应时间) * 线程数量,真正影响性能的是CPU执行时间。
再来分析下线程数是否对QPS的影响:不是线程数越多,QPS越高,因为线程上下文切换有消耗。所以需要合理的设置线程数,一般的计算公式为:线程数 = [(线程等待时间 + 线程CPU时间) / 线程CPU时间] * CPU数量,当然最好的方式是性能测试来确认。
如何发现瓶颈:就缓存系统而言,制约的是内存。存储系统的瓶颈是I/O。
秒杀系统的大部分瓶颈在CPU(使用JProfiler、YourKit),但不一定是CPU,有可能是其他部分,比如QPS达到极限时,CPU使用率是否超过95%,如果不是则可能是锁限制或过多本地I/O等待发生。
如何优化系统:
减少编码:java编码速度慢,涉及字符串操作(输入输出操作、I/O操作)比较消耗CPU资源。原因是磁盘、网络IO都需要将字符串转为字节,这个转换必须查表编码。可通过(OutputStream()直接进行流输出),可提高30%.
减少序列化:序列化与编码同时发生,所以需要减少。尽量减少RPC,将关联性强的应用服务合并。
Java极致优化:对大流量Web系统做静态化改造;直接使用Servlet,绕过框架多余处理逻辑;直接输出流数据。
并发读优化:秒杀系统单机缓存。不要求读一致性,但是写数据的时候要求强一致性。
秒杀系统设计的核心逻辑:
秒杀系统最重要要求是 “不要超卖”,关键在于减库存。
减库存方式(三种):
下单减库存:一定不会出现超卖情况,但是有些人下单完不付款会影响其他人。
付款减库款:付款减库存,可能会因为并发高导致付款时已经卖光,付不了款。
预扣库存:最常用,如下单后扣库存,保留十分钟,在十分钟内未付款就不保留。如果付款时发现库存不足则不允许付款。
减库存存在的问题:在下单减库存、预扣库存的情况下,有竞争对手恶意多账号下单导致库存降为0,那商品就无法正常卖。
解决办法:指定反作弊措施,如给经常下单不买的用户打标识、设置最大购买书、设置重复下单不付款操作数。
秒杀减库存极致优化:
秒杀商品减库存放缓存如redis。给热点商品提供独立的缓存层、DB层集群。使用排队如应用层排队、数据库层排队(阿里针对mysql的innodb做了补丁程序patch可对单行记录做并发排队)解决数据库并发锁问题。
设计Plan B兜底方案:
再牛逼的系统也会问题,比如超大流量导致宕机,出现最坏情况,所以需要设计Plan对应高可用性。
各阶段处理操作:
架构阶段:考虑拓展性和容错性,避免系统出现单点问题。
编码阶段:保证代码健壮性,合理设置超时退出机制,对于异常捕获后需要一个默认处理。
测试阶段:保证最坏情况下,也有相应处理流程。
发布阶段:需要有备份用于回滚。
运行阶段:系统监控和报警,如cat监控系统。
故障发生:及时止损,如下架标错价商品。
运行阶段详细处理:
降级:限制或关闭某些非核心功能,留给核心功能。如展示成功记录由30条变成5条。
限流:设置一个QPS阈值,达到则排队或丢弃。
拒绝:当连接数过大,cpu负载达到90%,就拒绝请求。
原文:https://blog.youkuaiyun.com/qq_28666081/article/details/83043215