redis基础知识

redis基础知识

redis

以下内容摘自redis中文官网:http://www.redis.cn/

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作**数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings)散列(hashes)列表(lists)集合(sets)有序集合(sorted sets)范围查询**, bitmapshyperloglogs地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication)LUA脚本(Lua scripting)LRU驱动事件(LRU eviction)事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)自动 分区(Cluster)提供高可用性(high availability)==>可以用来做集群

Redis-Key

redis的key值是二进制安全带,任何二进制序列都可以作为key值。从string到jpeg文件内容都可以,甚至是一个空字符串。

127.0.0.1:6379> exists name			#判断key是否存在,1表示存在,0表示不存在
(integer) 1
127.0.0.1:6379> exists name2
(integer) 0
127.0.0.1:6379> clear
127.0.0.1:6379> move name 1			#把这个key从当前数据库移动到第几个数据库,后面表示第几个数据库
(integer) 1
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> keys *
1) "name"
127.0.0.1:6379[1]> expire name 10	#给这个key设置超时时间
(integer) 1
127.0.0.1:6379[1]> ttl name			#获取这个key的有效时间,-2表示已经超时不存在了
(integer) -2
127.0.0.1:6379[1]> get name
(nil)
127.0.0.1:6379[1]> set name 7
OK
127.0.0.1:6379[1]> type name		#查看key的数据类型
string

运行redis

docker run

-d以守护进程方式启动,守护进程:参考java的守护线程,即使ctrl+c退出,也不会停止这个容器的运行

–name 设置name,所有需要用id访问的命令都可以用name替代

这里报错是因为我之前跑过这个redis,如果使用docker images 就会看到myredis这个容器已经存在了

mirana@末小七:/mnt/c/Users/末小七$ docker run -d --name myredis redis
docker: Error response from daemon: Conflict. The container name "/myredis" is already in use by container "3bfc98b3236f47bd46475bb3213bb0bef03782b01e2f3c1a9bde90899333af45". You have to remove (or rename) that container to be able to reuse that name.
See 'docker run --help'.

所以:

使用restart命令直接跑容器就可

mirana@末小七:/mnt/c/Users/末小七$ docker restart myredis
myredis

进入redis交互

mirana@末小七:/mnt/c/Users/末小七$ docker exec -it myredis redis-cli
127.0.0.1:6379>

这里歪个楼:

redis的端口为啥默认是6379?

拿出手机–敲下键盘–这其实是redis作者喜欢的一个明星名字,有兴趣可以去知乎搜下

redis基本命令

redis共16个数据库

查找redis的路径

mirana@末小七:/mnt/c/Users/末小七$ whereis redis
redis: /etc/redis

进入对应路径

mirana@末小七:/mnt/c/Users/末小七$ cd /etc/redis

展示当前目录下内容,打开redis.conf配置文件

就可以看到这行,redis默认有16个数据库,可以修改配置文件来自己定义

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OsqN49a2-1631719421521)(image-20210911120530196.png)]


select

切换数据库

redis默认是第0个数据库,可以通过select命令切换数据库

比如切换到第三个数据库

127.0.0.1:6379> select 3  
OK

dbsize

查看当前数据库多少个key

127.0.0.1:6379[3]> dbsize   
(integer) 0
127.0.0.1:6379[3]> set name 77  #set
OK
127.0.0.1:6379[3]> set name2 88
OK
127.0.0.1:6379[3]> dbsize
(integer) 2

keys

查看当前数据库所有的key

127.0.0.1:6379[3]> keys *   
1) "name2"
2) "name"

flushdb

清空当前数据库

127.0.0.1:6379[3]> flushdb 
OK
127.0.0.1:6379[3]> dbsize
(integer) 0

flushall

轻空所有的数据库

如下测试,先在第三个数据库塞值,再去第0个数据库塞值,然后flushdb

然后就会看到第0个数据库跟第3个数据库都空了

127.0.0.1:6379[3]> set name 777
OK
127.0.0.1:6379[3]> select 0
OK
127.0.0.1:6379> set name 77777
OK
127.0.0.1:6379> dbsize
(integer) 1
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> dbsize
(integer) 0
127.0.0.1:6379> select 3
OK
127.0.0.1:6379[3]> dbsize
(integer) 0

redisの数据结构

redis共八种数据结构,其中五种常用数据结构,3种特殊

五种常用数据结构:

String

