Summary for Redis

Rides笔记

Redis入门

1. 概述

Redis 能干嘛?

  1. 内存存储、持久化,内存中是断电即失,所以说持久化很重要(RDB、AOF)
  2. 效率高,可以用于高效缓存
  3. 发布订阅系统(消息队列)
  4. 地图信息分析
  5. 计时器、计数器(浏览量)

特性

  1. 多样的数据类型
  2. 持久化
  3. 集群
  4. 事务

学习中需要用到

  1. www.redis.io
  2. www.redis.cn

2. 测试性能

  • 使用redis-benchmark命令进行测试

    redis-benchmark -p 6379 -c 100 -n 100000
    

3. 基础知识

  • 默认有16个数据库

    • 切换数据库操作

      select 3
      
    • 查看当前数据库大小

      dbsize
      
    • 清除当前数据库

      flushdb
      
    • 清除所有数据库

      flushall
      
  • Redis是单线程的!
    Redis是很快的,官方表示,Redis是基于内存表示的,CPU不是redis的性能瓶颈,Redis的瓶颈是根据机器的内存和网络带宽的

    Redis为何单线程还这么快?

    1. 误区1: 高性能的服务器一定是多线程的?
    2. 误区2: 多线程(CPU上下文切换)一定比单线程效率高?

    核心:redis是将所有的数据全部放到内存当中的,而对于内存系统来说没有上下文切换就是效率最高的

      1. 基于内存

      redis的数据都是存储在内存中的,所有读写速度很快

      1. 单线程,不用维护锁机制
        redis采用单线程机制,指令串行,不用维护额外的锁机制,资源竞争等
      1. 数据结构简单,操作简单
        自己内部实现了各种数据结构,根据情况进行了优化
        (1)动态字符串结构
        (2)hash的字典结构的容量大小取2^N,使取模运算可以转换为按位运算,更快。同时扩容缩容采用采用渐进式rehash
        (3)zset采用跳表结构,范围查询更快
        (4)list采用压缩链表结构,内存空间连续

五大数据类型

Redis-Key

# 获取所有的key
keys *
# 判断当前的key是否存在
exists name
# 移除当前的key
move name 1
# 设置当前key的过期时间(s)
expire name 10
# 查看当前key的剩余时间
ttl name
# 查看当前key的类型
type name

String

set key1 v1 #设置值
get key1    #获得值
APPEND key1 "hello"  #追加字符串,如果不存在则新建
STRLEN key1          #获得key1的长度
127.0.0.1:6379[1]> set views 0 
OK
127.0.0.1:6379[1]> incr views  #对变量执行加一操作
(integer) 1
127.0.0.1:6379[1]> incr views
(integer) 2
127.0.0.1:6379[1]> decr views  # 对变量执行减一操作
(integer) 1
127.0.0.1:6379[1]> decr views
(integer) 0
127.0.0.1:6379[1]> decr views
(integer) -1
127.0.0.1:6379[1]> decr views
(integer) -2
127.0.0.1:6379[1]> get views
"-2"
#####################################
设置自增量
incrby key increment
decrby key increment

######################## ##################################################
获取字符串范围内的字符
getrange key   0 3
127.0.0.1:6379[1]> getrange name 0 5
"hello,"
127.0.0.1:6379[1]> getrange name 0 -1
"hello,wodeshen"

替换字符串范围内的字符
setrange key 0 3

127.0.0.1:6379[1]> set key2 "abcd"
OK
127.0.0.1:6379[1]> setrange key2 0 "xx" #替换指定位置开始的字符串
(integer) 4
127.0.0.1:6379[1]> get key2
"xxcd"
##########################################################################
设置过期时间
setex (set with expire)
语法:setex key seconds value
当不存在时设置(在分布式锁中常常使用)
setnx(set if not exist)

127.0.0.1:6379> setex key 10 ll #设置变量key-value :key-ll 并且10s过期
OK
127.0.0.1:6379> ttl key
(integer) 6
127.0.0.1:6379> ttl key
(integer) 4

127.0.0.1:6379> setnx key ll
(integer) 1
127.0.0.1:6379> get key
"ll"
127.0.0.1:6379> setnx key la
(integer) 0
127.0.0.1:6379> get key
"ll"

##########################################################################
批量设置和批量读取
mset
mget

127.0.0.1:6379> mset k1 v1 k2 v2
OK
127.0.0.1:6379> keys *
1) "k2"
2) "k1"
127.0.0.1:6379> mget k1 k2
1) "v1"
2) "v2"


127.0.0.1:6379> msetnx k1 v2 k4 v4
(integer) 0
#原子操作,前面的不成功后面的也不执行

#对象
set user:1{name:zhangsan, age:20} #设置对象user:1,对象值为json字符来保存一个对象
127.0.0.1:6379> set user:1 {name:zhangsan,age:20}
OK
或者:
127.0.0.1:6379> mset user:1:name zhangsan user:1:age 20
OK

127.0.0.1:6379> keys *
1) "k1"
2) "user:1"
3) "user:1:name"
4) "user:1:age"
127.0.0.1:6379> get user:1
"{name:zhangsan,age:20}"

##########################################################################
先get再set
getset

