Redis安装
- 百度一下,你就知道
Redis性能测试(redis-benchmark)
测试:本机100个并发连接,100000个请求
./redis-benchmark -h localhost -p 6379 -c 100 -n 100000
Redis基础知识
- redis默认有16个数据库,我们从配置文件中可以看到。我们默认使用的是第0个数据库
- 可以使用
select
命令切换数据库
127.0.0.1:6379> select 3
OK
127.0.0.1:6379[3]>
- 可以使用
dbsize
查看数据库大小
127.0.0.1:6379[3]> dbsize
(integer) 0
127.0.0.1:6379[3]> set name qxf
OK
127.0.0.1:6379[3]> dbsize
(integer) 1
- 可以使用
keys *
查看所有的key
127.0.0.1:6379[3]> keys *
1) "name"
- 可以使用
flushall
清空所有数据库,flushdb
清空当前库
127.0.0.1:6379[3]> select 0
OK
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> select 3
OK
127.0.0.1:6379[3]> keys *
(empty array)
127.0.0.1:6379[1]> select 0
OK
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> keys *
1) "k1"
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> keys *
(empty array)
- redis默认端口号是6379
- redis是单线程的,是基于内存操作的,cpu不是redis的性能瓶颈,redis的瓶颈是机器的内存和网络带宽
- redis是C语言开发的,官方提高的数据为100000+的QPS,完全不比Memecache差
- redis是将所有数据全部放在内存中的,所以说使用单线程操作效率就是最高的,相比于多线程,少了上下文的切换耗时的操作,多次读写都是在一个cpu上的
RedisKey的基本命令
- 使用
exists
可以查看redis中存储的key是否存在
127.0.0.1:6379> set name qxf
OK
127.0.0.1:6379> keys *
1) "name"
127.0.0.1:6379> set age 1
OK
127.0.0.1:6379> keys *
1) "name"
2) "age"
127.0.0.1:6379> exists name
(integer) 1
127.0.0.1:6379> exists name1
(integer) 0
- 使用
move
命令可以将当前数据库的key值移动到指定数据库中,比如下面将name移动到第1个数据库中
127.0.0.1:6379> keys *
1) "name"
2) "age"
127.0.0.1:6379> move name 1
(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]> get name
"qxf"
127.0.0.1:6379[1]> select 0
OK
127.0.0.1:6379> keys *
1) "age"
- 使用
expire
命令可以设置过期时间,使用ttl
可以查看key的剩余过期时间,比如下面设置age的10秒钟以后过期
127.0.0.1:6379> EXPIRE age 10
(integer) 1
127.0.0.1:6379> ttl age
(integer) 7
127.0.0.1:6379> ttl age
(integer) 5
127.0.0.1:6379> ttl age
(integer) 5
127.0.0.1:6379> ttl age
(integer) 4
127.0.0.1:6379> ttl age
(integer) 3
127.0.0.1:6379> ttl age
(integer) 2
127.0.0.1:6379> ttl age
(integer) 1
127.0.0.1:6379> ttl age
(integer) -2
127.0.0.1:6379> ttl age
(integer) -2
127.0.0.1:6379> get age
(nil)
- 使用
type
查看key的类型
127.0.0.1:6379> set name qxf
OK
127.0.0.1:6379> TYPE name
string
String类型
- 使用
append
命令可以给字符串后面添加值,如果当前key不存在,那么就会新建一个key
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> append k1 v2
(integer) 4
127.0.0.1:6379> get k1
"v1v2"
- 使用
strlen
可以查看字符串的长度
127.0.0.1:6379> STRLEN k1
(integer) 4
- 使用
incr
可以自增加一,使用decr
可以自减一
127.0.0.1:6379> set views 0
OK
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> INCR views
(integer) 1
127.0.0.1:6379> INCR views
(integer) 2
127.0.0.1:6379> INCR views
(integer) 3
127.0.0.1:6379> DECR views
(integer) 2
127.0.0.1:6379> DECR views
(integer) 1
127.0.0.1:6379> DECR views
(integer) 0
- 使用
incrby
可以指定加多少,使用decrby
可以指定减多少
127.0.0.1:6379> INCRBY views 10
(integer) 10
127.0.0.1:6379> get views
"10"
127.0.0.1:6379> DECRBY views 5
(integer) 5
127.0.0.1:6379> get views
"5"
- 使用
getrange
可以获取指定索引长度的字符串
127.0.0.1:6379> set k1 "hello,everyone"
OK
127.0.0.1:6379> get k1
"hello,everyone"
127.0.0.1:6379> GETRANGE k1 0 3 # 获取[0,3]索引之间的字符串
"hell"
127.0.0.1:6379> GETRANGE k1 0 -1 # 获取整个字符串
"hello,everyone"
- 使用
setrange
可以设置索引后的值
127.0.0.1:6379> set k2 abcdefg
OK
127.0.0.1:6379> get k2
"abcdefg"
127.0.0.1:6379> SETRANGE k2 1 xx # 设置k2第1个索引后的值为xx
(integer) 7
127.0.0.1:6379> get k2
"axxdefg"
- 使用
setex
可以设置过期时间并且设置值,setnx
如果不存在则设置值,如果存在那么就不设置
127.0.0.1:6379> SETEX k3 30 hello # 设置k3的值为hello,30秒以后过期
OK
127.0.0.1:6379> ttl k3
(integer) 25
127.0.0.1:6379> SETNX mykey redis # 如果mykey不存在,则创建mykey
(integer) 1 # 返回1代表成功
127.0.0.1:6379> SETNX mykey redis # 如果mykey存在,则创建失败
(integer) 0 # 返回0代码不成功
- 使用
mset
可以设置多个key和value,使用mget
可以获取多个值
127.0.0.1:6379> MSET k1 v1 k2 v2 k3 v3
OK
127.0.0.1:6379> keys *
1) "k1"
2) "k3"
3) "k2"
127.0.0.1:6379> mget k1 k2 k3
1) "v1"
2) "v2"
3) "v3"
- 使用
msetnx
可以批量设置多个值,如果不存在,则创建,如果存在,则创建失败,而且它是一个原子性操作,比如下面的命令,我们的k1已经存在,k4不存在,但是还是创建失败
127.0.0.1:6379> msetnx k1 v1 k4 v4 # 要么一起成功,要么一起失败
(integer) 0
127.0.0.1:6379> get k4
(nil)
mset
的高阶用法
# user:{id}:{filed},如此设计在redis中是完全可以的
127.0.0.1:6379> mset user:1:name zhangsan user:1:age 2
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "zhangsan"
2) "2"
- 使用
getset
可以先get再set
127.0.0.1:6379> getset db redis
(nil) # 如果不存在,则返回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"
- String的value除了是字符串,还可以是数字,它的用途:
- 计数器
- 统计多单位的数量
- 粉丝数
- 对象缓存存储
List(集合)
- redis里面,可以把List当成栈,队列,阻塞队列各种数据结构
- 所有的List命令都是以L开头的
- 使用
lpush
可以给List里面从左边添加值
127.0.0.1:6379> LPUSH list 1
(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
1) "3"
2) "2"
3) "1"
127.0.0.1:6379> LRANGE list 0 1
1) "3"
2) "2"
- 使用
rpush
可以给List里面从右边添加值
127.0.0.1:6379> RPUSH list2 1
(integer) 1
127.0.0.1:6379> RPUSH list2 2
(integer) 2
127.0.0.1:6379> RPUSH list2 3
(integer) 3
127.0.0.1:6379> LRANGE list2 0 -1
1) "1"
2) "2"
3) "3"
- 使用
lpop
可以从移除第一个元素,使用可以从移除最后一个元素,或者可以理解为:lpop可以从左边移除第一个元素,rpop可以从右边移除第一个元素
127.0.0.1:6379> LRANGE list 0 -1
1) "3"
2) "2"
3) "1"
127.0.0.1:6379> LPOP list
"3"
127.0.0.1:6379> rpop list
"1"
- 使用
lindex
可以获取指定下标索引的值
127.0.0.1:6379> lrange list2 0 -1
1) "1"
2) "2"
3) "3"
127.0.0.1:6379> LINDEX list2 1
"2"
- 使用
llen
可以查看List的长度
127.0.0.1:6379> LLEN list2
(integer) 3
- 使用
LREM
可以移除指定的值
127.0.0.1:6379> lrange list2 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "3"
127.0.0.1:6379> LREM list2 1 1 # 移除list2中的一个1
(integer) 1
127.0.0.1:6379> lrange list2 0 -1
1) "2"
2) "3"
3) "4"
4) "3"
127.0.0.1:6379> LREM list2 2 3 # 移除list2中的2个3
(integer) 2
127.0.0.1:6379> lrange list2 0 -1
1) "2"
2) "4"
- 使用
ltrim
可以截取List,截取以后,原本的List已经被修改了
127.0.0.1:6379> RPUSH list hello
(integer) 1
127.0.0.1:6379> RPUSH list hello1
(integer) 2
127.0.0.1:6379> RPUSH list hello2
(integer) 3
127.0.0.1:6379> RPUSH list hello3
(integer) 4
127.0.0.1:6379> LTRIM list 1 2 # 截取list的[1,2]
OK
127.0.0.1:6379> LRANGE list 0 -1
1) "hello1"
2) "hello2"
rpoppush
可以移除List中的最后一个元素,然后移动到新的列表中
127.0.0.1:6379> RPUSH list hello
(integer) 1
127.0.0.1:6379> RPUSH list hello1
(integer) 2
127.0.0.1:6379> RPUSH list hello2
(integer) 3
127.0.0.1:6379> RPUSH list hello3
(integer) 4
127.0.0.1:6379> RPOPLPUSH list otherlist
"hello3"
127.0.0.1:6379> LRANGE list 0 -1
1) "hello"
2) "hello1"
3) "hello2"
127.0.0.1:6379> LRANGE otherlist 0 -1
1) "hello3"
- 使用
exists
可以判断List是否存在,使用lset
可以修改List的某个值,但是如果List不存在,那么就会报错
127.0.0.1:6379> exists list # 判断list集合是否存在
(integer) 0
127.0.0.1:6379> lset list 0 item # 设置list集合的第0个索引的值为item
(error) ERR no such key # 因为list不存在,所以会报错
127.0.0.1:6379> lpush list qaq
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "qaq"
127.0.0.1:6379> lset list 0 item
OK
127.0.0.1:6379> lrange list 0 -1
1) "item"
- 使用
linsert
可以给List指定元素的前/后插入值
127.0.0.1:6379> rpush list hello
(integer) 1
127.0.0.1:6379> rpush list world
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "hello"
2) "world"
127.0.0.1:6379> LINSERT list before world other # 给world的前面插入other
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "hello"
2) "other"
3) "world"
127.0.0.1:6379> LINSERT list after world new # 给world的后面插入new
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "hello"
2) "other"
3) "world"
4) "new"
Set(集合)
set中的值是不能重复的
- 使用SADD可以给set集合添加值,如果添加失败,就会返回0
127.0.0.1:6379> SADD myset "hello"
(integer) 1
127.0.0.1:6379> SADD myset "hello"
(integer) 0
- SISMEMBER可以查看set集合
127.0.0.1:6379> SADD myset "hello2"
(integer) 1
127.0.0.1:6379> SADD myset "hello3"
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "hello2"
2) "hello1"
3) "hello"
4) "hello3"
- SISMEMBER可以判断当前值是否存在于集合中
127.0.0.1:6379> SISMEMBER myset "hello"
(integer) 1
127.0.0.1:6379> SISMEMBER myset "qxf"
(integer) 0
- SCARD可以查看set集合的长度
127.0.0.1:6379> SCARD myset
(integer) 5
- SREM可以删除set集合中的元素
127.0.0.1:6379> SREM myset "hello3"
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "qaq"
2) "hello2"
3) "hello"
4) "hello1"
- SRANDMEMBER可以随机选取几个元素
127.0.0.1:6379> SRANDMEMBER myset 2
1) "hello1"
2) "hello2"
127.0.0.1:6379> SRANDMEMBER myset 2
1) "hello1"
2) "hello"
127.0.0.1:6379> SRANDMEMBER myset 2
1) "hello1"
2) "hello"
127.0.0.1:6379> SRANDMEMBER myset 2
1) "hello1"
2) "hello2"
- SPOP可以随机删除一个元素
127.0.0.1:6379> SPOP myset
"qaq"
127.0.0.1:6379> SMEMBERS myset
1) "hello2"
2) "hello"
3) "hello1"
127.0.0.1:6379> SPOP myset
"hello1"
127.0.0.1:6379> SMEMBERS myset
1) "hello2"
2) "hello"
- SMOVE可以移动一个元素到另一个set集合
127.0.0.1:6379> SADD myset "hello"
(integer) 1
127.0.0.1:6379> SADD myset "hello1"
(integer) 1
127.0.0.1:6379> SADD myset "hello2"
(integer) 1
127.0.0.1:6379> SADD myset "hello3"
(integer) 1
127.0.0.1:6379> SMOVE myset myset2 "hello2"
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "hello1"
2) "hello"
3) "hello3"
127.0.0.1:6379> SMEMBERS myset2
1) "hello2"
- SDIFF可以取两个集合的差集,SINTER可以取两个集合的交集,SUNION可以取两个集合的并集
127.0.0.1:6379> sadd myset 1
(integer) 1
127.0.0.1:6379> sadd myset 2
(integer) 1
127.0.0.1:6379> sadd myset 3
(integer) 1
127.0.0.1:6379> sadd myset2 3
(integer) 1
127.0.0.1:6379> sadd myset2 4
(integer) 1
127.0.0.1:6379> sadd myset2 5
(integer) 1
127.0.0.1:6379> SDIFF myset myset2 # 取差集
1) "1"
2) "2"
127.0.0.1:6379> SINTER myset myset2 # 取交集,共同好友,共同爱好就可以使用这个实现
1) "3"
127.0.0.1:6379> SUNION myset myset2 # 取并集
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
Hash
Map集合,key-value集合,这个时候,值是一个map集合,本质和String类型没有太大区别,还是一个简单的key-value
- HSET可以给Hash设置值
HSET myhash k1 v1 k2 v2
(integer) 2
- HGET可以获取Hash的value值
127.0.0.1:6379> HGET myhash k2
"v2"
127.0.0.1:6379> HGET myhash k1
"v1"
- HMSET可以设置多个值,HMGET可以同时获得多个值,HGETALL可以获取所有值
127.0.0.1:6379> HMSET my k1 v1 k2 v2
OK
127.0.0.1:6379> HMGET my k1 k2
1) "v1"
2) "v2"
127.0.0.1:6379> HGETALL my
1) "k1"
2) "v1"
3) "k2"
4) "v2"
- HDEL可以删除指定的key值
127.0.0.1:6379> HDEL myhash my k1
(integer) 1
127.0.0.1:6379> HGETALL myhash
1) "k2"
2) "v2"
- HLEN可以获取长度
127.0.0.1:6379> HLEN my
(integer) 2
- HEXISTS判断指定字段是否存在
127.0.0.1:6379> hmset myhash k1 v1 k2 v2 k3 v3
OK
127.0.0.1:6379> HGETALL myhash
1) "k1"
2) "v1"
3) "k2"
4) "v2"
5) "k3"
6) "v3"
127.0.0.1:6379> HEXISTS myhash k2
(integer) 1
127.0.0.1:6379> HEXISTS myhash k5
(integer) 0
- HKEYS可以获取所有的key值,HVALS可以获取所有的value值
127.0.0.1:6379> HKEYS myhash
1) "k1"
2) "k2"
3) "k3"
127.0.0.1:6379> HVALS myhash
1) "v1"
2) "v2"
3) "v3"
- HINCRBY可以使值自增或者自减
127.0.0.1:6379> hset my k1 5
(integer) 1
127.0.0.1:6379> HINCRBY my k1 3
(integer) 8
127.0.0.1:6379> HINCRBY my k1 -1
(integer) 7
- hash的高阶用法,hash更加适合存储对象,String适合字符串存储
127.0.0.1:6379> hset user:1 name qxf
(integer) 1
127.0.0.1:6379> HGET user:1 name
"qxf"
127.0.0.1:6379> hset goods:food name bread
(integer) 1
127.0.0.1:6379> hget goods:food name
"bread"
Zset(有序集合)
在Set的基础上,增加了一个值
- ZADD可以添加值,但是每次添加值需要添加一个序号
127.0.0.1:6379> ZADD myset 1 one 2 two 3 three
(integer) 3
- ZRANGE可以查看set集合
127.0.0.1:6379> ZRANGE myset 0 -1
1) "one"
2) "two"
3) "three"
- ZRANGEBYSCORE可以降序排序集合
127.0.0.1:6379> ZADD salary 2500 xiaohong 5000 zhangsan 500 qxf
(integer) 3
127.0.0.1:6379> ZRANGE salary 0 -1
1) "qxf"
2) "xiaohong"
3) "zhangsan"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf # 从正无穷到负无穷显示salary
1) "qxf"
2) "xiaohong"
3) "zhangsan"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf withscores # 从正无穷到负无穷带上得分显示salary
1) "qxf"
2) "500"
3) "xiaohong"
4) "2500"
5) "zhangsan"
6) "5000"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf 2500 withscores # 从正无穷到2500带上得分显示salary
1) "qxf"
2) "500"
3) "xiaohong"
4) "2500"
127.0.0.1:6379> ZRANGEBYSCORE salary 0 1000 withscores # 从0到1000带上得分显示salary
1) "qxf"
2) "500"
- ZREVRANGE可以升序排列集合
127.0.0.1:6379> ZREVRANGE salary 0 -1 withscores
1) "zhangsan"
2) "5000"
3) "qxf"
4) "500"
- ZREM可以删除一个元素
127.0.0.1:6379> ZREM salary xiaohong
(integer) 1
127.0.0.1:6379> zrange salary 0 -1
1) "qxf"
2) "zhangsan"
- ZCARD可以查看集合的长度
127.0.0.1:6379> ZCARD salary
(integer) 2
- ZCOUNT可以获取某个区间内的成员数量
127.0.0.1:6379> ZCOUNT salary -inf +inf
(integer) 2
- Zset可以排序,用于做排行榜或者排序表
geospatial(地理位置)
- 用于存储经纬度信息,可以实现附近的人,直线距离计算等
- 在Redis3.2版本以后就推出了,可以计算方圆几里的人
- GEOADD可以添加地理位置
127.0.0.1:6379> GEOADD china:city 109.247 34.33 xian # 增加西安的经纬度
(integer) 1
127.0.0.1:6379> GEOADD china:city 116.23 40.22 beijing # 增加北京的经纬度
(integer) 1
127.0.0.1:6379> GEOADD china:city 113.88 22.55 shenzhen # 增加深圳的经纬度
(integer) 1
127.0.0.1:6379> GEOADD china:city 121.48 31.40 shanghai # 增加上海的经纬度
(integer) 1
- GEOPOS可以获得指定key的经纬度
127.0.0.1:6379> GEOPOS china:city shanghai # 查看上海的经纬度
1) "121.48000091314315796"
2) "31.40000025319353938"
- GEODIST可以获取两个经纬度之间的距离
127.0.0.1:6379> GEODIST china:city shanghai beijing km # 查看上海到北京的直线距离
"1088.7854"
- GEORADIUS可以查看以当前经纬度为圆心,附近半径内的经纬度
127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km # 查看以经纬度为110 30半径1000km的城市
1) "xian"
2) "shenzhen"
127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km withdist # 查看以经纬度为110 30半径1000km的城市的距离
1) 1) "xian"
2) "486.7952"
2) 1) "shenzhen"
2) "914.3335"
127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km withcoord # 查看以经纬度为110 30半径1000km的城市的经纬度
1) 1) "xian"
2) 1) "109.246998131275177"
2) "34.33000103844675976"
2) 1) "shenzhen"
2) 1) "113.87999922037124634"
2) "22.5500010475923105"
127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km withcoord count 2 #查看以经纬度为110 30半径1000km的城市(展示2个)
1) 1) "xian"
2) 1) "109.246998131275177"
2) "34.33000103844675976"
2) 1) "shenzhen"
2) 1) "113.87999922037124634"
2) "22.5500010475923105"
- GEORADIUSBYMEMBER可以找出指定key值周围的城市
127.0.0.1:6379> GEORADIUSBYMEMBER china:city xian 1000 km
1) "xian"
2) "beijing"
- GEO的底层实现原理就是ZSet,我们可以使用ZSet命令来操作GEO
127.0.0.1:6379> ZRANGE china:city 0 -1 # 查看地图中全部元素
1) "xian"
2) "shenzhen"
3) "shanghai"
4) "beijing"
127.0.0.1:6379> zrem china:city beijing # 删除指定元素
(integer) 1
127.0.0.1:6379> ZRANGE china:city 0 -1
1) "xian"
2) "shenzhen"
3) "shanghai"
Hyperloglog(基数统计)
- 基数=不重复的元素
- 主要用于做基数统计的算法
- 比如可以统计网站的UV(一个人多次访问同一个网站,但是还是算做同一个人)
- 传统的方式是使用set集合保存用户的id,如果保存大量的用户id,就会比较麻烦,我们的目的是为了计数,而不是为了保存用户id
- 优点:占用内存是固定的,2^64不同的元素,只需要12kb的内存,如果要从内存角度来比较的话,Hyperloglog就是我们的首选
- 缺点:存在0.81%的错误率,在一定范围内的错误率,我们还是可以接受的,如果不能接受的话,建议使用ZSet
- PFADD可以添加元素
127.0.0.1:6379> PFADD userid a b c d e f g h i j k
(integer) 1
- PFCOUNT可以统计元素(不重复的)
127.0.0.1:6379> PFCOUNT userid
(integer) 11
127.0.0.1:6379> PFADD userid2 a a a a b
(integer) 1
127.0.0.1:6379> PFCOUNT userid2
(integer) 2
- PFMERGE可以合并
127.0.0.1:6379> PFMERGE userid3 userid userid2
OK
127.0.0.1:6379> PFCOUNT userid3
(integer) 11
Bitmaps(位图)
- 位存储指的就是 0 1存储
- 可以用来统计用户信息(活跃,不活跃;登录,未登录;打卡,未打卡),只有两个状态
- 所有操作都是操作二进制位进行记录,就只有0和1两个状态
- SETBIT可以设置值(比如我们用Bitmaps来记录打卡记录)
127.0.0.1:6379> SETBIT sign 0 1
(integer) 0
127.0.0.1:6379> SETBIT sign 1 0
(integer) 0
127.0.0.1:6379> SETBIT sign 2 0
(integer) 0
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 1
(integer) 0
127.0.0.1:6379> SETBIT sign 6 1
(integer) 0
- GETBIT可以查看某一个key里面的状态
127.0.0.1:6379> GETBIT sign 3
(integer) 1
127.0.0.1:6379> GETBIT sign 0
(integer) 1
- BITCOUNT可以统计为1的个数
127.0.0.1:6379> BITCOUNT sign
(integer) 5
redis事务
Redis单条命令能够确保原子性,但是事务不保证原子性~
Redis事务的本质:一组命令一起执行!一个事务中的所有命令都会被序列化,在事务执行的过程中,会按照顺序执行
使用事务的方式:
1,开启事务(multi
)
2,执行事务(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
中断事务的例子:
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)> DISCARD # 中断事务
OK
127.0.0.1:6379> get k1
(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)> setget k3 v3 # 这是一条错误的命令!
(error) ERR unknown command `setget`, with args beginning with: `k3`, `v3`,
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
事务中如果执行的时候有异常,那么事务就会跳过当前命令,执行其他的命令
例如:我们在事务中给一个字符串类型的值进行加1操作,虽然执行命令的时候没有报错,但是提交事务的时候,这个加1操作会抛出异常,那么这个加1的命令就不会执行,而是会执行其他可以执行成功的命令
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set k1 "k1"
QUEUED
127.0.0.1:6379(TX)> INCR k1 # 提交事务的时候会抛出异常!
QUEUED
127.0.0.1:6379(TX)> EXEC
1) OK
2) (error) ERR value is not an integer or out of range
127.0.0.1:6379> get k1
"k1"
Redis实现乐观锁
悲观锁
- 认为什么时候都会出现问题,无论做什么操作都会加锁!
乐观锁
- 很乐观,认为什么时候都不会出现问题,所以不会上锁,只有更新数据的时候去判断一下,在此期间是否有人修改过这个数据,可以使用version去判断,如果version改变了,那么就会重新去获取最新的数据
redis里面有一个命令关键字watch
,可以用来监控我们要锁住的对象或者属性
下面是没有出现任何问题时候的乐观锁:
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> watch money # 监视money
OK
127.0.0.1:6379>
127.0.0.1:6379>
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> DECRBY money 20
QUEUED
127.0.0.1:6379(TX)> EXEC # 没有问题,事务正常结束,期间的money没有发生变动
1) (integer) 80
下面我们来模拟一下多个客户端操作同一个watch
的对象,看看会发生什么~
首先第一个客户端,给money变量添加watch
,在事务中修改完money变量以后,我们没有直接提交事务,而是等待另一个客户端获取到money变量,然后修改money变量的值,然后我们第一个客户端再提交事务
客户端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 # 执行完这一步,我们没有直接提交事务,而是等待客户端二修改money的值
QUEUED
127.0.0.1:6379(TX)> exec # 等待第2个客户端修改完money的值以后,我们再提交事务,会发现返回的是一个空值,也就说明我们的事务没有执行成功
(nil)
127.0.0.1:6379> get money
"1000"
客户端2
127.0.0.1:6379> get money
"80"
127.0.0.1:6379> set money 1000 # 我们在这个地方修改money的值
OK
127.0.0.1:6379> get money
"1000"
那么如果遇到这种事务执行失败的情况,我们应该怎么办?我们可以先执行unwatch
,放弃监视这个变量,然后再重新watch
这个变量,再重新执行我们的事务,就可以执行成功了
127.0.0.1:6379> UNWATCH # 首先放弃监视
OK
127.0.0.1:6379> watch money # 重新开始监视
OK
127.0.0.1:6379> MULTI # 开启事务
OK
127.0.0.1:6379(TX)> DECR money
QUEUED
127.0.0.1:6379(TX)> EXEC # 对比version是否发生改变,如果没有发生改变,那么就执行事务,否则就返回空
1) (integer) 999
Jedis
jedis
是官方推荐的java连接开发工具,使用java操作redis的中间件
PS:maven的官方仓库:https://mvnrepository.com/tags/maven
第一步:导入jedis的依赖包:
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
第二步:编码测试
如果远程连接不上jedis,那么可以参考下面的链接:
https://www.cnblogs.com/kevin860/p/11364358.html
- 连接数据库
- 操作命令
- 断开连接
public static void main(String[] args) {
// 连接数据库
Jedis jedis = new Jedis("127.0.0.1", 6379);
// 执行命令
System.out.println(jedis.ping());
jedis.set("k1", "v1");
System.out.println(jedis.get("k1"));
// 强制关闭jedis
jedis.shutdown();
}
redis执行事务:
public static void main(String[] args) {
// 连接数据库
Jedis jedis = new Jedis("127.0.0.1", 6379);
// 构造数据
JSONObject jsonObject = new JSONObject();
jsonObject.put("hello", "world");
jsonObject.put("name", "qxf");
// 开启事务
Transaction transaction = jedis.multi();
String result = jsonObject.toJSONString();
try {
// 执行命令
transaction.set("user1", result);
transaction.set("user2", result);
// 提交事务
transaction.exec();
} catch (Exception e) {
// 如果发生异常,那么事务回滚
transaction.discard();
e.printStackTrace();
} finally {
// 打印当前值
System.out.println(jedis.get("user1"));
System.out.println(jedis.get("user2"));
// 关闭客户端
jedis.close();
}
}
执行结果:
Springboot整合redis
- 在SpringBoot2.x之后,原来的jedis被替换为了lettuce
- jedis:底层采用的是直连,如果有多个线程操作的话,是不安全的,如果想要避免不安全的,使用jedis pool连接池,如果线程数量太多,就会导致服务端负载太大,相当于BIO
- lettuce:底层使用netty,实例可以在多个线程中进行共享,不存在线程不安全的情况,可以减少线程的数量,更像NIO模式
- RedisTemplate我们可以自定义,因为
@ConditionalOnMissingBean(name = "redisTemplate")
就是如果redisTemplate不存在,下面的配置才会生效
默认的RedisTemplate没有过多的设置,redis对象都是需要序列化的,两个泛型都是Object类型,我们使用的时候都需要强制转换
上面的这个是针对于String类型的操作
-
我们可以看到Redis默认的连接工厂有两个:Jedis和Lettuce
-
使用方式:
- 导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 配置文件
spring.redis.host=106.55.170.35
spring.redis.port=6379
- 连接测试
@SpringBootTest
class RedisDemoApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
redisTemplate.opsForValue().set("qxf", "101");
System.out.println(redisTemplate.opsForValue().get("qxf"));
}
}
运行结果:
我们通过命令行查询发行redis数据库中会乱码:
原因是这样的:
- 默认的序列化配置是jdk自带的序列化配置,我们可以定义一个User类,然后我们写一个测试类看下:
package com.demo.pojo;
/**
* @author qxf101
*/
public class User{
private String name;
private int age;
public User() {
}
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
然后我们写一个测试类测试序列化:
@Test
public void test() throws JsonProcessingException {
User user = new User();
user.setName("qxf");
user.setAge(27);
String jsonUser = new ObjectMapper().writeValueAsString(user);
redisTemplate.opsForValue().set("user", jsonUser);
System.out.println(redisTemplate.opsForValue().get("user"));
}
运行结果:
如果我们不序列化,我们可以看一下结果:
@Test
public void test() throws JsonProcessingException {
User user = new User();
user.setName("qxf");
user.setAge(27);
redisTemplate.opsForValue().set("user", user);
System.out.println(redisTemplate.opsForValue().get("user"));
}
我们发现就会报错,提示无法序列化,而且需要序列化以后才可以
- 我们也可以自己编写redisTemplate,如下:
@Configuration
public class RedisConfig {
/**
* 编写我们自己的redisTemplate
*
* @param factory redis连接工厂
* @return redisTemplate
*/
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(objectMapper);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
template.setValueSerializer(serializer);
template.setHashKeySerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
那我们的测试类就可以改成(记得把User类实现Serializable接口):
@SpringBootTest
class RedisDemoApplicationTests {
@Autowired
@Qualifier("redisTemplate")
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
redisTemplate.opsForValue().set("qxf", "101");
System.out.println(redisTemplate.opsForValue().get("qxf"));
}
@Test
public void test() throws JsonProcessingException {
User user = new User();
user.setName("qxf");
user.setAge(27);
redisTemplate.opsForValue().set("user", user);
System.out.println(redisTemplate.opsForValue().get("user"));
}
}
Redis配置文件详解
-
redis默认对大小写不敏感
-
redis配置里面可以包含更多的配置文件
-
redis默认是只允许本地应用访问的,我们也可以在配置文件中指定ip地址对redis进行远程访问
-
redis有一个保护模式,默认是开启的,如果没有给redis设置用户名和密码,那么如果开启保护模式,那么远程访问就会失败
-
redis默认的端口号是6379,我们可以在配置文件中修改我们redis的远程连接的端口号
-
redis可以设置redis在后台以守护进程运行
-
如果我们设置了redis以守护进程运行,那么我们可以设置我们pid的进程名称
-
redis可以设置日志的级别,默认是notice级别
-
结合上面的日志,我们可以设置我们的日志文件名称
-
redis默认有16个数据库,我们也可以在配置文件中设置数据库的个数
-
redis还可以设置启动时是否显示logo
-
在redis中,我们还可以自定义我们自己的持久化规则,因为redis是内存数据库,所以我们可以选择把数据持久化到本地
save 900 1 # 如果900秒之内,至少有一个key进行了修改,那么我们就进行持久化操作
save 300 10 # 如果300秒之内,至少有10个key进行了修改,那么我们就进行持久化操作
save 60 10000 # 如果60秒之内,至少有10000个key进行了修改,那么我们就进行持久化操作
-
redis还可以设置持久化失败以后的操作,默认是继续工作
-
redis可以设置是否压缩rdb文件,如果压缩的话,就会消耗一些系统资源
-
redis保存rdb文件的时候,是否会进行校验
-
redis可以设置持久化的目录,默认是当前目录
-
redis可以设置默认是否需要密码
127.0.0.1:6379> config get requirepass # 获取我们当前的密码
1) "requirepass"
2) ""
127.0.0.1:6379> config set requirepass "123456" # 设置我们当前的密码是123456
OK
127.0.0.1:6379> config get requirepass # 如果没有进行密码的校验,就会提示没有权限访问
(error) NOAUTH Authentication required.
127.0.0.1:6379> ping
(error) NOAUTH Authentication required.
127.0.0.1:6379> auth 123456 # 进行密码的认证操作
OK
127.0.0.1:6379> ping # 验证以后就可以进行操作了
PONG
- 设置redis默认的最大的连接用户数
- 设置redis的最大的内存容量
- redis内存满了以后的操作(一共6种策略)
Redis的持久化
- Redis是内存数据库,如果不将内存中的数据库中的数据保存到硬盘中,那么一旦服务器进程退出,服务器中的数据库状态也会消失,所以Redis就为我们提供了数据持久化的功能
- Redis持久化有两种方式:RDB和AOF
- RDB:在指定的时间间隔内将内存中的数据集快照写入磁盘中,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读入内存中。
- Redis会单独的创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能,如果需要进行大规模数据的恢复,且对数据恢复的完整性不是非常的敏感,那RDB方式要比AOF方式更加的高效,RDB的缺点就是最后一次持久化后的数据可能丢失。
- RDB保存的文件就是dump.rdb,我们可以在配置文件里面设置
- 如果我们的redis突然断电或者突然关机,如果我们配置的有RDB方式的持久化,那么即使我们的机器断电或者关机,那么下次重新启动redis的时候,我们的数据也会通过rdb文件进行恢复
- rdb文件的生成和配置文件中的save的规则是强相关的,如果满足我们的save规则,就会自动触发我们的rdb规则,生成rdb文件。如果执行flushall命令,也会触发我们的rdb规则,生成rdb文件。退出我们的redis,也会触发我们的rdb规则,生成rdb文件。
- 如果要恢复rdb文件,只需要将rdb文件放到我们的redis的启动目录下,redis在启动的时候就会自动扫描我们的rdb文件进行数据的恢复
127.0.0.1:6379> config get dir # 可以查看我们的redis的启动目录
1) "dir"
2) "/usr/local/redis/bin"
- RDB的优点:适合大规模的数据恢复,对数据完整性要求不高。缺点:需要一定的时间间隔进程操作,如果redis意外宕机,最后一次修改的数据就没有了。fork进程的时候,会占用一定的内存空间。
- 有时候在生产环境中,我们会将这个文件进行备份。