value的值可以是任何种类的字符串,包括二进制数据。最大不能超过512MB。除了是字符串也可以用数字,图片或者是序列化对象。

使用场景

  • 计数器

  • 统计多单位的数量

  • 粉丝数

  • 对象缓存、存储

  • 分布式锁


常见命令:

127.0.0.1:6379> set key name
OK
**************set支持类似json的写法,如下****************
127.0.0.1:6379> set user:1:name 77
OK
127.0.0.1:6379> set user:1:age 18
OK
127.0.0.1:6379> keys *
1) "user:1:age"
2) "user:1:name"
******************************
127.0.0.1:6379> exists name	#判断这个key是否存在
(integer) 0
127.0.0.1:6379> append key 77 #在key的value后面追加
(integer) 6
127.0.0.1:6379> get key
"name77"
127.0.0.1:6379> strlen key	 #获取这个key的value的长度
(integer) 6
127.0.0.1:6379> append key ",today"
(integer) 12
127.0.0.1:6379> strlen key
(integer) 12
127.0.0.1:6379> append key1 zhangsan	#append的时候,如果这个key不存在就set
(integer) 8
127.0.0.1:6379> keys *
1) "key1"
2) "key"

incr、decr、incrby、decrby

原子递增/递减

127.0.0.1:6379> set viewcount 0
OK
127.0.0.1:6379> get viewcount
"0"
127.0.0.1:6379> incr viewcount	 	#递增这个key的值
(integer) 1
127.0.0.1:6379> get viewcount
"1"
127.0.0.1:6379> incr viewcount
(integer) 2
127.0.0.1:6379> get viewcount
"2"
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> set view 0
OK
127.0.0.1:6379> incrby view 2	#按照步长递增这个key的值
(integer) 2
127.0.0.1:6379> incrby view 2
(integer) 4
127.0.0.1:6379> get view
"4"
127.0.0.1:6379> set total 100
OK
127.0.0.1:6379> decr total		#递减这个key的值
(integer) 99
127.0.0.1:6379> decr total
(integer) 98
127.0.0.1:6379> get total
"98"
127.0.0.1:6379> decrby total 2	#按照步长递减这个key的值
(integer) 96
127.0.0.1:6379> decrby total 2
(integer) 94
127.0.0.1:6379> get total
"94"

incr、decr都是原子操作


getrange

获取字符串

命令格式:GETRANGE key start end

当start end是0 -1 表示返回全部

127.0.0.1:6379> set return hello,77,fignting
OK
127.0.0.1:6379> get return
"hello,77,fignting"
127.0.0.1:6379> getrange return 0 2
"hel"
127.0.0.1:6379> getrange return 0 -1 
"hello,77,fignting"

setex

set with expire :给这个key设置值并设置过期时间

命令格式:SETEX key seconds value

127.0.0.1:6379> setex name 10 7
OK
127.0.0.1:6379> ttl name #获取key的有效时间
(integer) 8
127.0.0.1:6379> get name
"7"

setnx

这个key如果不存在就设置

127.0.0.1:6379> setnx address shenzhen
(integer) 1
127.0.0.1:6379> keys *
1) "return"
2) "address"
3) "age"
127.0.0.1:6379> setnx age 19  #如果这个key存在就会失败
(integer) 0

利用setnx实现分布式锁

经常用于分布式锁,不过更好的是用zk来实现。因为zk有临时节点,机制类似于sql的for update,一旦链接挂掉,这个锁会自动释放,就不需要考虑redis中下面这么多场景

setnx实现分布式锁,有几个需要注意的点:

1)要设置超时时间

防止setnx…业务代码…释放锁,在执行业务代码的时候jvm挂掉了,那这个就死锁了

2)释放锁的时候要判断set的值是不是你设置的值

​ jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

try{

​ //业务代码

}catch{

​ //异常处理

}finally{

​ //释放 即del key

}

如果你setnx的时候设置了超时时间5s,业务代码执行了6s,在这超时的1s,锁释放了,这时另外一个线程重新set了值,这时你的代码finally里释放的就是别人的锁。所以你需要保证你释放的是自己的锁(可以通过redis,给这个key设置个uuid,然后在finally中先获取key的值,然后判断是否是你设置的uuid来实现)

3)还是上个问题

finally代码:

​ getkey

​ delkey

如果在getkey,刚获取到,另外一个线程正好在你获取到之后来了,你del的就又是别人的锁了。

