NoSQL-KV模型


缓存是为了解決不同介质(磁盘/内存/网络等)的响应速度和成本差异的问题。缓存的使用可以提高系统的读取速度和性能。

数据存储介质 寻址 I/O buffer带宽 持久性
磁盘 ms G/M 持久
内存 ns 很大 断电丢失

s秒>ms毫秒>us微秒>ns纳秒 磁盘比内存在寻址上慢了10W倍。

磁盘=磁道+扇区。一扇区 512Byte,带来一个成本变大:索引。在操作系统中,无论你读多少,从磁盘拿数据最少4k。

解决方案:利用缓存折中

选择折中方案的原因:2个基础设施

1)冯诺依曼体系的硬件限制

2)以太网,tcp/ip的网络

请添加图片描述

缓存类型 优点 缺点 示例
本地缓存 速度快,无额外IO 集群模式下一致性难以保证,容量有限,数据丢失 Guava
分布式缓存 容量大,可集中管理,缓存维度数据一致性 网络IO,QPS受限 Redis
多级缓存 解决了本地缓存和缓存中间件的问题 复杂度较高,维护成本高 JetCache

本地缓存

GuavaCache

缓存在很多场景下都是相当有用的。例如,计算或检索一个值的代价很高,并且对同样的输入需要不止一次获取值的时候,就应当考虑使用缓存。

Guava Cache与ConcurrentMap的区别:
ConcurrentMap会一直保存所有添加的元素,直到显式地移除。

Guava Cache为了限制内存占用,通常都设定为自动回收元素。在某些场景下,尽管LoadingCache 不回收元素,它也是很有用的,因为它会自动加载缓存。

通常来说,Guava Cache适用于:
你愿意消耗一些内存空间来提升速度。
你预料到某些键会被查询一次以上。
缓存中存放的数据总量不会超出内存容量。(Guava Cache是单个应用运行时的本地缓存。它不把数据存放到文件或外部服务器。如果这不符合你的需求,请尝试Memcached这类工具)

https://blog.youkuaiyun.com/we2006mo/article/details/135417517

ListeningExecutorService refreshPools = MoreExecutors.listeningDecorator(new ThreadPoolExecutor(20, 20, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(5000)));

LoadingCache<Integer, List<AuthUserPermission>> cache = CacheBuilder.newBuilder()
        /** refresh < expire,减少高并发线程阻塞的概率 */
        .refreshAfterWrite(1, TimeUnit.MINUTES)
        .expireAfterWrite(2, TimeUnit.MINUTES)
        .build(new CacheLoader<Integer, List<AuthUserPermission>>() {
   
            /** 本地缓存命没有中时,调用方法获取结果,并将结果进行缓存 */
            @Override
            public List<AuthUserPermission> load(Integer key) {
   
                return authUserFacade.select(key);
            }

            /** 异步刷新本地缓存 */
            @Override
            public ListenableFuture<List<AuthUserPermission>> reload(Integer key, List<AuthUserPermission> oldValue) {
   
                return executor.submit(() -> load(key), XXX_XXX);
            }
        });

分布式缓存

Redis

Redis:REmote DIctionary Server(远程字典服务器)

默认端口:6379

  • 线程安全。因为work线程是单线程(避免上下文切换,无需创建/销毁线程,无资源竞争的问题)

  • 效率高。

    1)纯内存操作的Key-Value。

    2)I/O多路复用。启动一个select选择器线程,高并发请求都去选择器线程中,选择器通过轮训得到请求数据,并写到缓存区中,epool解决空轮询问题。

    3)5中数据类型,每个类型存在一个本地方法,计算向数据端移动。

中文网: redis.cn
官网: redis.io
jedis: https://github.com/redis/jedis
lettuce: https://github.com/lettuce-io/lettuce-core
spring集成redis: https://spring.io/projects/spring-data-redis

Redis使用场景
分布式锁 幂等;短信验证码;订单的有效期。
缓存热点数据 缓存热点数据,减轻数据库访问压力
token令牌 替换session,因为保存在当前jvm中,所以无法做分布式集群,把session存储到redis中。
网页计数器hypperloglog统计uv

Redis命令

