1.为什么需要缓存?
我们知道redis缓存是存储key-value的内存数据库,内存访问数据更快捷。而在web项目中,对于读多写少的高并发场景,我们会经常使用缓存来进行优化。redis缓存的优点:
1:读写性能极高 , redis读对的速度是11万次/s , 写的速度是8.1万次/s.
2:redis 支持多种数据类型,redis存储的是 key–value格式的数据,其中key是字符串,但是value就有多种数据类型了:String , list ,map , set ,有序集合(sorted set)
2.redis缓存架构
项目中redis的架构如下所示:
3.redis缓存遇到的问题
3.1 缓存和数据库写入一致性问题
当写入数据时,会遇到一个问题:是先将数据更新到缓存,还是将数据更新到数据库。必须要知道的是,更新缓存和更新数据库不会是原子性的,如果在更新缓存成功后,未更新数据库,会导致数据异常。如果更新数据库,但是未更新缓存,在从获取数据时,数据也是异常的。这两种操作都会导致数据不一致的问题。所以,我们得根据系统的需求来评估,是先更新缓存还是更新数据库。
由于我们的系统读大于写,在这种场景下,只需要以数据库为主,先写数据库,再写缓存就好了。
3.2 缓存击穿问题
指查询一个不存在的key,而这个key扛着大并发,这时由于缓存没有就需要到数据库中查找,则此时数据库将面临大量的请求压力。
- 将查询不到的key添加一个标志空值的value,比如:我们场景为查询一个标的的信息,如果这个标的信息不存在,我们设置一个空字符串返回。代码如下:
//无论数据是否为空,都推一个key到redis,防止数据为null时不走缓存
List jsonList = new ArrayList();
jsonList.add("");
- 把所有存在的key都存到另外一个存储的Set集合里,查询的时候将不符合规则的key进行过滤。
还有最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
基本原理及要点:对于原理来说很简单,位数组+k个独立hash函数。将hash函数对应的值的位数组置1,查找时如果发现所有hash函数对应位都是1说明存在,很明显这个过程并不保证查找的结果是100%正确的。 - 这些Key可能不是永远不存在,所以需要根据业务场景来设置过期时间。
3.3 缓存雪崩问题
缓存雪崩,是指在某一个时间段,缓存集中过期失效。这时客户端获取key时,无法获取到,只能从数据库中获取,那么短暂的时间内,大量请求都积压到了数据库,造成数据库雪崩。
- 这个没有完美解决办法,但可以分析用户行为,尽量让失效时间点均匀分布
- 批量缓存的对象是一个结果集,条目有10万条,缓存时间基础为 60602(sec),现在需要同时进行缓存。我的做法是默认生成一个随机数,如random(范围 0 - 1000),过期时间则设置为( 60602 + random ) 。
- 做二级缓存,或者双缓存策略。
4. 缓存搭建
我们的web项目中采用的是哨兵。一主两从三哨兵。
5. 实际运用
Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持。Redisson实现了分布式和可扩展的Java数据结构,和Jedis相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性。因此项目中选择了jedis作为java客户端。依赖如下:
redis.clients
jedis
${redis.version}
org.springframework.data
spring-data-redis
${spring.redis.version}
redis相关操作工具类
@Component
public class RedisUtil {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private HashOperations hashOps;
private ListOperations listOps;
// private SetOperations setOps;
private ValueOperations valueOps;
private ZSetOperations zsetOps;
@PostConstruct
public void setUp() {
hashOps = stringRedisTemplate.opsForHash();
listOps = stringRedisTemplate.opsForList();
// setOps = stringRedisTemplate.opsForSet();
valueOps = stringRedisTemplate.opsForValue();
zsetOps = stringRedisTemplate.opsForZSet();
}
/**
* key是否存在
*
* @param key
* @return
*/
public boolean hasKey(String key) {
return stringRedisTemplate.hasKey(key);
};
/**
*
* @param key
* @param hashKey
* @return
*/
public boolean hasHashKey(String key, String hashKey) {
return hashOps.hasKey(key, hashKey);
};
/**
* 删除key
*
* @param keys
*/
public void deleteKey(String... keys) {
stringRedisTemplate.delete(Arrays.asList(keys));
}
/**
* 通过key删除数据
*
* @param key
*/
public void deleteByKey(String key) {
stringRedisTemplate.delete(key);
}
/**
* key设置过期时间
*
* @param key
* @param timeout
* @param timeUnit
*/
public void expire(String key, Long timeout, TimeUnit timeUnit) {
stringRedisTemplate.expire(key, timeout, timeUnit);
}
/**
* 类似keys *
*
* @param pattern
* @return
*/
public Collection getKeys(String pattern) {
return stringRedisTemplate.keys(pattern);
}
public static boolean isEnableRedisCache = true;
/**
* 是否已缓存key
*
* @param key
* @return
*/
public boolean isCached(String key) {
return isEnableRedisCache && hasKey(key);
};
public boolean isCachedHashKey(String key, String hashKey) {
return isEnableRedisCache && hasHashKey(key, hashKey);
};
public final static int EXRP_MINUTE = 60; // 一分钟
public final static int EXRP_HOUR = 60 * 60; // 一小时
public final static int EXRP_DAY = 60 * 60 * 24; // 一天
public final static int EXRP_MONTH = 60 * 60 * 24 * 30; // 一个月
public final static int EXPR_YEAR = 12 * 60 * 60 * 24 * 30;// 一年
// 缓存有效期,单位秒
public static Map expireMap = new HashMap();
static {
// key过期设置
expireMap.put("User", 30 * EXRP_MINUTE);// 30分钟过期
}
/**
* 缓存单条记录
*
* @param
* @param id
* @param o
*/
public void setSingleObjectInCache(String key, Object obj) {
if (!isEnableRedisCache) {
return;
}
String singleJson = FastJosnUtils.toJson(obj);
if (expireMap.containsKey(obj.getClass().getSimpleName())) {
valueOps.set(key, singleJson,
expireMap.get(obj.getClass().getSimpleName()),
TimeUnit.SECONDS);
} else {
valueOps.set(key, singleJson, expireMap.get("default"),
TimeUnit.SECONDS);
}
}
/**
* 缓存单条记录,并设置过期时间
*
* @param key
* @param obj
* @param time
* @param unit
*/
public void setSingleObjectInCache(String key, Object obj, Integer time,
TimeUnit unit) {
if (!isEnableRedisCache) {
return;
}
String singleJson = FastJosnUtils.toJson(obj);
valueOps.set(key, singleJson, time, unit);
}
/**
* 根据key获取
*
* @param key
* @param clazz
* @return
*/
public T getByKeyFromCache(String key,Class clazz) {
if (!isEnableRedisCache) {
return null;
}
String jsonStr = valueOps.get(key);
return jsonStr == null ? null : (T) FastJosnUtils.toObject(jsonStr, clazz);
}
/**
* key的剩余有效期
*
* @param key
* @return
*/
public long ttl(String key) {
return valueOps.getOperations().getExpire(key, TimeUnit.SECONDS);
}
/**
* 缓存一个集合
*
* @param key
* 键
* @param collecation
* 要缓存的集合
* @param elementClass
* 集合中类的类型
*/
public void setCollectionInCache(String key, Collection collecation,
Class> elementClass) {
if (!isEnableRedisCache) {
return;
}
// 无论数据是否为空,都推一个key到redis,防止数据为null时不走缓存
List jsonList = new ArrayList();
jsonList.add("");
List list = (List) collecation;
for (int i = list.size() - 1; i >= 0; i--) {
jsonList.add(FastJosnUtils.toJson(list.get(i)));
}
listOps.getOperations().delete(key);
listOps.leftPushAll(key, jsonList);
if (expireMap.containsKey(elementClass.getSimpleName())) {
listOps.getOperations().expire(key,
expireMap.get(elementClass.getSimpleName()),
TimeUnit.SECONDS);
}
}
/**
* 从缓存中取出列表,支持分页
*
* @param
* @param
* @param id
* @param obj
* @return 若没有启用缓存或缓存中没有相应的key,返回null 若缓存中存在key,但value为null,返回空集合对象 example
* : new ArrayList()
*/
@SuppressWarnings("unchecked")
public , E> T getCollectionInCache(String key,
Class collectionClass, Class elementClass, int offset,
int size) {
if (!isEnableRedisCache) {
return null;
}
if (!hasKey(key)) {
return null;
}
T conllection = (T) new ArrayList();
List collecationJson = listOps
.range(key, offset, offset + size);
if (collecationJson == null || collecationJson.isEmpty()) {
return conllection;
}
for (int i = 0; i < collecationJson.size() - 1; i++) {
conllection.add((E) FastJosnUtils.toJson(collecationJson.get(i)));
}
return conllection;
}
/**
* 缓存一个Map
*
* @param
* @param key
* 键
* @param collecation
* 要缓存的集合
* @param elementClass
* 集合中类的类型
*/
public void setMapInCache(String key, Map map,
Class> elementClass) {
if (!isEnableRedisCache) {
return;
}
Map jsonMap = new ConcurrentHashMap();
for (Map.Entry entry : map.entrySet()) {
jsonMap.put((String) entry.getKey(), FastJosnUtils.toJson(entry.getValue()));
}
hashOps.getOperations().delete(key);
hashOps.putAll(key, jsonMap);
}
/**
* 添加元素到有序集合
*
* @param key
* @param obj
* @param score
*/
public void zSet(String key, Object obj, double score) {
if (!isEnableRedisCache) {
return;
}
zsetOps.add(key, FastJosnUtils.toJson(obj),score);
}
/**
* 获取有序集合中指定key-value的分数
* @param key
* @param obj
* @return
*/
public Double getScore(String key,Object obj) {
return zsetOps.score(key, obj);
}
/**
* 从有序集合中删除元素
* @param key
* @param values
* @return
*/
public Long remove(String key, Object... values){
return null;
}
}