java分布式缓存方案
作者:女友在高考
一、从数据说起
我们再做缓存之前需要把数据先分好类
按变化频率:
- 静态数据:一般不变的,类似于字典表
- 准静态数据:变化频率很低,部门结构设置,全国行政区划数据
- 中间状态数据:一些计算的可复用中间数据,变量副本,配置中心的本地副本
按使用频率:
- 热数据:使用频率高的
- 读写比大的:读的频率远大于写的频率
这些数据就比较适合使用缓存。
缓存无处不在。内存可以看作是cpu和磁盘之间的缓存。cpu与内存的处理速度也不一致,所以出现了L1&L2 Cache
缓存的本质:系统各级之间处理速度不匹配,利用空间换时间。
缓存加载时间
1. 启动时全量加载
2. 懒加载
2.1. 同步使用加载
先看缓存里是否有数据,没有的话从数据库读取。读取的数据,先放到内存,然后返回给调用方。
2.2. 延迟异步加载
从缓存里获取数据,不管有没有都直接返回。
策略1:如果缓存为空的话,则发起一个异步线程负责加载。
策略2:异步线程负责维护缓存的数据,定期或根据条件触发更新。
缓存过期策略
- 按FIFO或LRU
- 固定时间过期
- 根据业务进行时间的加权。
二、本地缓存
1.Map 缓存
public static final Map<String,Object> CACHE=new HashMap(); CACHE.put("key","value");
2.Guava缓存
Cache<String,String> cache = CacheBuilder.newBuilder() .maximumSize(1024) .expireAfterWrite(60,TimeUnit.SECONDS) .weakValues() .build(); cache.put("word","Hello Guava Cache"); System.out.println(cache.getIfPresent("word"));
3.Spring Cache
- 基于注解和AOP,使用方便
- 可以配置Condition和SPEL,非常灵活
- 需要注意:绕过Spring的话,注解无效
核心功能:@Cacheable、@CachePut、@CacheEvict
本地缓存的缺点:
- 在集群环境中,如果每个节点都保存一份缓存,导致占用内存变大
- 在JVM中长期存在,会影响GC
- 缓存数据的调度处理,影响业务线程,争夺资源
三、远程缓存
1.Redis
Redis是一个开源的使用ANSI C语言编写的,基于内存也可以持久化的key-value数据库,并提供多种语言的API
2. Memcached
memcached是一套分布式的高速缓存系统,由LiveJournal的Brad Fitzpatrick开发,但被许多网站使用。这是一套开放源代码软件,以BSD license授权发布。
四、内存网格
- Hazelcast
- lgnite
五、缓存常见问题
1. 缓存穿透
问题描述:大量并发查询不存在的KEY,导致都直接把压力透传到数据库上。
分析:因为数据库里没有值,所以没有建立缓存,导致一直打到数据库上。
解决办法:
- 缓存空值的KEY
- Bloom过滤或RoaringBitmap判断KEY是否存在
- 完全以缓存为准,使用延迟异步加载的方式去加载数据库数据到缓存。
Bloom过滤器示例:
(引入guava依赖)
public static void main(String[] args) { BloomFilter<CharSequence> filter = BloomFilter.create( Funnels.stringFunnel(Charsets.UTF_8),//Funnels.integerFunnel(), //数据格式 1000000,//预计存入数据量 0.01);//误判率 System.out.println(filter.mightContain("abcdefg")); filter.put("abcdefg"); System.out.println(filter.mightContain("abcdefg")); }
RoaringBitmap示例:
引入依赖:
<dependency> <groupId>org.roaringbitmap</groupId> <artifactId>RoaringBitmap</artifactId> <version>0.8.1</version> </dependency>
public static void test3(){ Roaring64NavigableMap roaring64NavigableMap = Roaring64NavigableMap.bitmapOf(3, 4, 5, 90); //是否包含 boolean contains = roaring64NavigableMap.contains(3); long l = roaring64NavigableMap.rankLong(3); System.out.println(l); System.out.println(contains); }
2. 缓存击穿
问题:当某个KEY失效的时候,正好有大量并发请求访问这个KEY
分析:跟缓存穿透比较像,这个是属于偶然的
解决办法:
- KEY的更新的时候添加全局互斥锁
- 完全以缓存为准,使用延迟异步加载的策略
3. 缓存雪崩
问题:当某一个时刻发生大规模的缓存失效的情况,会有大量请求打到数据库,导致数据库压力过大而宕机
分析:一般来说,由于更新策略、或者数据热点、缓存服务宕机等原因,导致缓存数据同时大规模不可以。
解决办法:
- 缓存更新、失效策略在时间上做到比较均匀
- 使用的热数据尽量分散到不同机器上
- 多台机器做主从复制,实现高可用
- 实现熔断限流机制,对系统进行负载能力控制
- 使用本地缓存兜底
番外:
布隆过滤器:
目标就是要基于过滤器已存储生成的原始元数据,进行比较过滤,如果是在原始元数据集合里面的,一定会被发现。也有可能不是里面的被误杀。
BloomFilter 会开辟一个m位的bitArray(位数组),开始所有数据都部署为0,当一个元素过来的时候,通过多个hash函数计算出不同的值,然后根据hash值找到对应的下标处,将里面的值改为1.
优点:使用计算,节省存储空间。
缺点:有失误率。不是在过滤器原始表里的数据也会被误算进去。
使用场景:目标就是要基于过滤器已存储生成的原始元数据,进行比较过滤,如果是在原始元数据集合里面的,一定会被发现。布隆过滤器核心正确的使用就是进行过滤禁止,进行正确的否定。
举例:如我们有100万个黑名单的url地址,过来一个地址我们算出来不在里面,那就肯定可以放行。
BitMap:
BitMap的基本思想是用一个bit位来标记某个元素对应的值,这样就可以大大节省空间。
在Java中一个int占4个字节,也就是32bit。按int存储和按位存储的大小差距是32倍。
那么怎么表示一个数呢?可以使用1表示存在,0表示不存在。
如下面:表示{2,6}
一个byte只有8个位置,如果想表示13怎么办呢?只能再用一个byte了,就成了一个二维数组了
1个int占32位,那么我们只需要申请一个int数组长度为 int tmp[1+N/32] 即可存储,其中N表示要存储的这些数中的最大值
使用场景:
- 1.快速排序
把数放进去之后,遍历一遍,把值是1的都取出来就排好序了。
- 2.快速去重
20亿个整数中找出不重复的整数的个数?
内存不足以容纳这20亿个整数。我们怎么表示数字的状态呢?一个数的状态可以分为3种,不存在、存在一次、存在两次及以上。这就需要两个bit来表示。00代表不存在,01代表一次,11代表两次及以上。
接下来我们就把这20亿个整数放进去,如果状态为00,就改为01,如果状态为01就改为11.如果状态为11,就不动了。都放完后,遍历取出值为01的,就是不重复的数据的个数。。
- 3. 快速查找
给定一个整数M,M/32就能得到int数组的下标,M%32就知道在这个下标里面的具体位置。
如13,就能算出在int[0]里面的第13个
到此这篇关于分布式缓存的文章就介绍到这了,更多相关缓存内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!