所以需要保证get跟del是同一条指令,这时用java代码已经无法保证了,要用lua脚本

更多注意事项,参考博客:https://mp.weixin.qq.com/s/qJK61ew0kCExvXrqb7-RSg


mset & mget、msetnx

批量设置、获取

msetnx 同setnx ,批量设置的原子操作

127.0.0.1:6379> mset name 77 age 17
OK
127.0.0.1:6379> keys *
1) "age"
2) "name"
127.0.0.1:6379> msetnx age 19 collage xidian
(integer) 0

getset

如果不存在,返回nil

存在,则返回原来的值并更新

127.0.0.1:6379> getset db redis
(nil)
127.0.0.1:6379> get db
"redis"
127.0.0.1:6379> getset db mysql
"redis"
127.0.0.1:6379> get db
"mysql"

scan

时间复杂度为O(1)

命令格式 SCAN cursor [MATCH pattern] [COUNT count]

以下摘自redis中文官网,有部分改动

命令解释
  • SCAN 命令用于迭代当前数据库中的key集合。
  • SSCAN 命令用于迭代SET集合中的元素。

  • HSCAN 命令用于迭代Hash类型中的键值对。

  • ZSCAN 命令用于迭代SortSet集合中的元素和元素对应的分值

以上列出的四个命令都支持增量式迭代,它们每次执行都只会返回少量元素,所以这些命令可以用于生产环境,而不会出现像 KEYS 或者 SMEMBERS 命令带来的可能会阻塞服务器的问题。

不过,SMEMBERS 命令可以返回集合键当前包含的所有元素, 但是对于SCAN这类增量式迭代命令来说,有可能在增量迭代过程中,集合元素被修改,对返回值无法提供完全准确的保证。

SCAN, SSCAN, HSCANZSCAN 四个命令的工作方式都非常相似,需要注意的是SSCAN, HSCAN ,ZSCAN命令的第一个参数总是一个key; SCAN 命令则不需要在第一个参数提供任何key,因为它迭代的是当前数据库中的所有key。

SCAN命令是一个基于游标的迭代器。这意味着命令每次被调用都需要使用上一次这个调用返回的游标作为该次调用的游标参数,以此来延续之前的迭代过程

SCAN命令的游标参数被设置为 0 时, 服务器将开始一次新的迭代, 而当服务器向用户返回值为 0 的游标时, 表示迭代已结束。

返回结果

SCAN增量式迭代命令并不保证每次执行都返回某个给定数量的元素,甚至可能会返回零个元素, 但只要命令返回的游标不是 0 , 应用程序就不应该将迭代视作结束。

不过命令返回的元素数量总是符合一定规则的, 对于一个大数据集来说, 增量式迭代命令每次最多可能会返回数十个元素;而对于一个足够小的数据集来说, 如果这个数据集的底层表示为编码数据结构(小的sets, hashes and sorted sets), 那么增量迭代命令将在一次调用中返回数据集中的所有元素。

如果需要的话,用户可以通过增量式迭代命令提供的COUNT选项来指定每次迭代返回元素的最大值。SCAN增量式迭代命令并不保证每次执行都返回某个给定数量的元素,甚至可能会返回零个元素, 但只要命令返回的游标不是 0 , 应用程序就不应该将迭代视作结束。

不过命令返回的元素数量总是符合一定规则的, 对于一个大数据集来说, 增量式迭代命令每次最多可能会返回数十个元素;而对于一个足够小的数据集来说, 如果这个数据集的底层表示为编码数据结构(小的sets, hashes and sorted sets), 那么增量迭代命令将在一次调用中返回数据集中的所有元素。

如果需要的话,用户可以通过增量式迭代命令提供的COUNT选项来指定每次迭代返回元素的最大值。

参数count

对于增量式迭代命令不保证每次迭代所返回的元素数量,我们可以使用COUNT选项, 对命令的行为进行一定程度上的调整。COUNT 选项的作用就是让用户告知迭代命令, 在每次迭代中应该从数据集里返回多少元素。使用COUNT 选项对于对增量式迭代命令相当于一种提示, 大多数情况下这种提示都比较有效的控制了返回值的数量。

  • COUNT 参数的默认值为 10
  • 数据集比较大时,如果没有使用MATCH 选项, 那么命令返回的元素数量通常和 COUNT 选项指定的一样, 或者比 COUNT 选项指定的数量稍多一些。
  • 在迭代一个编码为整数集合(intset,一个只由整数值构成的小集合)、 或者编码为压缩列表(ziplist,由不同值构成的一个小哈希或者一个小有序集合)时, 增量式迭代命令通常会无视 COUNT 选项指定的值, 在第一次迭代就将数据集包含的所有元素都返回给用户。(即如果数据量太小,count命令会失效)