127.0.0.1:6379> getset db ke
(nil)
127.0.0.1:6379> getset db no
"ke"
127.0.0.1:6379> get db
"no"

String 类型的使用场景:value除了是字符串还可以是数字

  • 计数器
  • 统计多单位的数量
  • 粉丝数
  • 对象缓存储存

List

基本的数据类型

可以将其是现成栈、队列、阻塞队列

  • 所有的list命令都是l开头的
  • 插入:lpush rpush linsert
127.0.0.1:6379> lpush list one # 向列表中添加元素one,插入到列表头部(左侧)
(integer) 1
127.0.0.1:6379> lpush list 2
(integer) 2
127.0.0.1:6379> lpush list 3
(integer) 3
127.0.0.1:6379> lrange list 0 -1 # 获取列表中的所有元素 start0 end-1
1) "3"
2) "2"
3) "one"
127.0.0.1:6379> lrange list 1 3 # 新加入的在最上面,标号最小
1) "2"
2) "one"
127.0.0.1:6379> lrange list 1 4
1) "2"
2) "one"
127.0.0.1:6379> lrange list 0 4
1) "3"
2) "2"
3) "one"

127.0.0.1:6379> rpush list right1 right2 #将元素插入到列表尾部
(integer) 5
127.0.0.1:6379> lrange list 0 -1
1) "3"
2) "2"
3) "one"
4) "right1"
5) "right2"
#################
linsert #在指定值前面或后面插入指定的值
127.0.0.1:6379> lpush letter you
(integer) 1
127.0.0.1:6379> lpush letter I
(integer) 2
127.0.0.1:6379> linsert letter before you love #指定值前面
(integer) 3
127.0.0.1:6379> lrange letter 0 -1
1) "I"
2) "love"
3) "you"
127.0.0.1:6379> linsert letter after  you too
(integer) 4

127.0.0.1:6379> lrange letter 0 -1
1) "I"
2) "love"
3) "you"
4) "too"
  • 移除元素:lpop rpop ltrim lrem rpoplpush

    LPOP
    RPOP
    127.0.0.1:6379> LPOP list
    "3"
    127.0.0.1:6379> rpop list
    "right2"
    127.0.0.1:6379> lrange list 0 -1
    1) "2"
    2) "one"
    3) "right1"
    
    ################
    移除指定的值
    # 取关 uid
    """ lrem """
    127.0.0.1:6379> lrange list 0 -1
    1) "3"
    2) "3"
    3) "2"
    4) "1"
    127.0.0.1:6379> lrem list 1 3
    (integer) 1
    127.0.0.1:6379> lrange list 0 -1
    1) "3"
    2) "2"
    3) "1"
    #################
    ltrim 截断操作
    127.0.0.1:6379> lrange list1 0 -1
    1) "1"
    2) "2"
    3) "3"
    4) "4"
    5) "5"
    6) "1"
    127.0.0.1:6379> ltrim list1 2 6 # [左闭右闭]
    OK
    127.0.0.1:6379> lrange list1 0 -1
    1) "3"
    2) "4"
    3) "5"
    4) "1"
    ##################################
    rpoplpush # 移除列表最后一个元素并将他添加到新的列表
    127.0.0.1:6379> lpush list 1 2 3 45D
    (integer) 4
    127.0.0.1:6379> rpoplpush list listCopy
    "1"
    
    
  • 获取值: lindex lrange Llen

    lindex
    127.0.0.1:6379> lrange list 0 -1
    1) "2"
    2) "one"
    3) "right1"
    127.0.0.1:6379> lindex list 1
    "one"
    127.0.0.1:6379> lindex list 2
    "right1"
    
    lrange list start end
    Llen #获得list长度
    127.0.0.1:6379> llen list
    (integer) 3
    
  • 修改:lset

    lset #必须是已存在的值
    127.0.0.1:6379> lrange list 0 -1
    1) "45D"
    2) "3"
    3) "2"
    127.0.0.1:6379> lset list 0 4
    OK
    127.0.0.1:6379> lrange list 0 -1
    1) "4"
    2) "3"
    3) "2"
    

总结

    1. 实际上是一个链表,before Node after,left,right 都可以插入值
            1. 如果key不存在,则创建新的链表
            2. 如果存在,则新增内容
                3. 如果移除了所有值,空链表也代表不存在
            4. 在两边插入或者改动值效率最高

应用

  1. 消息排队
  2. 消息队列(Lpush Rpop)
  3. 栈(Lpush Lpop)

Set

不能重复,操作以s开头

1. 添加
  • sadd

    127.0.0.1:6379> sadd s1 1 2 3 4
    (integer) 4
    127.0.0.1:6379> smembers
    
2. 查看
  • smembers

    (error) ERR wrong number of arguments for 'smembers' command
    127.0.0.1:6379> smembers s1 #查询某个元素是否在集合中
    1) "1"
    2) "2"
    3) "3"
    4) "4"
    
  • sIsMember

    127.0.0.1:6379> sismember s1 1 #----查询某个元素是否存在
    (integer) 1
    
  • scard

    127.0.0.1:6379> scard s1 #----获取集合中元素个数
    (integer) 4
    
  • srandMember

    127.0.0.1:6379> srandmember s1 # ---- 获取集合中随机的值
    "1"
    127.0.0.1:6379> srandmember s1
    "4"
    127.0.0.1:6379> srandmember s1
    "2"
    127.0.0.1:6379> srandmember s1 2 #----指定个数返回
    1) "1"
    2) "4"
    
