Day1.秒杀系统之多级缓存实现

缓存设计

在高并发的情况下,例如秒杀、抢票以及双11活动下单等场景,由于业务量巨大,同一时间打在服务器上的流量过大,就会导致超时响应的情况出现,严重的甚至还会致使服务器崩溃,这是任何一个开发者都不愿意见到的。

因此,在高性能的服务架构设计中,我们常使用缓存这一中间件来分担服务器的压力,最常见的就是使用Redis这一数据库了。不过,随着业务量的增大,仅仅使用Redis这种远程缓存可能还不够,所以这时候就需要在Web应用和Redis之间再增加一个本地缓存。以秒杀系统为例,先不考虑流量削峰及填谷的情况,当我们想要根据商品id查询其详情的时候,请求数据的流程图是下面这样的。

在这里插入图片描述

二级缓存的执行流程?

  • 浏览器发送请求,到达本地缓存,如果本地缓存中有数据,那么直接返回。
  • 如果本地缓存中无数据,请求到达Redis,Redis中若有数据,先将数据存放在本地缓存,然后再返回。
  • 如果Redis中无数据,那么将直接请求MySQL,MySQL将数据先写入二级缓存后,再将其返回给浏览器。

什么是本地缓存?

本地缓存是基于本机环境的缓存,它使用的是JVM内存,常见的实现有GuavaCaffeine,由于JVM内存是高度敏感的,所以我们一般都将那些经常被访问的热点数据存放在本地缓存中。

压力测试

我们可以使用Apache Jmeter工具来进行压力测试,本次测试在我的本机执行,线程组的配置如下,由于我是第一次使用Jmeter,所以参数设置方面还不是很理解,凑活着先用。

控制方法的代码如下所示,可以从缓存中查找商品详情,也可以从MySQL数据库中直接查找商品详情。

    @ResponseBody  
    @RequestMapping(path = "/detail/{id}", method = RequestMethod.GET)  
    public ResponseModel getDetail(@PathVariable("id") String id) {  
//        Item item = itemService.getDetailInCache(id);  
        Item item = itemService.getDetailByID(id);  
        if (item == null) {  
            return ResponseModel.createFailure(CommonErrorEnum.STOCK_NOT_EXISTS);  
        }  
        return ResponseModel.createSuccess(item);  
    }

无缓存时

在不使用任何缓存时,所有的流量打到MySQL服务器上,如下图所示,在全部15000次请求中,有386次失败,平均响应时间为744.5ms,TPS为2987.45。

使用Redis

下面我将展示使用Redis缓存时,服务器的性能能提高多少,这时我们查找商品详情先去Redis中找,Service层的代码如下所示,这里我对id做了一个校验,因为数据库中所有的id都是32位的,所以我希望传进来的查询字段也是32位的(其实应该在Controller层就校验的,but whatever)。

@Override  
public Item getDetailInCache(@Length(min = 32, max = 32, message = "参数不合法") String id) {  
    Item item = null;  
    // Redis  
    String key = "item:" + id;  
    item = (Item) redisTemplate.opsForValue().get(key);  
    if (item != null) {  
        return item;  
    }  
    // MySqL  
    item = this.getDetailByID(id);  
    if (item != null) {  
        // 放到Redis中去,3分钟后失效  
        redisTemplate.opsForValue().set(key, item, 3, TimeUnit.MINUTES);  
    }  
    return item;  
}

首先发起一次请求,先将商品详情添加到Redis中,然后再做一次同样的压测,结果如下图所示。可以看到,此时失败的请求数量降低到了12个,平均响应时间提高到了132.12ms,TPS也提高到了6541.65,看来加了一层Redis缓存后效果是显著的。

使用Caffeine

从SpringBoot2开始,spring官方已经放弃了Guava,而改用Caffeine作为默认的本地缓存组件,本着与时俱进的原则,我这里也使用Caffeine来做测试,它的Github地址是https://github.com/ben-manes/caffeine。