注意: **并非每次迭代都要使用相同的 COUNT 值 **,用户可以在每次迭代中按自己的需要随意改变 COUNT 值, 只要记得将上次迭代返回的游标用到下次迭代里面就可以了。

所以最好不要使用scan命令来做分页之类的功能,类似踩坑经验请看博客:

https://blog.csdn.net/zjcsuct/article/details/108138876


List

应用场景:

​ 可以用来实现栈(lpush、lpop)、队列、消息队列(lpush、rpop)、阻塞访问(lpush与rpop:可以利用阻塞式访问的brpop与blpop命令来实现)

  • before node after
  • 左右两段都可以插入
  • 如果key不存在 就创建新的
  • 如果key存在 就新增
  • 如果移除了所有,就是个空链表,也代表不存在
  • 在两边插入或者更改效率最高,如果在中间元素则效率偏低,如果想访问指定位置的元素,则效率偏低

lpush & lrange

可以想象成从左边(链表头)写入

127.0.0.1:6379> lpush list one two	   #可以看出,先进后出
(integer) 2
127.0.0.1:6379> lrange list 0 1
1) "two"
2) "one"
127.0.0.1:6379> lrange list 0 2
3) "two"
4) "one"
127.0.0.1:6379> lrange list 0 -1		#获取list的值,也可以根据index获取具体某个位置的值,如果 0 -1就是获取全部
1) "two"
2) "one"

rpush

从右边(链表尾部)写入

127.0.0.1:6379> rpush list zero
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
3) "zero"

lpop & rpop
127.0.0.1:6379> lpop list  #移除list的第一个元素
"two"
127.0.0.1:6379> lrange list 0 -1
1) "one"
2) "zero"
127.0.0.1:6379> rpop list	#移除list的最后一个值
"zero"
127.0.0.1:6379> lrange list 0 -1
1) "one"

lindex

获取指定元素的值

127.0.0.1:6379> lpush list zero one two three
(integer) 4
127.0.0.1:6379> lindex list 1
"two"
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "zero"

llen

获取list长度

127.0.0.1:6379> llen list
(integer) 4

lrem

移除list中指定个数的value

命令格式:LREM key count value

127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "zero"
5) "zero"
6) "zero"
7) "1"
127.0.0.1:6379> lrem list 6 1
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "zero"
5) "zero"
6) "zero"
127.0.0.1:6379> lrem list 2 zero
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "zero"

ltrim

截取list

命令格式:LTRIM key start stop

127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "zero"
127.0.0.1:6379> ltrim list 0 1
OK
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"

rpoplpush

移除列表最后一个元素,并且lpush到一个新的list

127.0.0.1:6379> lrange list 0 -1
1) "one"
2) "two"
3) "three"
4) "four"
127.0.0.1:6379> rpoplpush list otherlist
"four"
127.0.0.1:6379> lrange otherlist 0 -1
1) "four"

exists & lset

lset:

将列表中指定下标的值替换

先判断这个列表是否存在,如果不存在就会报错

再判断指定的下标是否存在,不存在也会报错

127.0.0.1:6379> exists list2	#判断这个list是否存在
(integer) 0
127.0.0.1:6379> lset list2 0 77	#这个list不存在就报错
(error) ERR no such key
127.0.0.1:6379> lpush list2 1
(integer) 1
127.0.0.1:6379> lset list2 1 77	#如果这个下标不存在也会报错
(error) ERR index out of range
127.0.0.1:6379> lset list2 0 77	#存在就会更新
OK
127.0.0.1:6379> lrange list2 0 -1
1) "77"

linsert

将某个具体的值插入到列表中的指定元素的前面或者后面

127.0.0.1:6379> lrange list2 0 -1
1) "77"
127.0.0.1:6379> linsert list2 before 77 88
(integer) 2
127.0.0.1:6379> lrange list2 0 -1
1) "88"
2) "77"
127.0.0.1:6379> linsert list2 after 77 66
(integer) 3
127.0.0.1:6379> lrange list2 0 -1
1) "88"
2) "77"
3) "66"

