zoukankan      html  css  js  c++  java
  • 缓存深入理解

    对于缓存大家都不会陌生,但如何正确和合理的使用缓存还是需要一定的思考,本文将基于Java技术栈对缓存做一个相对详细的介绍,内容分为基本概念、本地缓存、远程缓存和分布式缓存集群几个部分,重点在于理解缓存的相关概念,愿合理的使用Cache如下图的妹子一样美好。

    基本概念

    缓存是计算机系统中必不可少的一种解决性能问题的方法,常见的应用包括CPU缓存、操作系统缓存、本地缓存、分布式缓存、HTTP缓存、数据库缓存等。其核心就是用空间换时间,通过分配一块高速存储区域(一般来说是内存)来提高数据的读写效率,实现的难点就在于清空策略的实现,比较合理的思路就是定时回收与即时判断数据是否过期相结合

    缓存相关概念

    1. 命中率:命中率指请求次数与正确返回结果次数的比例,其影响因素包括数据实时性,如果股票类实时性要求很高的数据,缓存的命中率会很低;缓存粒度问题, 如果KEY值包含的条件太多,会出现缓存命中率特别低的情况。通常来说,提高缓存命中率的方法包括增大缓存空间的大小的;对热点数据进行实时更新;调整缓存KEY的算法,保证缓存KEY的细粒度,如key-value;根据业务需要合理调整缓存的过期策略。
    2. 最大元素:缓存中可以存放的元素的最大数量。
    3. 清空策略包括FIFO,最先进入缓存的数据在空间不够时会被优先清理;LFU一直以来最少被使用的元素会被清理,可以给缓存元素设置一个计数器实现;LRU最近最少使用的缓存元素会被清理,可以通过一个时间戳来讲最近未使用数据清除。
    4. 预热策略包括全量预热,一开始就加载全部数据,适用于不怎么变化的数据(地区数据);增量预热,查询不到时从数据源取出放入缓存。

    Tip:
    缓存在高并发场景下的常见问题

    缓存相关问题

    1. 缓存穿透:一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。如果key对应的value是一定不存在的,并且对该key并发请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。解决方法包括将查询结果为空的情况也进行缓存,缓存时间设置短一点,并在该key对应的数据insert之后清理缓存;对一定不存在的key进行过滤。
    2. 缓存雪崩
      缓存服务器重启或者大量缓存集中在某一个时间段失效,这时会给后端系统(比如DB)带来很大压力。解决方案包括在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量,比如对某个key只允许一个线程查询数据和写缓存,其他线程等待;不同的key设置不同的过期时间,让缓存失效的时间点尽量均匀;做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期。

    分布式缓存系统的需要注意缓存一致性、缓存穿透和雪崩、缓存数据的清理等问题。可以通过锁解决一致性问题;为了提高缓存命中率,可以对缓存分层,分为全局缓存,二级缓存,他们是存在继承关系的,全局缓存可以有二级缓存来组成。为了保证系统的HA,缓存系统可以组合使用两套存储系统(memcache,redis)。缓存淘汰的策略包括定时去清理过期的缓存、判断过期时间来决定是否重新获取数据。

    在java应用中通常由两类缓存,一类是进程内缓存,就是使用java应用虚拟机内存的缓存;另一个是进程外缓存,现在我们常用的各种分布式缓存。前者比较简单,而且在一个JVM中,快速且可用性高,但会存在多态负载均衡主机数据不一致的问题,因此适合最常用且不易变的数据。后者扩展性强,而且相关的方案多,比如Redis Cluster等。通常来说,从数据库读取一条数据需要10ms,从分布式缓存读取则只需要0.5ms左右,而本地缓存则只需要10μs,因此需要根据具体场景选出合适的方案。

    Local缓存

    Java的本地缓存很早就有了相关标准javax.cache,要求的特性包括原子操作、缓存读写、缓存事件监听器、数据统计等内容。实际工作中本地缓存主要用于特别频繁的稳定数据,不然的话带来的数据不一致会得不偿失。实践中,常使用Guava Cache,以及与Spring结合良好的EhCache.

    Guava Cache

    是一个全内存的本地缓存实现,它提供了线程安全的实现机制,简单易用,性能好。其创建方式包括cacheLoadercallable callback两种,前者针对整个cache,而后者比较灵活可以在get时指定。
    CacheBuilder.newBuilder()方法创建cache时重要的几个方法如下所示,之后是一个简单的使用示例。
    maximumSize(long):设置容量大小,超过就开始回收。
    expireAfterAccess(long, TimeUnit):在这个时间段内没有被读/写访问,就会被回收。
    expireAfterWrite(long, TimeUnit):在这个时间段内没有被写访问,就会被回收 。
    removalListener(RemovalListener):监听事件,在元素被删除时,进行监听。

    @Service
    public class ConfigCenterServiceImpl implements ConfigCenterService {
    	private final static long maximumSize = 20;
    	/**
    	 * 最大20个,过期时间为1天
    	 */
    	private Cache<String, Map<String, ConfigAppSettingDto>> cache = CacheBuilder.newBuilder().maximumSize(maximumSize)
    			.expireAfterWrite(1, TimeUnit.DAYS).build();
    	@Autowired
    	private ConfigAppSettingDAO configAppSettingDAO;
    
    	@Override
    	public ConfigAppSettingDto getByTypeNameAndKey(String configType, String appID, String key) {
    		Map<String, ConfigAppSettingDto> map = getByType(configType, appID);
    		return map.get(key);
    	}
    
    	/************************** 辅助方法 ******************************/
    	private Map<String, ConfigAppSettingDto> getByType(String configType, String appID) {
    		try {
    			return cache.get(configType, new Callable<Map<String, ConfigAppSettingDto>>() {
    				@Override
    				public Map<String, ConfigAppSettingDto> call() throws Exception {
    					Map<String, ConfigAppSettingDto> result = Maps.newConcurrentMap();
    					List<ConfigAppSetting> list = configAppSettingDAO.getByTypeName(configType, appID);
    					if (null != list && !list.isEmpty()) {
    						for (ConfigAppSetting item : list) {
    							result.put(item.getAppkey(), new ConfigAppSettingDto(item.getAppkey(), item.getAppvalue(),
    									item.getDescription()));
    						}
    					}
    					return result;
    				}
    			});
    		} catch (ExecutionException ex) {
    			throw new BizException(300, "获取ConfigAppSetting配置信息失败");
    		}
    	}
    }
    

    EHCache

    EHCache也是一个全内存的本地缓存实现,符合javax.cache JSR-107规范,被应用在Hibernate中,过去存在过期失效的缓存元素无法被GC掉,造成内存泄露的问题,其主要类型及使用示例如下所示。
    Element:缓存的元素,它维护着一个键值对。
    Cache:它是Ehcache的核心类,它有多个Element,并被CacheManager管理,实现了对缓存的逻辑操作行为。
    CacheManager:Cache的容器对象,并管理着Cache的生命周期。

    Spring Boot整合Ehcache示例

    //maven配置
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    <dependency>
        <groupId>net.sf.ehcache</groupId>
        <artifactId>ehcache</artifactId>
    </dependency>
    
    //开启Cache
    @SpringBootApplication
    @EnableCaching
    public class Application {
        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
        }
    }
    //方式1
    @CacheConfig(cacheNames = "users")
    public interface UserRepository extends JpaRepository<User, Long> {
        @Cacheable
        User findByName(String name);
    }
    //方式2
    @Service
    public class CacheUserServiceImpl implements CacheUserService {
    	@Autowired
    	private UserMapper userMapper;
    	@Override
    	public List<User> getUsers() {
    		return userMapper.findAll();
    	}
    	// Cacheable表示获取缓存,内容会存储在people中,包含两个Key-Value
    	@Override
    	@Cacheable(value = "people", key = "#name")
    	public User getUser(String name) {
    		return userMapper.findUserByName(name);
    	}
    	//put是存储
    	@CachePut(value = "people", key = "#user.userid")
    	public User save(User user) {
    		User finalUser = userMapper.insert(user);
    		return finalUser;
    	}
    	//Evict是删除
    	@CacheEvict(value = "people")
    	public void remove(Long id) {
    		userMapper.delete(id);
    	}
    }
    //在application.properties指定spring.cache.type=ehcache即可
    //在src/main/resources中创建ehcache.xml
    <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:noNamespaceSchemaLocation="ehcache.xsd">
        <cache name="users"
               maxEntriesLocalHeap="100"
               timeToLiveSeconds="1200">
        </cache>
    </ehcache>
    

    ehcache官方文档

    Remote缓存

    常见的分布式缓存组件包括memcached,redis等。前者性能高效,使用方便,但功能相对单一,只支持字符串类型的数据,需要结合序列化协议,只能用作缓存。后者是目前最流行的缓存服务器,具有高效的存取速度,高并发的吞吐量,并且有丰富的数据类型,支持持久化。因此,应用场景非常多,包括数据缓存、分布式队列、分布式锁、消息中间件等。

    Redis

    Redis支持更丰富的数据结构, 例如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 此外,Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。可以说Redis兼具了缓存系统和数据库的一些特性,因此有着丰富的应用场景。本文介绍Redis在Spring Boot中两个典型的应用场景。
    场景1:数据缓存

    //maven配置
    <dependency>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-redis</artifactId>  
    </dependency>
    
    //application.properties配置
    # Redis数据库索引(默认为0)
    spring.redis.database=0  
    # Redis服务器地址
    spring.redis.host=localhost
    # Redis服务器连接端口
    spring.redis.port=6379  
    # Redis服务器连接密码(默认为空)
    spring.redis.password=  
    # 连接池最大连接数
    spring.redis.pool.max-active=8  
    # 连接池最大阻塞等待时间(使用负值表示没有限制)
    spring.redis.pool.max-wait=-1  
    # 连接池中的最大空闲连接
    spring.redis.pool.max-idle=8  
    # 连接池中的最小空闲连接
    spring.redis.pool.min-idle=0
    
    //方法1
    @Configuration
    @EnableCaching
    public class CacheConfig {
        @Autowired
        private JedisConnectionFactory jedisConnectionFactory;
        @Bean
        public RedisCacheManager cacheManager() {
            RedisCacheManager redisCacheManager = new RedisCacheManager(redisTemplate());
            return redisCacheManager;
        }
        @Bean
        public RedisTemplate<Object, Object> redisTemplate() {
            RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<Object, Object>();
            redisTemplate.setConnectionFactory(jedisConnectionFactory);
            // 开启事务支持
            redisTemplate.setEnableTransactionSupport(true);
            // 使用String格式序列化缓存键
            StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
            redisTemplate.setKeySerializer(stringRedisSerializer);
            redisTemplate.setHashKeySerializer(stringRedisSerializer);
            return redisTemplate;
        }
    }
    
    //方法2,和之前Ehcache方式一致
    

    场景2:共享Session

    //maven配置
    <dependency>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session-data-redis</artifactId>
    </dependency>
    //Session配置
    @Configuration
    @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 86400*)//
    public class SessionConfig {
    }
    //示例
    @RequestMapping("/session")
    @RestController
    public class SessionController {
    	@Autowired
    	private UserRepository userRepository;
    
    	@RequestMapping("/user/{id}")
    	public User getUser(@PathVariable Long id, HttpSession session) {
    		User user = (User) session.getAttribute("user" + id);
    		if (null == user) {
    			user = userRepository.findOne(id);
    			session.setAttribute("user" + id, user);
    		}
    		return user;
    	}
    }
    

    tip:
    Jedis的使用请见《大型分布式网站架构》学习笔记--02基础设施
    Jedis的Github地址
    Spring Redis默认使用JDK进行序列化和反序列化,因此被缓存对象需要实现java.io.Serializable接口,否则缓存出错。
    过去写过一篇Redis快速入门,现在来看理解上还差的比较多,有些麻瓜。

    Redis集群

    实现包括如下3种方式,相对于传统的大客户端分片和代理模式,路由查询的方式比较新颖,具体解决方案推荐redis-cluster
    客户端分片:包括jedis在内的一些客户端,都实现了客户端分片机制。
    基于代理的分片:Twemproxy、codis,客户端发送请求到一个代理,代理解析客户端的数据,将请求转发至正确的节点,然后将结果回复给客户端。
    路由查询:Redis-cluster,将请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上执行,这个思路比较有意思。

    Docker部署Redis主从

    //1.获取redis
    $docker pull  redis:3.2
    //2.主从启动
    $docker run -p 6379:6379 --name redis01 -v $PWD/data01:/data -v $PWD/config01:/usr/local/redis  -d redis:3.2 redis-server  --appendonly yes
    //$docker start redis01 /usr/local/redis/redis.conf 
    //先建立容器,再做好配置,之后根据配置启动(这部分细节上掌握的有些问题,比如需要先run,stop再start,如何合理的使用create?)
    //配置到官网下载,docker中只需要修改appendonly yes, port xxxx
    //修改从库02,03的配置redis.conf,添加slaveof xxx.xxx.xxx.xxx 6379, docker exec -ti redis01 /bin/bash
    //docker run  -p 6380:6380  -v $PWD/config02:/usr/local/redis  -v $PWD/data02:/data --name redis02 -d redis:3.2 redis-server /usr/local/redis/redis.conf
    docker run  -p 6380:6379  -v $PWD/config02:/usr/local/redis  -v $PWD/data02:/data --name redis02 -d redis:3.2 redis-server --slaveof  xxx.xxx.xxx.xxx 6379 --appendonly yes
    docker run  -p 6381:6379  -v $PWD/config03:/usr/local/redis  -v $PWD/data03:/data --name redis03 -d redis:3.2 redis-server --slaveof  xxx.xxx.xxx.xxx 6379 --appendonly yes
    //3.在每个库都启动哨兵进程,用于协调选举,需要添加sentinel.conf配置文件,地址为端口都为主库的信息,其中最后一个参数2为哨兵最低通过票数(设置为哨兵总数的超过半数即可)
    sentinel monitor  redis01 xxx.xxx.xxx.xxx 6379 2 
    //4.启动哨兵进程,/usr/local/bin/redis-sentinel /usr/local/redis/sentinel.conf --sentinel
    //5.客户端连接,可以通过info replication查看情况
    public static void main(String[] args) {  
        JedisPoolConfig poolconfig = new JedisPoolConfig();  
        poolconfig.setMaxIdle(30);  
        poolconfig.setMaxTotal(1000);  
        Set<String> sentinels = new HashSet<String>();  
        sentinels.add("xxxxxx:xxx");  
        JedisSentinelPool jedisSentinelPool = new JedisSentinelPool("master",   
                sentinels, poolconfig, 3000, "redispass");  
        HostAndPort currentHostMaster = jedisSentinelPool.getCurrentHostMaster();  
        System.out.println("currentHostMaster : " + currentHostMaster.getHost() + " port: "  + currentHostMaster.getPort());  
        Jedis resource = jedisSentinelPool.getResource();  
        resource.setDataSource(jedisSentinelPool);  
        String value = resource.get("aa");  
        System.out.println(value);  
        resource.close();  
    }  
    

    tip:
    生产环境哨兵也需要做集群,避免单点故障,按照上面的方式启动多个哨兵进程即可,或者每个节点启动一个哨兵进程
    之后有空一定要把docker的compose模式学习好,进一步简化部署工作。
    Redis Sentinel机制与用法
    dockerhub-redis
    Docker化高可用redis集群

    Redis Cluster
    Redis Cluster至少需要3个主库和3个从库,目前还需要Ruby编写相关脚本,接下来简单的了解下唯品会Redis cluster大规模生产实践感谢唯品会的分享
    使用场景
    唯品会经历了从客户端分片->代理分片->路由查询的三个阶段。redis cluster用作内存存储服务,帮助大数据实时推荐/ETL、风控、营销三大业务,其单个cluster集群在几十个GB到上TB级别内存存储量(很可怕)。作为后端应用的存储,数据来源包含Kafka --> Redis Cluster,Storm/Spark实时;Hive --> Redis Cluster, MapReduce程序;MySQL --> Redis Cluster,Java/C++程序。说实话,居然不是用于Cache功能,确实有些出人意料。
    优点:无中心 架构;数据按照slot存储分布在多个redis实例上;增加slave做standby数据副本,用于failover,使集群快速恢复;实现故障auto failover;节点之间通过gossip协议交换状态信息;投票机制完成slave到master角色的提升。
    缺点:client实现复杂,驱动要求实现smart client,缓存slots mapping信息并及时更新;目前仅JedisCluster相对成熟,异常处理部分还不完善,比如常见的“max redirect exception”;客户端的不成熟,影响应用的稳定性,提高开发难度。节点会因为某些原因发生阻塞(阻塞时间大于clutser-node-timeout),被判断下线。这种failover是没有必要,sentinel也存在这种切换场景。
    Tip:
    推荐一个国内的Redis中文网,内容有很高的借鉴价值。

    参考资料
    Spring整合Ehcache管理缓存
    LocalCache本地缓存分享
    以Spring整合EhCache为例从根本上了解Spring缓存这件事
    Guava学习笔记:Guava cache
    Spring Boot中的缓存支持(一)注解配置与EhCache使用
    Spring Boot中Redis的使用
    Redis的两个典型应用场景
    Redis集群方案总结
    Redis cluster官方文档

  • 相关阅读:
    docker
    电商项目查询模块思路(ElasticSearch)
    Elasticsearch简单的使用步骤以及查询模块的实现
    string常用方法
    通用 Mapper常用方法
    Linux防火墙操作
    简单SQL语句
    InnoDB基础
    浅析索引
    python爬虫面试题集锦及答案
  • 原文地址:https://www.cnblogs.com/xiong2ge/p/Cache_Java_Profound.html
Copyright © 2011-2022 走看看