文章目录
缓存是为了解決不同介质(磁盘/内存/网络等)的响应速度和成本差异的问题。缓存的使用可以提高系统的读取速度和性能。
数据存储介质 | 寻址 | 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 字段名
-------------