Hash

本质就是个k-map结构,可用于存放经常需要改变的对象。string虽然也可以,但是最好的做法是用hash


hset & hget

存放/读取 hash中的某个key

hset也可以在hash中存放多个key,在Redis 4.0.0之后的版本,hmset已被弃用,使用hset即可

127.0.0.1:6379> hset myhash name 77 age 17 address shenzhen
(integer) 3
127.0.0.1:6379> hget myhash name
"77"

hmset & hmget

批量set/get

As per Redis 4.0.0, HMSET is considered deprecated. Please prefer HSET in new code.------源自redis官网

127.0.0.1:6379> hmset myhash name 88 name2 99
OK
127.0.0.1:6379> hmget myhash name name2
1) "88"
2) "99"
127.0.0.1:6379> hsetnx myhash name 00
(integer) 0

hsetnx

当key不存在的时候,设置key

key存在则设置失败

类似于string的setnx,可以用来做分布式

127.0.0.1:6379> hsetnx myhash name 00
(integer) 0
127.0.0.1:6379> hsetnx myhash name3 11
(integer) 1

hgetall

时间复杂度为O(N) ,当数据过多时,应使用hscan来查找

127.0.0.1:6379> hgetall myhash
 1) "name"
 2) "88"
 3) "age"
 4) "17"
 5) "address"
 6) "shenzhen"
 7) "name2"
 8) "99"
 9) "name3"
10) "11"

hscan

参考scan命令

127.0.0.1:6379> hscan myhash 111 match name count 1
1) "0"
2) 1) "name"
   2) "88"

hdel
127.0.0.1:6379> hdel myhash name2 name3
(integer) 2
127.0.0.1:6379> hgetall myhash
1) "name"
2) "88"
3) "age"
4) "17"
5) "address"
6) "shenzhen"

hlen
127.0.0.1:6379> hlen myhash
(integer) 3

hexists
127.0.0.1:6379> hexists myhash name
(integer) 1

hkeys &hvals
127.0.0.1:6379> hkeys myhash
1) "name"
2) "age"
3) "address"
127.0.0.1:6379> hvals myhash
1) "88"
2) "17"
3) "shenzhen"

hincrby
127.0.0.1:6379> hincrby myhash age 1
(integer) 18
127.0.0.1:6379> hget myhash age
"18"

Set

不重复的

应用场景

  • ​ 随机数
  • ​ 求并集、差集、交集(如社交需求)

sadd

批量添加

127.0.0.1:6379> sadd name1 77 88
(integer) 1

smembers

查看set的所有元素,同hget命令,效率较低,推荐用sscan

127.0.0.1:6379> smembers name1
1) "77"
2) "88"

sismember

查看这个元素在set中是否存在

127.0.0.1:6379> sismember name1 99
(integer) 0

scard

查看set中元素总数

127.0.0.1:6379> scard name1
(integer) 2

srem

移除set中的指定元素

127.0.0.1:6379> srem name1 88(integer) 1

srandmember

随机获取set中指定个数的元素

127.0.0.1:6379> srandmember name1 2
1) "77"
2) "88"
127.0.0.1:6379> srandmember name1 2
1) "99"
2) "88""

spop

随机移除set中指定个数的元素

127.0.0.1:6379> spop name1 1
1) "77"
127.0.0.1:6379> smembers name1
1) "88"
2) "99"

smove

把一个set中的指定元素移动到另一个set中

127.0.0.1:6379> sadd set1 11 22 33
(integer) 3
127.0.0.1:6379> sadd set2 99
(integer) 1
127.0.0.1:6379> smove set1 set2 33
(integer) 1
127.0.0.1:6379> smembers set2
1) "33"
2) "99"
127.0.0.1:6379> smembers set1
1) "11"
2) "22"

sdiff & sinter & sunion

sdiff:获取两个set的差集

sinter:获取两个set的交集

sunion:获取两个set的并集

127.0.0.1:6379> smembers set1
1) "11"
2) "22"
3) "33"
127.0.0.1:6379> smembers set2
1) "33"
2) "99"
127.0.0.1:6379> sdiff set1 set2
1) "11"
2) "22"
127.0.0.1:6379> sinter set1 set2
1) "33"
127.0.0.1:6379>  sunion set1 set2
1) "11"
2) "22"
3) "33"
4) "99"

ZSet