3. 删除
  • srem

    127.0.0.1:6379> srem s1 1 2 #---- 删除元素
    (integer) 2
    127.0.0.1:6379> smembers s1
    1) "3"
    2) "4"
    
  • spop

    127.0.0.1:6379> spop s1 #---- 随机删除集合中的元素
    "1"
    127.0.0.1:6379> spop s1
    "3"
    
  • smove

    #---- 把一个集合中的元素移动到另一个集合中
    27.0.0.1:6379> sadd s2 sqsq
    (integer) 1
    127.0.0.1:6379> smove s1 s2 4
    (integer) 1
    127.0.0.1:6379> smembers s1
    1) "2"
    127.0.0.1:6379> smembers s2
    1) "4"
    2) "sqsq"
    
4.集合运算
127.0.0.1:6379> sadd s1 a b c
(integer) 3
127.0.0.1:6379> sadd s2 c d e
(integer) 3
  • sdiff

    127.0.0.1:6379> sdiff s1 s2
    1) "b"
    2) "a"
    #----返回s1 - s2 ,即差集
    
  • sinter

    127.0.0.1:6379> sinter s1 s2
    1) "c"
    #---- 返回交集
    # 共同关注
    
  • sunion

    127.0.0.1:6379> sunion s1 s2
    1) "e"
    2) "a"
    3) "c"
    4) "b"
    5) "d"
    #---- 返回并集
    

Hash

Map集合 key --> key-value

命令以h开头

1. 新增
  • hset

    127.0.0.1:6379> hset myhash name lizzy
    (integer) 1
    
  • hmset

    一次设置多个field值

    127.0.0.1:6379> hmset myhash habbit tennis sex female
    OK
    
  • hsetnx

    当不存在时设置值

    127.0.0.1:6379> hsetnx myhash major ai
    (integer) 1
    127.0.0.1:6379> hsetnx myhash major cs
    (integer) 0
    
2. 读取
  • hget

    127.0.0.1:6379> hget myhash name
    "lizzy"
    
  • hmget

    一次获得多个field值

    127.0.0.1:6379> hmget myhash habbit sex
    1) "tennis"
    2) "female"
    
  • hgetall

    获得所有的键值对

    127.0.0.1:6379> hgetall myhash
    1) "name"
    2) "lizzy"
    3) "habbit"
    4) "tennis"
    5) "sex"
    6) "female"
    
  • hlen

    获取hash中有多少个键值对

    127.0.0.1:6379> hlen myhash
    (integer) 2
    
  • hexists

    判断某个field是否存在

    127.0.0.1:6379> hexists myhash sex
    (integer) 1
    
  • hkeys

    只获得所有的fields

  • hvals

    只获得所有的values

3. 删除
  • hdel

    删除某指定field

    127.0.0.1:6379> hdel myhash habbit
    (integer) 1
    127.0.0.1:6379> hgetall myhash
    1) "name"
    2) "lizzy"
    3) "sex"
    4) "female"
    
4.修改
  • Hincrby

    指定字段value加一

    127.0.0.1:6379> hset myhash age 20
    (integer) 1
    127.0.0.1:6379> hincrby myhash age 4
    (integer) 24
    
  • Hdecrby

    指定字段value减一

应用

  1. hash可以存储对象,如user: name , age等,适合经常变动的信息

    String更适合存字符串

Zset

有序集合,增加了一个值,set k1 v1 zset k1 score v1

1.新增
  • zadd

    127.0.0.1:6379> zadd myset incr 1 one
    "1"
    127.0.0.1:6379> zadd myset 2 two 3 three
    (integer) 2
    # 可以一次加多个值
    
2. 读取
  • zrange

    127.0.0.1:6379> zrange myset 0 -1
    1) "one"
    2) "two"
    3) "three"
    
  • zrangebyscore

    按照分数进行排序

    127.0.0.1:6379> zadd salary 5000 xiaoming
    (integer) 1
    127.0.0.1:6379> zadd salary 8000 xiaohong
    (integer) 1
    127.0.0.1:6379> zadd salary 50000 xiaoli
    (integer) 1
    127.0.0.1:6379> zadd salary 20000 xiaoshen
    (integer) 1
    127.0.0.1:6379> ZRANGEBYSCORE salary -inf inf
    1) "xiaoming"
    2) "xiaohong"
    3) "xiaoshen"
    4) "xiaoli"
    ## inf代表无穷大
    
  • zrevrangebyscore

    按照降序排列

    127.0.0.1:6379> ZREVRANGEBYSCORE salary +inf -inf
    1) "xiaoli"
    2) "xiaoshen"
    3) "xiaohong"
    4) "xiaoming"
    
  • zcard

    查看有多少个元素

    127.0.0.1:6379> zcard salary
    (integer) 3
    
  • zcount

    zcount 统计集合中分数区间内的元素个数

    127.0.0.1:6379> zcount salary 8000 +inf
    (integer) 2
    
