redis应用
redis 发布订阅
redis客户端可以订阅任意数量的频道
订阅方式
subscribe channel1 – 订阅了channel1频道
发布方式
订阅了之后,可以在任意客户端发布消息到指定channel
publish channel1 hello – 往channel发布hello,会返回订阅channel1的数量
redis bitmaps数据类型
redis提供了bitmaps以便对位进行操作,常用于签到、网站用户访问统计(用户id为32或64位,当天访问为1,不访问为0)等场景,命令格式如下:
setbit key offset value 其中,value值为0或1,offset为位的偏移量,从0开始
getbit key offset 获取key的offset位值
bitcount key [start end]获得key中start到end字节中存在bit为1的数量
bitop and/or/not/xor destkey key… 用来做多个bitmaps的复合操作,按位与或非并将结果保存在destkey中
redis hyperLogLog数据类型
常用与实际工作中统计uv,独立ip数,搜索记录数等需要去重和计数的集合中求不重复元素个数的基数问题。
虽然我们像mysql,redis中都有一些方案,像mysql中的distinct count,redis中的hash,set,bitmaps都可以用来处理这类问题,但在数据量很大的时候,存在内存空间问题。HyperLogLog的优势是在数据量非常大时,计算基数所需的空间是很小的,比较固定的。12KB的内存,可以计算接近2^64个元素基数
常用命令有:
pfadd key “xxx” 加入数据xxx,如果已经存在返回0,成功返回1
pfcount key 统计key的数量
pfmerge destkey srckey1 … 将多个key的数据合并到destkey中
geospetial数据类型
redis3.2以上的版本提供了geo的支持,包含了经纬度设置、查询、范围查询、距离查询、经纬度hash等操作。
常用命令
geoadd key latitude longtitude member … 添加一个或多个坐标集合,如geoadd Chinacity 122.34 24.54 xy_1 133.1 56.2 xy_2
有效精度在-180到180之间,有效维度从-85.05112878到85.05112878之间。坐标超出范围返回错误。
geopos key member 获得key集合中member的经纬度
geodist key member1 member [m/km/ft/mi]获得key集合中member1坐标和member2坐标的直线距离,默认距离以m(米为单位)
georadius key latitude longtitude radius [m|km|ft|mi] 以给定坐标为中心,找出某一半径内的元素,半径单位也可以选,默认米
redis事务
redis事务是一个单独的隔离操作,事务中所有命令都会序列化、按顺序地执行,事务在执行过程中,不会被其他客户端发来的命令请求打断,redis事务的作用主要是串联多个命令防止被其他命令插队。
redis事务命令
从输入Multi开始,输入的命令都会依次进入命令队列,但不会被执行,直到输入Exec,redis会将输入的命令依次执行,如果我们在输入命令的过程中,有些命令不想要了,可以用discard命令来丢弃。
如下为示例
redis事务错误处理
组队的时候如果有命令错误,最终都不会执行。组队命令没问题,执行的过程中有部分命令失败,有错误的命令执行失败,其他命令正常执行。
redis事务冲突与解决机制
为了解决多个请求同时操作同一个值的场景下,我们在编写redis事务操作时,可能认为某个值是A,但实际上在事务入队还没执行之前已经变成了B,如果我们继续按A去操作可能出现问题,从而出现一些非预期的结果(当然也可能没问题,但不可控)。至于像很多地方说的冲突就是10000块的账户减8000,减1000,减5000的问题,如果我就是允许它能为负值,那也没什么问题,我们冲突的关键在与人工判断的部分,我们在执行账户减少的时候我们的当前值是什么,能不能减,而不是只要有同时操作就会冲突。可以通过加锁的方式解决冲突。
悲观锁
每次操作数据都加锁,其他人想访问该数据需要等到锁被释放。传统的关系型数据库大多是这类,如表锁、行锁,都是在操作之前上锁。悲观锁的缺点在于效率比较低。
乐观锁
拿数据的时候不会上锁,在更新的时候判断别人是否有更新,如果有更新,版本不匹配,则不执行更新。判断是否有更新一般使用check-and-set 版本号机制。多用于读数据多的场景。像抢票的时候,我们一般只有一个人成功,其他人都会失败。
案例
在执行multi之前,先使用watch key [keyn…]监视一个或多个需要关注的key,在事务执行前监视的key被改动,事务将被打断。如果不再需要监视了可以在事务中用unwatch取消,如果exec或discard已经执行过了,那就不需要执行unwatch了。
redis事务的三个特性
- 单独的隔离操作–事务中所有命令都会序列化、按顺序地执行,不会被其他客户端发来的请求打断。
- 没有隔离级别–队列中的命令没有提交之前都不会被实际执行
- 不保证原子性–事务中如果有一条命令失败,其他命令仍然会被执行,没有回滚
多个事务操作同一个变量的解决方案
如果我们在使用事务时,希望能同时对一个变量操作,不要因为一个事务中断另一个事务的操作,比如我们有500张优惠券,希望在还没抢完的情况下,如果有用户同时抢,那么两个都可以抢到,而不是一个的事务影响到另一个,这时可以使用Lua脚本来解决。
一般一些redis客户端会提供lua脚本加载的接口,像jedis,redission中都有。
redis持久化
redis提供了两种持久化方式:RDB(redis database)和AOF(append of file)
RDB
RDB方式是在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的snapshot快照,它恢复时是将快照文件直接读到内存里。比如每过10秒将当前时间点的数据库中的数据写入磁盘。redis在操作的过程中会fork出一个子进程来执行持久化操作,在这个过程中会先写入到一个临时文件,等到持久化过程结束,再将临时文件替换上次保存好的持久化文件。整个过程中主进程是不进行任何IO操作的,因此性能比较高,如果需要大规模的数据恢复,且数据恢复的完整性要求不高,RDB方式要比AOF方式要高效,缺点是可能最后一次持久化的数据会丢失,因为间隔时间可能还没到但服务其挂了。
可以通过修改redis配置来设置RDB的持久化,可以配置成xx时间内有多少个key值变化就执行持久化。
redis启动时会自动加载dump出来的rdb文件
rdb默认开启
AOF
以日志的形式来记录每个写操作,以追加的方式写,redis启动的时候会执行一遍所有的写操作来恢复数据
AOF默认不开启,可以通过修改配置中的appendonly项修改为yes,RDB和AOF同时开启redis优先使用AOF.
redis AOF有异常恢复机制,可以通过/usr/local/bin/redis-check-aof --fix appendonly.aof来恢复
AOF有三种同步方式:
appendfsync always – 总是同步,每次写操作都马上同步到日志,这样保存的日志是最完整的,但同时也会导致性能下降
appendfsync everysec – 每秒同步一次,最多丢失前一秒操作的数据
appendfsync no – 不主动同步,由操作系统来决定什么时候同步
rewrite压缩
为了节省空间,redis有rewrite机制将对同一个元素的多次操作,只记录最终影响的那一次操作,当AOF大小超过阈值后就会启动该机制。也可以使用bgrewriteaof命令重写。当aof文件大于2倍basesize且当前大小大于64MB的情况下,redis会对aof文件重写。重写也是会fork一个子进程,产生一个临时文件,在写完后再替换原aof文件
如何选择持久化方案
如果对数据不敏感,可以用单独用rdb,不建议单独用aof,可能有bug,只做纯缓存用的话可以不用开启
redis应用问题缓存穿透
大量的请求访问redis时,如果较多请求在redis中不存在,频繁地直接查询数据库或访问后端服务,导致数据库或后端服务压力较大而崩溃。
解决方案:
- 对空值缓存 如果一个查询的返回数据为空,我们仍然对这个空结果做缓存,但需要对这个空值设置过期时间,一般来说过期时间不超过5分钟,但这个方案对攻击的随机性不太适用
- 设置可访问名单,使用bitmaps定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次均将query和bitmaps里面的id进行对比,效率较低
- 布隆过滤器,底层是hash加bitmaps,但存在误识别率
- 实时监控,如果redis的命中率降低,需要排查访问对象
redis缓存击穿
数据库访问压力瞬时增大,但redis中的key并没有出现大量过期,redis也在正常运行,可能原因是:
(1)redis中某个key刚好过期了但大量的访问都涉及到该key(热key)。
(2)在更新redis中数据时,delete老key时请求刚好并发量很大
解决方案:
- 预先设置热门数据,在高峰访问前,预先存入redis,并加大key的时长或者不设置失效时间
- 实时监控哪些数据热门,及时调整
- 互斥更新,双检加锁 在缓存失效时(拿到值为空时),不做任何处理的情况下,我们都会查询数据库来,然后重新设置缓存,这样对数据库的压力就会很大,我们可以先设置一个排他锁,如果排他锁已经存在,说明已经有别的线程在设置数据库更新缓存,如果成功我们再更新缓存,再释放锁。在获取到锁的时候再次查询redis,判断有没有其他用户已经更新过缓存了。
缓存雪崩
在极少时间段内,查询大量key过期的场景,会导致大量的请求直接打到mysql,导致服务器崩溃。
解决方案:
- 构建多级缓存:nginx缓存+redis缓存+其他缓存
- 使用锁或队列避免失效时大量并发请求落到底层存储系统上,不适用于高并发
- 设置过期标志(提前值),过期触发另外的线程去更新该key
- 分散缓存失效时间,避免key的集体过期