Redis学习经验

发布时间 2023-06-22 22:48:34作者: 炸天帮达令

Redis

各位读者朋友你们好,我是你们的好朋友IT黑铁,最近巩固加深Redis中将经验记录了下来,其中若有错误请多指教!

学习途径:

   B站黑马程序员的Redis视频

    注:由于部分ppt图片过于的好和部分知识基本不需要扩展,我就直接截取了黑马程序员的ppt图片,万分感谢!

概述
Redis是一款键值对(key-value)的非关系型(Not Only SQL)数据库,它是基于内存的,存储各种类型的数据。
SQL与NOSQL的比较
1. SQL存储结构化数据,NoSQL存储的数据是非结构化。
2. SQL存储有的是关联的数据,而NoSQL无关联的。
3. 查询命令不相同
4. SQL满足事务ACID的特性,NoSQL基本满足数据一致性(BASE)。
(一) 数据结构及命令
A、 通用命令
Redis的key层次结构:项目名:业务名:类型:id,方便管理。
KEYS:查看符合模板的所有key,不建议在生产环境设备上使用
DEL:删除一个指定的key
EXISTS:判断key是否存在
EXPIRE:给一个key设置有效期,到期后该key被自动删除
TTL:查看一个KEY的剩余有效期
B、 String
该存储类型,value是字符串,最大空间不超过512M,不过根据字符串的格式不同,又可以分为三类:string,普通字符串;int,整数类型,可以做自增、自减操作;float,浮点类型,也可以做自增、自减操作。
不过不管哪种格式,底层都是字节数组形式存储,只是编码方式不同。
SET:添加或者修改已经存在的一个String类型的键值对
GET:根据key获取String类型的value
MSET:批量添加多个String类型的键值对
MGET:根据多个key获取多个String类型的value
INCR:让一个整型的key的value自增1
INCRBY:让一个整型的key的value自增并指定步长
INCRBYFLOAT:让一个浮点类型的数字自增并指定步长
SETNX:根据是否存在,添加一个String类型的键值对
SETEX:添加一个人String类型的键值对,并且指定有效期



C、 HASH
散列类型的value是一个无需字典,类似于Java中的HashMap结构。
Hash结构可以将对象中的每个字段独立存储,可以针对单个字段做CRUD。
HSET key filed value:添加或者修改hash类型key的field的value
HGET key field:获取一个hash类型key的field的value
HMSET:批量添加多个hash类型key的field的value
HMGET:批量获取多个hash类型key的field的value
HGETALL:获取一个hash类型的key中的所有的field和value
HKEYS:获取一个hash类型的key中所有的field
HVALS:获取一个hash类型的key中所有的value
HINCRBY:获取一个hash类型key的字段value自增并指定步长
HSETNX:判断field是否存在,添加一个hash类型的key的field的value




D、 List类型
Redis中的List类型与Java中的LinkedList类似,可以看做是一个双向链表结构。该类型有序、元素可以重复、插入删除快、查询速度一般。
LPUSH key element:向列表左侧插入一个或多个元素
LPOP key:移除并返回列表左侧的第一个元素
RPUSH key element:向列表右侧插入一个或多个元素
RPOP key:移除并返回列表右侧的第一个元素
LRANGE key star end:返回一段角标范围内所有元素
BLPOP和BRPOP:阻塞式取元素。


E、 Set类型
Redis的Set结构与Java中的HashSet类似,可以看做是一个value为null的HashMap。其特征:无序、元素不可重复、查找快、支持交集并集差集等功能。
SADD key member:向set中添加一个或多个元素
SREM key member:移除set中的指定元素
SCARD key:返回set中元素的个数
SISMEMBER key member:判断一个元素是否存在于set中
SMEMBERS:获取set中的所有元素
SINTER key1 key2:求key1与key2的交集
SDIFF key1 key2:求key1与key2的差集






