redis缓存
Redis缓存
Redis 作为数据查询
redis mysql缓存模式一般采用cache Aside Pattern模式(该模式不是能够实现,缓存实现写入到数据库中,如果需要这样的,可以使用ignite缓存):
- 失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中
- 命中:应用程序从cache中取数据,取到后返回
- 更新:先把数据存到数据库中,成功后,再让缓存失效(更新数据库时,并不更新Redis缓存,而是在失效操作)。
作为缓存,需要应对几个问题 - 缓存穿透,缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。 key不存在时,大量的数据进来查询DB
- 缓存雪崩,某个时间点上,大量缓存都失效了,大量请求落在数据库上,而数据库无法承受造成雪崩
- 缓存击穿,在并发的情况下,大量请求同时请求某一个key,而该key失效了,并发的请求就会落在数据库上,而后压垮数据库
解决这些问题的方法:
- 有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
- 缓存雪崩的解决方法:缓存失效时的雪崩效应对底层系统的冲击非常可怕。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线 程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。这里分享一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
- 缓存击穿:原因是在某一时刻大量并发访问失效key时最后访问数据了,可以使用分布式锁,软过期,本地锁的方式。分布式锁,使用redis中setnx方式获取锁,只有一个获取到setnx设置的锁才能获取到key。本地锁,利用程序中锁方式,实现只有一个线程能够去获取key,软过期是redis存储缓存不使用缓存服务器提供的过期时间,而是将失效时间设置到key对应的value中,在获取时,找不到key,则利用后台线程去访问数据并刷新缓存,但是其他线程就可能获取失效的数据,能够有一定容忍性的项目可以使用。
缓存的配置
作为缓存查询,存储的内存和使用的过期策略也是很重要的
# redis配置文件中
maxmemory 50mb # 设置最大内存
maxmemory-policy volatile-lru # 设置过期策略
noeviction:达到内存限额后返回错误,客户尝试可以导致更多内存使用的命令(大部分写命令,但DEL和一些例外)
allkeys-lru:为了给新增加的数据腾出空间,驱逐键先试图移除一部分最近使用较少的(LRC)。
volatile-lru:为了给新增加的数据腾出空间,驱逐键先试图移除一部分最近使用较少的(LRC),但只限于过期设置键。
allkeys-random: 为了给新增加的数据腾出空间,驱逐任意键
volatile-random: 为了给新增加的数据腾出空间,驱逐任意键,但只限于有过期设置的驱逐键。
volatile-ttl: 为了给新增加的数据腾出空间,驱逐键只有秘钥过期设置,并且首先尝试缩短存活时间的驱逐键
缓存做为资源
缓存作为资源存储,数据库则作为一种备份,每一次数据的更新则必须要更新到缓存中。而作为资源则必须要要某些时刻只有一个线程去访问。本地锁,在访问资源时上锁,如Reentrantlock中lock和unlock方法。也可以使用redis分布式锁。
/** 尝试获得注册时的分布式锁. */
public boolean tryLock4Reg()
{
long ts = Cfg.clock;
while (!__tryLock__(DbRedis.KEY_PREFIX_REGLOCK))
{
Misc.sleep(50);
if (Cfg.clock - ts > DbRedis.USR_OPER_LOCK_TIMEOUT + 500)
return false;
}
return true;
}
/** 释放分布式锁. */
public void unLock4Reg()
{
this.delVal(DbRedis.KEY_PREFIX_REGLOCK);
}
/** 尝试获得分布式锁. */
private boolean __tryLock__(String key)
{
Jedis jedis = null;
try
{
jedis = this.getJedis();
String val = Cfg.clock + "";
if (jedis.setnx(key, val) == 1L)/* 不存在设置值. */
return true;
String oldVal = jedis.get(key);
if (oldVal == null)/* 锁刚被释放. */
return jedis.setnx(key, val) == 1L;
if (Cfg.clock - Misc.forceLongO(oldVal) < DbRedis.USR_OPER_LOCK_TIMEOUT)/* 锁未被超时. */
return false;
/* 锁超时. */
oldVal = jedis.getSet(key, val);
if (oldVal == null)/* 锁被释放. */
return jedis.setnx(key, val) == 1L;
return Cfg.clock - Misc.forceLongO(oldVal) >= DbRedis.USR_OPER_LOCK_TIMEOUT;/* 锁已经超时,并且没有任何线程干扰. */
} finally
{
this.close(jedis);
}
}
/** 删除一个值. */
public void delVal(String key)
{
Jedis jedis = null;
try
{
jedis = this.getJedis();
jedis.del(key);
} finally
{
this.close(jedis);
}
}
锁资源有一点需要注意的,资源锁住后,之前的并行操作,在该处都变成了串行操作,如果粒度太大,那么占用锁资源的时间消耗将非常大,需要选取尽可能小的粒度。