redis使用场景(String)
上文中我们讲到redis一共有5中数据结构(String,Hash,List,Set,Zset),大家了解到了其底层数据结构以及存储方式,那这五种数据结构怎么使用呢?本期带大家了解下redis各种数据结构的使用场景。
命令介绍
String类型作为我们开发日常使用redis时可谓是最常用的场景,简单的key-value的的存储不仅简单而且方便,常用作缓存某些热点数据(key-Json)、计数器(限流)、分布式锁等。这里只列举常用命令
//缓存热点数据 key-jsonString
#设置mykey的值为hello 默认的过时时间-1永久 通过EX 指定超时时间
set mykey hello
#获取mykey的值
get mykey (return : hello)
//计数器
#设置mykey 10
set mykey 10
#incr 对key的值进行加1操作 incrby x 指定加x操作
incr mykey (return : 11)
#程序中如果将redis中key值先拿出来 再加1 会出现线程安全问题 需要使用lua脚本解决此问题(将操作原子化 详情看使用场景)
//分布式锁
#setnx 如果key不存在则插入 如果存在不做任何操作
setnx mykey hello (return : 1)
setnx mykey world (return : 0)
get mykey (return : hello)
场景介绍
缓存热点数据
使用string结构缓存 一般用来缓存热点数据,防止过多的请求对数据库造成额外的压力(毕竟数据库的查询牵涉连接的建立、以及磁盘IO的消耗)。当请求过来时,先进行查询redis,根据指定的key查看是否有数据,如果有直接返回,如果没有查询数据库进行并进行缓存 设立自动过期时间(毕竟redis基于内存,没有足够的空间)。那么这么做有什么缺点吗?我们来讨论下。
-
缓存穿透
-
是什么?
key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库
-
怎么解决?
有很多方法可以解决缓存穿透的问题。最常见的做法是采用布隆过滤器,将所有可能存在的数据hash到一个足够大的bitmap中。一个不可能存在的key会被这个bitmap过滤掉,从而避免了对底层存储系统的查询压力。还有一种比较常用的暴力的方法,如果查询数据为空,那么给一个默认值也进行缓存,但是缓存失效时间相应少一点。
-
//1、使用布隆过滤器,该知识点较独立,如果需要后续会单独出一篇文章 //2、缓存空数据 //伪代码 public class A{ public Object getData(String key){ String result = redis.get(key); if(StringUtils.isNotBlank(reuslt)){ return result; } result = map.getDB(key); if(StringUtils.isBlank(reuslt)){ result = "empty"; } redis.set(key, result); return result; } }
-
-
-
缓存击穿
-
是什么?
缓存击穿是指在同一时刻大量的请求访问同一个数据,但是该数据在此刻缓存失效,所以这些请求全部去访问数据库。导致DB压力
-
怎么解决?
解决缓存击穿的最暴力无脑的方法当然是设置热点数据永不过期,当然这样对我们redis的内存占用也是大大的。着实不划算。所以一般的解决方式就是加锁,保证同一时间不会有大量的请求因为一条数据而多次访问数据库。废话不多说,上代码。
public Object getData(String key){ Object data = getRedisData(key); if(data == null){ boolean flag = redisLock.getLock(key); if(flag){ //拿key作锁的获取条件,保证获取别的数据不会等待 Object data = queryDataFromDB(key); setRedisData(key, data); redisLock.unLock(key); }else{ //为获取到锁 等待100毫秒 Thread.sleep(100); getData(key); } }else{ return data; } }
-
-
缓存雪崩
-
是什么?
缓存雪崩跟缓存击穿有点像,是指同一时间大量的key同时失效,多个请求访问不同的key击穿数据库。其实就是大批量的数据缓存失效,然后引起数据库的压力过大甚至down机。
-
怎么解决?
不要脸的解决方式当然是数据永久有效。当然也是不怎么会用的。那么如果采用解决缓存击穿的同步锁呢?当然是可以解决问题,但是每个请求等待的100毫秒将会是对用户体验产生巨大的影响。网上给出的方案大都是缓存失效时间随机,采用redis集群并且每个分片失效时间不等。但是我觉得这样的方案依然不能保证完全避免雪崩。我这边的解决方案如下(不保证完全正确,但是我是这样处理的,^_^)
// 采用一级缓存 二级缓存 做第一步缓冲 //缓存一个正常时间的数据 以及 一个有效时间较长的数据 public Object getData(String key){ //获取一级缓存 Object data = getRedisDataCache1(key); if(data != null){ return data; } //获取二级缓存 data = getRedisDataCache2(key); if(data != null){ return data; } //获取锁 bollean flag = redisLock.lock(key); if(flag){ data = getDataFromDB(key); setRedisCache1(key, data); setRedisCache2(key, data); redisLock.unlock(key); }else{ //等待100毫秒 Thread.sleep(100); data = getData(key); } return data; } //上述代码 加了二级缓存后 可以保证一级缓存不会造成雪崩 那二级缓存如果同时失效呢?总不能二级缓存永久有效吧 //这里有两种选择 其一:二级缓存有效时间较长,通过其他技术保证二级缓存存的都是热点数据, //击穿二级缓存的数据都是不经常使用的数据,当然,这样做比较吃redis内存。 //其二: 二级缓存时间相对也较短,对mysql层增加限流操作,或者熔断操作。 //当达到mysql瓶颈时,进行熔断,如果是需要补偿的场景,记录熔断后的id,后续进行处理 public Object getData(String key){ //获取一级缓存 Object data = getRedisDataCache1(key); if(data != null){ return data; } //获取二级缓存 data = getRedisDataCache2(key); if(data != null){ return data; } data = getDataFromDb(key); } //此方法 增加限流 熔断 以及补偿机制 public Object getDataFromDb(String key){ ..... }
-
计数器
计数器的使用场景很多,但是几乎大同小异。如上述我们提到的限流,就是一个经常使用的场景(当然并不是限流只能通过这种方法实现,常见的限流有计数器、漏桶、令牌桶 这里不多做介绍)。使用计数器做限流,记录一个方法的访问次数,当达到一定次数的时候,就阻止其继续访问。原理很简单,直接上代码。
//跟着上边的逻辑
public Object getDataFromDb(String key){
....
}
@Around
//切面 切上边的方法
public void incr(){
int count = redis.incr(key);
if(count > 100){
doOtherThing();
log.log("达到限流上限");
}
}
分布式锁
分布式锁直接上代码吧。没什么可说的,单机分布式锁就是基于SETNX。解锁使用lua脚本,操作原子性。
@Slf4j
public class RedisLock implements SimpleLock {
private static final ThreadLocal<String> THREAD_LOCAL = ThreadLocal.withInitial(() -> null);
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_SECONDS = "EX";
private static final Long RELEASE_SUCCESS = 1L;
private final JedisPool jedisPool;
private Random random = new Random();
private String lockKey;
private int expiredSeconds;
public RedisLock(JedisPool jedisPool, String lockKey, int expiredSeconds) {
this.jedisPool = jedisPool;
this.lockKey = lockKey;
this.expiredSeconds = expiredSeconds;
}
public boolean lock() {
String randomValue = generateRandomValue();
THREAD_LOCAL.set(randomValue);
boolean isLock = true;
try (Jedis jedis = jedisPool.getResource()) {
String result = jedis.set(lockKey, randomValue, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_SECONDS, expiredSeconds);
if (!LOCK_SUCCESS.equalsIgnoreCase(result)) {
log.warn("lock | Failed to set key: {}, value: {}, result: {}", lockKey, randomValue, result);
isLock = false;
}
return isLock;
} catch (Exception e) {
log.warn("lock | Failed to setnx key: {}, value: {}, expiredSeconds: {}",
lockKey, randomValue, expiredSeconds, e);
}
return false;
}
public void unlock() {
String randomValue = THREAD_LOCAL.get();
if (StringUtils.isBlank(randomValue)) {
return;
}
try (Jedis jedis = jedisPool.getResource()) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(randomValue));
if (!RELEASE_SUCCESS.equals(result)) {
log.warn("unlock | Failed to del key: {}, value: {}, result: {}", lockKey, randomValue, result);
}
} catch (Exception e) {
log.warn("unlock | Failed to unlock key: {}, value: {}", lockKey, randomValue, e);
}
}
public String getLockKey() {
return lockKey;
}
private String generateRandomValue() {
return String.valueOf(System.currentTimeMillis()) + String.format("%03d", random.nextInt(1000));
}
}
来源:CSDN
作者:mazhenxing0805
链接:https://blog.csdn.net/mazhenxing0805/article/details/103699899