F、 SortedSet类型
Redis中的SortedSet是一个可排序的set集合,与Java中的TreeSet有些类似,但底层数据结构却差别很大。SortedSet中每一个元素都带有一个score属性,可以基于score属性对元素排序,底层的实现是一个跳表(SkipList)加hash表。其特性:可排序、元素不重复、查询速度快。
ZADD key score member:添加一个或多个元素到sorted set,已经存在就更新
ZREM key member:删除sorted set中的一个元素
ZSCORE key member:获取sorted set中指定元素的score的value
ZRANK key member:获取sorted set中指定元素的排名
ZCARD key:获取sorted set中的元素个数
ZCOUNT key min max:统计score值在给定范围内的所有元素的个数
ZINCRBY key increment member:让sorted set中的指定元素自增指定步长
ZRANGE key min max:按照score排序后,获取指定排名范围内的元素
ZRANGEBYSCORE key min max:按照score排序后,获取指定score范围内的元素
ZDIFF、ZINTER‘ZUNION:求差集、并集、交集
H、 GEO类型
GEO代表地理坐标,在Redis3.2版本中加入了对GEO的支持,允许存储地理坐标信息,帮助我们根据经纬度来检索数据。
GEOADD:添加一个地理空间信息。
GEODIST:计算指定的两个点之间的距离并返回。
GEOHASH:将指定member的坐标转为hash字符串形式并返回
GEOPOS:返回指定的member坐标。
GEORADIUS:指定圆心、半径,找到该圆内包含的所有member,并按照与圆心直接的距离排序后返回。6.2版本已废弃。
GEOSEARCH:在指定范围内搜索member,并按照与指定点之间的距离排序后返回。范围可以是圆形或矩形。6.2版本新功能。
GEOSEARCHSTORE:与GEOSEARCH功能一致,不过可以把结果存储到一个指定的key。6.2新功能。
I、 BitMap
Redis利用String类型数据结构实现BitMap,因此最大上限是512M,转换为bit则是2^32个bit位。
SETBIT:向指定位置(offset)存入一个0或1
GETBIT:获取指定位置(offset)的bit值
BITCOUNT:统计BitMap中值为1的bit位的数量
BITFIELD:操作(查询、修改、自增)BitMap中bit数组中的指定位置(offset)的值
BITFIELD_RO:获取BitMap中bit数组,并以十进制形式返回
BITOP:将多个BitMap的结果做位运算(与、或异或)
BITPOS:查找bit数组中指定范围内第一个0或1出现的位置
J、 HyperLogLog
HyperLogLog(HLL)是从LogLog算法派生的概率算法,用于确定非常大的集合的基数,而不需要存储其所有值。Redis的HLL基于String结构实现,单个HLL内存永远小于16kb,内存占用低的令人发指!作为代价,其测量结果是概率性的,有小于0.81%的误差。
PFADD
PFCOUNT
PFMERGE
(二) SpringDataRedis
简介

A、 RedisTemplate的API

B、 RedisTemplate的序列化方式
第一种方式:RedisTemplate可以接收任意Object作为值写入Redis,只不过写入前会把Object序列为字节形式,默认是采用JDK序列化。缺点:可读性差;内存占用较大。
第二种方式:自定义序列化方式。key和hashkey采用string序列化,value和hashValue采用JSON序列化。缺点:为了反序列化,JSON序列化会将class类型写入json结果中,存入Redis,带来额外的内存开销。
第三种方式:手动地转JSON。Spring提供了StringRedisTemplate类,其key和value的序列方式都是String方式,省去自定义过程。
(三) Redis解决集群session不共享问题

(四) Redis缓存更新策略
A、 更新时机

B、 更新策略

选择第一种的原因:第二种还未有该产品服务;第三种虽然效率高,但编码复杂且存在并发问题。

第三个问题:先操作数据库,再删除缓存。因为数据库写操作一般比读操作慢。
C、 小结

(五) 缓存穿透、缓存击穿、缓存雪崩




(六) Redis的其他应用
A、 全局ID生成

解决问题:如果使用数据库自增ID会存在规律性明显、单表数据量的限制(京东淘宝这样的店铺)、唯一性不能保证(分布式数据库里的问题)。

B、 秒杀下单

存在的问题:所有事情都在Tomcat里做,性能较低。

解决方案:将并发安全判断由从数据库取出判断改为在Redis里做。这样做的好处有两点:一、Redis可使用Lua脚本保证原子性,不会引发多线程并发安全问题,就不需要再使用锁,后续使用锁只是兜底的作用,预防万一。二、从Redis里查库存比起数据库里查库存更加高效。


虽然效率问题得到改善,但存在一个问题:异步处理阻塞队列中的数据,是使用Java提供的阻塞队列,这样简陋地处理消息,存在JVM内存限制问题和数据安全问题。
解决方案:引入Redis消息队列MQ。
存在问题:大部分的事情仍然还是费时的。
解决方案:使用多级缓存机制。
C、 超卖问题

小记:悲观锁是任何时候都可用,乐观锁只有更新数据时才能用。