3. 删除
  • rem

    删除该集合中指定值的元素

    127.0.0.1:6379> zrem salary xiaohong
    (integer) 1
    127.0.0.1:6379> zrange salary 0 -1
    1) "xiaoming"
    2) "xiaoshen"
    3) "xiaoli"
    

应用

案例思路 : 排行榜应用、存储工资表排序等

普通消息1 、 重要消息2 带权重进行排序

排行榜应用

zset 数据结构

redis中是通过两种底层数据结构实现的。一种是ziplist压缩列表,另一种就是redis中最经典的数据结构skipList跳跃表。

  • 第一次插入数据结构的选择
  1. 在使用ZADD
    命令添加第一个元素到空key时,程序通过检查输入的第一个元素来决定该创建什么编码的有序集。
  2. 符合下面的条件就会创建ziplist
    • 服务器属性server.zset_max_ziplist_entries 的值大于 0
    • 元素的member长度小于服务器属性server.zset_max_ziplist_value的值(默认64)
  • 后期编码转换
  1. 当刚开始选择了ziplist,会在下面两种情况下转为skipList。
  • ziplist所保存的元素超过服务器属性server.zset_max_ziplist_entries
    的值(默认值为 128 )
  • 新添加元素的 member 的长度大于服务器属性
    server.zset_max_ziplist_value 的值(默认值为 64)
  1. 为什么需要转换呢?
    ziplist是一种紧挨着的存储空间,并且没有预留空间。ziplist的优势在于节省空间,但是当容量大到一定程度扩容就是最影响性能的因素。
  • SkipList

    简介

    1. redis的skipList
      因为是有序的,所以需要一个hash结构来存储value和score的对应关系,另一方面需要提供按照score来排序的功能,还能够指定score的范围来获取value列表的功能,上述也就是跳跃表要实现的功能
    2. 跳跃表(skiplist)是一种随机化的数据,
      skiplist以有序的方式在层次化的链表中保存元素, 效率和平衡树媲美
      ------ 查找、删除、添加等操作都可以在对数期望时间下完成,
      并且比起平衡树来说, 跳跃表的实现要简单直观得多

    基本数据结构

    1. Redis 的跳跃表共有 64 层,意味着最多可以容纳 2^64
      次方个元素。每一个 kv 块对应的结构如下面的代码中的 zslnode
      结构,kv header 也是这个结构,只不过 value 字段是 null
      值------无效的,score 是Double.MIN_VALUE,用来垫底的。kv
      之间使用指针串起来形成了双向链表结构,它们是有序
      排列的,从小到大。不同的 kv 层高可能不一样,层数越高的 kv
      越少。同一层的 kv 会使用指针串起来。每一个层元素的遍历都是从 kv
      header 出发。
    2. 更加清晰的的跳跃表实现文章:https://lotabout.me/2018/skip-l

    为什么要使用跳表而不是用红黑树

    1. 从内存占用上来比较,跳表比平衡树更灵活一些。平衡树每个节点包含
      2
      指针(分别指向左右子树),而跳表每个节点包含的指针数目平均为
      1/(1-p),具体取决于参数 p 的大小。如果像 Redis里的实现一样,取
      p=1/4,那么平均每个节点包含 1.33 个指针,比平衡树更有优势。

    2. 在做范围查找的时候,跳表比平衡树操作要简单。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在跳表上进行范围查找就非常简单,只需要在找到小值之后,对第
      1
      链表进行若干步的遍历就可以实现。

    3. 从算法实现难度上来比较,跳表比平衡树要简单得多。平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而跳表的插入和删除只需要修改相邻节点的指针,操作简单又快速。

三种特殊数据类型

geospatial

朋友的定位、打车的位置、附近的人

geo数据可以推算两地之间的距离,方圆几里的人

1.geoadd

添加地理位置

  • 将指定的地理空间位置(经度、纬度、名称)添加到指定的key中,这些数据将会储存到sorted
    set中
  • 两极无法直接添加,一般会下载城市数据,通过Java程序一次性导入
127.0.0.1:6379> geoadd china:city 116.40   39.90 beijing   121.47 31.23 shanghai 

(integer) 2
127.0.0.1:6379> geoadd china:city 91.23 29.66 lhasa 103.83 36.05 lanzhou
(integer) 2
2.geodist

返回两个给定位置之间的距离。

如果两个位置之间的其中一个不存在, 那么命令返回空值。

指定单位的参数 unit 必须是以下单位的其中一个:

  • m 表示单位为米。
  • km 表示单位为千米。
  • mi 表示单位为英里。
  • ft 表示单位为英尺。

如果用户没有显式地指定单位参数, 那么 GEODIST 默认使用米作为单位。

GEODIST 命令在计算距离时会假设地球为完美的球形, 在极限情况下,
这一假设最大会造成 0.5% 的误差。

127.0.0.1:6379> geodist china:city lhasa lanzhou
"1373410.1899"
3. geohash

返回一个或多个位置元素的
Geohash 表示。

  • 连续不断的二分
  • 经度放在偶数位,纬度放在奇数位
  • 再做base32位编码
127.0.0.1:6379> geohash china:city lanzhou beijing
1) "wq3v4dwwqq0"
2) "wx4fbxxfke0"
4.geopos

