缓存作用
主要是解决读多写上的场景,降低系统的CPU和IO的使用,提高系统的性能。
在一些需要高并发的场景,也可以使用类似Redis解决,如:库存的管理。
经典问题
1、缓存穿透
2、缓存击穿
3、雪崩
针对上面的缓存问题代码实现如下:
package com.newretail.account.service;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@Service
public class CacheService {
@Autowired
private RedisTemplate redisTemplate;
private Cache<String, Object> dataCache = CacheBuilder.newBuilder() // 使用 CacheBuilder 创建
.expireAfterAccess(Duration.ofSeconds(30)) // 设置过期时间,在每次访问的半小时后过期,(再次访问则重新等待半小时)
// .expireAfterWrite(Duration.ofHour(2)) 在每次写入缓存2小时后过期
.maximumSize(10240) // 缓存最大数量
.concurrencyLevel(4) // 指定并发修改的数量,默认4
.initialCapacity(2048) // 设置初始化大小,避免经常扩容
.build(); // 无参的 build 方法创建 Cache
private Map<String, Object> locks = new ConcurrentHashMap<>();
private AtomicBoolean casLock = new AtomicBoolean(false);
public <T> List<T> getCache(String key, DBData<T> dbUtils, int expire) {
List<T> data = getCacheData(key);
if (data == null) {
// 先获取指定key的锁,在获取前,先得加锁,保证每个key只能获取一个Key
Object lock = null;
//此处用cas保证只有一个线程进来,主要原因是获取key锁都是内存操作,时间短
if (casLock.compareAndSet(false, true)) {
lock = locks.get(key);
if (lock == null) {
lock = new Object();
locks.put(key, lock);
}
casLock.set(false);
}
synchronized (lock) {
//DCL检查,保证获取锁的线程直接读取Redis的数据,不要执行DB操作
data = getCacheData(key);
if (data == null) {
data = dbUtils.getDBData();
setCacheData(key, expire, data);
}
}
// 清除内存,防止内存溢出,释放的时候,可能会有使一个key有多个锁,但是缓存中已经有了数据,不会有大面积请求请求到数据库
locks.remove(key);
}
return data;
}
private <T> List<T> getCacheData(String key) {
List<T> localCache = (List<T>) dataCache.getIfPresent(key);
if (localCache != null) {
return localCache;
}
List<T> redisCache = (List<T>) redisTemplate.opsForValue().get(key);
if (redisCache != null) {
dataCache.put(key, redisCache);
return redisCache;
}
return null;
}
private void setCacheData(String key, int expire, List cacheData) {
redisTemplate.opsForValue().set(key, cacheData, expire, TimeUnit.SECONDS);
dataCache.put(key, cacheData);
}
}
interface DBData<T> {
List<T> getDBData();
}
4、数据一致性
5、拒绝bigkey(防止网卡流量、慢查询)
如果出现下面两种情况,我们会认为它是bigkey。
- 字符串类型:它的big体现在单个value值很大,一般认为超过10KB就是bigkey。
- 非字符串类型:哈希、列表、集合、有序集合,它们的big体现在元素个数太多。
一般来说,string类型控制在10KB以内,hash、list、set、zset元素个数不要超过5000。
反例:一个包含200万个元素的list。
非字符串的bigkey,不要使用del删除,使用hscan、sscan、zscan方式渐进式删除,同时要注意防止bigkey过期时间自动删除问题(例如一个200万的zset设置1小时过期,会触发del操作,造成阻塞)
bigkey的危害:
- 导致redis阻塞
- 网络拥塞
bigkey也就意味着每次获取要产生的网络流量较大,假设一个bigkey为1MB,客户端每秒访问量为1000,那么每秒产生1000MB的流量,对于普通的千兆网卡(按照字节算是128MB/s)的服务器来说简直是灭顶之灾,而且一般服务器会采用单机多实例的方式来部署,也就是说一个bigkey可能会对其他实例也造成影响,其后果不堪设想。
- 过期删除
有个bigkey,它安分守己(只执行简单的命令,例如hget、lpop、zscore等),但它设置了过期时间,当它过期后,会被删除,如果没有使用Redis 4.0的过期异步删除(lazyfree-lazy-expire yes),就会存在阻塞Redis的可能性。
bigkey的产生:
一般来说,bigkey的产生都是由于程序设计不当,或者对于数据规模预料不清楚造成的,来看几个例子:
- 社交类:粉丝列表,如果某些明星或者大v不精心设计下,必是bigkey。
- 统计类:例如按天存储某项功能或者网站的用户集合,除非没几个人用,否则必是bigkey。
- 缓存类:将数据从数据库load出来序列化放到Redis里,这个方式非常常用,但有两个地方需要注意,第一,是不是有必要把所有字段都缓存;第二,有没有相关关联的数据,有的同学为了图方便把相关数据都存一个key下,产生bigkey。
如何优化bigkey
- 拆
big list: list1、list2、...listN
big hash:可以讲数据分段存储,比如一个大的key,假设存了1百万的用户数据,可以拆分成200个key,每个key下面存放5000个用户数据
- 如果bigkey不可避免,也尽量不要每次把所有元素都取出来(例如有时候仅仅需要hmget,而不是hgetall),删除也是一样,尽量使用优雅的方式来处理。
参考: