缓存,已经是现在系统中必不可少的内容,如何使用好缓存,对系统的性能和效率至关重要,这里我就来分析一下使用缓存的正确姿势吧。
如今的微服务项目,都是前后端分离,上面就是简单的服务架构图。在整个服务器项目中,有哪些需要我们做缓存呢,这里大致有:客户端缓存、文件缓存及网络加速和后端数据缓存。
缓存技术简介
1,客户端缓存
客户端缓存主要包含如客户使用的PC上的浏览器、手机App、微信小程序等。客户端缓存的原则就是尽量减少或消灭请求,以达到降低服务器压力和提升用户体验的效果。静态文件,例如Js、html、css、图片等内容,它们的很多可以只请求1次,然后缓存在本地,之后就优先从本地缓存中获取,这样就减少了对Web服务器的频繁请求。
如常见的浏览器缓存,可以通过Http的头信息控制缓存是通过Expires(强缓存)、Cache-control(强缓存)、Last-Modified/If-Modified-Since(协商缓存)、Etag/If-None-Match(协商缓存)实现。具体使用方法和区别,这里就不赘叙了哈。
客户端缓存的优缺点:
优点:减少网络传输,加快页面内容展示速度,提升用户体验。
成本:占用客户端的部分内存和磁盘,影响实时性。
2,文件缓及网络加速
除了前面说的前端缓存,针对一些静态资源,我们可以一次加载,实现本地缓存。那如果同时有一万、十万甚至更多的用户初次使用呢,这就使得大量的请求同时请求web服务器,势必给服务器带来很大的压力。基于这种情况,我们又如何来解决应对呢?这就是要说的新的内容--网络加速。目前流行的网络加速解决方案主要是CDN技术,那什么又是CDN呢,我这里做简单介绍。
CDN(Content Delivery Network)是指内容分发网络,也称为内容传送网络,为了能在传统IP网上发布丰富的宽带媒体内容,在现有互联网基础上建立一个内容分发平台专门为网站提供服务。CDN网络是在用户和服务器之间增加了一层缓存层,将用户的请求引导到最优的缓存节点而不是服务器源站,从而加块访问速度。
CDN的一般使用场景:
- 加速静态资源; 网站中有大量的html、js、css、图片等文件,可以将这些静态内容推送到CDN。
- 大文件的下载;软件下载,视频点播等存储网站。
- 部分与用户无关的高频接口加速,在高并发情况下提升接口响应时间。
关于CDN的接入使用,使用云服务器的可以直接通过域名去云服务厂商去买就行,这里不在多说。
3,数据缓存
这是整个整个项目中最重要的,也就是我们所说的服务端缓存。服务端缓存有服务自身的Session,也有第三方的缓存技术。在分布式微服务的环境下,几乎没人使用Session作为服务端缓存,基本都采用了第三方的Nosql缓存技术。
常用的Nosql缓存有:Memcached、Redis、mongoDB。那么我们到底选用那个呢,先来看看他们的优缺点。
1)Memcached
优点:可以利用多核优势,单实例吞吐量极高,可以达到几十万QPS(取决于key、value的字节大小以及服务器硬件性能,日常环境中QPS高峰大约在4-6w左右)。适用于最大程度扛量。
缺点:只支持简单的key/value数据结构,不像Redis可以支持丰富的数据类型。无法进行持久化,数据不能备份,只能用于缓存使用,且重启后数据全部丢失。无法进行数据同步,不能将MC中的数据迁移到其他MC实例中。
2)Redis
优点:支持多种数据结构,支持持久化操作,可以进行aof及rdb数据持久化到磁盘,从而进行数据备份或数据恢复等操作,较好的防止数据丢失的手段。支持通过Replication进行数据复制,通过master-slave机制,可以实时进行数据同步复制,支持多级复制和增量复制,master-slave机制是Redis进行HA的重要手段。
缺点:只能使用单线程,性能受限于CPU性能,故单实例CPU最高才可能达到5-6wQPS每秒。支持简单的事务需求,但业界使用场景很少,并不成熟,既是优点也是缺点。Redis在string类型上会消耗较多内存,可以使用dict(hash表)压缩存储以降低内存耗用。MC和Redis都是Key-Value类型,不适合在不同数据集之间建立关系,也不适合进行查询搜索。
3)mongoDB
mongoDB 是一种文档性的数据库。先解释一下文档的数据库,即可以存放xml、json、bson类型系那个的数据。这些数据具备自述性(self-describing),呈现分层的树状数据结构。
mongoDB 存放json格式的数据。适合场景如:事件记录、内容管理或者博客平台,比如评论系统。
mongodb与MC和Redis不同的是,它可以使用语句进行CRUD操作,处理和获取文档数据。操作语句如下:
- 查询:db.col.find().pretty();
- 添加:db.col.insert({name:'Lily',sex:'female',age:'18'});
- 删除:db.col.remove(...);
- 更新:db.col.update(...);
mongodb与mysql不同,mysql的每一次更新操作都会直接写入硬盘,但是mongo不会,做为内存型数据库,数据操作会先写入内存,然后再会持久化到硬盘中去。
服务端缓存选取接入
经过上面的介绍说明,考虑到服务端接口数据类型已经持久化等,Redis似乎是我们的首选,当然你也可以选择Memcached,这个完全取决与自己的项目。这里我介绍下Redis在Springboot SpringCloud微服务项目的接入。
1,首先在pom.xml里引入Redis依赖
代码语言:javascript复制<!-- Redis version: 2.1.5.RELEASE -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>${springboot-redis.version}</version>
</dependency>
2,配置Redis连接属性(application.yml)
代码语言:javascript复制spring:
redis:
# Redis库索引(默认为0)
database: 0
# Redis服务器地址
host: 192.168.1.1
# Redis服务端口
port: 6379
# Redis连接密码(默认为空)
password:
lettuce:
pool:
# 连接池最大连接数(默认为8)
max-active: 8
# 连接池最大空闲连接(默认为8)
max-idle: 8
# 连接池最小空闲连接(默认为0)
min-idle: 0
# 连接池最大阻塞等待时间
max-wait: -1ms
3,创建RedisConfig
代码语言:javascript复制@Configuration
public class RedisConfig implements RedisSerializer<Object> {
private Converter<Object, byte[]> serializer = new SerializingConverter();
private Converter<byte[], Object> deserializer = new DeserializingConverter();
static final byte[] EMPTY_ARRAY = new byte[0];
@Autowired
RedisConnectionFactory redisConnectionFactory;
@Bean
public RedisTemplate<String, Object> functionDomainRedisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
initDomainRedisTemplate(redisTemplate, redisConnectionFactory);
return redisTemplate;
}
/**
* 设置数据存入 redis 的序列化方式
* @param redisTemplate
* @param factory
*/
private void initDomainRedisTemplate(RedisTemplate<String, Object> redisTemplate, RedisConnectionFactory factory) {
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());
redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
redisTemplate.setConnectionFactory(factory);
}
@Bean
public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForHash();
}
@Bean
public ValueOperations<String, Object> valueOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForValue();
}
@Bean
public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForList();
}
@Bean
public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForSet();
}
@Bean
public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForZSet();
}
@Override
public Object deserialize(byte[] bytes) {
if (bytes == null || bytes.length == 0) {
return null;
}
try {
return deserializer.convert(bytes);
} catch (Exception ex) {
throw new SerializationException("Cannot deserialize", ex);
}
}
@Override
public byte[] serialize(Object object) {
if (object == null) {
return EMPTY_ARRAY;
}
try {
return serializer.convert(object);
} catch (Exception ex) {
return EMPTY_ARRAY;
}
}
}
4,缓存操作接口及实现
代码语言:javascript复制public interface CacheService {
boolean hasKey(String key);
boolean delete(String key);
Object get(String key);
void set(String key, Object value);
void set(String key, Object value, long timeout);
}
代码语言:javascript复制@Component
public class CacheServiceImpl implements CacheService {
@Autowired
private ValueOperations<String, Object> valueOperation;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
@Override
public boolean delete(String keys) {
return redisTemplate.delete(keys);
}
@Override
public void set(String key, Object value) {
valueOperation.set(key, value);
}
@Override
public Object get(String key) {
return valueOperation.get(key);
}
@Override
public void set(String key, Object value, long timeout) {
valueOperation.set(key,value,timeout,TimeUnit.SECONDS);
}
}
5,编写测试实战
代码语言:javascript复制@RestController
@RequestMapping(value = "/customer/")
public class CustomerController {
@Autowired
private CustomerService customerService;
@Autowired
private CacheService cacheService;
@GetMapping("/id")
public Object findCustomerById(Integer customerId) {
Object obj = null;
if ((obj = cacheService.getCacheByKey(CustomerConstant.CUSTOMER_CACHE_PREFIX customerId)) != null) {
return ResultData.ok((CustomerInfo) obj);
} else {
// 从数据库获取
CustomerInfo customerInfo = customerService.findById(customerId);
if(customerInfo != null) {
cacheService.setCacheToRedis(CustomerConstant.CUSTOMER_CACHE_PREFIX customerId,customerInfo,3600);
}
return ResultData.ok(customerInfo);
}
}
}
今天就说到这里,下一篇继续说说缓存使用中的注意事项,以及面试时经常问到的缓存使用的相关问题。
推荐阅读:
SpringCloud微服务项目实战 - 限流、熔断、降级处理
SpringCloud微服务项目实战 - API网关Gateway详解实现
SpringCloud微服务项目实战 - 网关zuul详解及搭建
SpringCloud微服务项目实战 - 微服务调用详解(附面试题)
SpringCloud微服务项目实战,服务注册与发现(附面试题)