SpringCloud微服务项目实战 - 缓存详解及高效缓存接入

2020-09-01 14:51:33 浏览数 (1)

缓存,已经是现在系统中必不可少的内容,如何使用好缓存,对系统的性能和效率至关重要,这里我就来分析一下使用缓存的正确姿势吧。

如今的微服务项目,都是前后端分离,上面就是简单的服务架构图。在整个服务器项目中,有哪些需要我们做缓存呢,这里大致有:客户端缓存、文件缓存及网络加速和后端数据缓存。

缓存技术简介

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的一般使用场景:

  1. 加速静态资源; 网站中有大量的html、js、css、图片等文件,可以将这些静态内容推送到CDN。
  2. 大文件的下载;软件下载,视频点播等存储网站。
  3. 部分与用户无关的高频接口加速,在高并发情况下提升接口响应时间。

关于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微服务项目实战,服务注册与发现(附面试题)

0 人点赞