性能测试

Redis秒级10万操作,关系型数据库秒级1K操作

------------------------------------------------------------------------
##连接本地redis服务
redis-benchmark
# 例子:测试host为localhost 端口为6379 100个并发连接,100000个请求 的redis服务器性能
redis-benchmark -h localhost -p 6379 -c 100 -n 100000
选项 描述 默认值
-h 指定服务器主机名 127.0.0.1
-p 指定服务器端口 6379
-s 指定服务器 socket
-c 指定并发连接数 50
-n 指定请求数 10000
-d 以字节的形式指定 SET/GET 值的数据大小 2
-k 1=keep alive 0=reconnect 1
-r SET/GET/INCR 使用随机 key, SADD 使用随机值
-P 通过管道传输 请求 1
-q 强制退出 redis。仅显示 query/sec 值
–csv 以 CSV 格式输出
-l 生成循环,永久执行测试
-t 仅运行以逗号分隔的测试命令列表。
-I Idle 模式。仅打开 N 个 idle 连接并等待。
连接
------------------------------------------------------------------------
##连接本地redis服务
redis-cli
------------------------------------------------------------------------
##连接远程redis服务
redis-cli -h host -p port -a password
------------------------------------------------------------------------
##连接到redis后,检测 redis 服务是否启动
PING
------------------------------------------------------------------------
##授权密码
AUTH password
数据库
------------------------------------------------------------------------
##在某些场景下,可能多个应用同时使用一个redis,那我们希望不同应用的redis数据是隔离的,这时就可以采用设置不同redis数据库的方式
##查询数据库
config get databases
------------------------------------------------------------------------
##切换数据库
select 数据库编号
##注:redis.conf中默认是0号数据库,最大是16
------------------------------------------------------------------------
##查看数据库大小
dbsize
------------------------------------------------------------------------
##清空当前数据库
flushdb
------------------------------------------------------------------------
##清空全部数据库
flushall
key-value
------------------------------------------------------------------------
##设置k-v并且设置过期秒数
setex key 到期秒数 
set key ex 到期秒数 
------------------------------------------------------------------------
##设置k-v并且设置过期毫秒数
psetex key 到期毫秒数 


------------------------------------------------------------------------
##移动key到其他数据库,当前库会删除这个key
move key 数据库编号

------------------------------------------------------------------------
##查询value
get key

------------------------------------------------------------------------
##查询所有的key
keys *

------------------------------------------------------------------------
##查看key的数据类型
type key 

------------------------------------------------------------------------
##判断key是否存在
exists key
过期时间
------------------------------------------------------------------------
##设置k-v并且设置过期秒数
setex key 到期秒数 
------------------------------------------------------------------------
##设置k-v并且设置过期毫秒数
psetex key 到期毫秒数 
------------------------------------------------------------------------
##将键的过期时间设为 ttl 秒
EXPIRE KEY TTL
------------------------------------------------------------------------
##将键的过期时间设为 ttl 毫秒
PEXPIRE KEY TTL
------------------------------------------------------------------------
##将键的过期时间设为 秒时间戳
EXPIREAT KEY timestamp
------------------------------------------------------------------------
##将键的过期时间设为 毫秒时间戳.
PEXPIREAT KEY timestamp


------------------------------------------------------------------------
##KEY永不过期:一)没有设置过期时间。二)移除过期时间
##移除过期时间。
persist KEY

------------------------------------------------------------------------
## 查看还有多少秒过期。-1 表示永不过期,-2 表示已过期,其他数字代表指定键的剩余生存秒数
ttl KEY
------------------------------------------------------------------------
## 查看还有多少秒过期。-1 表示永不过期,-2 表示已过期,其他数字代表指定键的剩余生存毫秒数
pttl KEY

Redis数据类型

key数据类型 底层数据结构
String 字符串 SDS简单动态字符串

1)在O(1)的时间复杂度中获取字符串长度

C字符串不记录自身的长度,所以为了获取一个字符串的长度程序必须遍历这个字符串,直至遇到’0’为止,整个操作的时间复杂度为O(N)。

2)二进制安全的。

SDS使用len属性的值判断字符串是否结束。