查询指定位置的经度、纬度

127.0.0.1:6379> geopos china:city beijing
1) 1) "116.39999896287918091"
   2) "39.90000009167092543"
5.georadius

以给定的经纬度为中心,给出指定半径内的元素

127.0.0.1:6379> georadius china:city 110 30 1000 km
1) "lanzhou"
127.0.0.1:6379> georadius china:city 110 30 10000 km
1) "lhasa"
2) "lanzhou"
3) "shanghai"
4) "beijing"
127.0.0.1:6379> georadius china:city 110 30 10000 km count 2
1) "lanzhou"
2) "shanghai"
count 参数返回指定数量的值
6.给radiusbyMember

指定集合内的某个元素为中心点查询半径范围内的地点

127.0.0.1:6379> georadiusbymember china:city lanzhou 1500 km
1) "lhasa"
2) "lanzhou"
3) "beijing"
底层原理

底层使用Zset,可以使用zset命令操作geo

127.0.0.1:6379> zrem china:city shanghai
(integer) 1
127.0.0.1:6379> zrange china:city 0 -1
1) "lhasa"
2) "chengdu"
3) "lanzhou"
4) "beijing"

hyperloglog

什么是基数

A {1,3,5,7,8,5}

B {1,3,5,4,7}

基数------不重复的元素 = 6 , 可以接受误差!

简介

redis hyperloglog 基数统计的算法

网页的UV(一个人访问网站多次,但是还是算作一次访问量)

传统的方式:使用set保存用户的uid,使用uid作为唯一量的衡量

缺点:使用该方法会储存大量的uid,属于无效数据会占用大量内存

Hyperloglog优点:占用固定内存,2^64个不同的元素只需要12kb内存

命令以PF开头:

  • pfadd

    添加数据

127.0.0.1:6379> pfadd mykey 1 2 3 4 5
(integer) 1
127.0.0.1:6379> pfcount mykey
(integer) 12
  • pfcount

    统计不重复的数据

  • pfmerge

    合并集合

    127.0.0.1:6379> pfadd mykey2 9 10 11
    (integer) 1
    127.0.0.1:6379> pfmerge mykey mykey2
    OK
    127.0.0.1:6379> pfcount mykey
    (integer) 15
    
    
    

bitmaps

统计两个状态的需求,都可以用bitmaps,如:是否打卡、是否登录…

  • setbit

    设置每一位的状态 0、1

    127.0.0.1:6379> setbit sign 0 0
    (integer) 0
    127.0.0.1:6379> setbit sign 1 0
    (integer) 0
    127.0.0.1:6379> setbit sign 2 1
    (integer) 0
    
  • getbit

    设置某一位的状态 0、1

    127.0.0.1:6379> getbit sign 2
    (integer) 1
    127.0.0.1:6379> getbit sign 1
    (integer) 0
    
  • bitcount

    统计1的个数

    127.0.0.1:6379> setbit sign 3 1 
    (integer) 0
    127.0.0.1:6379> setbit sign 4 1
    (integer) 0
    127.0.0.1:6379> setbit sign 5 0
    (integer) 0
    127.0.0.1:6379> bitcount sign
    (integer) 3
    

事务

1. 基本操作

Redis事务本质:一组命令的集合!一个事务中的所有命令都会被序列化,在事务执行过程中会按照顺序执行。

---队列---
------ 命令1 命令2 命令3 ... 命令n -----> 执行

1. Redis事务没有隔离级别的概念!

所有的命令在事务中,并没有被直接执行!只有发起执行命令的时候才会执行!

2. Redis单条命令保证原子性,但是redis的事务不保证原子性!

3. Redis事务的执行步骤:

  1. 开启事务(multi)
  2. 命令入队(…)
  3. 执行事务(exec)
127.0.0.1:6379> multi 
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) OK
3) "v2"
4) OK

放弃事务

  • discard
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1 
QUEUED
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> discard
OK
127.0.0.1:6379> get k4
(nil)

编译型异常(代码有问题,编译不通过)

事务中所有的命令都不会执行!

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> getset k3 v3
QUEUED
127.0.0.1:6379(TX)> getset k4
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379(TX)> set k5 v5
QUEUED
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.

运行时异常

如果队列中存在语法性异常,那么执行命令其他命令可以正常执行

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> incr k1 #执行时出错
QUEUED
127.0.0.1:6379(TX)> set k2 v3
QUEUED
127.0.0.1:6379(TX)> getset k2 v2
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
4) "v3"
5) "v2"

2. 实现乐观锁

使用Watch

悲观锁:什么时候都会出问题,无论做什么都会加锁

乐观锁:

  • 认为什么时候都不会出问题,所以不回加锁!在更新数据时判断,在此期间是否有人修改过这个数据
  • 获取Version
  • 更新的时候比较Version
测试Watch

正常执行成功!

127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby money 20
QUEUED
127.0.0.1:6379(TX)> incrby out 20
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 80
2) (integer) 20
  1. 多线程操作

    线程1:

    127.0.0.1:6379> watch money
    OK
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379(TX)> decrby money 20
    QUEUED
    127.0.0.1:6379(TX)> INCRBY out 20
    QUEUED
    127.0.0.1:6379(TX)> exec #模拟线程2改动了money数值,所以执行不成功
    (nil)
    

    线程2:

    127.0.0.1:6379> get money
    "80"
    127.0.0.1:6379> set money 1000
    OK
    
  2. 执行失败后续操作

    1. 如果发现事务执行失败,先使用unwatch解锁
    2. 获取最新的值再次监视