有序的set

应用场景:

  • ​ 排序:排行榜、新闻排序啦、姓名排序、电话号码排序
  • ​ 加权排序


zadd & zrange

zadd:添加

zrange:查看set中元素,0 -1就是返回全部,zscan效率较高

127.0.0.1:6379> zadd myzset 0 77 1 88
(integer) 2
127.0.0.1:6379> zrange myzset 0 -1
1) "77"
2) "88"
127.0.0.1:6379>  zadd myzset 5 99 4 10
(integer) 2
127.0.0.1:6379> zrange myzset 0 -1
1) "77"
2) "88"
3) "10"
4) "99"

zrangebyscore & zrevrangebyscore

命令格式

ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]

ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]

zrangebyscore :score按升序在指定区间(两边均可指定>或者≥,<或者≤)内返回,可做分页,可指定返回数量

zrevrangebyscore:score按降序返回

127.0.0.1:6379> zadd myzset 1 one 2 two 3 three
(integer) 3
127.0.0.1:6379> zrange myzset 0 -1
1) "one"
2) "two"
3) "three"
127.0.0.1:6379> zrangebyscore myzset -inf +inf
1) "one"
2) "two"
3) "three"
127.0.0.1:6379> zrangebyscore myzset -inf +inf withscores
1) "one"
2) "1"
3) "two"
4) "2"
5) "three"
6) "3"
127.0.0.1:6379> zrevrangebyscore myzset +inf -inf
1) "three"
2) "two"
3) "one"

zrangebylex & zrevrangebylex

ZRANGEBYLEX key min max [LIMIT offset count]
ZREVRANGEBYLEX key max min [LIMIT offset count]

命令说明,这两个命令的前提是zset中的score权重必须一样

zrangebylex:按字典顺序升序排列 (两边均可指定>或者≥,<或者≤),可指定区间,也可以做分页,指定返回条数

zrevrangebylex:降序

127.0.0.1:6379> zadd myzset 0 a 0 c 0 b
(integer) 3
127.0.0.1:6379>  zrangebylex myzset - +
1) "a"
2) "b"
3) "c"
127.0.0.1:6379> zadd names 0 Toumas 0 Jake 0 Bluetuo 0 Gaodeng 0 Aimini 0 Aidehua
(integer) 6
127.0.0.1:6379> zrevrangebylex names + -
1) "Toumas"
2) "Jake"
3) "Gaodeng"
4) "Bluetuo"
5) "Aimini"
6) "Aidehua"

zrem

移除zset的指定元素

127.0.0.1:6379> zrem myzset zero
(integer) 1

zcard

返回zset中元素总数

127.0.0.1:6379> zcard myzset
(integer) 3

zcount

返回指定score范围的元素个数

127.0.0.1:6379> zcount myzset 1 3
(integer) 3

三种特殊数据结构:


Geospatial

底层实现原理就是zset,因此zset的所有指令都可以使用,可以查看最后面论证

应用场景:

  • 地理位置,存储地理位置信息
  • 计算两地距离
  • 查看附近的人

geoadd

添加

127.0.0.1:6379> geoadd china 121.43333 34.50000 shanghai 117.20000 39.13333 tianjing
(integer) 2

geodist

计算两地的距离,可以指定km、m、mi、ft

127.0.0.1:6379> geodist china beijing shanghai
"748346.9287"

geopos

返回指定城市的经纬度

127.0.0.1:6379> geoadd china 116.41667 39.91667 beijing
(integer) 1
127.0.0.1:6379> geopos china beijing
1) 1) "116.41667157411575317"
   2) "39.91667095273589183"

georadius

以指定经纬度为圆心,指定半径来查找,withcoord可以返回结果的精度 ,withdist返回结果到圆心的距离

127.0.0.1:6379> geoadd china 106.45000 29.56667 chongqing
(integer) 1
127.0.0.1:6379> georadius china 110 30 1000 km
1) "chongqing"
127.0.0.1:6379> georadius china 110 30 1000 km withcoord withdist
1) 1) "chongqing"
   2) "346.0548"
   3) 1) "106.4500012993812561"
      2) "29.56666939001875249"
127.0.0.1:6379> georadius china 110 30 1000 km count 1
1) "chongqing"

georadiusbymember

同上个命令,圆心换做指定的城市

127.0.0.1:6379> georadiusbymember china beijing  1000 km
1) "shanghai"
2) "tianjing"
3) "beijing"
4) "xian"

geohash

返回指定元素的哈希值

127.0.0.1:6379> geohash china beijing
1) "wx4g14s53n0"

用zset命令操作
127.0.0.1:6379> zrange china 0 -1
1) "chongqing"
2) "xian"
3) "shanghai"
4) "tianjing"
5) "beijing"
127.0.0.1:6379> zrem china tianjing
(integer) 1

Hyperloglog

基数统计算法的数据结构,虽然通过其他的数据类型也可以实现,但是Hyperloglog占用内存是固定的,用2^64不同元素的技术,只占用12kb内存。

应用场景:

​ 需要统计不重复的数据,比如统计网站的UV

​ 有0.81%错误率


pfadd

添加元素

127.0.0.1:6379> pfadd test 0 1 1 2 3 3
(integer) 1

pfcount

查看不重复的元素总数

127.0.0.1:6379> pfcount test
(integer) 4

pfmerge

将test跟test2合并到test3

127.0.0.1:6379> pfmerge test3 test test2
OK
127.0.0.1:6379> pfcount test3
(integer) 6

Bitmaps

位存储,位图,是种数据结构。都是二进制位,就只有0跟1两种状态,所以非常节省空间、高效率。

应用场景:

​ 统计只有两种状态的场景:用户信息(活跃/不活跃),用户登陆/未登录


setbit

set

127.0.0.1:6379> setbit mybitmap 0 0
(integer) 0
127.0.0.1:6379> setbit mybitmap 1 0
(integer) 0
127.0.0.1:6379> setbit mybitmap 2 1
(integer) 0
127.0.0.1:6379> setbit mybitmap 3 1
(integer) 0
127.0.0.1:6379> setbit mybitmap 4 0
(integer) 0

getbit

获取bitmap中指定偏移量的值

127.0.0.1:6379> getbit mybitmap 3
(integer) 1

bitcount

统计

127.0.0.1:6379> bitcount mybitmap
(integer) 2

事务

redis事务的本质:一组命令的集合,一个事务中的所有命令都会被序列化。在事务执行的过程中,按顺序执行,其他客户端提交的命令请求不会插入到事务执行命令序列中。

redis的事务没有mysql隔离级别的概念,也就不存在脏读、幻度之类

总结redis的事务:一次性、顺序性、排他性的执行一系列的命令

redis执行事务的流程:

  • 开启事务:multi

  • 一系列命令入队(这时,这些命令并没有被直接执行,要在发起执行命令的时候才会被执行)

  • 执行事务/撤销事务 exec

    redis的单条命令保证原子性,但是redis的事务并不保证原子性!(见运行时异常)


正常执行(multi-xxx-exec)

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set key1 77
QUEUED
127.0.0.1:6379(TX)> set key2 88
QUEUED
127.0.0.1:6379(TX)> set key3 99
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) OK
3) OK
127.0.0.1:6379> keys *
1) "key2"
2) "key1"
3) "key3"

放弃事务(discard)

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set key1 11
QUEUED
127.0.0.1:6379(TX)> set key2 22
QUEUED
127.0.0.1:6379(TX)> set key3 33
QUEUED
127.0.0.1:6379(TX)> discard
OK
127.0.0.1:6379> get key3
(nil)

乐观锁(watch & unwatch)

​ 悲观锁

​ 很悲观,无论什么时候都加锁。比如java的synchronized

​ 乐观锁

​ 很乐观,认为什么时候都不会有问题,所以就不会加锁,在更新的时候判断下,是否有人在这个期间更改过数据

​ java:cas

​ mysql:存的时候比较下version

​ redis:watch


watch & unwatch

watch :

监视key ,如果在事务执行之前,该key 被其他命令所改动,那么事务将被打断。

可以被用来做乐观锁

unwatch:

取消watch对key的监视

==========================窗口1模拟线程1==========================
127.0.0.1:6379> set account1 1000
OK
127.0.0.1:6379> set account2 10
OK
127.0.0.1:6379> watch account1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby account1 200
QUEUED
127.0.0.1:6379(TX)> incrby account2 100
QUEUED
127.0.0.1:6379(TX)> exec	#这里执行的时候,account1的值被修改了,事务就执行失败了
(nil)
127.0.0.1:6379> get account1	#account1的值还是线程2赋值的900
"900"
==========================窗口2模拟线程2==========================
127.0.0.1:6379> set account1 900(在执行事务中重新给account1赋值)
OK