乐观锁虽然能解决并发安全问题,但如果用得不恰当,会导致业务失败率高的问题,如:乐观锁为查询的库存是等于当前查到的库存,该锁会让其他线程不能修改数据成功,但这样并不理想,比如一个优惠券有很多张,而一个在抢,其他人就不能抢了,这并不恰当。所以乐观锁应当是控制安全范围,比如查询的库存大于0就行了。
D、 一人一单(分布式锁问题)

 synchronized (userId.toString().intern()){
// Spring是通过代理做的事务,要得到代理对象,还需在启动类上配置@EnableAspectJAutoProxy(exposeProxy = true)
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
// createVoucherOrder调用等同于this.createVoucherOrder,这样是非代理对象的调用不具备事务功能。
// return proxy.createVoucherOrder(voucherId);
return proxy.createVoucherOrder(voucherId);
}
 一人一单必须判断订单是否存在,查的时候一个用户可能发起多个线程,就会导致并发安全问题,这里就只能采用悲观锁,且锁的应该是用户id,而不是整个流程方法。
 锁住用户id值(而不锁方法),toString内部也会再创建一个对象,而intern()是去字符串常量池找相同值的对象。
该流程存在的问题:锁是由JVM控制,一个JVM控制多个线程访问共享资源的锁。在集群情况下,有多台JVM,各个JVM只能控制它管辖的线程,外面进程里的线程它就管不着了,就会出现并发安全问题。

解决办法:采用分布式锁。



由于锁有超时时间,存在一个问题:二次释放锁,即当一个线程得到锁后,久久不释放锁,因为超时而被动释放锁,在之后该线程完成了后续操作又手动释放锁。此时如果别的线程得到一个锁,还没等它自己释放自己的锁,就被上一个线程释放掉了(因为锁是唯一的)。这样又有下一个线程可以得到锁,第一个线程和第二个线程这时是“同时”进行修改共享资源,就会出现并发安全问题。

解决办法:锁加入线程标识,释放的时候就可以避免上一个线程释放别人的锁的问题。


按理说上述解决方案已经不会发生释放别人锁的问题了,因为多了一个判断锁值是否是自己的逻辑,而就算是超时了也不会释放别人的锁。但还有更恐怖的情况,如:GC垃圾回收时会阻塞所有代码块,而此时若是上一个线程执行到判断了锁值是自己的,正要进行下面的释放锁流程,而就被阻塞了,导致锁超时被其他JVM的线程得到锁,等到被阻塞的线程恢复了它在阻塞前已经判断了是自己的锁,就会出现误删别人锁的情况。这时第二个线程的锁被释放了,被第三个线程拿到,而第二个线程还没有执行完业务,产生并发安全问题。

解决办法:保证释放锁的原子性,即要么同时成功,要么同时失败,Redis提供了Lua脚本功能。

虽然已经比较完善,但仍然存在下面的问题。

解决方案:第三方Redisson提供了众多Redis解决方案,就包括了分布式锁。

可重入锁是指:一个线程得到一把锁后,其内执行方法若要再申请一把锁,则还能够用这把锁。

可重试实现解决方案:利用了等待时间,剩余存活时间,消息订阅,信号量等机制。
超时释放解决方案:延时任务续约。

主从一致解决方案:联锁,多个节点的锁必须同时申请成功,才能得到一把联锁。

E、 消息队列










F、 点赞排行榜

H、 Feed流







I、 签到


J、 UV统计

(七)分布式Redis

A、 持久化
Redis有两种持久化机制,一种是RDB,一种是AOF。
RDB默认开启,即当Redis关机(服务停止)时,重启会使用RDB恢复数据。





B、 集群架构

通过配置slaveof(永久生效)或客户端连接slave命令(临时),建立主从关系后,主结点可以知道从节点信息。




C、 哨兵




使用redis-sentinel服务来根据不同的配置文件(配置端口,监听的主节点,以及工作目录等)启动哨兵结点,这与redis服务一样。
D、 分片集群

使用redis-server服务根据自定义集群配置文件启动后,再使用redis-cli服务使用创建集群选项建立集群。

扩容:利用redis-cli服务使用选项add node来添加结点,然后再使用选项reshard来转移插槽。
故障转移:不需要使用哨兵,Redis集群自动进行数据迁移。手动故障转移使用failover命令。
(八)多级缓存

本地进程缓存:Caffeine
业务Nginx缓存:OpenResty




(九)缓存预热和缓存同步策略


异步通知有两种方式:一种是基于MQ,一种是基于Canal(零侵入)。


(十)最佳实践

note:key这里指一个数据整体。









Spring封装的template已经实现了并行slot。










(十一) 原理
A、 底层数据结构
动态字符串SDS


note:还有很多不同字节编码的sds结构体

IntSet



Dict







ZipList





QuickList



SkipList

RedisObject



B、 网络模型




阻塞IO

非阻塞IO

IO多路复用







信号驱动IO

异步IO

C、 RESP通信协议


D、 内存策略
过期策略





淘汰策略




相关CASE
(1) RDM安装包:Releases · lework/RedisDesktopManager-Windows (github.com)
(2)
(3) RedisTemplate的哨兵模式
在配置文件中指定Sentinel的信息就行,不需要指定Redis服务集群信息。
(4) 集群配置读写分离