Jedis

使用java来操作redis

jedis是Redis官方推荐的java连接开发工具!

编码测试

  • 连接数据库
  • 操作命令
  • 结束测试

事务

  • jedis.multi()

    使用该命令开启事务

  • multi.exec()

    使用该命令执行事务队列

SpringBoot整合

说明:在SpringBoot2.x之后,原来使用的jedis替换成为了lettuce

jedis:采用直连,多个线程操作是不安全的。如果想避免不安全的,则采用jedis pool连接池!更像BIO模式

lettuce:采用netty,实例可以在多个现称中共享,不存在线程不安全的情况!减少线程数量,更像NIO模式

源码分析

//默认的RedisTemplate没有过多的设置,Redis对象都是需要序列化的
//可以自己定义一个redisTemplate来替换这个默认的
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnKnownHostException{
    //两个泛型都是Object类型,后续使用需要强制转换<String,Object>
    RedisTemplate<Object, Object> template = new RedisTemplate<>();                     template.setConnectionFactory(redisConnectionFactory);
    return template;
}

@Bean
@ConditionalOnMissingBean
public StringRedisTemplate<Object, Object> stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnKnownHostException{
    StringRedisTemplate template = new StringRedisTemplate();                       template.setConnectionFactory(redisConnectionFactory);
    return template;
}

整合测试

  1. 导入依赖

  2. 配置连接

    使用yml文件或者properties配置

    spring:
      redis:
        # Redis本地服务器地址,注意要开启redis服务,即那个redis-server.exe
        host: 127.0.0.1
        # Redis服务器端口,默认为6379.若有改动按改动后的来
        port: 6379
        #Redis服务器连接密码,默认为空,若有设置按设置的来
        password:
        jedis:
          pool:
            # 连接池最大连接数,若为负数则表示没有任何限制
            max-active: 8
            # 连接池最大阻塞等待时间,若为负数则表示没有任何限制
            max-wait: -1
            # 连接池中的最大空闲连接
            max-idle: 8
    
    #Redis本地服务器地址,注意要开启redis服务,即那个redis-server.exe
    spring.redis.host=127.0.0.1
    #Redis服务器端口,默认为6379.若有改动按改动后的来
    spring.redis.port=6379
    #Redis服务器连接密码,默认为空,若有设置按设置的来
    spring.redis.password=
    #连接池最大连接数,若为负责则表示没有任何限制
    spring.redis.jedis.pool.max-active=8
    #连接池最大阻塞等待时间,若为负责则表示没有任何限制
    spring.redis.jedis.pool.max-wait=-1
    #连接池中的最大空闲连接
    spring.redis.jedis.pool.max-idle=8
    
  3. 测试

Redis.conf详解

启动时就需要配置文件

1. 包含

可以包含多个配置文件

2. 网络

bind 绑定的ip
protected-mode yes #保护模式
port 6379 # 端口模式

3.通用

daemonize yes  #以守护进行的方式运行,默认是no,打开为yes(即后台运行)
pidfile /var/run/redis_6379.pid   #如果后台方式运行,需要指定一个pid文件

# 日志
loglevel notice # debug 、 verbose、notice、warning
logfile '' #生成的日志文件地址

4. 快照

持久化,在规定的时间内执行了多少次操作就会持久化到 .rdb .aof 文件

redis是内存数据库如果不持久化断电即失

Redis持久化

持久化是重点

RDB(Redis DataBase)

  • 触发规则:
    1. save规则满足的条件下
    2. flushall的命令下
    3. 退出redis
  • 如何恢复
    1. 只需要将rdb文件放在redis启动目录下,就会在启动时自动检查dump.rdb并恢复其中数据
  • 优点:
    1. 适合大规模的数据恢复!
    2. 对数据的完整性要求不高则效率很高!
  • 缺点:
    1. 需要一定的时间间隔进行操作,如果意外宕机,则最后一次数据会丢失
    2. fork进程时,会占用一定的内存空间

AOF(Append Only File)

将所有命令都记录下来,history,恢复时执行所有历史命令

  • 如果aof文件出错,redis启动不起来,需要使用redis-check-aof进行修复

  • 优点:

  1. 每一次修改都同步,文件的完整性会更好

  2. 默认每秒同步一次,可能会丢失一秒的数据

  • 缺点:
    1. 相对于数据文件大小来说,aof文件远大于rdb,修复速度也比rdb慢
    2. aof运行效率也低于rdb

Redis发布订阅

redis发布订阅(pub/sub)
是一种消息通信模式:发送者发送消息,订阅者接收消息