3)减少了内存重分配次数

空间预分配策略:在增长过程中不会频繁的进行空间分配。

情性空间释放机制:字符串缩短时,只更新 SDS 的len属性,多出来的空间供将来使用。并不立即使用内存重分配来回收缩短后多出来的空间

4)自动扩容机制

扩容阶段:

若 SDS 中剩余空闲空间 avail 大于新增内容的长度 addlen,则无需扩容;
若 SDS 中剩余空闲空间 avail 小于或等于新增内容的长度 addlen:
若新增后总长度 len+addlen < 1MB,则按新长度的两倍扩容;
若新增后总长度 len+addlen > 1MB,则按新长度加上 1MB 扩容。

内存分配阶段:

根据扩容后的长度选择对应的 SDS 类型:
若类型不变,则只需通过 s_realloc_usable扩大 buf 数组即可;
若类型变化,则需要为整个 SDS 重新分配内存,并将原来的 SDS 内容拷贝至新位置。

请添加图片描述

Value数据类型 底层数据结构
String 字符串 int整型、embstr编码的简单动态字符串、raw编码的简单动态字符串
List 列表 linkedlist双向链表,ziplist压缩表
Hash 哈希 ziplist压缩表,hashtable(对应Java的HashMap)
Set 集合 intset整数集合,hashtable(对应Java的HashMap)
Zset 有序集合 ziplist压缩表,skiplist跳表
geo 地理位置
hyperloglog 基数统计
bitmap 位图

请添加图片描述

ziplist压缩表

当hash,list, zset,set等结构在数据量较小的时候会采用压缩列表的格式进行存储,因为 一个线性数组通常会被CPU的缓存更好的命中(线性数组有更好的局部性),从而提升了访问的速度。

一个普通的双向链表,链表中每一项都占用独立的一块内存,各项之间用地址指针(或引用)连接起来。这种方式会带来大量的内存碎片,而且地址指针也会占用额外的内存。而ziplist却是将表中每一项存放在前后连续的地址空间内,一个ziplist整体占用一大块内存。它是一个表(list),但其实不是一个链表(linked list)。

<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
  • zlbytes:表示这个ziplist占用了多少空间,或者说占了多少字节,这其中包括了zlbytes本身占用的4个字节;
  • zltail:表示到ziplist中最后一个元素的偏移量,有了这个值,pop操作的时间复杂度就是O(1)了,即不需要遍历整个ziplist;
  • zllen:表示ziplist中有多少个entry,即保存了多少个元素。由于这个字段占用16个字节,所以最大值是216-1,也就意味着,如果entry的数量超过216-1时,需要遍历整个ziplist才知道entry的数量;
  • entry:真正保存的数据,有它自己的编码;
  • zlend:专门用来表示ziplist尾部的特殊字符,占用8个字节,值固定为255,即8个字节每一位都是1。
 [0f 00 00 00] [0c 00 00 00] [02 00] [00 f3] [02 f6] [ff]
    	|       				|     		|    		|   	 |   |
  	zlbytes    			zltail  	entries  "2"   	"5"  end

intset整数集合

是一个有序的整型数组。它在内存分配上与ziplist有些类似,是连续的一整块内存空间,而且对于大整数和小整数采取了不同的编码,尽量对内存的使用进行了优化。

skiplist跳表

skiplsit跳跃链表=有序链表+多级索引;

空间换时间。

链表 跳表
查询的时间复杂度 O(n) O(log(n))

请添加图片描述

查找、插入、删除以及迭代输出有序序列这几个操作,红黑树也可以完成,时间复杂度和跳表是一样的。但是使用跳表而不是红黑树的原因:

1)按照区间查找数据这个操作,红黑树的效率没有跳表高。跳表可以在 O(logn)时间复杂度定位区间的起点,然后在原始链表中顺序向后查询就可以了。

2)相比于红黑树,跳表还具有代码更容易实现、可读性好、不容易出错、更加灵活等优点。

3)插入、删除时跳表只需要调整少数几个节点,红黑树需要颜色重涂和旋转,开销较大。