异常


编译型异常(代码有问题/命令有错)

事务内的所有命令都不会被执行

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> setget k1 v2	#这条命令有误
(error) ERR unknown command `setget`, with args beginning with: `k1`, `v2`,
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.

运行时异常(语法有错)

错误的那条命令不会被执行,并抛出异常

其余的命令依旧会被执行。所以说redis的事务不保证原子性

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set key1 value1
QUEUED
127.0.0.1:6379(TX)> set key2 value2 key3 value3	#这条语法有误
QUEUED
127.0.0.1:6379(TX)> set key4 value4
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (error) ERR syntax error
3) OK

jedis

测试:

pom文件引入jedis:

 <!--jedis-->
        <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>

编写测试类

     @Test
    public void connectTest(){
        Jedis jedis=new Jedis("127.0.0.1",6379);
        System.out.println(jedis.ping());
    }

运行:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zMa2dtKK-1631719421534)(image-20210913155114126.png)]

api基本跟redis的一致,这里不再过多重复


jedis实现事务

 @Test
    public void lock() {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        Transaction multi = jedis.multi();
        try {
            //业务代码
            multi.set("key1", "77");
            multi.set("key2", "88");
            int i = 1 / 0;//制造个bug
            multi.exec();
            System.out.println(jedis.mget("key1", "key2"));
        } catch (Exception e) {
            //放弃事务
            multi.discard();
            //异常处理
            e.printStackTrace();
        } finally {
            //关闭链接
            jedis.close();
        }
    }

运行后查看redis:

127.0.0.1:6379> dbsize
(integer) 0

springboot 整合

SpringBoot操作数据库:spring-data、jpa、jdbc、mongodb、redis

springboot2.x以后的版本,使用的不再是jedis,而是lettuce

jedis:采用直连,多个线程操作是不安全的。如果想要避免,要使用jedis pool,类似BIO

lettuce:采用netty,实例key在多个线程*享,不存在线程不安全的情况。减少线程数据,更像NIO

测试:

pom文件引入:

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

在springboot自动配置jar包的spring.factories(springboot的工厂加载机制)中查找spring-boot-start-redis相关配置类:

org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\

接下来去查看RedisAutoConfiguration:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

	@Bean
	@ConditionalOnMissingBean(name = "redisTemplate")
	@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    //从我们可以定制自己的RedisTemplate
	public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
		RedisTemplate<Object, Object> template = new RedisTemplate<>();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

	@Bean
	@ConditionalOnMissingBean
	@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
	public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
		StringRedisTemplate template = new StringRedisTemplate();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

}

查看RedisProperties配置文件属性类:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uGuuxLl0-1631719421537)(image-20210913170417095.png)]

那么我们就知道了,配置文件中,这个jar包的配置属性前缀是spring.redis。如:

spring.redis.host=127.0.0.1
spring.redis.database=3

写测试代码:

@SpringBootTest
public class RedisTest {
    @Autowired
    private RedisTemplate redisTemplate;
    @Test
    public void connectTest(){
        redisTemplate.opsForValue().set("name","77");
        redisTemplate.opsForValue().set("age","17");
        System.out.println(redisTemplate.opsForValue().get("name"));
    }
}

RestTemplate

序列化


redis发布订阅

命令测试:

消息订阅端:

127.0.0.1:6379> subscribe channel1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel1"
3) (integer) 1
1) "message"
2) "channel1"
3) "hello"

消息发送端:

127.0.0.1:6379> publish channel1 hello
(integer) 1

原理:

​ Redis是通过C实现的,Redis通过PUBLISH、SUBSCRIBE、PSUBSCRIBE等命令来实现发布与订阅功能。

通过SUBSCIBE命令订阅某个频道后,redis-service里会维护一个字典,字典的键就是一个个channel,而字典的值则是一个链表。链表中保存了所有订阅这个channel的客户端,SUBSCRIBE命令的关键,就是将客户端添加到给定的channel订阅链中。

通过PUBLISH命令向订阅者发送消息,redis-server会使用给定的频道作为键,在它所维护的channel字典中,查找记录了订阅这个频道的链表,遍历,将消息发布给所有的订阅者。

应用场景:

​ 实时消息系统

​ 订阅、关注系统

上一篇:NOSQL的Redis的基础


下一篇:NoSQL之redis配置与基础命令