redis客户端可以订阅任意数量的频道

  • 测试:

    • 接收端
    SUBSCRIBE lizzy
    
    • 发送端

      127.0.0.1:6379> publish lizzy "youarehere"
      (integer) 1
      
    • 结果

      1) "message"
      2) "lizzy"
      3) "youarehere"
      
  • 原理

    Redis通过PUBLISH、SUBSCRIBE、PSUBSCRIBE等命令实现发布和订阅功能

    1. 通过Subscribe命令订阅某频道后,redis-server里维护了一个字典

      • 字典的键就是一个个频道,
      • 字典的值是一个链表,链表中保存了订阅了这个频道的所有客户端

      Subscribe命令的本质就是将客户端添加到指定的channel的订阅链表中

    2. 通过publish向订阅者发送消息,redis-server会使用给定的频道作为键,在它所维护的channel字典中查找记录了订阅这个频道的所有客户端的链表,遍历链表发送消息

  • 使用场景:

    1. 实时消息系统

    2. 实时聊天(频道当作聊天室,将消息回显给所有人即可)

    3. 订阅发布,关注系统

      TIPS:稍微复杂些的场景用消息中间件做

Redis 主从复制

适用于大部分情况,因为大部分都是读多写少

需要修改每个从库的config文件

  • 修改项
  1. 端口号

  2. daemonize 改为yes,使其后台运行

  3. pid文件

  4. logfile------否则会重复

  5. rdb文件名称

直接include redis.conf

然后添加这些信息即可

  • 配置

    1. 在从机中执行命令
    slaveof localhost  6379
    127.0.0.1:6380> SLAVEOF localhost 6379
    OK
    info replication
    # Replication
    role:slave
    master_host:localhost
    master_port:6379
    master_link_status:down
    master_last_io_seconds_ago:-1
    master_sync_in_progress:0
    slave_read_repl_offset:0
    slave_repl_offset:0
    master_link_down_since_seconds:-1
    slave_priority:100
    slave_read_only:1
    replica_announced:1
    connected_slaves:0
    master_failover_state:no-failover
    master_replid:c2996c05bc9498b14d92da6500dc57df86b89688
    master_replid2:0000000000000000000000000000000000000000
    master_repl_offset:0
    second_repl_offset:-1
    repl_backlog_active:0
    repl_backlog_size:1048576
    repl_backlog_first_byte_offset:0
    repl_backlog_histlen:0
    
    1. 或者直接在从机配置文件中配置

      replicaof localhost 6379
      

    在主机中查看

    info replication
    127.0.0.1:6379> info replication
    # Replication
    role:master
    connected_slaves:2
    slave0:ip=::1,port=6380,state=online,offset=350,lag=1
    slave1:ip=::1,port=6381,state=online,offset=350,lag=1
    master_failover_state:no-failover
    master_replid:ecff98bc0008e740607f7999d5fa1a527054412a
    master_replid2:0000000000000000000000000000000000000000
    master_repl_offset:350
    second_repl_offset:-1
    repl_backlog_active:1
    repl_backlog_size:1048576
    repl_backlog_first_byte_offset:1
    repl_backlog_histlen:350
    
  • 复制原理

    1. Slave启动成功后连接到master后会发送一个sync同步命令
    2. Master接收到命令,启动后台的存盘程序,同时收集所有接收到的用于修改数据集的命令,在后太执行完毕之后,master将传送整个数据文件到slave,并完成一次完全同步
    3. 全量复制:slave接受到数据库文件数据后,将其存盘并加载到内存中
    4. 增量复制:master将新收集到的命令依次传给slave完成同步

    只要重新连接,执行全量复制

哨兵模式(自动选取老大)

配置哨兵文件sentinel.conf


sentinel monitor myredis ::1 6380 1

在主机宕机后,随机选择一个从机(投票)选举为主机

当主机连接回来之后,只能作为从机

  • 优点:
    1. 哨兵集群,基于主从复制模式,所有主从配置的优点全由
    2. 主从可以切换,故障可以转移,系统可用性更好
    3. 哨兵模式是主从模式的升级,手动变成自动,更加健壮
  • 缺点:
    1. redis不好在线扩容,集群容量到达上限后,在线扩容很麻烦
    2. 实现哨兵模式的配置很麻烦,有很多选择

全部配置

port 26379 #如果哨兵集群,需要配置每个哨兵的端口

sentinel monitor mymaster 127.0.0.1 6379 2 #默认主机

Redis 缓存穿透和雪崩

缓存穿透

1. 概念

查询数据时,redis内存数据库中没有,也就是缓存没有命中,于是向数据库中查询,当请求量很大的时候会给数据库造成很大压力,这时候就相当于出现了缓存穿透

2. 解决方案
布隆过滤器

是一种数据结构,对所有可能查询的参数以hash形式储存,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力

缓存空对象

当存储层不命中后,即使返回空对象也将其缓存起来,同时会设置一个过期时间,之后再次访问这个数据将会从缓存中获取,保护了后端数据源

缓存击穿

1. 概述:
1. 缓存击穿:是指一个key非常热点,在不停的扛着大并发,大并发集中对着一个点进行访问,当这个key失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上穿了一个洞
2. 当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并写回缓存,会对数据库瞬间造成很大压力
2. 解决方案
1. 设置热点数据永不过期
2. 加互斥锁

setnx
    分布式锁:使用分布式锁,确保对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可

缓存雪崩

1.概述:
再某一个时间段,所有的缓存集中过期失效
2.解决方案:
1. redis高可用
    搭建redis集群,一台崩掉还能使用其他的