typedef struct redisObject {
    //具体的类型如String、hash、set、list、sortedset等。通过type命令进行查看,其实主要是约束api使用
    unsigned type:4;
    //更加深层次的类型,代表底层的优化。如int,embstr等。通过object encoding命令进行查看。
    unsigned encoding:4;
    //设置内存淘汰策略时使用 24byte
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    //用于垃圾回收的引用计数 4byte
    int refcount;
    //真实存储value的指针 8byte
    void *ptr;
} robj;
String字符串

可以包含任何数据,value最大是512M。

SDS最长为什么是512MB?

因为len是使用int修饰的,int最大数值换算一下就是512MB

Value 应用场景
字符串 session共享
uuid数据库ID
VFS in mem 虚拟文件系统;set /root/路径 值;get /root/路径;注:小文件
数值 限流器
统计
点击率
bitmap
图片
-------------------------- 增 ----------------------------------------------
#设置k-v
set key value
##设置k-v,如果key不存在则设置,如果key存在,则什么也不做。成功返回1,失败返回0
setnx key value
#批量设置k-v
MSET key1 "value1" key2 "value2" keyn "valuen" 
#批量设置k-v。原子性操作:key都不存在则设置,存在一个key则失败
MSETNX key1 "value1" key2 "value2" keyn "valuen" 
# 传统对象缓存 set 对象:id {属性:value,属性:value}
set user:1 {
   name:zhangsan,age:27}
# 可以用来缓存对象 mset 对象:id:属性 value 对象:id:属性 value
mset user:1:name zhangsan user:1:age 22 

mget user:1:name user:1:age

---------------------------- 删 --------------------------------------------
##删除key,时间复杂度为O(1)
del key

---------------------------- 改 --------------------------------------------
#数字++
incr key
#数字--
decr key
#数字+=数值
incrby key 数值
#数字-=数值
decrby key 数值
#value追加字符串。如果当前key不存在,就相当于set key
append key "字符串"
#替换指定位置的字符
setrange key index "字符串"
#获取字符串value并设置为新字符串value。CAS的一种方案
GETSET key "new字符串"

------------------------------ 查 ------------------------------------------
##查询value
get key
#查询value的长度
strlen key
#字符串截取值【startIndex,endIndex】
getrange key startIndex endIndex
#返回所有指定的key的字符串value
MGET key1 key2 keyn
List列表

list 实际是一个链表,左右都可以插入值。根据放入顺序有序,redis不会去排序

Value 应用场景
栈(存取同向) JVM 宕机数据消失,迁移到redis,服务无状态
队列(存取异向) JVM 宕机数据消失,迁移到redis,服务无状态
数组 JVM 宕机数据消失,迁移到redis,服务无状态
消息排队
删除数据,保留热数据 分页,微信小红包
----------------------------- 增 -------------------------------------------
##将一个或多个值插入到列表头部
Lpush key value
##将一个或多个值插入到列表尾部
rpush key value
##在列表的value前before或者后after插入值
Linsert key before|after value 值


------------------------------- 删 时间复杂度为O(M) ---------------------------
##移除key中指定个数的value
lrem key 移除个数 value
##获取并移除列表头部
Lpop key
##获取并移除列表尾部
Rpop key

-------------------------------- 改 ----------------------------------------
##指定下标赋值
Lset key index 值
##key截取指定下标的元素
ltrim key startIndex endIndex
##移除列表最后一个元素并移动到新列表中
rpoplpush key new-key

---------------------------------- 查 --------------------------------------
##查询List中的值。endIndex=-1代表查询所有值
lrange key startIndex endIndex
##获取指定下标的值
Lindex key index
##列表长度
llen key
Hash哈希

类似java里的map<key,map<key,value>>

Value 应用场景
对象存储
商品详情页
聚合场景
----------------------------- 增 -------------------------------------------
##新增
hset key 字段名 value
##批量新增
hmset key 字段名1 value 字段名1 value
##设置k-v,如果key不存在则设置,如果key存在,则什么也不做。成功返回1,失败返回0
hsetnx key 字段名 value
##新增对象
set 对象:id:属性K v
get 对象:id:属性K

------------------------------- 删 时间复杂度为O(M) ------------------------------
##删除指定字段
hdel key 字段名

-------------
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值