Caffeine简介

根据官网介绍,Caffeine是Java缓存库近乎最优的实践,它的底层类似于ConcurrentMap,但又有所不同,其最大的区别在于Caffeine会自动驱逐那些可能不被使用的元素,Caffeine基于三种缓存淘汰策略:内存优先、超时优先和引用优先,这个留待我们后续再来研究,先来看看如何使用Caffeine。

使用Caffeine

  1. 首先需要引入Caffeine依赖,这里需要注意,如果使用的是JDK11及以上的版本,那么就引入3.1.x的Caffeine,否则就引入2.9.x,我使用的是JDK8。
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.1.1</version>
</dependency>
  1. 编写缓存配置类
@Configuration  
public class CacheConfig {  
    @Bean  
    public Cache<String, Object> caffeineCache() {  
        return Caffeine.newBuilder()  
                .expireAfterWrite(10, TimeUnit.MINUTES) // 写入10分钟后过期  
                .initialCapacity(100) // 初始缓存空间大小  
                .maximumSize(1000) // 最大缓存数量  
                .build();  
    }  
}

这样就通过配置类的方式将caffeineCache这个Bean注入到容器中了,后面要使用这个缓存也就非常简单了。

  1. 使用本地缓存

下面将服务层的代码修改如下

@Service  
public class ItemServiceImpl implements ItemService {  
    @Autowired  
    private ItemMapper itemMapper;  
    @Autowired  
    private ItemStockMapper itemStockMapper;  
    @Autowired  
    private PromotionMapper promotionMapper;  
    @Autowired  
    private RedisTemplate redisTemplate;  
    @Autowired  
    private Cache<String, Object> caffeineCache;  
 
    @Override  
    public Item getDetailByID(String id) {  
        // 查出来一个商品  
        Item item = itemMapper.selectByPrimaryKey(id);  
        // 添加库存信息  
        ItemStock itemStock = itemStockMapper.selectByItemId(id);  
        item.setItemStock(itemStock);  
        // 添加促销活动信息  
        Promotion promotion = promotionMapper.selectByItemId(id);  
        item.setPromotion(promotion);  
        return item;  
    }  
  
    @Override  
    public Item getDetailInCache(@Length(min = 32, max = 32, message = "参数不合法") String id) {  
        Item item = null;  
        // Caffeine  
        String key = "item:" + id;  
        item = (Item) caffeineCache.getIfPresent(key);  
        if (item != null) {  
            return item;  
        }  
        // Redis  
        item = (Item) redisTemplate.opsForValue().get(key);  
        if (item != null) {  
            // 将商品其放入本地缓存中  
            caffeineCache.put(key, item);  
            return item;  
        }  
        // MySqL  
        item = this.getDetailByID(id);  
        if (item != null) {  
            // 放到Redis中去,3分钟后失效  
            redisTemplate.opsForValue().set(key, item, 3, TimeUnit.MINUTES);  
        }  
        return item;  
    }  
}

再使用Jmeter做压力测试,结果如下图所示。平均响应时间已经来到了10.08ms,较只使用Redis做缓存时又提高了一大截,然而看起来TPS似乎略有下降,于是我测试了很多次,大多数情况下TPS都是大于7000的,总之也有所提高,这里就不做截图展示了。

补充

Caffeine提供了四种缓存加载的策略,分别是:手动加载、自动加载、手动异步加载和自动异步加载,我们这里使用的是最简单的手动加载方式,关于其他的实现请参考官方文档

总结

使用这种本地+远程二级缓存的方式固然能够大大提升并发次数,应对更为复杂的业务场景,但我们在学习Redis的时候就知道,当数据量巨大时,如何保证缓存和MySQL中的数据一致性就成了问题,有关分布式缓存中存在的各种问题,等我学习消化完毕后,再记录在另一篇文章里。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值