Redis
redis简介
NoSQL
NoSQL:Not-Only SQL(泛指非关系型数据库),作为关系型数据库的补充,用于海量用户和海量数据的高并发数据处理。
redis
redis:Remote DIctionary Server,是一个由Salvatore Sanfilippo写的key-value存储系统,是NoSQL数据库的一种。Redis 的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此 Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
redis是一个开源的使用ANSIC语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。它通常被称为数据结构服务器,因为值(value)可以是字符串(String), 哈希(Hash), 列表(list),集合(sets) 和 有序集合(sorted sets)等类型。
redis的特点:
- 数据间没有必然的关联关系
- 内部采用单线程进行处理
- 性能高
- 支持多种数据类型
支持持久化
redis的应用场景:
加速查询热点信息,如热点新闻、热点商品等
- 任务队列,如秒杀、排队购票等
- 即时信息查询,如排行榜、在线人数等
- 时效性信息控制,如验证码、投票信息榜等
- 分布式数据共享,主要指分布式集群架构中的session分离
- 消息队列
- 分布式锁
Redis下载与安装
下载
Linux版(企业级应用)
[https://redis.io/](https://redis.io/),(使用redis-5.0.5.tar.gz)。
windows版(入门学习)
[https://github.com/microsoftarchive/redis/releases/tag/win-3.2.100](https://github.com/microsoftarchive/redis/releases/tag/win-3.2.100),(使用Redis-x64-3.2.100.zip)。
安装
解压即可使用。
使用解压目录中的可执行文件redis-server.exe启动redis服务,redis-cli.exe启动客户端。
windows(入门)
Redis基本操作
添加信息/获取信息
set:添加信息,redis以key-value形式保存数据,value始终以字符串形式体现。
语法:set key value
案例:set name zhangsan
get:获取信息,当get key不存在时,返回nil,表示空。
语法:get key
案例:get name
清屏指令
clear
帮助信息
语法1:help commands(指令) 案例1:help set
语法2:help @group(群组) 案例2:help @string
退出
方式1:quit
方式2:exit
方式3:按esc键(直接双击redis-cli.exe开启的客户端,从命令行启动的不会)
redis中key的命名规范:**表名:主键名:主键值:字段(属性)名**
Redis基本数据类型
redis自身是一个map,以key-value形式存储数据。
redis的数据类型指的是value部分,key永远都是string,redis常用数据类型有5种:
- 字符串(string)—————————————->String
- 哈希(hash)———————————————->HashMap
- 列表(list)—————————————————>LinkedList
- 集合(set)—————————————————>HashSet
- 有序集合(sorted set)—————————>TreeSet
string
string:字符串,用于存储单个数据,一个存储空间存储一个值,是redis中最常见的数据类型,最大存储量为512MB。如果字符串是以数值形式存在,可以当成数值进行操作。
基本操作
set key value:添加/修改字符串
get key:获取字符串
del key:删除字符串
del name
strlen key:获取字符串长度
strlen name
mset key1 value1 key2 value2 ... keyn valuen:一次添加多个字符串
mset name zhangsan age 20 sex 1 address shanghai
mget key1 key2 ... keyn:一次获取多个字符串
mget name age sex
append key value:追加字符串(原始数据存在即追加,不存在则新增)
append address pudong
set与mset的选择: mset的执行效率要比set高,因为在发送多条数据时,mset发送和接收返回结果都只需要一次,而set需要发送多次,接收多次; 但这并不代表着所有场景都应该使用mset,如果发送的数据量太大,则必须将数据拆分成多次发送,因为reids是单线程执行的,其他线程都得等当前线程结束操作,而数据量太大时会导致执行时间过长,因此反而会影响性能。
扩展操作
数值增/减操作
incr key:自增
语法:incr key,类似于++操作
案例:incr age
decr key:自减
语法:decr key,类似于--操作
案例:decr age
incrby key value:增加指定数值
语法:incrby key value,类似于+=,value值可以为负
incrby age 10
decrby key value:减少指定数值
语法:decrby key value,类似于-=,value值可以为负
decrby age 5
incrbyfloat key value:增加指定小数
语法:incrbyfloat key value,类似于+=,value值可以为负
incrbyfloat age 1.5
string在redis中就是字符串,数值型的字符串在遇到incr或decr操作时会转换成数值来进行操作,数值最大不能超过java.lang.Long.MAX_VALUE。
使用redis控制数据库表的主键id,为数据库表的主键id提供生成策略,确保数据库表主键不重复,主要应用于数据库表进行分表操作,此方案适用于所有数据库,且支持数据库集群。
数据时效性控制
setex key seconds value设置数据有效时间(秒)
语法:setex key seconds value
案例:setex username 10 admin
psetex key milliseconds value:设置数据有效时间(毫秒)
语法:psetex key milliseconds value
案例:psetex password 10 admin
使用redis控制数据生命周期,可以通过数据是否失效来控制业务行为,适用于所有具有时效性限定控制的操作。
ttl key:查看数据有效期
语法:ttl key
案例:ttl password
ttl指令返回值有3种:
- 具体数值:有效期
- -1:永久有效
- -2:已经过期/未定义/被删除
判断数据是否存在
setnx key value:当key有值时,执行失败,无值时设置成功,可使用其实现分布式锁的功能。
语法:setnx key value
案例:setnx password 1
应用案例
使用redis存储用户信息,用户有名称,性别,年龄属性。
第一种解决方案,逐个属性存储
set user:id:1:name zhangsan
set user:id:1:sex 1
set user:id:1:age 20
第二种解决方案:以json字段串存储全部属性
set user:id:2 {name:lisi,sex:0,age:18}
两种方案的不同点在于:
- 方案1存储的数据没有关联性,处于分离状态,因此可以直接对单个属性进行修改;
- 方案2存储的数据是json字符串,无法单独对某个属性进行修改,一旦需要更新某个属性的值,必须更新整个json字符串;
hash
hash:用于存储多个键值对的数据,一个存储空间存储多个数据,可以使用hash存储对象信息。使用string存储对象信息的两种方式各有其缺点,可以使用hash来解决。(类似于Map套Map)
基本操作
hset key field value:添加/修改
存储用户属性
hset user:id:2 name wangwu
hset user:id:2 age 25
hset user:id:2 sex 1
hgetall key:获取全部属性
hgetall user:id:2
hget key field:获取单个属性
hget user:id:2 name
hdel key field1 field2 ... fieldn:删除属性
hdel user:id:2 sex
hmset key field1 value1 field2 value2 ... fieldn valuen:添加多个
hmset user:id:3 name lisi age 24 sex 0
hmget key field1 field2 ... fieldn:获取多个
hmget user:id:3 name age sex
hlen key:获取hash中field属性个数
hlen user:id:3
hexists key field:判断hash中是否存在某个field
hexists user:id:3 address
扩展操作
hkeys key:获取hash中所有field
hkeys user:id:3
hvals key:获取hash中所有value
hvals user:id:3
hsetnx key field value:判断hash中是否有某个field,如果有,不做任何操作,如果没有,将该field存储到hash中。
hsetnx user:id:3 address chongqing
hincrby key field increment:设定hash某个field对应的value增加整数值
hincrby user:id:3 age 5
hincrbyfloat key field increment:设定hash某个field对应的value增加小数值
hincrbyfloat user:id:3 age 0.5
hash类型数据操作注意事项:
- hash中value只能存字符串,不允许存储其他数据类型的值;
- 每个hash中可以存储键值对上限为2的32次方-1个;
- hash数据类型设计之初不是为了存储大量对象使用的,不要将其视为专门存储对象使用的数据类型;
- hgetall可以获取全部属性,内部field过多时遍历整体数据效率会变得很低,可能造成数据访问瓶颈;
应用案例
消费者购物车
使用redis存储购物车信息,购物车中有用户id,商品信息(id,商品名称,价格,产地等),单件商品购买数量,购物车中存放的商品数量。
hmset user:id:1 goods:nums 2 goods:info {id:1,name:water,price:2.5,placeOfOrigin:beijing}
此时的做法可以实现从购物车中加载所有商品信息,但会出现大量的数据重复,因为该商品每次被购买时,都会在redis中将goods:info信息保存一次。
一般的做法是将goods:info部分单独提取出来做起一个单独的hash,每当有用户购买该商品时,将对应商品信息保存到该hash中。为了避免该hash中出现重复的商品信息,可以使用**hsetnx**指令,当商品信息存在时,不保存,不存在时才保存。
商家限购商品
使用redis存储商家限购商品信息,假设格力在双11活动中推出3种类型的特价空调(sca1、sca2、sca3),数量分别为200台,300台和500台。
hmset business:id:1 sac1 200 sac2 300 sac3 500
当商品被抢购时,可以使用hincrby指令,设置增加值为负数的形式设置对应的特价商品数量。
使用string存储json对象与使用hash存储对象的区别及如何选择:
- string存储json对象讲究整体性,所有信息都在同一个json字符串中,一般当数据只被用于读取时,建议使用string存储json的方式;
- hash存储对象是使用同一个key,在value中使用field属性将对象属性进行了分离,便于修改,在数据会被频繁修改时,建议使用hash存储对象;
list
list:存储多个数据,并对数据进入存储空间的顺序进行了区分,底层使用了双向链表来进行实现,list着重考虑的是**数据进入存储空间和从存储空间取出数据的顺序**。
由于采用了双向链表的数据结构,因此list存储数据时,从左右两边都可以进行存储,同理,取出数据时,左右两边也都可以取出。
基本操作
lpush key value1 value2 ... valuen:从左边向链表中存储数据
rpush key value1 value2 ... valuen:从右边向链表中存储数据
lpush user:id:3 {name:tom,age:20,sex:1} {name:jack,age:25,sex:1}
rpush user:id:3 {name:john,age:22,sex:1} {name:rose,age:18,sex:0}
lindex key index:从左向右,根据下标获取链表中的value
lindex user:id:3 2
lrange key start stop:从左向右,获取链表中的部分元素,start(开始位置的下标),stop(结束位置下标)。当不清楚list中有多少个元素时,可将stop取值为-1,代表取到list最后一个为止。
lrange user:id:3 0 -1
lpop key:从左边开始,从list中获取第一个元素,并将该元素从list中移除。
rpop key:从右边开始,从list中获取第一个元素,并将该元素从list中移除。
rpop user:id:3
lpop user:id:3
扩展操作
blpop key:在指定时间内获取list中左边第一个元素并移除
brpop key:在指定时间内获取list中右边第一个元素并移除
当list中有元素时,以上两条指令立即获取并移除元素,当list中没有元素时,会阻塞并等待指定时间。如果在指定的时间内list中有元素存储进来,会立即将该元素获取并移除,如果指定时间内list中没有元素,会返回nil。
blpop user:id:3 90
brpop user:id:3 90
验证时需要开启两个客户端,一个用于获取,一个用于存储。
lrem key count value:从左向右,删除list中指定数据。由于list中的数据是可以重复的,因此可以使用count来指定删除多少个。
lrem user:id:3 1 {name:jack,age:26,sex:1}
list数据操作注意事项:
- list中保存的数据都是string类型的,数据容量上限为2的32次方-1;
- list中有索引概念,但实际操作过程中通常采用队列(两端操作,先进先出)或栈(同一端操作,先进后出)的方式;
- list可以用于分页操作,一般第一页的数据是从内存中拿,后续页面的数据从数据库中加载;
应用案例
在分布式系统中,服务器会有多台,每台服务器都会记录自己的日志,当有异常情况产生时,需要去查看日志信息。如果每台服务器都单独记录自己的日志,此时阅读日志信息将变得十分困难,需要不断的来回比对日志信息的记录时间。可以使用redis的list,在每台服务器产生日志信息时都存储到list中,此时读取redis,可得到全部服务器记录的日志信息。
验证时需要开启多个客户端,分别向保存日志记录的list中存储日志信息。此后可通过任一客户端查看该list的信息,此时会发现所有客户端存储的日志都可以查看。
set
set:存储多个数据。
list与set的区别:
- list底层为双向链表,set底层为hash,在查询效率上来说,set优于list;
- list有序,set无序;
- list元素可重复,set元素不允许重复;
基本操作
sadd key member1 member2 ... membern :添加数据
sadd user:id:1 {name:tom,age:25,sex:1} {name:rose,age:22,sex:0} {name:jack,age:24,sex:1}
smembers key:获取全部数据
smembers user:id:1
srem key member1 member2 ... membern :删除数据
srem user:id:1 {name:jack,age:24,sex:1}
scard key:判断set中存储的元素个数
scard user:id:1
sismember key member:判断set中是否存在某个元素
sismember user:id:1 {name:rose,age:22,sex:0}
扩展操作
随机获取
srandmember key [count]:随机从set中获取count个元素,如不设置count,默认获取一个。
srandmember user:id:1 3
spop key [count]:随机从set中获取并移除count个元素,如不设置count,默认获取并移除一个。
spop user:id:1 3
获取集合的交、并、差集
sadd user:id:1 {name:tom,age:25,sex:1} {name:jack,age:22,sex:1}
sadd user:id:2 {name:rose,age:20,sex:0} {name:kate,age:18,sex:0} {name:tom,age:25,sex:1}
sinter key1 [key2]:返回两个set的交集(两个set中同时存在的元素的集合)
sinter user:id:1 user:id:2
sunion key1 [key2]:返回两个set的并集(两个set中存在的所有元素,会自动去重)
sunion user:id:1 user:id:2
sdiff key1 [key2]:返回两个set的差集,注意,返回的是key1去除与key2中相同的元素后的集合
sdiff user:id:1 user:id:2 ---->返回{name:jack,age:22,sex:1}
sdiff user:id:2 user:id:1 ---->返回{name:rose,age:20,sex:0} {name:kate,age:18,sex:0}
sinterstore destination key1 key2:将key1和key2的交集存储到destination中
sinterstore user:id:3 user:id:1 user:id:2
sunionstore destination key1 key2:将key1和key2的并集存储到destination中
sunionstore user:id:4 user:id:1 user:id:2
sdiffstore destination key1 key2:将key1和key2的差集存储到destination中
sdiffstore user:id:5 user:id:1 user:id:2
smove source destination member:将member从source中移动到destination中
smove user:id:1 user:id:5 {name:tom,age:25,sex:1}
使用set的交、并、差集,可以实现QQ共同好友之类的操作。
应用案例
权限校验
利用set的交、并、差集方式,可以实现权限校验。
假设有两个角色:角色1有getall、getbyid、getbyname三种权限,角色2有getall、insert、update、delete权限,此时需要给用户授权,同时拥有角色1和角色2的所有权限。
添加角色
sadd role:id:1 getall getbyid getbyname
sadd role:id:2 getbyid update insert delete
为用户授权
sunionstore user:id:1 role:id:1 role:id:2
具体检验时有两种方式:
smembers useri:id:1
获取所有权限,在程序中通过条件进行判断;sismember useri:id:1 insert
判断是否存在set元素,在数据层面进行判断;此外,利用set中元素唯一的特性,redis可以使用set来制作黑白名单(黑名单:在set中的不允许访问,白名单:不在名单中的不允许访问)。
sorted_set
sorted_set:用于存储可排序的多个数据,sorted_set是基于set的,在set的基础上增加了score用于排序。
基本操作
zadd key score1 member1 score2 member2:添加数据
zadd class:id:1 20 {name:tom,age:25,sex:1} 15 {name:kate,age:20,sex:0} 32 {name:jack,age:22,sex:1} 8 {name:rose,age:18,sex:0}
zrange key start stop [withscores] :正序获取sorted_set中的数据
zrevrange key start stop [withscores] :倒序获取sorted_set中的数据
withscores用于是否显示score列的值。
zrevrange class:id:1 0 -1
zrange class:id:1 0 -1 withscores
zrem key member1 member2 ... membern:删除指定元素。
zrem class:id:1 {name:jack,age:22,sex:1}
zrangebyscore key min max [withscores] [limit offset count]:正序查询score值在**min到max**之间的所有数据
zrevrangebyscore key max min [withscores] [limit offset count]:倒序查询score值在**max到min**之间的所有数据
limit关键字可以用来进行分页查询,offset代表从哪个下标开始查,count代表查几个数据。
zrangebyscore class:id:1 15 20 withscores limit 0 2
zrevrangebyscore class:id:1 100 0 withscores limit 2 2
zremrangebyscore key min max:删除score值处于min到max范围内的元素
zremrangebyscore class:id:1 10 20
zremrangebyrank key start stop:删除索引start到索引stop之间的元素
zremrangebyrank class:id:1 0 2
zcard key:统计sorted_set中元素个数
zcount key min max:统计score值在min到max区间的元素个数
zcard class:id:1
zcount class:id:1 0 20
扩展操作
zinterstore destination numkeys key1 key2 ... keyn [aggregate]:返回多个sorted_set的交集
zunionstore destination numkeys key1 key2 ... keyn [aggregate]:返回多个sorted_set的并集。
numkeys为求交集的sorted_set个数,numkeys指定数量后,key代表的sorted_set个数必须与之吻合,否则语法报错。
aggregate可以设置求交、并集时,对score列做的处理:求和,求最小值,求最大值,默认求和。
提供第二个sorted_set,除id不同外,score值也不同,
zadd class:id:2 40 {name:tom,age:25,sex:1} 30 {name:kate,age:20,sex:0} 50 {name:jack,age:22,sex:1} 60 {name:rose,age:18,sex:0}10 {name:jhon,age:26,sex:1} 12 {name:hanson,age:28,sex:1} 19 {name:jackson,age:24,sex:1} 2 {name:lili,age:16,sex:0}
求交集
zinterstore class:id:3 2 class:id:1 class:id:2 aggregate max
zunionstore class:id:4 3 class:id:1 class:id:2 class:id:3
zrank key member:正序获取sorted_set元素的索引值
zrevrank key member:倒序获取sorted_set元素的索引值
以上两条指令返回的是索引值,因此如果返回值为0时不要认为指令执行失败,而是当前查询的数据score值最小,由于sorted_set默认升序,因此score值最小时,返回的索引值就是0。
zrank class:id:1 {name:rose,age:18,sex:0}----->返回0
zrevrank class:id:1 {name:rose,age:18,sex:0}-->返回3
zscore key member:获取元素的score值
zincrby key increment member:对元素的score值增加指定数值increment
zscore class:id:1 {name:rose,age:18,sex:0}
zincrby class:id:1 3 {name:rose,age:18,sex:0}
redis借助sorted_set的zrank、zrevrank可以完成排行榜数据的存储。
通用指令
key通用指令
key的相关状态
del key:删除key
type key:查询key的类型
type class:id:1
exists key:判断key是否存在
exists user:id:1
key的时效性控制
设置有效时间
expire key seconds:设置key的有效时间,以秒计数
pexpire key milliseconds:设置key的有效时间,以毫秒计数
expire class:id:1 10
expireat key timestamp:使用时间戳设置key的有效时间,Linux系统中常用
pexpireat key milliseconds-timestamp:使用时间戳设置key的有效时间,Linux系统中常用
获取有效时间
ttl key:获取key的有效时间(秒)
pttl key:获取key的有效时间(毫秒)
ttl key返回值有3类:
- -1:没有给key设置过有效期;
- -2:给key设置过有效期,并已经过期(在redis中清空);
- 具体的数值:有效期剩余时间;
expire class:id:2 30
ttl class:id:2
取消有效期
persist key:取消有效期,将设置了有效期的数据转换为没有设置有效期的状态
设置有效期
expire class:id:3 60
查看有效期
ttl class:id:3
取消有效期
persist class:id:3
查看有效期,此时返回-1
ttl class:id:3
key的快速查询
keys pattern:使用pattern查询key
pattern中可以使用*、?、[]三种符号:
- *表示任意个任意字符;
- ?表示一个任意字符;
- []表示在[]内的字符中任意匹配一个即可;
keys *
keys class:id:?
keys s[eub]x
key的其他操作
重命名
rename key newkey:如果newkey存在,执行覆盖操作,如果不存在,执行重命名操作
renamenx key newkey:如果newkey存在,操作失败,如果不存在,执行重命名操作
排序
sort key:对key中的数据进行排序,注意:string和hash不适用。
sort排序时对非数值型的数据无法实现排序,对于list和sorted_set,非数值型数据采用对score值进行排序的方式实现。
lpush nums 5 4 7 2 9 1 3 8 6
sort nums---->正常排序,默认升序
lpush strs d a e b f c
sort strs---->报错
sort strs by score---->正常排序,默认升序
zadd strsvalue 5 g 3 l 6 a 2 r 7 t 1 y 9 z 8 e
sort strsvalue by score---->按照score的值进行排序
数据库相关通用指令
redis为每个服务都提供了16个数据库,序号从0-15,每个数据库的数据相互独立,所有数据库共用一块空间。每个服务的默认使用的数据库是0号数据库。
select index:切换数据库,index为数据库序号。
127.0.0.1:6379> select 12
OK
127.0.0.1:6379[12]>
ping:测试客户端与服务端是否连通,执行ping指令时,如果服务端给予回应,代表连通,一直等待则代表没有连通。
move key db:从当前数据库将数据移动到另一个数据库。数据移动成功后,当前数据库中将不再有该数据。此外,如果目标数据库中有对应数据,会移动失败。
127.0.0.1:6379> set name zs
OK
127.0.0.1:6379> move name 1
(integer) 1
127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> get name
"zs"
flushdb:清除当前数据库中所有数据
flushall:清除所有数据库中所有数据(用不上)
dbsize:查看当前数据库中现有的数据量
Jedis
java语言操作redis的工具之一,除Jedis以外,在实际开发过程中,spring data-redis也很常用。
下载
[https://search.maven.org/](https://search.maven.org/),输入jedis搜索,下载即可。jedis依赖于commons-pool2,要想正常使用jedis,还需要导入该jar包。
也可使用maven导入相关依赖,在[https://mvnrepository.com](https://mvnrepository.com)中查找jedis依赖信息。
使用
jedis的使用特别简单,因为jedis的方法与redis的指令完全一致,可以调用jedis对象的相关方法进行redis相关操作。
jedis入门案例
步骤1:创建maven项目, 修改pom.xml。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.wsjy</groupId>
<artifactId>redis</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<dependencies>
<!-- 引入jedis依赖 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<!-- 引入单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
</project>
步骤2:在test/java目录下新建测试类TestJedis.java
public class TestRedis {
@Test
public void test1(){
//1、连接redis
Jedis jedis = new Jedis("localhost", 6379);
//2、操作redis,jedis的方法名及作用与redis的指令完全一致。
jedis.set("name","zhangsan");
System.out.println(jedis.get("name"));
//3、释放资源
jedis.close();
}
}
jedis工具类
上例中jedis对象的ip地址和端口号是在编程中手动设置的,而开发过程中不可能手动去管理jedis对象,因为修改ip或端口时需要对源码进行修改,因此一般的做法是通过编写工具类对jedis对象进行管理。
需求:使用配置文件进行软编码,从配置文件中控制jedis对象的端口和ip。
步骤1:创建配置文件jedis.properties
jedis.port=6379
jedis.host=127.0.0.1
#连接池的配置在这里都可以配置
#jedis.maxTotal=30
#jedis.maxIdle=10
步骤2:创建jedis工具类JedisUtils.java
public class JedisUtils {
private static JedisPool jp=null;
private static String host=null;
private static Integer port;
// private static Integer maxTotal;
// private static Integer maxIdle;
static {
//读取配置文件
ResourceBundle rb=ResourceBundle.getBundle("jedis");
host = rb.getString("jedis.host");
port = Integer.parseInt(rb.getString("jedis.port"));
// maxTotal = Integer.parseInt(rb.getString("jedis.maxTotal"));
// maxIdle = Integer.parseInt(rb.getString("jedis.maxIdle"));
JedisPoolConfig jpc = new JedisPoolConfig();
// jpc.setMaxIdle(maxIdle);
// jpc.setMaxTotal(maxTotal);
//获取连接池对象
jp = new JedisPool(jpc,host,port);
}
public static Jedis getJedis(){
//从连接池中获取连接(jedis)
return jp.getResource();
}
}
步骤3:修改测试类代码,验证工具类是否可以正常使用
public class TestRedis {
@Test
public void test1(){
//1、连接redis
Jedis jedis = JedisUtils.getJedis();
//2、操作redis,jedis的方法名及作用与redis的指令完全一致。
jedis.set("name","zhangsan");
System.out.println(jedis.get("name"));
//3、释放资源
jedis.close();
}
}
Linux(应用)
安装:
# 下载redis6.0.9,上传到服务器并解压
# redis编译时依赖于gcc,安装gcc,默认版本4.8.5,与新版本redis版本不兼容,需要升级
yum -y install gcc automake autoconf libtool make
# 升级gcc
yum -y install centos-release-scl
yum -y install devtoolset-9-gcc*
# 激活
scl enable devtoolset-9 bash
# 进入redis解压目录中编译安装
make MALLOC=libc
启动:默认安装到解压目录中src目录下,进入到src目录中,redis-server指令启动服务端,redis-cli指令启动客户端。
启动多个redis服务:指定端口号进行启动,以进行区分
使用port参数指定端口进行启动,注意必须是"--"
redis-server --port 6380
客户端连接指定端口的redis服务
连接指定端口的redis服务,如不加参数,默认连接6379端口
redis-cli -p 6380
实际开发过程中,**使用配置文件来启动多台redis服务**。
配置文件位于redis的解压目录中,文件名为redis-conf,该文件中内容很多,同时有大量注释,会导致阅读变得很困难,并且也不可能直接修改该配置文件,因为要启动多台redis服务时,需要有多个配置文件。此时可以使用cat命令可以过滤掉无关内容,并将需要的内容存入新配置文件。
需求:使用配置文件启动多个redis服务。
步骤1:在redis解压目录中新建conf目录(用于保存配置文件)和data目录(用于保存日志等文件)
mkdir conf
mkdir data
步骤2:将redis.conf中除去注释和空白的内容保存到新配置文件中
grep -v "#" ---->过滤注释
grep -v "^$" ---->过滤空白
cat redis.conf |grep -v "#" |grep -v "^$" > conf/redis-6379.conf
步骤3:编辑修改新配置文件
vim redis-6379.conf
bind 127.0.0.1 ---->绑定IP
databases 16 ---->设置redis数据库数量
port 6379 ---->端口号
daemonize yes ---->是否以守护进程启动(控制台不打印日志信息)
logfile "6379.log" ---->日志文件名称
loglevel notice ---->日志级别,debug|verbose|notice|warning,默认verbose,生产环境notice
dir /usr/local/redis-5.0.5/data ---->文件存储路径
maxclients 0 ---->客户端最大数量,默认为0,不限制
timeout 300 ---->客户端连接超时时间
步骤4:启动redis服务
启动服务
redis-server conf/redis-6379.conf
验证是否启动成功
1、查看reids服务进程
ps -ef |grep redis-
连接服务
2、redis-cli -p 6379
如需启动多个redis服务,只需要将新建的配置文件复制多份,将其内容中的端口号和日志文件名称进行修改即可。
cp redis-6379.conf redis-6380.conf
vim redis-6380.conf
port 6380
daemonize yes
logfile "6380.log"
dir /usr/local/redis-5.0.5/data
#也可使用以下命令实现复制同时直接修改
sed "s/6379/6380/g" redis-6379.conf >redis-6380.conf
rdm工具要连接远程服务器上的redis,需要修改远程服务器上redis配置文件中bind的ip地址,否则连不上。 第一种:bind 真实ip 第二种:bind 0.0.0.0 firewall-cmd —add-port=6379/tcp —zone=public —permanent firewall-cmd —reload
持久化
持久化两种方式:
- 数据快照:以数据快照的形式将数据状态进行保存,存储结构简单,关注的是数据本身;
日志文件:以日志文件的形式将数据操作过程进行保存,存储结构复杂,关注数据操作过程;
redis支持以上两种持久化方式:数据快照(RDB)、日志文件(AOF)。
RDB
redis的RDB方式持久化有三种方式:save指令、bgsave指令、通过配置自动持久化。
RDB方式的相关配置
vim redis-6380.conf
port 6380
daemonize yes
logfile "6380.log"
dir /usr/local/redis-5.0.5/data
dbfilename dump-6380.rdb ---->持久化文件名称,通常为"dump-端口号.rdb"
rdbcompression yes ---->是否进行压缩,默认压缩,配置为no效率高,但文件会变得非常大
rdbchecksum yes ---->是否进行rdb格式校验,默认校验,关闭可提升效率,但有可能造成数据损坏
重新启动redis服务进程:
查看之前启动的redis服务进程
ps -ef |grep redis-
根据进程id关闭进程
kill -s 9 进程id
启动redis服务
redis-server conf/redis-6380.conf
save(同步)
在redis中执行save指令,即可以数据快照形式对redis中的内容进行持久化,此时会在data目录中生成后缀为.rdb的文件,该文件内容保存了redis中的数据快照。
set user:id:1 {name:zs,age:20,sex:1}
save
客户端连接后向redis中保存数据并执行save指令时,data目录下会生成dump-6380.rdb文件。
测试数据恢复:关闭服务和连接(使用kill指令,使用ps指令查看确保进程被关闭),再重启redis服务和连接,此时dump-6380.rdb中的内容会被自动恢复到redis中。
注: 由于redis是单线程的,save指令也会加入到redis的指令执行队列中,当save指令执行时间过长时,会阻塞redis服务,造成后续指令等待时间过长,因此线上环境中千万不要使用save指令。
bgsave(异步)
在redis中执行bgsave指令,也可以数据快照形式进行持久化。该指令不会由当前进程执行,而是生成子进程来执行bgsave指令,因此不会阻塞当前进程中的其它指令。一般RDB方式进行持久化,都会使用bgsave指令。
使用bgsave进行持久化操作时,一般会在启动的配置文件中配置“stop-writes-on-bgsave-error yes”,该配置含义为后台持久化操作过程中出现错误时,是否停止持久化操作,默认开启。
自动持久化
在redis的配置文件中,可以使用save配置来实现redis的RDB方式自动持久化。
语法:save seconds changes
其含义为:在seconds内有changes个key值发生变化,则使用bgsave自动进行持久化
案例:
vim redis-6380.conf
port 6380
daemonize yes
logfile "6380.log"
dir /usr/local/redis-5.0.5/data
dbfilename dump-6380.rdb
rdbcompression yes
rdbchecksum yes
stop-writes-on-bgsave-error yes
save 10 2 ---->设置自动持久化,每10s内有2个key发生变化就自动持久化,使用bgsave方式
注: 做自动持久化的配置时,要根据业务情况来设置seconds与changes,保存频度过高会极大的影响性能,过低则有可能造成数据丢失,两者都有可能造成灾难性后果。 对于seconds和changes,一般会配置成一大一小:即seconds大时,changes小,seconds小时,changes大。
RDB的其它形式:全量复制、服务器重启(debug reload)、服务器关闭(shutdown save)。
RDB的优缺点:
- 优点:
RDB生成的持久化文件是一个紧凑压缩的二进制文件,存储效率高;
存储的是某个时间点的数据快照,非常适合数据备份和全量复制;
恢复数据速度很快,比AOF方式快得多; - 缺点:
无法实现实时存储,可能造成数据丢失;
bgsave指令需要创建子进程,会牺牲部分性能;
redis版本的RDB文件格式不统一,兼容性差;
AOF
与RDB不同的是,AOF是使用日志来记录数据变化的过程,不记录数据本身。AOF解决了数据持久化过程中的实时性问题,当前AOF方式已经是redis持久化的主流方式。
AOF执行过程:当redis中有写入操作时,redis会生成一个缓冲区,将这些写入指令放到缓冲区内,满足一定条件后,生成后缀为.aof的文件并将缓冲区内的指令同步到文件中。
AOF持久化的三种策略:
- always(每次):每次写入都将缓冲区中内容同步到.aof文件中,数据零误差,性能低;
- everysec(每秒):每秒将缓冲区中内容同步到.aof文件中,数据准确性高(最多丢失1秒的数据),性能较高。默认配置,建议使用;
- no(系统控制):由系统控制缓冲区中内容同步到.aof文件的周期,整体过程不可控;
修改配置文件,开启AOF
vim redis-6380.conf
port 6380
daemonize yes
logfile "6380.log"
dir /usr/local/redis-5.0.5/data
dbfilename dump-6380.rdb
rdbcompression yes
rdbchecksum yes
stop-writes-on-bgsave-error yes
save 10 2
appendonly yes ---->开启AOF支持
appendfsync everysec ---->设置AOF策略
appendfilename appendonly-6380.aof ---->持久化文件名,一般为“appendonly-端口号.aof”
AOF重写概念和命令执行
当写命令不断同步到aof文件时,该文件会越来越大,为解决这个问题,redis引入了AOF重写机制来压缩aof文件体积。
AOF文件重写是将redis进程中的数据转化为写命令并同步到aof文件的过程,简单来说就是将对同一个数据的多个命令执行结果转化为最终结果对应的指令进行记录(比如对name执行了5次set操作,只记录最后一个set指令)。
AOF重写的好处:
- 降低磁盘占用量,提高磁盘利用效率;
- 提高持久化效率,降低持久化操作的写时间,提高IO性能;
降低数据恢复用时,提高数据恢复效率;
AOF重写规则:
进程内超时数据不再写入;
- 忽略无效数据,重写时使用进程内数据直接生成;
将进程内对同一数据的多条命令合并成一条指令;
AOF重写方式:
手动重写:
bgrewriteaof
测试:
客户端 set name zs set name ls set name ww
查看data目录中的appendonly-6380.aof cat appendonly-6380.aof
自动重写:
auto-aof-rewrite-percentage 50 #自动改写百分比50 auto-aof-rewrite-min-size 64mb #自动改写最小大小64mb
当AOF日志大小增加指定百分比时,redis可以自动重写日志文件,隐式调用BGREWRITEAOF。
redis会在最近一次重写后记住AOF文件的大小:aof_base_size(如果自重新启动以来未发生任何重写,则使用启动时AOF的大小)。每次AOF文件大小发生改变时会使用aof_current_size进行记录,当(aof_current_size-aof_base_size)/aof_base_size>=auto-aof-rewrite-percentage时,触发重写。
auto-aof-rewrite-min-size指定要重写的AOF文件的最小大小,这对于避免重写AOF文件非常有用,即使达到百分比增加,但它仍然很小。
指定零百分比以禁用自动AOF重写功能。
RDB与AOF的比较
持久化方式 | RDB | AOF |
---|---|---|
占用存储空间 | 小(数据级:压缩) | 大(指令级:重写) |
存储速度 | 慢 | 快 |
恢复速度 | 快 | 慢 |
数据安全 | 数据会丢失 | 根据策略决定 |
资源消耗 | 高/重量级 | 低/轻量级 |
启动优先级 | 低 | 高 |
RDB和AOF如何选择?
- 对数据非常敏感,建议AOF,因为always策略可以保证数据完全不丢失;
- 数据呈现阶段有效性(比如游戏),建议RDB,恢复速度快;
- 也可同时使用两种策略,重启后优先使用AOF恢复数据,降低数据丢失量;
事务
redis事务就是一个指令的执行队列,将一系列预定义的指令集合包装为一个整体(指令队列)。当执行时,一次性按照添加顺序依次执行,这个执行过程是排它的,也就意味着该执行过程不会被打断和干扰。
事务基本操作
开启事务:multi
multi
执行multi指令后,事务开启,之后的所有指令都会依次添加到事务(指令队列)中,此后所有指令都不会执行,直到提交事务后才开始执行事务中的指令队列。
提交事务:exec
exec
执行exec指令时,将会依次执行事务中的指令。
取消事务:discard
discard
在事务的指令队列中如果出现错误,可以使用discard指令取消事务。
注: 开启事务后,有指令格式输入错误,整个事务将被取消,即所有指令都不会执行; 开启事务后,指令格式没有错误,但出现了执行错误(比如对set数据类型的key执行lpush指令),此时事务的指令队列中能正常执行的指令继续执行,执行错误的指令则不执行。也就是说,redis事务中出现执行错误的指令,不会自动回滚,需要程序员手动控制。
锁
加锁
redis中提供了锁的概念,即操作redis的某个key之前,可以使用watch指令来监控该key的值。在exec指令执行之前,如果该key发生了变化,则取消事务的执行。
语法:watch key1,key2,...,keyn
案例:
步骤1:开启两个客户端,连接同一个redis服务。
步骤2:在连接1中进行如下操作
添加数据
set user:id:1 {name:zs,age:20,sex:1}
监控数据
watch user:id:1
开启事务
multi
添加数据
set person:id:1 {name:tom,age:30,sex:1}
步骤3:在连接2中进行如下操作
对user:id:1进行修改
set user:id:1 {name:kate,age:20,sex:2}
步骤4:在第一个连接中提交事务
exec
此时可看到第一个连接中的执行结果为空,即事务没有执行,其原因是watch监控了`user:id:1`,当第二个连接中对该key进行修改时,第一个连接中的事务会被取消,因此提交事务的结果为nil。
需要注意的是,watch指令只能在事务外部执行,不能放到事务中,否则会报错。
解锁
如果需要取消对key的监控,可执行unwatch指令,该指令一旦执行,不管之前watch指令监控了多少个key,都会被全部取消。
unwatch
分布式锁
分布式锁:一种基于特定条件的事务执行的锁。
使用watch指令对某个key加锁的操作在高并发场景下并不适用:比如抢购或抢票场景,不但有高并发且必须对商品或票的数量进行改变,使用watch就解决不了,因为watch加锁后对被监控的key的修改操作都会被拒绝。
这个问题可以使用string类型的setnx指令设置公共锁来解决,需要注意的是,此处的公共锁只是设计概念上的锁,它锁定的不是具体的数据。
语法:setnx lock-key value
setnx指令中的key如果有值,返回失败,如果没有值,则设置成功。
案例:
步骤1:开启两个客户端,连接同一个redis服务。
步骤2:在连接1中进行如下操作
设置商品或票的数量
set num 10
上锁,lock-num不是规定语法,只是一个普通的key,其值可随意设置,判断的是该key是否有值,值是什么并不重要
setnx lock-num 1
设置商品数量自增
incrby num -1
步骤3:在连接2中进行如下操作
操作连接1的lock-num,如果lock-num有值,执行失败,如果没值,执行成功,当执行失败时,意味着有锁存在,此时该线程阻塞等待,直到连接1释放锁。
setnx lock-num 1
步骤4:连接1中释放锁
del lock-num
此处应注意,不论是哪个线程进行操作,操作的都是同一个key:lock-num,否则锁不住。也就是说,每个线程要对num进行操作之前,都应该去获取锁:`setnx lock-num 1`,操作完成后也必须释放锁:`del lock-num`,否则其他线程就无法获取锁,从而造成死锁。
分布式锁改良
使用setnx指令实现分布式锁时,当某个线程获取锁后由于某种原因没有释放锁,此时会出现死锁情况,为了解决死锁问题,一般会通过给锁设定有效期的方式来解决。
使用expire或pexpire指令给锁设定有效期,从而避免某个线程长期不释放锁而造成死锁。
案例:
连接1:
set num 10
setnx lock-num 1
expire lock-num 30
连接2:
连接1中没有主动释放锁,但30s后,连接2可以获取锁,因为锁的有效期为被设定为30s
setnx lock-num 1
删除策略
数据删除策略
过期数据是指redis中设置了有效期,并且有效期已过的数据,cpu会根据指定的删除策略处理过期数据。 一般来讲,redis中的过期数据删除策略有有三种:定时删除、惰性删除、定期删除。
定时删除
创建定时器,当key设置了有效期,并且有效期已过,由定时器任务立即删除过期数据。
定时删除的优点是节约内存,到点删除,快速释放对内存的不必要占用。其缺点也很明显:cpu压力很大,因为定时删除不去衡量cpu负载的高低,都会占用cpu,会影响redis服务器响应时间和指令吞吐量,会牺牲cpu性能换取内存空间(以时间换空间)。
惰性删除
数据有效期已过,此时不做处理,数据还存在于内存中,直到该数据下一次被访问时,cpu才删除该数据。
惰性删除的优点是需要删除的时间才删除,不影响cpu性能,缺点是数据一直留存于内存中,大量内存被占用,内存压力会很大,会以存储空间换取cpu性能(以空间换时间)。
定期删除
周期性轮询redis库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除的频度。
定期删除的特点是:
- cpu性能占用设置了峰值,检测频度可自定义设置;
内存占用并不是很大,长期不用的数据会被持续清理;
redis中采用了惰性删除和定期删除策略。
数据逐出策略
数据删除策略是对过期数据而言的,除此以外,有一种可能是内存中的数据都没有过期,redis向内存中添加新数据时,发现空余内存不够了,此时redis会根据设置的数据逐出策略,临时性的删除部分数据腾出内存保存新数据。
需要注意的是,redis不保证数据逐出一定成功,当redis反复执行数据逐出操作还是不能成功时,redis会报错。
影响数据逐出的设置有以下几项:
- maxmemory:最大可使用内存,也就是redis占据物理内存的比例,默认值为0,即redis占全部物理内存。实际生产过程中一般设置在50%-70%之间;
- maxmemory-samples:每次选取待删除数据的个数,选取数据时不要进行全库扫描,这样会导致严重的性能消耗,降低IO性能。一般采用随机获取数据的方式选取待检测删除数据;
maxmemory-policy:删除策略,达到最大内存后,对挑选出来的数据执行的删除策略;
数据逐出策略就是对maxmemory-policy的设置,有以下三种配置:
检测易失数据(可能会过期的数据):
volatile-lru:逐出最长时间不被使用的数据
volatile-lfu:逐出规定时间内使用次数最少的数据
volatile-ttl:逐出将要过期的数据
volatile-random:随机逐出数据- 检测全库数据
allkeys-lru:逐出最长时间不被使用的数据
allkeys-lfu:逐出规定时间内使用次数最少的数据
allkeys-random:随机逐出数据 - 放弃数据逐出no-enviction:
禁止逐出,此时会报错(out of memory)
生产环境中,一般配置
maxmemory-policy volatile-lru
。
高级数据类型
redis高级数据类型都是为了解决一些特定问题设计的。
bitmaps
redis提供的bitmaps这个“数据结构”可以实现对位的操作。bitmaps本身不是一种数据结构,实际上就是字符串,但是它可以对字符串的位进行操作。
可以把Bitmaps想象成一个以位为单位数组,数组中的每个单元只能存0或者1,数组的下标在bitmaps中叫做偏移量。单个bitmaps的最大长度是512MB,即2^32个比特位。
基本操作
setbit key offset value:设置key中offset位上的2进制值。
需求:设置2020年5月5日id为1的独立访问用户访问系统bitmaps,0表示未访问,1表示已访问
setbit unique:user:id:1:2020-5-5 0 1
getbit key offset:获取key中某个位上的值
getbit unique:user:id:1:2020-5-5 0
注:如果setbit时,如果设置的值特别大,此时会在设置位前补0,很耗时间,当设置的值非常大且连续时,一般建议会删除一个固定值后再做对应设置。
扩展操作
bitcount key [start end]:计算key 所储存的字符串值中,指定字节区间[start,end]被设置为 1 的比特位的数量。注意:redis的setbit设置或清除的是bit位置,而bitcount计算的是byte位置,1byte=8bit。
setbit a 1000 1
setbit a 10000 1
setbit a 100000 1
setbit a 1000000 1
setbit a 10000000 1
setbit a 100000000 1
setbit a 1000000000 1
bitcount a
bitcount a 0 1000
bitcount a 0 1000000
bitop and|or|not|xor destkey key1 key2 ... keyn:对指定的key进行与、或、非、异或操作,并将生成结果保存到destkey中。
setbit a 1 1
setbit a 3 1
setbit a 5 1
setbit a 7 1 ---->a:10101010
setbit b 0 1
setbit b 2 1
setbit b 4 1
setbit b 6 1 ---->b:01010101
bitop or c a b ---->c:11111111,或运算,两个位中有一个是1,结果得1
bitop and d a b ---->d:00000000,与运算,两个位中必须全部是1,结果才得1
bitop not e a ---->e:01010101,非运算,原来是1,变为0,原来是0,变为1
bitop xor f a b ---->f:11111111,异或,两个位值相同,得0,不相同得1
HyperLogLog
HyperLogLog用于统计不重复的数据数量,该数据类型基于基数进行统计。
基数就是数据集合去重后的元素个数。比如{5,6,4,3,4,6,5,8,9,4,5,5,6,8},该集合中元素有14个,其中5有4个,6有3个,4有3个,8有两个,该集合去重后的元素为{5,6,4,3,8,9},基数为6。
基本操作
pfadd key element1 element2 ... elementn:向key中添加元素
pfadd a 1 2 3 4
pfadd b 2 3 4 5
pfadd c 3 4 5 6
pfcount key1 key2 ...keyn:统计key中的元素个数
pfcount a b c ---->返回6,去重后的结果
pfmerge destkey key1 key2 ... keyn:将key1到keyn的元素合并到destkey中,保存去重后的结果
pfmerget d a b c
注: HyperLogLog用于做基数统计,不是集合,不保存数据,只记录数量不记录数据,因此无法从中获取实际数据;其核心是基数估算算法,因此最终值会存在一定的误差(0.81%);占用内存很小,上限为12KB。
GEO
GEO功能在redis3.2版本提供,支持存储地理位置信息用来实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能,GEO的数据类型为zset。
基本操作
geoadd key longitude latitude member [longitude latitude member...] :
将给定的空间元素(longitude:经度、latitude:纬度、member:名字)添加到指定的键里面。
这些数据会以有序集合的形式被储存在键里面,从而使得georadius和georadiusbymember这样的命令可以在之后通过位置查询取得这些元素。
geoadd命令以标准的(x,y)格式接受参数,所以必须先输入经度,然后再输入纬度。
geoadd能够记录的坐标是有限的,非常接近两极的区域无法被索引,精确的坐标限制由EPSG:900913 / EPSG:3785 / OSGEO:41001 等坐标系统定义:有效的经度介于-180-180度之间,有效的纬度介于-85.05112878 度至 85.05112878 度之间。当用户尝试输入一个超出范围的经度或者纬度时,geoadd命令将返回一个错误。
geoadd cities:location 113.45 77.33 A
geoadd cities:location 85.24 45.63 B
geopos key member:获取坐标组中对应名字的坐标点。
geopos cities:location A
geodist key member1 member2 [unit]:计算坐标组中名字1与名字2之间的距离,unit为距离单位,默认m,可设为km。
geodist cities:location A B km
georadius key longitude latitude radius m|km|ft|mi [withcoord][withdist][withhash][asc|desc][count count] :以给定的经纬度为中心,返回key包含的位置元素当中,与中心的距离不超过给定最大距离(radius)的所有位置元素。
范围可以使用以下其中一个单位:
- m 表示单位为米。
- km 表示单位为千米。
- mi 表示单位为英里。
ft 表示单位为英尺。
在给定以下选项时,命令会返回额外的信息:
withdist:在返回位置元素的同时,将位置元素与中心之间的距离也一并返回。距离的单位和用户给定的范围单位保持一致。
- withcoord:将位置元素的经度和纬度也一并返回。
withhash:以52位有符号整数的形式,返回位置元素经过原始geohash编码的有序集合分值。这个选项主要用于底层应用或者调试,实际中的作用不大。
命令默认返回未排序的位置元素,通过以下两个参数,用户可以指定被返回位置元素的排序方式:
asc:根据中心的位置,按照从近到远的方式返回位置元素
desc:根据中心的位置,按照从远到近的方式返回位置元素。
在默认情况下,georadius命令会返回所有匹配的位置元素。虽然用户可以使用count选项去获取N个匹配元素,但是因为命令在内部可能会需要对所有被匹配的元素进行处理,所以在对一个非常大的区域进行搜索时,即使只使用count选项去获取少量元素,命令的执行速度也可能非常慢。但从另一方面说,使用count选项去减少需要返回的元素数量,对于减少带宽来说仍然是非常有用的。 georadius命令返回一个数组,具体来说:
在没有给定任何with选项的情况下,命令只会返回一个像[“Beijing”,”Tianjin”]这样的线性列表;
- 在指定了withcoord、withdist、withhash等选项的情况下,命令返回一个二层嵌套数组,内层的每个子数组就表示一个元素;
- 在返回嵌套数组时,子数组的第一个元素总是位置元素的名字。至于额外的信息,则会作为子数组的后续元素,按照以下顺序被返回:
(1).以浮点数格式返回的中心位置元素之间的距离,单位与用户指定范围时的单位一致。
(2).geohash整数
(3).由两个元素组成的坐标,分别为经度和纬度。
添加数据:
geoadd cities:location 1.1 1.1 bj
geoadd cities:location 1.1 2.2 tj
geoadd cities:location 1.1 3.3 nj
geoadd cities:location 2.2 1.1 cq
geoadd cities:location 3.3 1.1 cd
geoadd cities:location 2.2 2.2 cs
geoadd cities:location 2.2 3.3 wh
geoadd cities:location 3.3 2.2 fj
geoadd cities:location 3.3 3.3 gz
geoadd cities:location 2.2 5.5 sz
以上数据在水平上的布置:
bj tj nj
cq cs wh
cd fj gz
sz
以2.2 2.2为中心点获取其附近的数据
georadius cities:location 2.2 2.2 180 km ---->返回不包括sz在内的所有值
georadius cities:location 2.2 2.2 1800 km ---->返回所有值,包括sz
georadiusbymember key member radius m|km|ft|mi [withcoord][withdist][withhash][asc|desc][count count] :
这个命令和georadius命令一样,都可以找出位于指定范围内的元素,但是georadiusbymember的中心点是由给定的位置元素名称决定的,而不是像georadius那样,使用输入的经度和纬度来决定中心点。
georadiusbymember cities:location cs 180 km withdist
geohash key member [member...] :redis使用geohash将二维经纬度转换为一维字符串,字符串越长表示位置更精确,两个字符串越相似表示距离越近。
geohash cities:location cs
redis集群
主从复制
在生产环境中,如果只有一台数据库服务器,不但存在容量瓶颈,还有可能出现数据丢失的情况。尤其对于redis来说,更是如此。
redis运行在内存中,由于成本和硬件技术限制,一定会出现容量瓶颈问题,而且内存中的数据是属于瞬时状态的,一旦系统崩溃或硬件故障,一定会出现数据丢失。
在实际生产环境中,一般会使用主从结构来解决以上问题。
主从结构其实就是一种数据备份的方案。简单来说,就是使用两个或两个以上相同的数据库(在生产环境中,这些数据库一定不是在同一台计算机上),将其中一个数据库当做主数据库(master),而其它的数据库当做从数据库(replica--复制品,5.0之前的版本叫slave--奴隶)。主数据库只负责记录数据(写),从数据库只负责对外提供查询(读)。
当有数据写入主数据库时,将对应数据同步更新到从数据库,保证主数据库与从数据库的数据统一。实现数据同步的过程就是主从复制。
使用主从结构的好处:
- 当主数据库宕机后,可以使用某种策略让某台从数据库来代替主数据库,避免数据的丢失;
- 实现读写分离:
主数据库只负责写,从数据库只负责读,避免出现多个数据库之间数据不同步的问题。如果多个数据库之间数据不同步,就失去了数据备份的意义;
减轻数据库压力,在同一个数据库进行读写操作时,写操作比读操作更耗时,如果不进行读写分离,写操作会影响读操作性能;
搭建主从结构
需求:搭建主从结构(1主3从)。
步骤1:启动虚拟机,在Xshell中开8个连接,分别为master服务器(6379),replica服务器(6380),replica服务器(6381),replica服务器(6382),master客户端,replica客户端(6380),replica客户端(6381),replica客户端(6382)。
步骤2:在redis解压目录/conf中,将redis-6379.conf复制3份,分别为redis-6380.conf、redis-6381.conf、redis-6382.conf,使用sed指令可快速复制。
sed 's/6379/6380/g' redis-6379.conf > redis-6380.conf
步骤3:在master服务器(6379)连接中启动redis服务,在replica服务器(6380)连接中启动redis客户端。
步骤4:
使用客户端发送slaveof命令建立主从连接replicaof
在master客户端连接中建立与master服务器6379的连接 redis-cli -p 6379 在replica客户端(6380)建立与replica服务器6380的连接 redis-cli -p 6380 在6380中使用slaveof命令 slaveof 127.0.0.1 6379 验证: 1.查看master服务器和replica服务器的日志信息 2.在master客户端中set key value,在replica客户端中get key
启动服务器时设置参数建立主从连接
在replica服务器(6381)连接中启动redis客户端 redis-server conf/redis-6381.conf --slaveof 127.0.0.1 6379 验证: 1.查看master服务器和replica服务器的日志信息 2.在replica客户端中get key
修改配置文件建立主从连接
修改redis-6382.conf vim redis-6382.conf 在配置文件中新增配置:slaveof 127.0.0.1 6379 在replica服务器(6382)连接中启动redis客户端 redis-server conf/redis-6382.conf 验证: 1.查看master服务器和replica服务器的日志信息 2.在replica客户端中get key
哨兵模式
使用主从结构时,如果主数据库master宕机,则整个系统只能提供读服务,无法提供写服务,这种结果肯定是不能接受的。
因此,在master宕机后,需要通过某种策略,在现有的replica中选择一个,使其转化为一个master,从而保证系统的正常运行——采用哨兵模式解决这个问题。
哨兵(sentinel)是一个分布式系统,用于监控主从结构中的每台服务器,当出现故障时通过投票机制选择新的master,并将其余replica重新连接到新的master上。
哨兵也是一台redis服务器,但不对外提供数据服务。由于投票机制的存在,哨兵的数量通常设置为3以上的单数。
搭建哨兵结构
需求:搭建数量为3个哨兵结构——在上例的基础上增加3个哨兵即可。
步骤1:配置哨兵,哨兵的启动通过sentinel.conf实现。
复制哨兵配置文件到conf目录
cat sentinel.conf |grep -v "#" |grep -v "^$" > conf/sentinel-26379.conf
在conf目录中修改哨兵配置文件
vim sentinel-26379.conf
port 26379
daemonize no
pidfile /var/run/redis-sentinel.pid
#logfile ""
#指定文件存放路径
dir /usr/local/redis-5.0.5/data
#哨兵监控的主机ip及端口,mymaster位置为主机名,可自定义,
#端口后的2为哨兵投票的值,(n/2)+1个哨兵认为主机宕机时,开始进行投票,n为哨兵个数
sentinel monitor mymaster 127.0.0.1 6379 2
#设置主机未响应时间上限,超出上限哨兵认为主机已宕机
sentinel down-after-milliseconds mymaster 30000
#设定数据同步的从数据库个数,个数越小,压力越小,时间越长
sentinel parallel-syncs mymaster 1
#设定数据同步时间上限,超时同步失败
sentinel failover-timeout mymaster 180000
sentinel deny-scripts-reconfig yes
步骤2:在Xshell中开启3个连接:哨兵(26379)、哨兵(26380)、哨兵(26381),启动哨兵(哨兵结构的启动顺序为master->replica->sentinel)
在哨兵(26379)连接中启动哨兵
redis-sentinel conf/sentinel-26379.conf
在哨兵(26380)连接中启动哨兵
redis-sentinel conf/sentinel-26380.conf
在哨兵(26381)连接中启动哨兵
redis-sentinel conf/sentinel-26381.conf
观察哨兵启动后的日志信息,可看到主数据库、从数据库以及哨兵信息。也可以启动哨兵客户端,通过info命令查看对应信息,与此同时,对应的哨兵配置文件都会发生相应变化。
在Xshell中新开连接,启动哨兵客户端
redis-cli -p 26379
通过info命令查看哨兵信息
info sentinel
信息如下:
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=127.0.0.1:6379,slaves=3,sentinels=3
在conf目录中查看哨兵配置文件
cat sentinel-26379.conf
配置文件内容会发生变化,内容如下:
port 26379
daemonize no
pidfile "/var/run/redis-sentinel.pid"
#logfile ""
dir "/usr/local/redis-5.0.5/data"
sentinel myid c4b9c9d0e0249d0a7490e69de254c7eb5a948c8a
sentinel deny-scripts-reconfig yes
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel config-epoch mymaster 0
sentinel leader-epoch mymaster 0
# Generated by CONFIG REWRITE
protected-mode no
sentinel known-replica mymaster 127.0.0.1 6381
sentinel known-replica mymaster 127.0.0.1 6380
sentinel known-replica mymaster 127.0.0.1 6382
sentinel known-sentinel mymaster 127.0.0.1 26381 75dd8cecd56df75e1705ab46970e19b98458518b
sentinel known-sentinel mymaster 127.0.0.1 26380 4d85ed65ece17264767c00d0ac67b08f15dc2d56
sentinel current-epoch 0
步骤3:验证哨兵结构——手动让master宕机(在master服务器连接中ctrl+c,中断服务),30秒后,哨兵会投票产生一台新的master。
集群(Redis Cluster )
Redis 是一个开源的 key-value 存储系统,由于出众的性能,大部分互联网企业都用来做服务器端缓存。Redis 在3.0版本前只支持单实例模式,虽然支持主从模式、哨兵模式部署来解决单点故障,但是现在互联网企业动辄几百G的数据,可完全是没法满足业务的需求,所以,Redis 在 3.0 版本以后就推出了集群模式。
Redis 集群采用了P2P的模式,完全去中心化。Redis 把所有的 Key 分成了 16384 个 slot(Hash槽),每个 Redis 实例负责其中一部分 slot 。集群中的所有信息(节点、端口、slot等),都通过节点之间定期的数据交换而更新。<br /> Redis 客户端可以在任意一个 Redis 实例发出请求,如果所需数据不在该实例中,通过重定向命令引导客户端访问所需的实例。
redis3.0以上支持集群,自带集群管理工具redis-trib.rb。redis5.0以上版本不再依赖于ruby,可直接使用redis-cli创建集群,5.0以下版本则必须安装ruby环境。linux默认ruby版本较低,一般为2.0以下版本,需要安装2.0以上版本。
安装ruby环境步骤: 步骤1:安装开发工具
yum groupinstall "Development tools"
步骤2:清理已安装过的ruby
yum erase ruby ruby-libs ruby-mode ruby-rdoc ruby-irb ruby-ri ruby-docs
步骤3:安装依赖
yum -y install zlib-devel curl-devel openssl-devel httpd-devel apr-devel apr-util-devel mysql-devel
步骤4:安装ruby,将ruby-2.7.1.tar.gz上传到/usr/local/src,解压后将ruby-2.7.1移动到/usr/local/ruby,进入ruby目录后依次执行下面命令。
./configure
#如./configure执行出错:configure: error: You need a C++ compiler for C++ support.
#执行命令:yum install -y gcc gcc-c++
make && make install
步骤5:查看ruby信息
ruby -v
如当前系统没有安装redis,可下载redis,并上传解压安装,也可使用以下命令安装redis最新版本。
gem install redis
需求:配置3主3从的redis集群环境。
步骤1:清空/usr/local/redis/data和/usr/local/redis/conf目录,将redis.conf复制一份到conf目录下。
cat redis.conf |grep -v "#" |grep -v "^$" > conf/redis-6379.conf
步骤2:编辑redis-6379.conf。
内容如下:
bind 127.0.0.1 --->实际企业开发,IP地址不会在同一台机器上
port 6379
daemonize no
databases 16
dbfilename dump-6379.rdb
save 10 2
dir /usr/local/redis/data
appendonly yes
appendfilename "appendonly-6379.aof"
appendfsync always
rdbcompression yes
rdbchecksum yes
#表示当前服务器是cluster集群中的一个节点
cluster-enabled yes
#设置clcluster节点的配置文件名(以端口号区分),避免启时使用默认配置文件而导致冲突
cluster-config-file nodes-6379.conf
#cluster节点超时下线时间,毫秒
cluster-node-timeout 10000
步骤3:复制配置文件。
sed "s/6379/6380/g" redis-6379.conf >redis-6380.conf
sed "s/6379/6381/g" redis-6379.conf >redis-6381.conf
sed "s/6379/6382/g" redis-6379.conf >redis-6382.conf
sed "s/6379/6383/g" redis-6379.conf >redis-6383.conf
sed "s/6379/6384/g" redis-6379.conf >redis-6384.conf
步骤4:在Xshell中开启8个连接,分别为master(6379)、master(6380)、master(6381)、replica(6382)、replica(6383)、replica(6384),并分别在每个连接中使用不同的配置文件启动redis服务。
master(6379):redis-server /usr/local/redis/conf/redis-6379.conf
master(6380):redis-server /usr/local/redis/conf/redis-6380.conf
master(6381):redis-server /usr/local/redis/conf/redis-6381.conf
replica(6382):redis-server /usr/local/redis/conf/redis-6382.conf
replica(6383):redis-server /usr/local/redis/conf/redis-6383.conf
replica(6384):redis-server /usr/local/redis/conf/redis-6384.conf
步骤5:进入/usr/local/redis/src,使用对应命令搭建redis集群。
#5.0及以上版本,使用以下命令:--cluster-replicas后的数字用于设置主服务器对应的从服务器个数
redis-cli --cluster create 127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384 --cluster-replicas 1
#5.0以下版本,使用以下命令:--replicas后的数字用于设置主服务器对应的从服务器个数
./redis-trib.rb create --replicas 1 127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384
执行命令后,在提示输入处输入yes
>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460 -->创建主,分配槽为0-5460
Master[1] -> Slots 5461 - 10922 -->创建主,分配槽为5461 - 10922
Master[2] -> Slots 10923 - 16383 -->创建主,分配槽为10923 - 16383
Adding replica 127.0.0.1:6383 to 127.0.0.1:6379 -->为6379添加从6383
Adding replica 127.0.0.1:6384 to 127.0.0.1:6380 -->为6380添加从6384
Adding replica 127.0.0.1:6382 to 127.0.0.1:6381 -->为6381添加从6382
>>> Trying to optimize slaves allocation for anti-affinity
[WARNING] Some slaves are in the same host as their master
M: da61fc33fefec6fb0d78d022efd417109f6d78d1 127.0.0.1:6379
slots:[0-5460] (5461 slots) master
M: 4693349c34ddc231dfdb0c92c6dded518b1e54ff 127.0.0.1:6380
slots:[5461-10922] (5462 slots) master
M: 0e8fdc76c1827c1c49276bb0c0068b754b3a0669 127.0.0.1:6381
slots:[10923-16383] (5461 slots) master
S: 75b01f1a677f45e385ea30bf24faa01a3a3e710f 127.0.0.1:6382
replicates 4693349c34ddc231dfdb0c92c6dded518b1e54ff
S: ae2e48154d6525ada192b7ed2ac455fd2efecdc2 127.0.0.1:6383
replicates 0e8fdc76c1827c1c49276bb0c0068b754b3a0669
S: 956c299ba71b0547aedec0d270fc9068d600da15 127.0.0.1:6384
replicates da61fc33fefec6fb0d78d022efd417109f6d78d1
Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join
...
>>> Performing Cluster Check (using node 127.0.0.1:6379)
M: da61fc33fefec6fb0d78d022efd417109f6d78d1 127.0.0.1:6379
slots:[0-5460] (5461 slots) master
1 additional replica(s)
S: 75b01f1a677f45e385ea30bf24faa01a3a3e710f 127.0.0.1:6382
slots: (0 slots) slave
replicates 4693349c34ddc231dfdb0c92c6dded518b1e54ff
M: 0e8fdc76c1827c1c49276bb0c0068b754b3a0669 127.0.0.1:6381
slots:[10923-16383] (5461 slots) master
1 additional replica(s)
M: 4693349c34ddc231dfdb0c92c6dded518b1e54ff 127.0.0.1:6380
slots:[5461-10922] (5462 slots) master
1 additional replica(s)
S: 956c299ba71b0547aedec0d270fc9068d600da15 127.0.0.1:6384
slots: (0 slots) slave
replicates da61fc33fefec6fb0d78d022efd417109f6d78d1
S: ae2e48154d6525ada192b7ed2ac455fd2efecdc2 127.0.0.1:6383
slots: (0 slots) slave
replicates 0e8fdc76c1827c1c49276bb0c0068b754b3a0669
出现如下内容,集群搭建成功。
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
步骤6:使用redis集群存取数据。在XShell中开启两个客户端,一个主客户端,一个从客户端。
主客户端:
redis-cli -c -->连接默认的6379端口的服务器,-c参数为集群独有,必须写,否则报错,无法操作集群
执行保存命令,会自动根据key值找到对应的Hash槽,由客户端重定向发送请求存入对应的Hash槽
set name tom -->Redirected to slot [5798] located at 127.0.0.1:6380
从客户端:
redis-cli -c -p 6382
get name -->tom,自动重定向到对应的Hash槽获取数据
查看集群节点信息:
cluster info
cluster nodes
查看某个节点信息
info
**设备故障后集群中的主从切换**
- 从服务器故障掉线:当从服务器故障下线后,其绑定的主服务器中会标记从服务器掉线,与此同时会向集群中的其他主服务器和从服务器该服务器下线信息。当该从服务器重新上线后,会自动与对应的主服务器同步。
- 主服务器故障掉线:当主服务器掉线后,其绑定的从服务器会每隔一秒向主服务器发送连接请求,在达到配置文件中通过clster-node-timeout属性设置的超时上限,而主服务器没有重新上线,此时从服务器会切换成主服务器。
Spring Data Redis
jedis客户端在编程实施方面存在如下不足:
- connection管理缺乏自动化,connection-pool的设计缺少必要的容器支持。
- 数据操作需要关注“序列化”/“反序列化”,因为jedis的客户端API接受的数据类型为string和byte,对结构化数据(json,xml,pojo等)操作需要额外的支持。
- 事务操作纯粹为硬编码。
pub/sub功能,缺乏必要的设计模式支持,对于开发者而言需要关注的太多。
spring-data-redis针对jedis提供了如下功能:
连接池自动管理,提供了一个高度封装的“RedisTemplate”类。
针对jedis客户端中大量api进行了归类封装,将同一类型操作封装为operation接口。
ValueOperations:简单K-V操作 SetOperations:set类型数据操作 ZSetOperations:zset类型数据操作 HashOperations:针对map类型的数据操作 ListOperations:针对list类型的数据操作
提供了对key的“bound”(绑定)便捷化操作API,可以通过bound封装指定的key,然后进行一系列的操作而无须“显式”的再次指定Key,即BoundKeyOperations。
BoundKeyOperations包含以下组件: BoundValueOperations BoundSetOperations BoundListOperations BoundSetOperations BoundHashOperations。
将事务操作封装,有容器控制。
针对数据的“序列化/反序列化”,提供了多种可选择策略(RedisSerializer)。
JdkSerializationRedisSerializer:
POJO对象的存取场景,使用JDK本身序列化机制,将pojo类通过ObjectInputStream/ObjectOutputStream进行序列化操作,最终redis-server中将存储字节序列。是默认的序列化策略。
StringRedisSerializer/GenericToStringSerializer:
Key或者value为字符串的场景,根据指定的charset对数据的字节序列编码成string,是“new String(bytes, charset)”和“string.getBytes(charset)”的直接封装。是最轻量级和高效的策略。
JacksonJsonRedisSerializer/GenericJackson2JsonRedisSerializer:
jackson-json工具提供了javabean与json之间的转换能力,可以将pojo实例序列化成json格式存储在redis中,也可以将json格式的数据转换成pojo实例。因为jackson工具在序列化和反序列化时,需要明确指定Class类型,因此此策略封装起来稍微复杂。【需要jackson-mapper-asl工具支持】
OxmSerializer:
提供了将javabean与xml之间的转换能力,目前可用的三方支持包括jaxb,apache-xmlbeans;redis存储的数据将是xml工具。不过使用此策略,编程将会有些难度,而且效率最低;不建议使用。【需要spring-oxm模块的支持】 针对“序列化和发序列化”,JdkSerializationRedisSerializer和StringRedisSerializer是最基础的策略。 原则上,我们可以将数据存储为任何格式以便应用程序存取和解析(其中应用包括app,hadoop等其他工具),不过在设计时仍然不推荐直接使用“JacksonJsonRedisSerializer”和“OxmSerializer”,因为无论是json还是xml,他们本身仍然是String。 如果你的数据需要被第三方工具解析,那么数据应该使用StringRedisSerializer而不是JdkSerializationRedisSerializer。如果你的数据格式必须为json或者xml,那么在编程级别,在redisTemplate配置中仍然使用StringRedisSerializer,在存储之前或者读取之后,使用“SerializationUtils”工具转换成json或者xml。
基于设计模式和JMS开发思路,将pub/sub的API设计进行了封装,使开发更加便捷。
- spring-data-redis中,并没有对sharding提供良好的封装,如果你的架构是基于sharding,那么你需要自己去实现,这也是spring-data-redis和jedis相比,唯一缺少的特性。
入门案例
需求:使用spring data redis实现对远程服务器中的redis存储。
步骤1:环境准备:
准备一台安装redis的虚拟机,并启动redis服务,安装并启动redisDesktopManager(redis桌面管理系统),连接虚拟机上的redis服务。
安装并启动redisDesktopManager(redis桌面管理系统),连接虚拟机上的redis服务。
步骤2:创建maven项目,导入依赖。
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.wsjy</groupId>
<artifactId>springdataredis</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!--jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.3</version>
</dependency>
<!-- jpa相关 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.17</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>5.4.10.Final</version>
</dependency>
<!-- spring相关 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
<!-- spring整合jpa相关 -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>2.1.8.RELEASE</version>
</dependency>
<!-- spring整合redis相关 -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>2.1.8.RELEASE</version>
</dependency>
<!--jackson数据转换-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
</dependencies>
</project>
步骤3:创建spring核心配置文件applicationContext-redis.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!--配置jedis连接池参数-->
<!-- JedisPoolConfig的父类GenericObjectPoolConfig中有三个参数用于设置连接池信息
maxTotal:最大连接数
maxIdle:最大空闲连接数
minIdle:最小空闲连接数
三个参数有默认值,如需修改,可使用bean标签的子元素<property>进行设置,
也可使用p命名空间设置bean标签属性方式实现
-->
<bean id="jedisPoolConfig"
class="redis.clients.jedis.JedisPoolConfig"
p:maxTotal="30" p:maxIdle="20" p:minIdle="10" />
<!-- 配置jedis连接工厂
使用p命名空间为工厂对象设置主机名称,端口和连接池参数
-->
<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
p:hostName="192.168.0.111" p:port="6379" p:poolConfig-ref="jedisPoolConfig"/>
<!--配置redis模板:引用jedis连接工厂获取jedis连接-->
<bean id="redisTemplate"
class="org.springframework.data.redis.core.RedisTemplate"
p:connectionFactory-ref="jedisConnectionFactory"/>
</beans>
步骤4:测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext-redis.xml")
public class SpringDateRedisTest {
//自动注入redis模板对象
@Autowired
private RedisTemplate redisTemplate;
//测试保存
@Test
public void testSet(){
//获取操作简单键值对的对象
ValueOperations vo = redisTemplate.opsForValue();
vo.set("name","tom");
}
}
执行测试方法后,在redisDesktopManager中可看到数据保存成功,但数据是以二进制的方式进行的保存。这种原因是由于此时采用的默认的序列化策略JdkSerializationRedisSerializer。
如果希望改变序列化策略,可以在核心配置文件中修改redisTemplate,也可以在方法中调用redisTemplate的setter方法进行设置。
修改序列化策略
修改核心配置文件
<!--
配置redis模板:引用jedis连接工厂获取jedis连接
设置序列化策略
p:keySerializer-ref="stringRedisSerializer"
p:valueSerializer-ref="stringRedisSerializer"
当操作数据类型为hash时,则需要使用
p:hashKeySerializer-ref=""
p:hashValueSerializer-ref=""
直接在redisTemplate中设置序列化策略的影响范围太大,不建议,
一般会在redisTemplate对象被调用前,通过setter方法进行序列化策略的修改
-->
<bean id="redisTemplate"
class="org.springframework.data.redis.core.RedisTemplate"
p:connectionFactory-ref="jedisConnectionFactory"
p:keySerializer-ref="stringRedisSerializer"
p:valueSerializer-ref="stringRedisSerializer"
/>
<!--序列化策略:提供给redisTemplate使用-->
<bean id="stringRedisSerializer"
class="org.springframework.data.redis.serializer.StringRedisSerializer">
</bean>
调用setter方法
@Test
public void testSet(){
//调用setter方法设置序列化策略
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
ValueOperations vo = redisTemplate.opsForValue();
vo.set("name3","tom3");
}
常用数据类型操作
string
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext-redis.xml")
public class RedisStringTest {
@Autowired
private RedisTemplate redisTemplate;
private ValueOperations vo=null;
@Before
public void init(){
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
vo = redisTemplate.opsForValue();
}
/******************删除******************/
@Test
public void testDelete(){
//vo对象本来就是一个操作键值对的对象,无法执行删除操作
//只能由redisTemplate来执行删除操作
//delete(key|collection)
//删除key对应的键值对
// redisTemplate.delete("name");
//批量删除
ArrayList<String> strs = new ArrayList<>();
strs.add("a");
strs.add("b");
strs.add("c");
strs.add("d");
strs.add("e");
redisTemplate.delete(strs);
}
/******************新增******************/
@Test
public void testAdd(){
//set(key,value)
//vo.set("name","abcdef");
//保存或替换(对字符串进行修改)
//set(key,value,offset)
//当key存在,将key对应的值从offset处起,使用value进行替换
//当key不存在,执行保存操作,value值放到offset处
//vo.set("name1","dd",2);
//有时间限制的保存
//set(key,value,time,timeUnit)
//10秒后数据自动被清除
//vo.set("name","usa",10, TimeUnit.SECONDS);
//保存,如果key不存在,如果存在,什么都不做
//vo.setIfAbsent("name1","haha");
//批量保存
// Map map=new HashMap<>();
// map.put("a","1");
// map.put("b","2");
// map.put("c","3");
// map.put("d","4");
// vo.multiSet(map);
//追加:key存在则追加,key不存在则保存
vo.append("e","0");
}
/******************查询******************/
@Test
public void testQuery(){
//获取
//get(key)
//System.out.println(vo.get("name"));
//get(key,start,end)
//注意,此处是左右都闭合,即,查询结果中会包含start和end
//System.out.println(vo.get("name", 2, 4));
//批量获取
// ArrayList<String> strs = new ArrayList<>();
// strs.add("a");
// strs.add("b");
// strs.add("c");
// strs.add("d");
// System.out.println(vo.multiGet(strs));
//size(key)
System.out.println(vo.size("name"));
}
/******************修改******************/
@Test
public void testUpdate(){
//对数值型的数据进行修改
//增加
// vo.increment("a");//自增1
//increment(key,step):step值可为整数也可为浮点数
// vo.increment("a",5);//自增5
// vo.increment("a",0.2);//自增0.2
// System.out.println(vo.get("a"));
//减少
//increment(key[,step]):step值只能是整数,
//要减少浮点数可以使用increment(key,step),step传负值的浮点即可
// 此外,如果key对应的value值不为整数,不能做自减操作
// vo.increment("a",0.8);
// vo.decrement("a");
// vo.decrement("a",3);
// System.out.println(vo.get("a"));
//修改:
//getAndSet(key,value)
//如果key存在,用value覆盖key对应的值
//如果key不存在,执行新增操作
vo.getAndSet("name2","tom");
}
}
hash(key-value)
实体类:Subject.java
package com.wsjy.domain;
import java.io.Serializable;
//必须实现序列化接口
public class Subject implements Serializable {
private Integer subNo;
private String subName;
public Integer getSubNo() {
return subNo;
}
public void setSubNo(Integer subNo) {
this.subNo = subNo;
}
public String getSubName() {
return subName;
}
public void setSubName(String subName) {
this.subName = subName;
}
@Override
public String toString() {
return "Subject{" +
"subNo=" + subNo +
", subName='" + subName + '\'' +
'}';
}
}
测试类:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext-redis.xml")
public class RedisHashTest {
@Autowired
private RedisTemplate redisTemplate;
private HashOperations<String,String, Subject> ho=null;
@Before
public void init(){
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());
ho = redisTemplate.opsForHash();
}
/************************新增***********************/
@Test
public void testAdd(){
Subject sub1 = new Subject();
sub1.setSubNo(1);
sub1.setSubName("java");
Subject sub2 = new Subject();
sub2.setSubNo(2);
sub2.setSubName("redis");
Subject sub3 = new Subject();
sub3.setSubNo(3);
sub3.setSubName("maven");
//新增一个hash键值对
//put(key,hashkey,hashvalue)
//在key和hashkey都已经存在时,设置不同的hashvalue会将原来的hashvalue覆盖掉,
ho.put("subject","1",sub3);
//如果key存在,向key中新增一个hash键值对,
//如果key不存在,新建一个key,将hash键值对存进去
// ho.putIfAbsent("subject","2",sub2);
//批量新增
//putAll(key,Map<hashkey,hashvalue>)
// HashMap<String, Subject> map = new HashMap<>();
// map.put("1",sub1);
// map.put("2",sub2);
// ho.putAll("subject",map);
}
/************************查询***********************/
@Test
public void testQuery(){
//判断key下的hashkey是否存在,存在返回true,不存在返回false
Boolean flag = ho.hasKey("subject", "1");
System.out.println(flag);
//根据key和hashkey获取对应的hashvalue
Subject subject = ho.get("subject","1");
System.out.println(subject);
//获取所有hashkey
Set<String> keys = ho.keys("subject");
System.out.println(keys);
//获取所有的hashvalue
System.out.println(ho.values("subject"));
//获取所有的hash键值对
Map<String, Subject> map = ho.entries("subject");
System.out.println(map);
}
/************************删除***********************/
@Test
public void testDelete(){
//删除指定key下的hashkey对应的键值对
//delete(key,hashkey不定长数组)
//要注意的是,如果将key中的所有键值对都删除了,此时key对的hash数据也就被删除了
ho.delete("subject","1","2");
}
}
list
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext-redis.xml")
public class RedisListTest {
@Autowired
private RedisTemplate redisTemplate;
private ListOperations<String, String> lo=null;
@Before
public void init(){
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
lo = redisTemplate.opsForList();
}
@Test
public void testDelete(){
//从左边删除第一个
// lo.leftPop("subjects");
//从右边删除第一个
// lo.rightPop("subjects");
//list中可以存在重复的内容,使用remove()可以指定删除重复内容中的第几个
//remove(key,count,object)
//remove删除时会从左到右,使用count记录object在key中出现的次数,
//执行删除操作时会根据count的值删除对应出现次数的object
lo.remove("subjects",1,"html");
}
@Test
public void testAdd(){
//左:
//leftPush(key,value):添加一个
lo.leftPush("subjects","java");
//leftPushAll(key,value...):添加多个
lo.leftPushAll("subjects","mysql","javascript","html");
//右:
// lo.rightPush("subjects","jquery");
// lo.rightPushAll("subjects","mybatis","linux","git");
//如果key存在,将value保存到key中,如果key不存在,什么操作都不做
// lo.leftPushIfPresent("subjects","spring");
lo.leftPushAll("students","tom","kate","jack","rose");
//rightPopAndLeftPush(key1,key2)
//rightPop操作key1
//LeftPush操作key2
//从key1中删除最右边的元素并将其添加到key2的最左边
lo.rightPopAndLeftPush("students","subjects");
}
@Test
public void testQuery(){
//index(key,index)
//index》=0,从左向右找
//index<0,从右向左找
System.out.println(lo.index("subjects", -1));
//范围查询
//range(key,start,end):全闭合,查询结果包含start和end
//只能从左向右找,即start值一定会>=0
//小技巧:查询全部,可以使用0,-1
System.out.println(lo.range("subjects", 0, -1));
}
}
set
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext-redis.xml")
public class RedisSetTest {
@Autowired
private RedisTemplate redisTemplate;
private SetOperations<String, String> so=null;
@Before
public void init(){
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
so = redisTemplate.opsForSet();
}
@Test
public void testDelete(){
//指定删除元素
//remove(key,value...)
// so.remove("subjects1","mysql","java");
//随机删除
//一个
// so.pop("subjects1");
//多个
so.pop("subjects1",3);
}
@Test
public void testAdd(){
//add(key,value...)
//注意:add()方法内value值可以重复,但重复的值无法被保存
so.add("subjects1","java","mysql","maven","mybatis");
so.add("subjects2","java","mysql","spring","html");
}
@Test
public void testQuery(){
//查询所有
// System.out.println(so.members("subjects1"));
//随机查询一个
// System.out.println(so.randomMember("subjects1"));
//随机查询多个
//注意:随机查询多个时,有可能出现重复查询
// System.out.println(so.randomMembers("subjects1", 3));
//去重
System.out.println(so.distinctRandomMembers("subjects", 2));
//多集合联合查询
//求交集:取集合中同时存在的元素
// Set<String> intersect = so.intersect("subjects1", "subjects2");
// System.out.println(intersect);
//求并集:返回所有集合中的元素,去重之后的结果
// Set<String> union = so.union("subjects1", "subjects2");
// System.out.println(union);
//求差集:返回key1与key2不同的内容
//difference(key1, key2):
// System.out.println(so.difference("subjects1", "subjects2"));
//differenceAndStore(key1, key2,key3)
//将key1和key2的差集存入key3
so.differenceAndStore("subjects1", "subjects2","subjects");
}
}
zset(sorted_set)
zset通过score来进行排序。
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext-redis.xml")
public class RedisZSetTest {
@Autowired
private RedisTemplate redisTemplate;
private ZSetOperations<String, String> zso=null;
@Before
public void init(){
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
zso = redisTemplate.opsForZSet();
}
@Test
public void testDelete(){
//删除key中指定value
// zso.remove("aaa","桃子");
//范围删除
//removeRange(key,start,end):删除指定索引范围的元素
// zso.removeRange("bbb",2,6);
//removeRange(key,min,max):删除指定score范围的元素
zso.removeRangeByScore("bbb",2.5,35);
}
@Test
public void testAdd(){
//增加一个
//add(key,value,score)
zso.add("foods","桃子",5.2);
//增加多个
//TypedTuple是ZSetOperations接口的内部接口,对应的实现类为DefaultTypedTuple
// Set<ZSetOperations.TypedTuple<String>> typedTuples = new HashSet<>();
// typedTuples.add(new DefaultTypedTuple<>("苹果",8.6));
// typedTuples.add(new DefaultTypedTuple<>("芒果",25.0));
// typedTuples.add(new DefaultTypedTuple<>("香蕉",6.5));
// typedTuples.add(new DefaultTypedTuple<>("凤梨",3.6));
//
// zso.add("goods",typedTuples);
// Set<ZSetOperations.TypedTuple<String>> typedTuples = new HashSet<>();
// typedTuples.add(new DefaultTypedTuple<>("猪肉",30.0));
// typedTuples.add(new DefaultTypedTuple<>("牛肉",45.0));
// typedTuples.add(new DefaultTypedTuple<>("羊肉",45.0));
// typedTuples.add(new DefaultTypedTuple<>("鱼肉",15.0));
// typedTuples.add(new DefaultTypedTuple<>("鸡肉",25.0));
//
// zso.add("foods",typedTuples);
}
@Test
public void testQuery(){
//统计key中value的个数
// System.out.println(zso.zCard("goods"));
// System.out.println(zso.size("goods"));
//查询value在key中的索引:从0开始
//rank(key, value)-->正序/reverseRank(key, value)-->倒序
// System.out.println(zso.rank("goods", "桃子"));
// System.out.println(zso.reverseRank("goods", "桃子"));
//范围查询
//range(key,start,end)/reverseRange(key,start,end):
//全闭合,包含start和end,start和end为索引值
// System.out.println(zso.range("goods", 0, -1));
//统计指定score范围内元素个数
//count(key,min,max)
//全闭合,包含min和max
// System.out.println(zso.count("goods", 6.5, 15.0));
//获取有序集合中score在指定的最小值与最大值之间的所有成员集合 闭合区间
//rangeByScore(key, min, max)
// System.out.println(zso.rangeByScore("goods", 2.5, 10));
//从有序集合中从指定位置(offset)开始,取 count个范围在(min)与(max)之间的成员集合
//rangeByScore(key,min,max,offset,count)
// System.out.println(zso.rangeByScore("goods", 2.5, 10, 0, 3));
// 获取有序集合中分数在指定的最小值与最大值之间的所有成员的TypedTuple集合 闭合区间
//rangeByScoreWithScores(key, min, max)
// Set<ZSetOperations.TypedTuple<String>> goods = zso.rangeByScoreWithScores("goods", 2.5, 10);
// for (ZSetOperations.TypedTuple<String> good : goods) {
// System.out.println(good.getValue()+":"+good.getScore());
// }
//rangeByScoreWithScores(key,min,max,offset,count)
// Set<ZSetOperations.TypedTuple<String>> goods = zso.rangeByScoreWithScores("goods", 2.5, 10,0,2);
// for (ZSetOperations.TypedTuple<String> good : goods) {
// System.out.println(good.getValue()+":"+good.getScore());
// }
//求两个有序集合key和key1中相同成员的交集(score相加),并存到目标集合key2中。没有共同的成员则忽略
//intersectAndStore(key,key1,key2)
// zso.intersectAndStore("goods","foods","aaa");
//求两个有序集合key和key1的并集,并存到目标集合key2中。 如果存在相同的成员,则分数相加。
//unionAndStore(key,key1,key2)
zso.unionAndStore("goods","foods","bbb");
}
@Test
public void testUpdate(){
//修改score的值
//incrementScore(key,value,step)
//step取正,增加,取负,减少
zso.incrementScore("goods","桃子",-0.8);
}
}
springboot集成redis
1 引入依赖
<dependencies>
<!--spring-data-redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.17</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.22</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
2 配置文件
server:
servlet:
context-path: /subject
port: 8888
spring:
datasource: #配置数据源
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///taotao
username: root
password: root
thymeleaf: #配置thymeleaf模板
servlet:
content-type: text/html
encoding: UTF-8
prefix: classpath:/templates/
suffix: .html
enabled: true
redis: #配置redis
database: 0
host: localhost
port: 6379
password:
mybatis: #mybatis配置
type-aliases-package: com.wsjy.domain
mapper-locations: classpath:/com/wsjy/mapper/*.xml
注意:启动类上要加@MapperScan(“dao包全限定名”)
3 开发实体类
Subject.java
//注意:要使用redis缓存实体类对象,必须让实体类实现序列化接口,否则在redis序列化过程中会报错
@Data
@Accessors(chain = true)
public class Subject implements Serializable {
private Integer sub_no;
private String sub_name;
}
4 开发DAO接口以及Mapper(resources/com/wsjy/mapper/subjectMapper.xml)
SubjectDao.java
public interface SubjectDao {
List<Subject> findAll();
}
subjectMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wsjy.dao.SubjectDao">
<select id="findAll" resultType="Subject">
select * from t_subject
</select>
</mapper>
5 开发service及实现
public interface SubjectService {
List<Subject> getAllSubjects();
}
@Service
public class SubjectServiceImpl implements SubjectService {
@Autowired
private SubjectDao subjectDao;
@Autowired
private RedisTemplate<Object,Object> redisTemplate;
@Override
public List<Subject> getAllSubjects() {
List<Subject> subs = (List<Subject>) redisTemplate.opsForValue().get("subs");
if (null == subs) {//从数据库中获取
System.out.println("mysql");
subs=subjectDao.findAll();
redisTemplate.opsForValue().set("subs",subs);
}else{//从redis中获取
System.out.println("redis");
}
return subs;
}
}
6 开发controller
@Controller
public class SubjectController {
@Autowired
private SubjectService subjectService;
@GetMapping("/getAll")
public String getAll(Model model){
List<Subject> subs = subjectService.getAllSubjects();
model.addAttribute("subs",subs);
return "index";
}
}
7 编写前端:
index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<table border="1" align="center">
<tr th:each="sub:${subs}">
<td th:text="${sub?.sub_no}"></td>
<td th:text="${sub?.sub_name}"></td>
</tr>
</table>
</body>
</html>
8 测试
- http://localhost:8888/subject/getAll
9 修改序列化器
测试访问后,使用keys *命令查看,可见redis的0号库中已经存在了一个key:”\xAC\xED\x00\x05t\x00\x04subs”,不便于阅读,这是由于redis默认使用了JdkSerializationRedisSerializer序列化器,如果希望看到key为subs,需要更改redis序列化器为StringRedisSerializer。
@Service
public class SubjectServiceImpl implements SubjectService {
@Autowired
private SubjectDao subjectDao;
@Autowired
private RedisTemplate<Object,Object> redisTemplate;
@Override
public List<Subject> getAllSubjects() {
//创建序列化器
RedisSerializer serializer = new StringRedisSerializer();
//设置redis序列化策略:StringRedisSerializer
redisTemplate.setKeySerializer(serializer);
List<Subject> subs = (List<Subject>) redisTemplate.opsForValue().get("subs");
if (null == subs) {//从数据库中获取
System.out.println("mysql");
subs=subjectDao.findAll();
redisTemplate.opsForValue().set("subs",subs);
}else{//从redis中获取
System.out.println("redis");
}
return subs;
}
}
10 redis配置类
在上述编码过程中,会出现大量设置序列化器的代码,实际工作中肯定不能允许出现如此多的代码冗余,一般会在配置类中对序列化器进行相应设置。
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String, Object> stringObjectRedisTemplate = new RedisTemplate<>();
stringObjectRedisTemplate.setConnectionFactory(redisConnectionFactory);
//key序列化为string
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(Charset.forName("utf-8"));
stringObjectRedisTemplate.setKeySerializer(stringRedisSerializer);
stringObjectRedisTemplate.setHashKeySerializer(stringRedisSerializer);
//value序列化为JSON
Jackson2JsonRedisSerializer<Object> objectJackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
stringObjectRedisTemplate.setValueSerializer(objectJackson2JsonRedisSerializer);
stringObjectRedisTemplate.setHashValueSerializer(objectJackson2JsonRedisSerializer);
return stringObjectRedisTemplate;
}
}
要注意的是,spring-data-redis新版本中,默认使用的不再是Jedis,而是lettuce。在配置类中构建自定义的 ReisTemplate中注入RedisConnectionFactory时,需要依赖lettuce,因此,必须引入相应依赖。
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</dependency>
11 高并发访问时,如何保证只查询一次数据库?
当系统出现高并发访问时,以上代码无法保证只查询一次数据库,其余请求到redis中查询。
@Controller
public class SubjectController {
@Autowired
private SubjectService subjectService;
@GetMapping("/getAll")
public String getAll(Model model){
// List<Subject> subs = subjectService.getAllSubjects();
// model.addAttribute("subs",subs);
//模拟10000个线程同时发送请求
ExecutorService executorService = Executors.newFixedThreadPool(20);
for (int i = 0; i <10000; i++) {
executorService.submit(()->subjectService.getAllSubjects());
}
return "index";
}
}
执行测试,可看到控制台中输出了大量的mysql,证明高并发下,确实是无法保证只查询数据库一次的。要确保redis中subs为空也只有一个线程去查询数据库,其他的线程全从redis中获取数据,可使用线程同步来解决。
@Service
public class SubjectServiceImpl implements SubjectService {
@Autowired
private SubjectDao subjectDao;
@Autowired
private RedisTemplate<Object,Object> redisTemplate;
@Override
public List<Subject> getAllSubjects() {
List<Subject> subs = (List<Subject>) redisTemplate.opsForValue().get("subs");
//使用双重检测锁来保证多线程并发时,只有一个线程去数据库中查询,其他的线程都去redis缓存中获取
if (null == subs) {
synchronized (this){
subs = (List<Subject>) redisTemplate.opsForValue().get("subs");
if (null == subs) {
System.out.println("mysql");
subs=subjectDao.findAll();
redisTemplate.opsForValue().set("subs",subs);
}else{
System.out.println("redis");
}
}
}else{
System.out.println("redis");
}
return subs;
}
}
12 其它redis操作
/*新增*/
//controller
@GetMapping("/addSubject/{sub_no}/{sub_name}")
public String addSubject(@PathVariable("sub_no")Integer sub_no,
@PathVariable("sub_name")String sub_name,
Model model){
subjectService.addSubject(sub_no,sub_name);
return getAll(model);
}
//service
public void addSubject(Integer sub_no,String sub_name){
RedisSerializer serializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(serializer);
List<Subject> subs = getAllSubjects();
subs.add(new Subject().setSub_no(sub_no).setSub_name(sub_name));
redisTemplate.opsForValue().set("subs",subs);
subjectDao.addSubject(sub_no,sub_name);
}
//dao
void addSubject(@RequestParam("sub_no") Integer sub_no,
@RequestParam("sub_name") String sub_name);
//xml映射文件
<insert id="addSubject">
insert into t_subject(sub_no,sub_name) values(#{sub_no},#{sub_name})
</insert>
/*修改*/
//controller
@GetMapping("/updateSubject/{sub_no}/{sub_name}")
public String updateSubject(@PathVariable("sub_no")Integer sub_no,
@PathVariable("sub_name")String sub_name,
Model model){
subjectService.updateSubject(sub_no,sub_name);
return getAll(model);
}
//service
public void updateSubject(Integer sub_no,String sub_name){
List<Subject> subs = getAllSubjects();
for (Subject sub : subs) {
if (sub_no.equals(sub.getSub_no())) {
sub.setSub_name(sub_name);
break;
}
}
redisTemplate.opsForValue().set("subs",subs);
subjectDao.updateSubjectBySub_no(sub_no,sub_name);
}
//dao
void updateSubjectBySub_no(@RequestParam("sub_no") Integer sub_no,
@RequestParam("sub_name") String sub_name);
//xml映射文件
<update id="updateSubjectBySub_no">
update t_subject set sub_name=#{sub_name} where sub_no=#{sub_no}
</update>
/*删除*/
//controller
@GetMapping("/delete/{key}")
public String delete(@PathVariable("key") String key){
subjectService.deleteRedisContent(key);
return "index";
}
//service
public void deleteRedisContent(String key){
RedisSerializer serializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(serializer);
redisTemplate.delete(key);
}
注意:如果缓存到redis中时设置了序列化策略,则在删除时也必须设置相同的序列化策略,否则redisTemplate会使用默认的JdkSerializationRedisSerializer,这会导致程序执行成功,但无法删除redis键的情况出现。
13 案例
需求:模拟实现QQ音乐排行榜,记录音乐的访问量、点赞数、根据音乐点赞量显示音乐排行榜。
步骤1:导入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.43</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.22</version>
</dependency>
</dependencies>
步骤2:配置文件
server:
port: 8888
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.0.116:3306/music?characterEncoding=UTF-8&useSSL=false
username: root
password: root
redis:
host: 192.168.0.116
port: 6379
步骤3:设计数据库
CREATE TABLE `t_music` (
`id` int(6) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`create_time` bigint(20) DEFAULT NULL,
`visit_count` int(11) DEFAULT NULL,
`likes_count` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8
步骤4:创建实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@TableName("t_music")
public class Music {
@TableId(type=IdType.AUTO)
private Integer id;
private String name;
private Long createTime;
private Integer visitCount;
private Integer likesCount;
}
步骤5:dao、service、controller
//dao
public interface MusicMapper extends BaseMapper<Music> {
}
//dto
@Data
@Builder
public class MusicScore implements Comparable<MusicScore> {
private Integer id;
private Integer likesScore;
private Integer visitScore;
//比较并排序
@Override
public int compareTo(MusicScore musicScore) {
if (likesScore > musicScore.getLikesScore()) {
return 1;
} else if (likesScore == musicScore.getLikesScore()) {
if (visitScore > musicScore.getVisitScore()) {
return 1;
} else if (visitScore == musicScore.getVisitScore()) {
return 0;
}
}
return -1;
}
}
//service
public interface MusicService extends IService<Music> {
//添加音乐
void addMusic(Music music);
//点赞数变更
Integer updateLikesById(Integer id);
//访问量变更
Integer updateVisitById(Integer id);
//刷新排行榜
void flushSort();
//查询排行榜
List<Music> findSort();
}
//service实现类
@Service
@Transactional
public class MusicServiceImpl extends ServiceImpl<MusicMapper,Music> implements MusicService {
@Resource
private MusicMapper musicMapper;
@Resource
private StringRedisTemplate stringRedisTemplate;
public void addMusic(Music music){
music.setCreateTime(System.currentTimeMillis());
music.setLikesCount(0);
music.setVisitCount(0);
//向数据库保存的同时,将点赞数和访问量保存到redis,
//要注意的是,排行榜是不会实时更新的,否则大量对数据库进行写操作,会极大的影响程序性能。
//将所有音乐id保存到集合中,便于后期刷新排行榜时,从hash中得到所有音乐的点赞数和访问量相关数据
if (save(music)) {
stringRedisTemplate.opsForHash().put("music:"+music.getId(),
"likes:count",
music.getLikesCount().toString());
stringRedisTemplate.opsForHash().put("music:"+music.getId(),
"visit:count",
music.getVisitCount().toString());
stringRedisTemplate.opsForSet().add("music:ids",music.getId().toString());
}
}
public Integer updateLikesById(Integer id){
//先去redis中查询
Object likesCount = stringRedisTemplate.opsForHash().get("music:" + id, "likes:count");
Integer updateLikesCount=null;
//如果没有值,前往数据库中查询
if (ObjectUtils.isEmpty(likesCount)) {
Music music = musicMapper.selectById(id);
updateLikesCount=music.getLikesCount()+1;
stringRedisTemplate.opsForHash().put("music:"+id,"likes:count",updateLikesCount.toString());
return updateLikesCount;
}
updateLikesCount=Integer.parseInt(likesCount.toString())+1;
stringRedisTemplate.opsForHash().put("music:"+id,"likes:count",updateLikesCount.toString());
return updateLikesCount;
}
public Integer updateVisitById(Integer id){
//先去redis中查询
Object visitCount = stringRedisTemplate.opsForHash().get("music:" + id, "visit:count");
Integer updateVisitCount=null;
//如果没有值,前往数据库中查询
if (ObjectUtils.isEmpty(visitCount)) {
Music music = musicMapper.selectById(id);
updateVisitCount=music.getVisitCount()+1;
stringRedisTemplate.opsForHash().put("music:"+id,"visit:count",updateVisitCount.toString());
return updateVisitCount;
}
updateVisitCount=Integer.parseInt(visitCount.toString())+1;
stringRedisTemplate.opsForHash().put("music:"+id,"visit:count",updateVisitCount.toString());
return updateVisitCount;
}
/**
* 刷新排行榜
* 根据点赞数来决定音乐排行
*/
public void flushSort(){
//查询所有音乐的id值
Set<String> ids = stringRedisTemplate.opsForSet().members("music:ids");
List<MusicScore> sortMusicScores=new ArrayList<>();
//根据id从redis的hash中查询所有音乐的点赞数量和访问数量
//使用一个传递值的dto对象来存储音乐id,点赞数,访问数
//该对象实现Comparable接口,在类中实现了比较排序的方法(根据点赞数和访问量)
ids.forEach(id->{
Object visitCount = stringRedisTemplate.opsForHash().get("music:" + id, "visit:count");
Object likesCount = stringRedisTemplate.opsForHash().get("music:" + id, "likes:count");
sortMusicScores.add(MusicScore.builder().id(Integer.parseInt(id))
.likesScore(Integer.parseInt(likesCount.toString()))
.visitScore(Integer.parseInt(visitCount.toString()))
.build());
});
//使用工具类对所有音乐进行排序
Collections.sort(sortMusicScores);
sortMusicScores.forEach(System.out::println);
//每次刷新排行榜时,都删除原有排行榜,然后重新生成
stringRedisTemplate.delete("music:sort");
int j=0;//指定显示排行榜的音乐数量
for (int i = sortMusicScores.size()-1; i>=0 ; i--) {
if (++j>3) break;
MusicScore musicScore = sortMusicScores.get(i);
//将排完序后的音乐id保存至zset中
stringRedisTemplate.opsForZSet().add("music:sort",musicScore.getId().toString(),musicScore.getLikesScore());
}
}
public List<Music> findSort(){
//查询所有排过序的音乐id
Set<String> sorts = stringRedisTemplate.opsForZSet().range("music:sort", 0, -1);
//根据id从数据库中查询数量并返回
//以下代码为正序排序
Object[] objects = sorts.toArray();
ArrayList<Music> musics = new ArrayList<>();
for (int i = objects.length-1; i >=0; i--) {
musics.add(musicMapper.selectById(Integer.parseInt(objects[i].toString())));
}
return musics;
}
}
//controller
@RestController
@RequestMapping("music")
public class MusicController {
@Resource
private MusicService musicService;
@GetMapping
public Result findAll(){
return new Result(true, StatusCode.OK,"查询成功",musicService.list(null));
}
@PostMapping
public Result addMusic(Music music){
musicService.addMusic(music);
return new Result(true, StatusCode.OK,"新增成功");
}
@GetMapping("/{id}")
public Result findById(@PathVariable Integer id){
return new Result(true,StatusCode.OK,"根据id查询成功",musicService.getById(id));
}
@PutMapping("likes/{id}")
public Result updateLikesById(@PathVariable Integer id){
return new Result(true,StatusCode.OK,"点赞数量更新成功",musicService.updateLikesById(id));
}
@PutMapping("visit/{id}")
public Result updateVisitById(@PathVariable Integer id){
return new Result(true,StatusCode.OK,"访问数量更新成功",musicService.updateVisitById(id));
}
@GetMapping("flushSort")
public Result flushSort(){
musicService.flushSort();
return new Result(true,StatusCode.OK,"刷新排行榜成功");
}
@GetMapping("findSort")
public Result showSort(){
return new Result(true,StatusCode.OK,"查看排行榜成功",musicService.findSort());
}
}
//启动类
@SpringBootApplication
@EnableTransactionManagement
@MapperScan("com.woniu.mapper")
public class RedisMusicApplication {
public static void main(String[] args) {
SpringApplication.run(RedisMusicApplication.class, args);
}
}