一: 性能监控指标
使用info命令可以查看redis状态
connected_clients:68 # 连接的客户端数量used_memory_rss_human:847.62M # 系统给 redis 分配的内存used_memory_peak_human:794.42M # 内存使用的峰值大小total_connections_received:619104 # 服务器已接受的连接请求数量instantaneous_ops_per_sec:1159 # 服务器每秒钟执行的命令数量 qpsinstantaneous_input_kbps:55.85 #redis 网络入口 kpsinstantaneous_output_kbps:3553.89 #redis 网络出口 kpsrejected_connections:0 # 因为最大客户端数量限制而被拒绝的连接请求数量expired_keys:0 # 因为过期而被自动删除的数据库键数量evicted_keys:0 # 因为最大内存容量限制而被驱逐( evict )的键数量keyspace_hits:0 # 查找数据库键成功的次数keyspace_misses:0 # 查找数据库键失败的次数
二:缓存问题
1、缓存穿透
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,比如暴力攻击,查询一个不存在的id,请求穿透redis造成db压力过大。
解决办法:
缓存空对象: 当第一次查询db后,就往redis存空对象。 后来的请求就不会访问db了。
这种情况需要将过期时间设置短一点,但是也不能避免该时段内,出现数据不一致情况。。另外,缓存大量的空对象,也对redis造成浪费。
使用布隆过滤器(Bloom Filter):
在db和redis插入数据时,可以将redis的key存入bloom。 这样在查询前先校验bloom是否存在key,有才查redis和db
直观的说,bloom算法类似一个hash set,用来判断某个元素(key)是否在某个集合中。
和一般的hash set不同的是,这个算法无需存储key的值,对于每个key,只需要k个比特位,每个存储一个标志,用来判断key是否在集合中。算法:
1. 首先需要k个hash函数,每个函数可以把key散列成为1个整数
2. 初始化时,需要一个长度为n比特的数组,每个比特位初始化为0
3. 某个key加入集合时,用k个hash函数计算出k个散列值,并把数组中对应的比特位置为1
4. 判断某个key是否在集合时,用k个hash函数计算出k个散列值,并查询数组中对应的比特位,如果所有的比特位都是1,认为在集合中。优点:不需要存储key,节省空间,查询效率很高。
缺点:
1. 算法判断key在集合中时,有一定的概率key其实不在集合中
2. 无法删除。所以校验key有的时候,实际数据可能已经不存在了。
2、缓存击穿
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),如果是热点数据,可能并发很大,会造成db压力过大。
解决办法:
加锁。 当redis不存在需要从db查时,加锁,获取锁后,再从redis查一次,如果有,释放。如果没有从db查出来,存入redis。
3、缓存雪崩
某一时间大量key失效(比如key同时失效或重启redis),造成db压力过大。
解决办法:
1、做高可用,将数据分散到不通的实例
2、不同的key设置不通的缓存过期时间
3、可以再设置二集缓存
三:分布式锁
使用redis分布式锁,要注意几个最基本的原则:
首先,我们在使用分布式锁的时候,不能只依靠手动解锁,一定需要设置过期时间。 否则,如果程序执行失败,则会造成死锁。
其次,分布式锁的key-value中,value也需要保持唯一(通常跟上当前线程id),用来保证手动解锁的时候,一定是释放掉自己上的锁。
下面来看一下实现redis分布式锁的几种方式:
1、setnx
早期的redis使用setnx,原理是往redis添加一个key-value,如果key不存在则成功。 如果设置成功,再设置过期时间。 但是呢,这些操作都是非原子操作,存在一下问题:
1、查看key不存在后,set值可能失败。
2、set值成功后,设置过期时间可能失败。
3、在master-slave模式中,如果set值到master成功,还没有copy到slave,master挂掉,slave切换成新的master,则其他线程会获取到锁。
4、不能续约,如果执行时间超过过期时间,程序没有完成,也会释放掉锁。
2、set
String set(String key, String value, String nxxx, String expx, long time);
该方法是: 存储数据到缓存中,并制定过期时间和当Key存在时是否覆盖。nxxx: 只能取NX或者XX,如果取NX,则只有当key不存在是才进行set,如果取XX,则只有当key已经存在时才进行set
expx: 只能取EX或者PX,代表数据过期时间的单位,EX代表秒,PX代表毫秒。
time: 过期时间,单位是expx所代表的单位。该方法获取锁是使用lua脚本实现,具有原子性。 问题在于释放锁的时候,比如,在finally中释放锁,当校验到自己上锁后,可能释放锁的操作失败了,因为校验和释放非原子操作。(校验是否自己上的锁是必须的,因为,可能自己的锁达到过期时间自动释放了,这个时候校验的锁:是别的线程加的锁)
3、redisson (推荐)
基本使用
使用其实就是先获取锁(代码中创建锁),再加锁和解锁的过程(这里操作redis)。 将一段逻辑命令发使用lua脚本发送给redis,保证原子性。来看一下实现:
加锁机制
以上的逻辑就是:
a、如果没加过锁,则加锁,设置过期时间
b、如果已加过锁,则加重入锁(上锁次数+1),重新设置锁的过期时间
c、其他client已上锁,返回锁的时间
其中参数的意思是:
KEYS[1]) : 加锁的key
ARGV[1] : key的过期时间,默认为30秒
ARGV[2] : 加锁的value,需要包含客户端ID (UUID.randomUUID()) + “:” + threadId)
此外,redisson还实现了过期时间的续约机制,一旦client端加锁成功就会启动一个后台线程watch dog,每10s检查,如果client端还持有锁,则延长key的生存时间。
流程如下:a、如果锁已释放,发布解锁消息
b、如果不是自己上的锁,不能解
c、如果是自己上的锁,看是否是多次,将次数-1。 否则,删除锁,发送解锁消息
redis的实现方式和zk不同,zk是通过创建临时顺序节点,并获取所有节点来判断:是否当前自己是最小节点,如果是,代表获取到锁。 网上很多人在对比,说redis需要自己不断的尝试来获取,性能差,而zk只需要注册监听; redis集群在master宕机后,会造成多client上锁成功。 但这种东西吧,并不绝对,还是要根据实际情况来选择。 比如,zk的优点是分布式调度,如果程序中上锁的地方非常多,在高并发情况下,就会不断的创建和删除节点,并且该操作只能leader执行,然后同步给其他follower。 加上各种监听再获取所有节点遍历是否最小,性能也好不了多少。 反而这种情况下,对redis存取数据的特性来说,会好很多。。。另外,假如项目架构中有redis,没有需要使用zk的地方,也没必要为了使用分布式锁专门去维护zk。