对于缓存大家都不会陌生,但如何正确和合理的使用缓存还是需要一定的思考,本文将基于 Java 技术栈对缓存做一个相对详细的介绍,内容分为基本概念、本地缓存、远程缓存和分布式缓存集群几个部分,重点在于理解缓存的相关概念,愿合理的使用 Cache 如下图的妹子一样美好。
缓存是计算机系统中必不可少的一种解决性能问题的方法,常见的应用包括 CPU 缓存、操作系统缓存、本地缓存、分布式缓存、HTTP 缓存、数据库缓存等。其核心就是用空间换时间,通过分配一块高速存储区域(一般来说是内存)来提高数据的读写效率,实现的难点就在于清空策略的实现,比较合理的思路就是定时回收与即时判断数据是否过期相结合。
Tip:
缓存在高并发场景下的常见问题
分布式缓存系统的需要注意缓存一致性、缓存穿透和雪崩、缓存数据的清理等问题。可以通过锁解决一致性问题;为了提高缓存命中率,可以对缓存分层,分为全局缓存,二级缓存,他们是存在继承关系的,全局缓存可以有二级缓存来组成。为了保证系统的 HA,缓存系统可以组合使用两套存储系统(memcache,redis)。缓存淘汰的策略包括定时去清理过期的缓存、判断过期时间来决定是否重新获取数据。
在 java 应用中通常由两类缓存,一类是进程内缓存,就是使用 java 应用虚拟机内存的缓存;另一个是进程外缓存,现在我们常用的各种分布式缓存。前者比较简单,而且在一个 JVM 中,快速且可用性高,但会存在多态负载均衡主机数据不一致的问题,因此适合最常用且不易变的数据。后者扩展性强,而且相关的方案多,比如 Redis Cluster 等。通常来说,从数据库读取一条数据需要 10ms,从分布式缓存读取则只需要 0.5ms 左右,而本地缓存则只需要 10μs,因此需要根据具体场景选出合适的方案。
Java 的本地缓存很早就有了相关标准
,要求的特性包括原子操作、缓存读写、缓存事件监听器、数据统计等内容。实际工作中本地缓存主要用于特别频繁的稳定数据,不然的话带来的数据不一致会得不偿失。实践中,常使用
- javax.cache
,以及与 Spring 结合良好的
- Guava Cache
.
- EhCache
是一个全内存的本地缓存实现,它提供了线程安全的实现机制,简单易用,性能好。其创建方式包括
和
- cacheLoader
两种,前者针对整个
- callable 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 也是一个全内存的本地缓存实现,符合
规范,被应用在 Hibernate 中,过去存在过期失效的缓存元素无法被 GC 掉,造成内存泄露的问题,其主要类型及使用示例如下所示。 Element:缓存的元素,它维护着一个键值对。 Cache:它是 Ehcache 的核心类,它有多个 Element,并被 CacheManager 管理,实现了对缓存的逻辑操作行为。 CacheManager:Cache 的容器对象,并管理着 Cache 的生命周期。
- javax.cache JSR-107
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 官方文档
常见的分布式缓存组件包括 memcached,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 快速入门,现在来看理解上还差的比较多,有些麻瓜。
实现包括如下 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 角色的提升。
- 客户端分片->代理分片->路由查询
参考资料
Spring 整合 Ehcache 管理缓存
LocalCache 本地缓存分享
以 Spring 整合 EhCache 为例从根本上了解 Spring 缓存这件事
Guava 学习笔记:Guava cache
Spring Boot 中的缓存支持(一)注解配置与 EhCache 使用
Spring Boot 中 Redis 的使用
Redis 的两个典型应用场景
Redis 集群方案总结
Redis cluster 官方文档
来源: http://www.cnblogs.com/wanliwang01/p/cache01.html