一、分布式锁
redis的setnx被广泛应用在分布式锁中,使用setnx时,如果值存在,则设置失败,返回0,否则设置成功返回1,因为redis主要工作线程时单线程执行,所以把这个当作分布式锁来可使用。
简单的分布式锁实现如下(缺少锁续命步骤,可以使用成熟的分布式锁框架redisson):
@RestController
public class RedisLockTest {
@Autowired
JedisCluster jedisCluster;
@RequestMapping("/getValue")
public String getValue(){
//释放锁时确保是当前线程加的锁
String uuid = UUID.randomUUID().toString();
//redis集群setnx方法,并且设置过期时间
String lock = jedisCluster.set("lock", uuid, "nx", "PX", 30000);
//返回非OK,则获取锁失败
if(!"OK".equalsIgnoreCase(lock)){
return "服务器繁忙,请稍后再试!";
}
int stock=0;
try{
stock = Integer.parseInt(jedisCluster.get("stock"));
if(stock>0){
stock-=1;
jedisCluster.set("stock",stock+"");
System.out.println("减库存成功,剩余库存:"+stock);
}else {
System.out.println("减库存失败,剩余库存不足!");
}
}finally {
//释放锁
if (uuid.equals(jedisCluster.get("lock"))){
jedisCluster.del("lock");
}
}
return "剩余库存:"+stock;
}
}
二、缓存穿透
缓存穿透是指从缓存中查不到数据,再到数据库去查也查不到,这样如果频繁的查询会导致系统性能的下降,甚至遭到恶意攻击时系统瘫痪。
解决办法:
1、是即使从数据库查询的数据为空,也要将查询的KEY值缓存在redis中,但是要设置一个过期时间,在短时间内查询数据虽然不一致,但是确保最终是一致的。
2、使用布隆过滤器,对于恶意攻击,向服务器请求大量不存在的key,可以使用布隆过滤器有效防止缓存穿透。其原理是事先将所有数据hash后存在bitmap中,通过查询布隆过滤器,可以过滤掉确定不存在的值。布隆过滤器说数据存在实际不一定存在,说不存在则一定不存在。
布隆过滤器实现代码:
@RestController
public class RedissonBloomFilter {
@Autowired
JedisCluster jedisCluster;
@Autowired
Redisson redisson;
@RequestMapping("/getBloomFileter/{value}")
public boolean getBloomFileter(@PathVariable(value = "value") String value){
RBloomFilter<Object> bloomFilter = redisson.getBloomFilter("bloomFilter");
初始化布隆过滤器:预计元素为1000000000L,误差率为2%
bloomFilter.tryInit(1000000000L,0.02);
//预设值
bloomFilter.add("bloom");
//查询是否存在
boolean contains = bloomFilter.contains(value);
System.out.println("查缓存值:"+value+" 是否存在:"+contains);
return contains;
}
}
三、缓存失效(击穿)
由于大量缓存在同一时间过期失效,导致大量请求同时访问数据库,使系统压力瞬间增大甚至挂掉。解决办法是设置过期时间时不要设置一样的时间,而是随机产生一个大概的时间范围,避免发生缓存击穿的现象。
四、缓存雪崩
由于大量的请求访问导致缓存层扛不住而大面积挂掉时,所有的请求都打到存储层,以至于存储层也扛不住这么大的并发量,而使系统全面瘫痪的局面。
解决办法:
1、保证缓存层的高可用,使用redis sentinel或redis cluster
2、依赖隔离组件为后端限流降级,比如Hystrix限流降级组件,当流量过大时,限制访问后端,快速返回预定的结果。
3、提前做好高并发测试演练,在缓存层宕机后出现的问题提前预案。
五、缓存与数据库数据不一致问题
在并发情况下,同时操作缓存和数据库会有数据不一致性问题。正常情况下我们更新数据的操作顺序是,先删除缓存,然后更新数据库,等有查询请求的时候先从缓存查,查不到再去数据库查,然后再缓存到redis。在单线程情况或并发不高的时候是没有问题提的,但是在高并发情况下,如果不是串行执行,没办法控制执行的顺序,当有读写操作同时进行时,就会出现,写请求先删除缓存数据,然后读操作抢先更新缓存数据,写操作再更新数据库,这就会导致缓存和数据库数据不一致问题。
解决办法:
1、对于用户个人维度的数据,一般不会出现并发的问题,只要给缓存的数据加一个过期时间,在可以容忍数据在小时间段内的数据不一致,但最终数据是一致的。
2、在大部分情况下,只要能容忍数据在小时间段内不一致,可以使用延迟双删+缓存过期时间,就可以解决大部分的并发数据不一致的问题。延迟双删就是在写操作时,先删除缓存,然后更新数据库,更新完延迟一段时间再删除缓存。
3、如果没办法容忍任何时间段内的数据不一致,例如电商库存,可以使用分布式读写锁,读读不互斥,写读互斥,写完再更新缓存,这样就不会出现数据不一致的问题了。
分布式锁解决方案:
@RestController
public class CacheAndDB {
@Autowired
JedisPool jedisPool;
@Autowired
Redisson redisson;
@RequestMapping("/getTotal")
public String getTotal() throws InterruptedException {
Jedis jedis = jedisPool.getResource();
RReadWriteLock rwLock = redisson.getReadWriteLock("RWLock");
RLock rLock = rwLock.readLock();
rLock.lock();
System.out.println("获取读锁成功!");
String total = jedis.get("total");
if (StringUtils.isEmpty(total)){
//模拟查询数据
total = jedis.get("totalDB");
System.out.println("查询数据库total: "+total);
//设置到缓存
jedis.set("total",total);
}
rLock.unlock();
return total;
}
@RequestMapping("/setTotal/{value}")
public String setTotal(@PathVariable(value = "value") int value){
Jedis jedis = jedisPool.getResource();
RReadWriteLock rwLock = redisson.getReadWriteLock("RWLock");
RLock rLock = rwLock.readLock();
rLock.lock();
jedis.del("total");
System.out.println("获取写锁成功!");
//模拟更新数据库
String set = jedis.set("totalDB", String.valueOf(Integer.parseInt(jedis.get("totalDB")) - value));
rLock.unlock();
return set;
}
}
六、redis对过期键删除策略
1、被动删除:当key的过期时间已经过了,redis不会立即删除,而是当有客户端访问这个key时才会被动去清除这个过期的key
2、主动删除:redis底层有会自动定期清除过期key的线程,主动去删除过期的key
3、当redis使用的内存超过设置的maxmemory最大限定,redis会触发主动清除策略
主动清除策略:
-
volatile-ttl: 清除过期时间最早的数据
-
volatile-random: 随机清除设置了过期时间的数据
-
volatile-lru: 清除设置了过期时间中最近最少使用的数据(最近使用时间排序)
-
volatile-lfu: 清除设置了过期时间中最不经常使用的数据(最近使用的次数排序)
-
allkeys-random: 从所有键中随机删除
-
allkeys-lru: 从所有键中清除最近最少使用的数据(最近使用时间排序)
-
allkeys-lfu: 从所有键中清除最不经常使用的数据(最近使用的次数排序)
-
noevitaion: 不会清除任何数据,读数据正常执行,写数据拒绝写入