2. 限流降级
    缓存失效后,通过加锁或者队列来控制都数据库的线程数量
3. 数据预热
    在正式部署之前,把可能的数据先预先访问一遍,将可能大量访问的数据加到缓存当中。在即将发生大并发访问之前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均衡。

缓存读写策略

1. Cache Aside Pattern

服务器同时维护db和cache,并且以db的结果为准

写:

  • 先更新db
  • 直接删除cache

读:

  • 首先从cache中读取数据,命中则直接返回
  • 否则从数据库中读取,并写入cache
  • 问题:
  1. 写数据时,能先删除cache,后更新db吗? 会造成数据不一致
    先写后读会造成不一致:1先写A时,先把cache中A删了,2直接从db中读到A之前的数据并写入cache,db再更新,造成不一致。

  2. 先更新db,再删除cache 同样也会造成不一致
    先读后写,如果cache不存在A,1从db中读取A,此时2更新db中的A(因为cache中不存在,所以不用删),1返回A并写入cache,造成不一致。

缺陷:

  1. 首次请求数据一定不在cache中: 可以将热点数据提前存到cache中

  2. 写操作频繁会导致cache中的数据频繁被删除,影响命中率

    1. 强一致:更新db时同时更新cache,加分布式锁来保证不存在线程安全问题
    2. 短暂允许不一致,给cache数据设置一个短的过期时间,保证不一致影响较小

2. Read/Write Through Pattern(读写穿透)

Read/Write Through Pattern中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache
服务负责将此数据读取和写入 db,从而减轻了应用程序的职责。

写:

  • 先更新cache
  • 再由cache更新db

读:

  • 从cache读,有则返回
  • 没有的话从db中读再写入cache

Read-Through Pattern 实际只是在 Cache-Aside Pattern 之上进行了封装。在 Cache-Aside Pattern
下,发生读请求的时候,如果 cache中不存在对应的数据,是由客户端自己负责把数据写入 cache,而 Read Through Pattern 则是 cache 服务自己来写入缓存的,这对客户端是透明的。

缺陷:
也有首次请求数据一定不再 cache 的问题,对于热点数据可以提前放入缓存中。

3. Write Behind Pattern

由cache直接操作db,只更新cache,再由cache服务异步批量的更新db,一致性有更大的挑战,cache还没更新数据库,cache就挂了

Write Behind Pattern 下 db的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。
同时访问数据库来查询最新数据,并写回缓存,会对数据库瞬间造成很大压力

2. 解决方案
1. 设置热点数据永不过期
2. 加互斥锁

setnx
    分布式锁:使用分布式锁,确保对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可

缓存雪崩

1.概述:
再某一个时间段,所有的缓存集中过期失效
2.解决方案:
1. redis高可用
    搭建redis集群,一台崩掉还能使用其他的
2. 限流降级
    缓存失效后,通过加锁或者队列来控制都数据库的线程数量
3. 数据预热
    在正式部署之前,把可能的数据先预先访问一遍,将可能大量访问的数据加到缓存当中。在即将发生大并发访问之前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均衡。

缓存读写策略

1. Cache Aside Pattern

服务器同时维护db和cache,并且以db的结果为准

写:

  • 先更新db
  • 直接删除cache

读:

  • 首先从cache中读取数据,命中则直接返回
  • 否则从数据库中读取,并写入cache
  • 问题:
  1. 写数据时,能先删除cache,后更新db吗? 会造成数据不一致
    先写后读会造成不一致:1先写A时,先把cache中A删了,2直接从db中读到A之前的数据并写入cache,db再更新,造成不一致。

  2. 先更新db,再删除cache 同样也会造成不一致
    先读后写,如果cache不存在A,1从db中读取A,此时2更新db中的A(因为cache中不存在,所以不用删),1返回A并写入cache,造成不一致。

缺陷:

  1. 首次请求数据一定不在cache中: 可以将热点数据提前存到cache中

  2. 写操作频繁会导致cache中的数据频繁被删除,影响命中率

    1. 强一致:更新db时同时更新cache,加分布式锁来保证不存在线程安全问题
    2. 短暂允许不一致,给cache数据设置一个短的过期时间,保证不一致影响较小

2. Read/Write Through Pattern(读写穿透)

Read/Write Through Pattern中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache
服务负责将此数据读取和写入 db,从而减轻了应用程序的职责。

写:

  • 先更新cache
  • 再由cache更新db

读:

  • 从cache读,有则返回
  • 没有的话从db中读再写入cache

Read-Through Pattern 实际只是在 Cache-Aside Pattern 之上进行了封装。在 Cache-Aside Pattern
下,发生读请求的时候,如果 cache中不存在对应的数据,是由客户端自己负责把数据写入 cache,而 Read Through Pattern 则是 cache 服务自己来写入缓存的,这对客户端是透明的。

缺陷:
也有首次请求数据一定不再 cache 的问题,对于热点数据可以提前放入缓存中。

3. Write Behind Pattern

由cache直接操作db,只更新cache,再由cache服务异步批量的更新db,一致性有更大的挑战,cache还没更新数据库,cache就挂了

Write Behind Pattern 下 db的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值