Redis

Redis

文章目录

一. Redis缓存入门

1.1 简介
1.1.1 背景

我们现在的项目架构中,基本上是Web服务器(Tomcat)和数据库独立部署,独占服务器资源,随着用户数的增长,并发读写数据库,会加大数据库访问压力,导致性能的下降,严重时直接导致系统宕机,例如:

此时,我们可以在Tomcat同服务器上中增加本地缓存,并在外部增加分布式缓存,缓存热门数据。也就是通过缓存能把绝大多数请求在读写数据库前拦截掉,大大降低数据库压力。例如:

基于这样的一种架构设计,于是类似redis的一些分布式数据库就诞生了。

1.1.2 Redis 概述

Redis是一个key-value存储系统(官网:http://redis.io),是一个分布式缓存数据库。在DB-Engines.com的数据库排行中, Redis上升排行第七,如图所示:

1.1.3 版本及参考说明

​ Redis的次版本号(第一个小数点后的数字)为偶数的版本是稳定版本(2.4、2.6等),奇数为非稳定版本(2.5、2.7),一般推荐在生产环境使用稳定版本。最新版本6.2.2,新增了stream的处理方式,性能更高。Redis官方是不支持windows平台的,windows版本是由微软自己建立的分支,基于官方的Redis源码上进行编译、发布、维护的,所以windows平台的Redis版本要略低于官方版本。

Redis 相关参考网址如下所示:

Bootnb 相关:https://www.runoob.com/redis/redis-tutorial.html
Redis 官网:https://redis.io/
源码地址:https://github.com/redis/redis
Redis 在线测试:http://try.redis.io/
Redis 命令参考:http://doc.redisfans.com/
1.2 Redis初始操作
1.2.1 启动redis服务

​ Docker 环境下的启动(docker环境启动多个需要运行多个容器):

docker start redis01 #底层也是通过redis-server启动,start单词后的redis01为容器名

​ docker 中查看redis 服务

docker ps

​ 查看启动的redis进程信息

ps -ef|grep redis
root      3511     1  0 16:29 ?   00:00:01 redis-server *:6379
root      3515     1  0 16:29 ?   00:00:01 redis-server 127.0.0.1:6380
1.2.2 进入redis容器

docker exec -it redis01 bash #redis01 为容器名

1.2.3 登陆redis服务

​ 登陆本地redis

redis-cli
或者
redis-cli -p 6379
或者
redis-cli -p 6379 -a  password #-a后面为password,此操作需要开启redis.conf文件中的 requirepass选项

​ 登陆远程redis

redis-cli  -h ip  -p 6379  -a  password
1.2.3 查看redis信息

首先登陆redis,然后输入info指令,例如

127.0.0.1:6379> info		#查看当前redis节点的详细配置信息
1.2.4 清空redis屏幕

清除redis屏幕内容

127.0.0.1:6379> clear
1.2.5 退出redis服务

退出redis服务,例如

127.0.0.1:6379> exit
1.2.6 关闭redis服务

关闭redis服务,例如:

127.0.0.1:6379> shutdown
1.2.7 系统帮助

可以基于help指令查看相关指令帮助,例如

127.0.0.1:6379> help
redis-cli 2.8.19
Type: "help @<group>" to get a list of commands in <group>
      "help <command>" for help on <command>
      "help <tab>" to get a list of possible help topics
      "quit" to exit
127.0.0.1:6379> help type

  TYPE key
  summary: Determine the type stored at key
  since: 1.0.0
  group: generic
1.3 Redis数据存储操作
1.3.1 简易数据存取

​ 基于查看redis中的key

127.0.0.1:6379> keys *
(empty list or set)

​ 基于key/value形式存储数据

127.0.0.1:6379> set test1 123
OK
127.0.0.1:6379> set test2 ab
OK
127.0.0.1:6379> keys *
1) "test1"
2) "test2"

​ 基于key获取redis中存储的数据

127.0.0.1:6379> get test1
"123"
127.0.0.1:6379> get test2
"ab"
127.0.0.1:6379> get test3
(nil)
127.0.0.1:6379>

清除当前数据库数据

127.0.0.1:6379> flushdb
OK

清除所有数据库数据

127.0.0.1:6379> flushall
OK
1.3.2 Key有效时间设计

实际工作中我们经常要控制redis中key的有效时长,例如秒杀操作的计时,缓存数据的有效时长等。

​ Expire (设置生效时长-单位秒)

语法:EXPIRE key seconds

127.0.0.1:6379> set bomb tnt
OK
127.0.0.1:6379> expire bomb 10
(integer) 1
127.0.0.1:6379> ttl bomb
(integer) 5
127.0.0.1:6379> ttl bomb
(integer) 3
127.0.0.1:6379> ttl bomb
(integer) 3
127.0.0.1:6379> ttl bomb
(integer) 2
127.0.0.1:6379> ttl bomb
(integer) 1
127.0.0.1:6379> ttl bomb
(integer) -2
127.0.0.1:6379> ttl bomb
(integer) -2
127.0.0.1:6379>

其中,TTL查看key的剩余时间,当返回值为-2时,表示键被删除。
当 key 不存在时,返回 -2 。 当 key 存在但没有设置剩余生存时间时,返回 -1 。

​ Persist (取消时长设置)

通过persist让对特定key设置的生效时长失效。

语法:PERSIST key

127.0.0.1:6379> set bomb tnt
OK
127.0.0.1:6379> expire bomb 60
(integer) 1
127.0.0.1:6379> ttl bomb
(integer) 49
127.0.0.1:6379> persist bomb
(integer) 1
127.0.0.1:6379> ttl bomb
(integer) -1
127.0.0.1:6379>

其中,设置新的数据时需要重新设置该key的生存时间,重新设置值也会清除生存时间。

​ pexpire (单位毫秒)

pexpire 让key的生效时长以毫秒作为计量单位,这样可以做到更精确的时间控制。例如,可应用于秒杀场景。

语法:PEXPIRE key milliseconds

127.0.0.1:6379> set bomb tnt
OK
127.0.0.1:6379> pexpire bomb 10000
(integer) 1
127.0.0.1:6379> ttl bomb
(integer) 6
127.0.0.1:6379> ttl bomb
(integer) 3
127.0.0.1:6379> ttl bomb
(integer) -2
127.0.0.1:6379>

二. Redis常用数据类型

2.1 简介
2.1.1 概述

​ Redis作为一种key/value结构的数据存储系统,为了便于对数据进行进行管理,提供了多种数据类型。然后,基于指定类型存储我们项目中产生的数据,例如用户的登陆信息,购物车信息,商品详情信息等等。

2.1.2 常用数据类型

Reids中基础数据结构包含字符串、散列,列表,集合,有序集合。工作中具体使用哪种类型要结合具体场景。

2.2 String类型操作实践

​ 字符串类型是redis中最简单的数据类型,它存储的值可以是字符串,其最大字符串长度支持到512M。基于此类型,可以实现博客的字数统计,将日志不断追加到指定key,实现一个分布式自增iid,实现一个博客的的点赞操作等

2.2.1 incr/incrby

当存储的字符串是整数时,redis提供了一个实用的命令INCR,其作用是让当前键值递增,并返回递增后的值。

语法:INCR key

127.0.0.1:6379> set num 1
(integer) 1
127.0.0.1:6379> incr num
(integer) 2
127.0.0.1:6379> keys *
1) "num"	
127.0.0.1:6379> incr num
127.0.0.1:6379>

说明,如果num不存在,则自动会创建,如果存在自动+1。

​ 指定增长系数

语法:INCRBY key increment

127.0.0.1:6379> incrby num 2
(integer) 5
127.0.0.1:6379> incrby num 2
(integer) 7
127.0.0.1:6379> incrby num 2
(integer) 9
127.0.0.1:6379>
2.2.2 decr/decrby

减少指定的整数
DECR key 按照默认步长(默认为1)进行递减

DECRBY key decrement 按照指定步长进行递减

127.0.0.1:6379> incr num
(integer) 10
127.0.0.1:6379> decr num
(integer) 9
127.0.0.1:6379> decrby num 3
2.2.3 append

向尾部追加值。如果键不存在则创建该键,其值为写的value,即相当于SET key value。返回值是追加后字符串的总长度。

语法:APPEND key value

127.0.0.1:6379> keys *
1) "num"
2) "test1"
3) "test"
127.0.0.1:6379> get test
"123"
127.0.0.1:6379> append test "abc"
(integer) 6
127.0.0.1:6379> get test
"123abc"
127.0.0.1:6379>
2.2.4 strlen

字符串长度,返回数据的长度,如果键不存在则返回0。注意,如果键值为空串,返回也是0。
语法:STRLEN key

127.0.0.1:6379> get test
"123abc"
127.0.0.1:6379> strlen test
(integer) 6
127.0.0.1:6379> strlen tnt
(integer) 0
127.0.0.1:6379> set tnt ""
OK
127.0.0.1:6379> strlen tnt
(integer) 0
127.0.0.1:6379> exists tnt
(integer) 1
127.0.0.1:6379>
2.2.5 mset/mget

同时设置/获取多个键值
语法:MSET key value [key value …]
MGET key [key …]

127.0.0.1:6379> flushall
OK
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> mset a 1 b 2 c 3
OK
127.0.0.1:6379> mget a b c
1) "1"
2) "2"
3) "3"
127.0.0.1:6379>
2.2.6 del
127.0.0.1:6379> set a 100
OK
127.0.0.1:6379> del a
(integer) 1
127.0.0.1:6379> keys *
(empty array)
2.2.7 小节面试分析
  • 博客的字数统计如何实现?(strlen)
  • 如何将审计日志不断追加到指定key?(append)
  • 你如何实现一个分布式自增id?(incr-雪花算法)
  • 如何实现一个博客的的点赞操作?(incr,decr)
2.3 Hash类型应用实践

​ Redis散列类型相当于Java中的HashMap,实现原理跟HashMap一致,一般用于存储对象信息,存储了字段(field)和字段值的映射,一个散列类型可以包含最多232-1个字段。

2.3.1 hset/hget

语法结构

HSET key field value
HGET key field
HMSET key field value [field value…]
HMGET key field [field]
HGETALL key 获得对象中所有值

HSET和HGET赋值和取值

127.0.0.1:6379> hset user username chenchen
(integer) 1
127.0.0.1:6379> hget user username
"chenchen"
127.0.0.1:6379> hset user username chen
(integer) 0
127.0.0.1:6379> keys user
1) "user"
127.0.0.1:6379> hgetall user
1) "username"
2) "chen"
127.0.0.1:6379> 
127.0.0.1:6379> hset user age 18
(integer) 1
127.0.0.1:6379> hset user address "xi'an"
(integer) 1
127.0.0.1:6379> hgetall user
1) "username"
2) "chen"
3) "age"
4) "18"
3) "address"
4) "xi'an"
127.0.0.1:6379>

HSET命令不区分插入和更新操作,当执行插入操作时HSET命令返回1,当执行更新操作时返回0。

2.3.2 hincrby
127.0.0.1:6379> hdecrby article total 1		#执行会出错
127.0.0.1:6379> hincrby article total -1		#没有hdecrby自减命令
(integer) 1
127.0.0.1:6379> hget article total			#获取值
2.3.3 hmset/hmget

HMSET和HMGET设置和获取对象属性

127.0.0.1:6379> hmset person username tony age 18
OK
127.0.0.1:6379> hmget person age username
1) "18"
2) "tony"
127.0.0.1:6379> hgetall person
1) "username"
2) "tony"
3) "age"
4) "18"
127.0.0.1:6379>

注意:上面HMGET字段顺序可以自行定义

2.3.4 hexists
属性是否存在
127.0.0.1:6379> hexists killer
(error) ERR wrong number of arguments for 'hexists' command
127.0.0.1:6379> hexists killer a
(integer) 0
127.0.0.1:6379> hexists user username
(integer) 1
127.0.0.1:6379> hexists person age
(integer) 1
127.0.0.1:6379>
2.3.5 hdel

删除属性

127.0.0.1:6379> hdel user age
(integer) 1
127.0.0.1:6379> hgetall user
1) "username"
2) "chen"
127.0.0.1:6379> hgetall person
1) "username"
2) "tony"
3) "age"
4) "18"
127.0.0.1:6379>
2.3.6 hkeys/hvals

只获取字段名HKEYS或字段值HVALS

127.0.0.1:6379> hkeys person
1) "username"
2) "age"
127.0.0.1:6379> hvals person
1) "tony"
2) "18"
2.3.7 hlen

元素个数

127.0.0.1:6379> hlen user
(integer) 1
127.0.0.1:6379> hlen person
(integer) 2
127.0.0.1:6379>
2.3.8 小节面试分析
  • 发布一篇博客需要写内存吗?(需要,hmset)

  • 浏览博客内容会怎么做?(hmget)

  • 如何判定一篇博客是否存在?(hexists)

  • 删除一篇博客如何实现?(hdel)

  • 分布式系统中你登录成功以后是如何存储用户信息的?(hmset)

2.4 List类型应用实践

​ Redis的list类型相当于java中的LinkedList,其原理就就是一个双向链表。支持正向、反向查找和遍历等操作,插入删除速度比较快。经常用于实现热销榜,最新评论等的设计。

2.4.1 lpush

在key对应list的头部添加字符串元素

redis 127.0.0.1:6379> lpush mylist "world"
(integer) 1
redis 127.0.0.1:6379> lpush mylist "hello"
(integer) 2
redis 127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "world"
redis 127.0.0.1:6379>

​ 其中,Redis Lrange 返回列表中指定区间内的元素,区间以偏移量 START 和 END 指定。 其中 0 表示列表的第一个元素, 1 表示列表的第二个元素,以此类推。 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推

2.4.2 rpush

在key对应list的尾部添加字符串元素

redis 127.0.0.1:6379> rpush mylist2 "hello"
(integer) 1
redis 127.0.0.1:6379> rpush mylist2 "world"
(integer) 2
redis 127.0.0.1:6379> lrange mylist2 0 -1
1) "hello"
2) "world"
redis 127.0.0.1:6379>
2.4.3 del

清空集合元素,例如

redis 127.0.0.1:6379> del mylist
2.4.4 linsert

在key对应list的特定位置之前或之后添加字符串元素

redis 127.0.0.1:6379> rpush mylist3 "hello"
(integer) 1
redis 127.0.0.1:6379> rpush mylist3 "world"
(integer) 2
redis 127.0.0.1:6379> linsert mylist3 before "world" "there"
(integer) 3
redis 127.0.0.1:6379> lrange mylist3 0 -1
1) "hello"
2) "there"
3) "world"
redis 127.0.0.1:6379>
2.4.5 lset

设置list中指定下标的元素值(一般用于修改操作)

redis 127.0.0.1:6379> rpush mylist4 "one"
(integer) 1
redis 127.0.0.1:6379> rpush mylist4 "two"
(integer) 2
redis 127.0.0.1:6379> rpush mylist4 "three"
(integer) 3
redis 127.0.0.1:6379> lset mylist4 0 "four"
OK
redis 127.0.0.1:6379> lset mylist4 -2 "five"
OK
redis 127.0.0.1:6379> lrange mylist4 0 -1
1) "four"
2) "five"
3) "three"
redis 127.0.0.1:6379>
2.4.6 lrem

从key对应list中删除count个和value相同的元素,count>0时,按从头到尾的顺序删除

redis 127.0.0.1:6379> rpush mylist5 "hello"
(integer) 1
redis 127.0.0.1:6379> rpush mylist5 "hello"
(integer) 2
redis 127.0.0.1:6379> rpush mylist5 "foo"
(integer) 3
redis 127.0.0.1:6379> rpush mylist5 "hello"
(integer) 4
redis 127.0.0.1:6379> lrem mylist5 2 "hello"
(integer) 2
redis 127.0.0.1:6379> lrange mylist5 0 -1
1) "foo"
2) "hello"
redis 127.0.0.1:6379>
count<0时,按从尾到头的顺序删除
redis 127.0.0.1:6379> rpush mylist6 "hello"
(integer) 1
redis 127.0.0.1:6379> rpush mylist6 "hello"
(integer) 2
redis 127.0.0.1:6379> rpush mylist6 "foo"
(integer) 3
redis 127.0.0.1:6379> rpush mylist6 "hello"
(integer) 4
redis 127.0.0.1:6379> lrem mylist6 -2 "hello"
(integer) 2
redis 127.0.0.1:6379> lrange mylist6 0 -1
1) "hello"
2) "foo"
redis 127.0.0.1:6379>
count=0时,删除全部
redis 127.0.0.1:6379> rpush mylist7 "hello"
(integer) 1
redis 127.0.0.1:6379> rpush mylist7 "hello"
(integer) 2
redis 127.0.0.1:6379> rpush mylist7 "foo"
(integer) 3
redis 127.0.0.1:6379> rpush mylist7 "hello"
(integer) 4
redis 127.0.0.1:6379> lrem mylist7 0 "hello"
(integer) 3
redis 127.0.0.1:6379> lrange mylist7 0 -1
1) "foo"
redis 127.0.0.1:6379>
2.4.7 ltrim

保留指定key 的值范围内的数据

redis 127.0.0.1:6379> rpush mylist8 "one"
(integer) 1
redis 127.0.0.1:6379> rpush mylist8 "two"
(integer) 2
redis 127.0.0.1:6379> rpush mylist8 "three"
(integer) 3
redis 127.0.0.1:6379> rpush mylist8 "four"
(integer) 4
redis 127.0.0.1:6379> ltrim mylist8 1 -1
OK
redis 127.0.0.1:6379> lrange mylist8 0 -1
1) "two"
2) "three"
3) "four"
redis 127.0.0.1:6379>
2.4.8 lpop

从list的头部删除元素,并返回删除元素

redis 127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "world"
redis 127.0.0.1:6379> lpop mylist
"hello"
redis 127.0.0.1:6379> lrange mylist 0 -1
1) "world"
redis 127.0.0.1:6379>
2.4.9 rpop

从list的尾部删除元素,并返回删除元素:

redis 127.0.0.1:6379> lrange mylist2 0 -1
1) "hello"
2) "world"
redis 127.0.0.1:6379> rpop mylist2
"world"
redis 127.0.0.1:6379> lrange mylist2 0 -1
1) "hello"
redis 127.0.0.1:6379>
2.4.10 llen

返回key对应list的长度:

redis 127.0.0.1:6379> llen mylist5
(integer) 2
redis 127.0.0.1:6379>
2.4.11 lindex

返回名称为key的list中index位置的元素:

redis 127.0.0.1:6379> lrange mylist5 0 -1
1) "three"
2) "foo"
redis 127.0.0.1:6379> lindex mylist5 0
"three"
redis 127.0.0.1:6379> lindex mylist5 1
"foo"
redis 127.0.0.1:6379>
2.4.12 rpoplpush

从第一个list的尾部移除元素并添加到第二个list的头部,最后返回被移除的元素值,整个操作是原子的.如果第一个list是空或者不存在返回nil:
rpoplpush lst1 lst1
rpoplpush lst1 lst2

2.4.13 小节面试分析
  • 如何基于redis实现一个队列结构?(lpush/rpop)

  • 如何基于redis实现一个栈结构?(lpush/lpop)

  • 如何基于redis实现一个阻塞式队列?(lpush/brpop)

  • 如何实现秒杀活动的公平性?(先进先出-FIFO)

  • 通过list结构实现一个消息队列(顺序)吗?(可以,FIFO->lpush,rpop)

  • 用户注册时的邮件发送功能如何提高其效率?(邮件发送是要调用三方服务,底层通过队列优化其效率,队列一般是list结构)

  • 如何动态更新商品的销量列表?(卖的好的排名靠前一些,linsert)

  • 商家的粉丝列表使用什么结构实现呢?(list结构)

2.5 Set类型应用实践

​ Redis的Set类似Java中的HashSet,是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。Redis中Set集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。

2.5.1 sadd

添加元素,重复元素添加失败,返回0

127.0.0.1:6379> sadd name tony
(integer) 1
127.0.0.1:6379> sadd name hellen
(integer) 1
127.0.0.1:6379> sadd name rose
(integer) 1
127.0.0.1:6379> sadd name rose
(integer) 0
2.5.2 smembers

获取集合中成员,例如

127.0.0.1:6379> smembers name
1) “hellen”
2) “rose”
3) “tony”
2.5.3 spop

移除并返回集合中的一个随机元素

127.0.0.1:6379> smembers internet
1) "amoeba"
2) "redis"
3) "rabbitmq"
4) "nginx"
127.0.0.1:6379> spop internet
"rabbitmq"
127.0.0.1:6379> spop internet
"nginx"
127.0.0.1:6379> smembers internet
1) "amoeba"
2) "redis"
2.5.4 scard

获取集合中的成员个数

127.0.0.1:6379> scard name
(integer) 3
2.5.5 smove

移动一个元素到另外一个集合

127.0.0.1:6379> sadd internet amoeba nginx redis
(integer) 3
127.0.0.1:6379> sadd bigdata hadopp spark rabbitmq
(integer) 3
127.0.0.1:6379> smembers internet
1) "amoeba"
2) "redis"
3) "nginx"
127.0.0.1:6379> smembers bigdata
1) "hadopp"
2) "spark"
3) "rabbitmq"
127.0.0.1:6379> smove bigdata internet rabbitmq
(integer) 1
127.0.0.1:6379> smembers internet
1) "amoeba"
2) "redis"
3) "rabbitmq"
4) "nginx"
127.0.0.1:6379> smembers bigdata
1) "hadopp"
2) "spark"
127.0.0.1:6379>
2.5.6 sunion

实现集合的并集操作

127.0.0.1:6379> sunion internet bigdata
1) "redis"
2) "nginx"
3) "rabbitmq"
4) "amoeba"
5) "hadopp"
6) "spark"
2.5.7 sismember

判断集合中元素是否存在,存在则返回1,否则返回0

127.0.0.1:6379> sismember s1 C
(integer) 1
2.5.8 小节面试分析
  • 朋友圈的点赞功能你如何实现?(sadd,srem,smembers,scard) srem移除
  • 如何实现一个网站投票统计程序?
  • 你知道微博中的关注如何实现吗?

三. Java中操作redis实践

3.1 Redis 客户端
3.1.1 概述

Redis 是一种C/S 架构的分布式缓存数据库,它有自带的命令行客户端,也有对应的Java或其它语言客户端,可以在这些客户端中通过一些API对redis进行读写操作。

3.1.2 准备工作

第一步:创建工程。

创建maven父工程,例如03-redis,并在此工程下创建两个子工程,一个为sca-jedis,一个为sca-template,例如:

第二步:添加父工程依赖

修改父工程pom.xml文件,添加编译配置

<build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
3.2 Jedis的基本应用
3.2.1 简介

Jedis是Java中操作redis的一个客户端,类似通过jdbc访问mysql数据库。

3.2.2 准备工作

在sca-jedis 工程添加如下依赖

<dependencies>
 <dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.5.2</version>
 </dependency>

 <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
 </dependency>

 <dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.6</version>
 </dependency>
<dependencies>
3.2.3 Jeids基本应用实践

在Jedis工程中的src/test/java目录创建单元测类,例如:

package com.jt;

import org.junit.Test;
import redis.clients.jedis.Jedis;

import java.util.concurrent.TimeUnit;

public class JedisTests {

    @Test
    public void testGetConnection(){
        //假如不能连通,要注释掉redis.conf中 bind 127.0.0.1,
        //并将protected-mode的值修改为no,然后重启redis再试
        Jedis jedis=new Jedis("192.168.126.130",6379);
        //jedis.auth("123456");//假如在redis.conf中设置了密码
        String ping = jedis.ping();
        System.out.println(ping);
    }

    //字符串类型练习
    @Test
    public void testStringOper() throws InterruptedException {
        //建立链接(与redis建立链接)
        Jedis jedis=new Jedis("192.168.126.130",6379);
        //存储数据(key/value)
        jedis.set("count","1");
        jedis.set("id","10001");
        jedis.set("content","aaaaaaaadfas");
        //更新数据
        jedis.expire("id",1);//设置key的有效时长
        jedis.incr("count");//对key的值进行自增操作
        //获取数据
        String count = jedis.get("count");
        //TimeUnit是Java中枚举类型,SECONDS为枚举类型的实例,sleep底层会调用Thread.sleep()方法
        //TimeUnit.SECONDS.sleep(1);//休眠一秒
        Thread.sleep(1000);
        String id=jedis.get("id");
        Long num=jedis.strlen("content");
        System.out.println("cart.count="+count);
        System.out.println("id="+id);
        System.out.println("num="+num);
        //释放资源
        jedis.close();
    }
//json数据练习
@Test
 public void testJsonOper(){
     //构建对象
      Map<String,Object> map=new HashMap<>();
      map.put("id",100);
      map.put("title","spring 认证");
      map.put("content","very good");
     //将对象转换为json格式字符串
      Gson gson=new Gson();
      String jsonStr=gson.toJson(map);
      //将json字符串写入到redis
      Jedis jedis=new Jedis("192.168.126.128",6379);
      jedis.set("user",jsonStr);
      //读取redis中数据
      jsonStr=jedis.get("user");
      System.out.println(jsonStr);
      Map<String,Object> obj=gson.fromJson(jsonStr,Map.class);
      System.out.println(obj);
      jedis.close();
 }
//hash类型练习
  @Test
  public void testHashOper01(){
        //1.建立连接
        Jedis jedis=new Jedis("192.168.126.130",6379);
        //2.基于hash类型存储对象信息
        jedis.hset("member","id","101");
        jedis.hset("member","username","jack");
        jedis.hset("member","mobile","3333333");
        //3.更新hash类型存储的数据
        jedis.hset("member","username","tony");
        //4.获取hash类型数据信息
        String username=jedis.hget("member","username");
        String mobile = jedis.hget("member", "mobile");
        System.out.println(username);
        System.out.println(mobile);
        //5.释放资源
        jedis.close();
    }
//hash类型练习(直接存储map对象)
@Test
public void testHashOper02(){
    //1.建立连接
    Jedis jedis=new Jedis("192.168.126.130",6379);
    //2.存储一篇博客信息
    Map<String,String> map=new HashMap<>();
    map.put("x","100");
    map.put("y","200");
    jedis.hset("point",map);
    //3.获取博客内容并输出
    map=jedis.hgetAll("point");
    System.out.println(map);
    //4.释放资源
    jedis.close();
}
   /**
     * 测试:redis中list结构的应用
     * 基于FIFO(First In First Out)算法,借助redis实现一个队列
     */
    @Test
    public void testListOper01(){
        //1.建立连接
        Jedis jedis=new Jedis("192.168.126.130",6379);
        //2.存储数据
        jedis.lpush("lst1","A","B","C","C");
        //3.更新数据
        Long pos=jedis.lpos("lst1","A");//获取A元素的位置
        jedis.lset("lst1",pos,"D");//将A元素位置的内容修改为D
        //4.获取数据
        int len=jedis.llen("lst1").intValue();//获取lst1列表中元素个数
        List<String> rpop = jedis.rpop("lst1",len);//获取lst1列表中所有元素
        System.out.println(rpop);
        //5.释放资源
        jedis.close();
    }



//list类型练习:实现一个阻塞式队列

@Test
public void testListOper02(){
    //1.连接redis
    Jedis jedis=new Jedis("192.168.126.128",6379);
    //2.向队列存数据
    //jedis.lpush("list1","A","B","C");
    //3.按先进先出的顺序从队列取数据
    List<String> list= jedis.brpop(40,"list1");
    System.out.println(list);
    jedis.brpop(40,"list1");
    jedis.brpop(40,"list1");
    jedis.brpop(40,"list1");
    //4.释放资源
    jedis.close();
}
//set类型练习
@Test
public void testSetOper01() {
    //1.连接redis
    Jedis jedis = new Jedis("192.168.126.128", 6379);
    //2.朋友圈点赞
    jedis.sadd("count", "1", "1", "2");
    //3.取出点赞数
    Set<String> set = jedis.smembers("count");
    System.out.println(set);
    //4.释放资源
    jedis.close();
}
}
package com.jt;
import com.google.gson.Gson;
import org.junit.Test;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisSentinelPool;

import java.lang.reflect.MalformedParameterizedTypeException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

public class JedisTests {
    /**
     * 课堂练习:
     * 基于hash类型将testStringOper02中对象写入到redis,
     * 并且尝试进行查询,修改,删除等操作。
     */
    @Test
    public void testHash01(){
        //1.建立连接
        Jedis jedis=new Jedis("192.168.126.129",6379);
        //2.执行hash数据操作
        //2.1新增数据
        String key= UUID.randomUUID().toString();
        jedis.hset(key, "id", "500");
        jedis.hset(key, "name", "Jack");
        //2.2修改数据数据
        jedis.hset(key, "name", "Jim");
        //2.3查询数据
        Map<String, String> map = jedis.hgetAll(key);
        System.out.println(map);
        //2.4删除数据
        //jedis.del(key);
        //定时删除数据
        jedis.expire(key, 10);
        //3.释放资源
        jedis.close();
    }

    /**
     * 测试字符串操作
     */
    @Test
    public void testStringOper02(){
        //1.建立连接
        Jedis jedis=new Jedis("192.168.126.129",6379);
        //2.数据操作(将一个map对象转换为json字符串,然后写入到redis)
        //2.1构建map对象
        Map<String,String> map=new HashMap<>();
        map.put("id", "100");
        map.put("name", "Mike");
        //2.2将map转换为字符串
        Gson gson=new Gson();
        String jsonStr = gson.toJson(map);
        //2.3将字符串写入到redis
        String key= UUID.randomUUID().toString();
        jedis.set(key, jsonStr);
        //3.释放资源
        jedis.close();
    }

    /**
     * 测试字符串操作
     */
    @Test
    public void testStringOper01(){
        //1.建立连接
        Jedis jedis=new Jedis("192.168.126.129",6379);
        //2.执行redis数据操作(增删改查)
        //2.1新增数据
        jedis.set("id", "100");
        jedis.set("name", "tony");
        //2.2修改数据
        jedis.incr("id");
        jedis.incrBy("id", 100);//201
        jedis.set("name", "Mike");//Mike
        //2.3查询数据
        String id = jedis.get("id");
        String name = jedis.get("name");
        System.out.println("id="+id+";name="+name);
        //2.4删除数据
        jedis.del("name");
        //3.释放资源
        jedis.close();
    }
    /**
     * 测试是否可以连通redis。
     */
    @Test
    public void testGetConnection(){
        Jedis jedis=new Jedis("192.168.126.129",6379);
        String result = jedis.ping();
        System.out.println(result);
    }
}
3.2.4 连接池JedisPool连接池应用

我们直接基于Jedis访问redis时,每次获取连接,释放连接会带来很大的性能开销,可以借助Jedis连接池,重用创建好的连接,来提高其性能,简易应用方式如下:

package com.jt;

import org.junit.Test;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;


public class JedisPoolTests {
    @Test
   public void testJedisPool(){
        //定义连接池的配置
        JedisPoolConfig config=new JedisPoolConfig();
        config.setMaxTotal(1000);//最大连接数
        config.setMaxIdle(60);//最大空闲数
        //创建连接池
        JedisPool jedisPool=
        new JedisPool(config,"192.168.126.130",6379);
        //从池中获取一个连接
        Jedis resource = jedisPool.getResource();
        resource.auth("123456");
        //通过jedis连接存取数据
        resource.set("class","cgb2004");
        String clazz=resource.get("class");
        System.out.println(clazz);
        //将链接返回池中
        resource.close();
        //关闭连接池
        jedisPool.close();
    }
}

我们可以基于池对象,设计一个数据源,将来在业务中通过一个数据源对象,从池中获取连接,不用每次获取连接都要创建池对象,例如:

package com.jt.redis;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class JedisDataSource {

    private static final String IP="192.168.126.128";
    private static final int PORT=6379;//redis.conf 默认端口
    /**
     * volatile 关键通常用于修饰属性:
     * 1)保证多线程之间的可见性(一个线程对这个变量的值修改后,其它的线程立刻可见,一般用在多CPU或多核CPU环境下)
     * 2)禁止指令重排序 
     * 3)不能保证其原子性(不保证线程安全)
     */
    private static volatile JedisPool jedisPool;
    //方案1:饿汉式池对象的创建
    /*static{
        JedisPoolConfig config=new JedisPoolConfig();
        config.setMaxTotal(16);
        config.setMaxIdle(8);
        jedisPool=new JedisPool(config,IP,PORT);
    }
    public static Jedis getConnection(){
        return jedisPool.getResource();
    }*/
    //方案2:懒汉式池对象的创建
    public static Jedis getConnection(){
        if(jedisPool==null) {
            synchronized (JedisDataSource.class) {
                if (jedisPool == null) {
                    JedisPoolConfig config = new JedisPoolConfig();
                    config.setMaxTotal(16);
                    config.setMaxIdle(8);
                    jedisPool = new JedisPool(config, IP, PORT);
                    //创建对象分析(理论上是如下这个步骤,但是JVM内部会有指令重排序,可能先执行1,4 再执行2,3)
                    //1.开辟内存空间
                    //2.执行属性的默认初始化
                    //3.执行构造方法
                    //4.将创建的对象的内存地址赋值给jedisPool变量
                    //假如使用了volatile修饰jedisPool变量,可以保证如上几个步骤是顺序执行的
                }
            }
        }
        return jedisPool.getResource();
    }
    public static void close(){
        jedisPool.close();
    }
}
3.3 RedisTemplate基本应用
3.3.1 简介

RedisTemplate为SpringBoot工程中操作redis数据库的一个Java对象,此对象封装了对redis的一些基本操作。

3.3.2 准备工作

第一步:添加在sca-template工程添加依赖

<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.3.2.RELEASE</version>
                <scope>import</scope>
                <type>pom</type>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

第二步:创建工程配置文件application.yml,其内容如下:

spring:
  redis:
    host: 192.168.64.129  #写自己的ip
    port: 6379

第三步:创建工程启动类,例如:

package com.jt;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class RedisApplication {
    public static void main(String[] args) {
        SpringApplication.run(RedisApplication.class,args);
    }
}
3.3.3 RedisTemplate对象应用实践

RedisTemplate是一个专门用于实现对远端redis数据进行操作的对象,默认会采用JDK序列化方式存取数据,应用案例如下:

package com.jt;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jt.com.jt.pojo.Blog;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.serializer.RedisSerializer;

import java.time.Duration;
import java.util.Map;
import java.util.UUID;

@SpringBootTest
public class RedisTemplateTest {

    /*
    * 使用基于此对象(此对象为操作redis的一个模板对象)去操作远端的redis
    * 此对象进行数据存储时,数据会进行序列化,序列化方式为JDK自带的序列化机制
    * */
    @Autowired
    private RedisTemplate redisTemplate;

    /*
    * 测试是否能够连通
    * */
    @Test
    void testGetConnection(){
        RedisConnectionFactory connectionFactory = redisTemplate.getConnectionFactory();
        RedisConnection connection = connectionFactory.getConnection();
        String pang = connection.ping();
        System.out.println(pang);
    }

    @Test
    void testStringOper01(){
        //1.获取字符串操作对象
        ValueOperations vo = redisTemplate.opsForValue();
        //2.操作redis数据
        vo.set("x", 100);
        Object x = vo.get("x");
        System.out.println(x);
        //如果使用set方式存储数据后,就不可以再使用increment方式了
        //vo.increment("x");//不可以  会出现不是整数或者越界异常

        //如果使用increment自增方式存储数据后,则不可以使用get
        Long y = vo.increment("y");//此方法的返回值解释get key的值,但是我们不可以自己执行get
        //Object y = vo.get("y");//不可以,会出现反序列化异常
        System.out.println(y);

        //存储key/value并设置key的有效期 10秒
        vo.set("z",100, Duration.ofSeconds(10));
    }
    @Test
    void testStringOper02(){
        //1.获取字符串操作对象
        ValueOperations vo = redisTemplate.opsForValue();
        //2.按默认序列化方式存储数据
        String token = UUID.randomUUID().toString();
        vo.set(token, "admin");
        //3.指定序列化方式进行数据存储
        redisTemplate.setKeySerializer(RedisSerializer.string());
        redisTemplate.setValueSerializer(RedisSerializer.string());
        vo.set(token, "Mike");
        //4.更新数据(假如有对应的key,则直接进行覆盖操作)
        vo.set(token, "Jack");
        Object value = vo.get(token);
        System.out.println(value);
        //5.删除数据(当前对象不提供主动删除操作)(存数据时给定有效期-生产环境必须设置)
        vo.set("permission", "sys:resource:update",Duration.ofSeconds(5));
    }

    @Test
    void testHashOper01(){
        //1.获取Hash操作对象
        HashOperations ho = redisTemplate.opsForHash();
        //2.以Hash类型存储数据
        ho.put("blog101", "id", 100);
        ho.put("blog101", "title", "redis...");
        //3.获取数据
        Object id = ho.get("blog101", "id");
        Object title = ho.get("blog101", "title");
        System.out.println("id = "+id+" ; title = "+title);
        Map blog101 = ho.entries("blog101");//取key对应的所有的值
        System.out.println(blog101);
    }
    
    @Test
    void testListOper(){
      //向list集合放数据
        ListOperations listOperations = redisTemplate.opsForList();
        listOperations.leftPush("lstKey1", "100"); //lpush
        listOperations.leftPushAll("lstKey1", "200","300");
        listOperations.leftPush("lstKey1", "100", "105");
        listOperations.rightPush("lstKey1", "700");
        Object value= listOperations.range("lstKey1", 0, -1);
        System.out.println(value);
      //从list集合取数据
        Object v1=listOperations.leftPop("lstKey1");//lpop
        System.out.println("left.pop.0="+v1);
        value= listOperations.range("lstKey1", 0, -1);
        System.out.println(value);
    }

    @Test
    void testSetOper(){
        SetOperations setOperations=redisTemplate.opsForSet();
        setOperations.add("setKey1", "A","B","C","C");
        Object members=setOperations.members("setKey1");
        System.out.println("setKeys="+members);
        //........
    }

   @Test
    void testFlushdb(){
        redisTemplate.execute(new RedisCallback() {
            @Override
            public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
               //redisConnection.flushDb();
                redisConnection.flushAll();
                return "flush ok";
            }
        });
    }

}

创建Blog对象,然后基于RedisTemplate进行序列化实践,Blog代码如下

package com.jt.com.jt.pojo;


import java.io.Serializable;

public class Blog implements Serializable {
    private static final long serialVersionUID = -4208318918213062047L;
    private String title;
    private Long id;

    public String getTitle() {
        return title;
    }

    public Blog setTitle(String title) {
        this.title = title;
        return this;
    }

    public Long getId() {
        return id;
    }

    public Blog setId(Long id) {
        this.id = id;
        return this;
    }

    @Override
    public String toString() {
        return "Blog{" +
                "title='" + title + '\'' +
                ", id=" + id +
                '}';
    }
}

在RedisTemplateTests类中添加如下单元测试方法,进行测试,例如:

 /*
    * 设计一个blog对象,然后通过redisTemplate将此对象写入到redis数据库
    * 两种方案:
    *  方案一:基于ValueOperations对象实现数据存取 特点:操作方便,修改麻烦
    *  方案一:基于HashOperations对象实现数据存取 特点:操作麻烦,修改方便
    * */
    @Test
    void testPojoOper01(){
        //1.获取字符串操作对象
        ValueOperations vo = redisTemplate.opsForValue();
        //存取blog对象
        Blog blog = new Blog();
        blog.setId(100L).setTitle("redis");
        /*
         * 如果序列化对象没有实现Serializable这个接口的话,要将序列化方式调整为其他序列化方式,
         * 否则会序列化失败,如果实现了序列化接口的话,存数据的序列化方式可以用默认的,也可以用其他的
         * */
        //redisTemplate.setKeySerializer(RedisSerializer.json());
        //redisTemplate.setValueSerializer(RedisSerializer.json());
        vo.set("blog-jack",blog); //序列化
        Object blog1 = vo.get("blog-jack"); //反序列化
        System.out.println(blog1);
    }
    @Test
    void testPojoOper02() throws JsonProcessingException {
        //1.获取字符串操作对象
        HashOperations ho = redisTemplate.opsForHash();
        //存取blog对象
        Blog blog = new Blog();
        blog.setId(100L).setTitle("redis");
        //基于HashOperations存取Blog对象
        ObjectMapper objectMapper = new ObjectMapper();//jackson
        //将对象转为json
        String jsonStr = objectMapper.writeValueAsString(blog);
        //将json转为map
        Map map = objectMapper.readValue(jsonStr, Map.class);
        System.out.println(map);
        //存入redis
        ho.putAll("block-mike", map);
        //可以修改对应的值
        ho.put("block-mike", "id", "200");
        //取出键值对
        Map entries = ho.entries("block-mike");
        System.out.println(entries);
        
        //方向操作获取blog对象
        //将map转为json串
        String s = objectMapper.writeValueAsString(map);
        //将json串转为blog对象
        Blog blog1 = objectMapper.readValue(s, Blog.class);
        //输出blog对象
        System.out.println(blog1);
    }
3.3.4 StringRedisTemplate 对象应用实践

StringRedisTemplate 是一个特殊的RedisTemplate对象,默认基于字符串序列化方式存取数据,其应用方式如下:

package com.jt.redis;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@SpringBootTest
public class StringRedisTemplateTests {
    /**
     * 此对象为操作redis的一个客户端对象,这个对象
     * 对key/value采用了字符串的序列化(StringRedisSerializer)
     * 方式进行,redis数据的读写操作.
     */
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Test
    void testHashOper01(){
       //1.获取hash操作的对象
        HashOperations<String, Object, Object> vo =stringRedisTemplate.opsForHash();
       //2.读写redis数据
       //2.1存储一个对象
       vo.put("user", "id", "100");
       vo.put("user", "username", "tony");
       vo.put("user", "status", "1");
       //2.2获取一个对象
       //2.2.1获取对象某个属性值
       Object status =vo.get("user","status");
       System.out.println(status);
        //2.2.2获取对象某个key对应的所有值
       List<Object> user = vo.values("user");
       System.out.println(user);
    }
    @Test
    void testStringOper02() throws JsonProcessingException {
        //1.获取字符串操作对象(ValueOperations)
        ValueOperations<String, String> vo =
                stringRedisTemplate.opsForValue();
        //2.读写redis中的数据
        Map<String,String> map=new HashMap<>();
        map.put("id","100");
        map.put("title","StringRedisTemplate");
        //将map对象转换为json字符串写到redis数据库
        String jsonStr=//jackson (spring-boot-starter-web依赖中自带)
        new ObjectMapper().writeValueAsString(map);
        vo.set("blog", jsonStr);
        jsonStr=vo.get("blog");
        System.out.println(jsonStr);
        //将json字符串转换为map对象
        map=
        new ObjectMapper().readValue(jsonStr, Map.class);
        System.out.println(map);
    }
    @Test
    void testStringOper01(){
        //1.获取字符串操作对象(ValueOperations)
        ValueOperations<String, String> vo =
                stringRedisTemplate.opsForValue();
        //2.读写redis中的数据
        vo.set("x", "100");
        vo.increment("x");
        vo.set("y", "200", 1, TimeUnit.SECONDS);
        String x = vo.get("x");
        String y = vo.get("y");
        System.out.println("x="+x+",y="+y);
    }
    @Test
    void testGetConnection(){
        RedisConnection connection =
                stringRedisTemplate.getConnectionFactory()
                        .getConnection();
        String ping = connection.ping();
        System.out.println(ping);
    }

}
3.3.5 基于业务定制RedisTemplate对象(拓展)

我们知道系统中的RedisTemplate默认采用的是JDK的序列化机制,假如我们不希望使用默认的JDK方式序列化,可以对RedisTemplate对象进行定制,指定自己的序列化方式,例如:

package com.jt.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;

import java.net.UnknownHostException;
import java.security.PublicKey;

/*
* 在配置类中自定义RedisTemplate的配置
* */
@Configuration
public class RedisConfig {

    /*
    * 简单定制
    * */
//    @Bean
//    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
//            throws UnknownHostException {
//        RedisTemplate<Object, Object> template = new RedisTemplate<>();
//        template.setConnectionFactory(redisConnectionFactory);
//        /*
//        * 修改默认的序列化方式(默认为JDK的序列化方式)
//        * 序列化底层规则:
//        *   1.将对象中的get方法为私有的,则存数据时候不报错,但存不进去相应的属性和值.
//        *   2.将对象中的get方法由getTitle(),改为getXxx(),存数据的时候存进去的是Xxx和对应的Title属性的值
//        *     也就是说,存数据的规则是取得到属性值的get方法的后缀名和返回值属性存入数据库中
//        *   3.属性不赋值,为null的情况下也会被序列化进去
//        * */
//        //设置Key的序列化方式
//        template.setKeySerializer(RedisSerializer.string());//大key
//        template.setHashKeySerializer(RedisSerializer.string());//小key(value为hash类型需要单独设置)
//        //设置值的序列化方式
//        template.setValueSerializer(RedisSerializer.json());
//        template.setHashValueSerializer(RedisSerializer.json());
//        return template;
//    }

    /*
    * 高级定制
    * */
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
            throws UnknownHostException {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        //设置Key的序列化方式
        template.setKeySerializer(RedisSerializer.string());//大key
        template.setHashKeySerializer(RedisSerializer.string());//小key(value为hash类型需要单独设置)
        //设置值的序列化方式
        template.setValueSerializer(jsonSerializer());
        template.setHashValueSerializer(jsonSerializer());
        //官方建议:只要改变的默认配置,要执行一下如下语句,更新一下属性配置
        template.afterPropertiesSet();
        return template;
    }

    //配置json的序列化
    public RedisSerializer<Object> jsonSerializer() {
        //1.定义json序列化和反序列化对象 这里选择Object.class代表可以对Object一下所有的子类都可以序列化
        Jackson2JsonRedisSerializer<Object> jsonSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        //2.定义序列化规则(序列化规则由ObjectMapper定义)
        //2.1 创建ObjectMapper对象
        ObjectMapper objectMapper = new ObjectMapper();
        //2.2 指定按什么规则进行序列化
        //2.2.1 按什么方法规则进行序列化(例如,任意访问修饰符的get方法)
        objectMapper.setVisibility(PropertyAccessor.GETTER/*类中的get方法*/, JsonAutoDetect.Visibility.ANY/*任意访问修饰符*/);
        //2.2.2 假如属性值为null,则不再进行序列化
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        //2.2.3 将对象类型写入到序列化的字符串中
        //假如没有这个配置,redis存储数据时不存储类型,反序列化时会默认将其数据存储到map
        objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(),//多态校验
                                            ObjectMapper.DefaultTyping.NON_FINAL,//非final类型允许序列化
                                            JsonTypeInfo.As.PROPERTY);//类型以属性形式进行储存 表示类型会以json对象属性形式存储
        jsonSerializer.setObjectMapper(objectMapper);
        return jsonSerializer;
    }
}

测试:

package com.jt;

import com.jt.com.jt.pojo.Blog;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

@SpringBootTest
public class RedisConfigTest {
    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    void testBlogOper01(){
        ValueOperations vo = redisTemplate.opsForValue();
        Blog blog = new Blog();
        blog.setId(100L).setTitle("hello redis");
        vo.set("blog-redis", blog);
    }
    @Test
    void testBlogOper02(){
        HashOperations ho = redisTemplate.opsForHash();
        ho.put("blog-template", "id", "200L");
        ho.put("blog-template", "title", "hello:redis template");
    }

}
3.4 基于Redis的项目业务分析及实践
3.4.1 分布式id

业务描述

在分布式系统中,数据量将越来越大时,就需要对数据进行分表操作,但是,分表后,每个表中的数据都会按自己的节奏进行自增,很有可能出现ID冲突。这时就需要一个单独的机制来负责生成唯一ID,生成出来的ID也可以叫做 分布式ID,这里我们借助redis实现一个简易的分布式id进行实现,当然还有一些第三方的系统,可以帮你生成这样的id,可以自己进行拓展学习.

关键代码实现

package com.jt.demos;

import redis.clients.jedis.Jedis;

/**
 * 需求:生成一个分布递增的id
 * 多张表基于这个方法中生成的id作为主键id值(分布式环境不会采用数据库
 * 表中自带的自增策略-auto_increment)
 */
public class IdGeneratorDemo01 {
    public static Long getId(){
          Jedis jedis=new Jedis("192.168.126.130",6379);
          //jedis.auth("123456");//假如redis设置了密码,连接redis时需要指定密码
          Long id = jedis.incr("id");
          jedis.close();
          return id;
    }
    //自己创建线程执行任务
    static void execute01(){
         for(int i=0;i<10;i++) {
              new Thread(){
                  @Override
                  public void run() {
                      String tName=Thread.currentThread().getName();
                      System.out.println(tName+"->"+
                              IdGeneratorDemo01.getId());
                  }
              }.start();
          }
    }
     //基于线程池执行任务  
     static void execute02(){
      //构建一个最多只有3个线程的线程池
        ExecutorService es=
        Executors.newFixedThreadPool(3);
        for(int i=1;i<=10;i++){
            //从池中取线程执行任务
            es.execute(new Runnable() {//这个任务会存储到阻塞式任务队列中
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()
                            +"->"+getId());
                }
            });
        }
    }
    public static void main(String[] args) {
        //execute01();
          execute02();
    }
}
3.4.2 单点登陆(SSO)

业务描述

在分布式系统中,通过会有多个服务,我们登录了一个服务以后,再访问其它服务时,不想再登录,就需要有一套单独的认证系统,我们通常会称之为单点登录系统,在这套系统中提供一个认证服务器,服务完成用户身份认证,在一些中小型分布式系统中中,我们通常会借助redis存储用户的认证信息,例如:

关键代码实现

package com.jt;

import redis.clients.jedis.Jedis;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/*
 * 单点登录Demo,
 * 1.要完成用户身份认证
 * 2.完成用户资源访问时的授权
 * SSO系统具体实现
 *  1.用户登录成功后,将登陆状态信息存储到redis,要返回给用户一个token(UUID)
 *  2.用户登录成功后可携带token访问资源,资源服务器检查token有效性,假如有效则继续执行,无效则提示重新登录
 *  3.判定用户是否有资源的访问权限,有权限则授权访问
 * */
public class SSODemo01 {
    //执行登录逻辑
    public static String doLogin(String username, String password) {
        //1.判定参数的有效性
        if(username==null || "".equals(username)){
            throw new IllegalArgumentException("username can not be empty");
        }
        //2.判定用户是否存在,(一般要查询数据库)
        if(!"tony".equals(username)){//假设数据库中只有一个tony数据
            throw new IllegalArgumentException("user is not exists");
        }
        //3.判断密码是否正确(一般是要加密后比对)
        if(!"123456".equals(password)){
            throw new IllegalArgumentException("密码错误");
        }
        //4.将用户信息写入到redis服务器(key为一个随机字符串,值为用户信息)
        Jedis jedis = JedisDataSource.getConnection();
        String token = UUID.randomUUID().toString();
        jedis.hset(token, "username", "tony");
        jedis.hset(token, "permissions", "sys:res:view,sys:res:update");
        jedis.expire(token, 10);
        jedis.close();
        //5.返回token(UUID)
        return token;
    }

    //执行资源访问逻辑
    public static Object doGetResource(String token) {
        //1.检验token是否为空
        if(token==null || "".equals(token)){
            throw new IllegalArgumentException("请先登录");
        }
        //2.基于token查询用户信息
        Jedis jedis = JedisDataSource.getConnection();
        Map<String, String> userMap = jedis.hgetAll(token);
        //3.判定用户是否已经登录
        if(userMap==null || userMap.size()==0){
            throw new IllegalArgumentException("用户登录超时,请重新登录");
        }
        //4.判定用户有资源的访问权限
        String permissionsStr = userMap.get("permissions");
        if(permissionsStr==null || "".equals(permissionsStr)){
            throw new IllegalArgumentException("你无权访问这个资源");
        }
        String[] permissionArray = permissionsStr.split(",");
        List<String> permissions = Arrays.asList(permissionArray);
        if(!permissions.contains("sys:res:view")){
            throw new IllegalArgumentException("你无权访问这个资源");
        }
        return "your resource";
    }

    public static void main(String[] args) {
        //1.执行登录操作
        String username = "tony";
        String password = "123456";
        String token = doLogin(username, password);
        System.out.println("login success,token is "+token);
        //2.携带令牌去访问资源
        Object resource = doGetResource(token);
        System.out.println(resource);
    }
}
3.4.3 简易秒杀队列

业务描述

在设计一个秒杀或抢购系统时,为了提高系统的响应速度,通常会将用户的秒杀或抢购请求先存储到一个redis队列,这里我们就基于redis实现一个先进先出队列,例如:

关键代码实现

package com.jt.demos;

import redis.clients.jedis.Jedis;

//秒杀队列演示
//描述逻辑中会将商品抢购信息先写到redis(以队列形式进行存储),
//因为写redis内存数据库要比写你的mysql数据库快很多倍
//算法:先进先出(FIFO)-体现公平性
public class SecondKillDemo01 {

    //商品抢购首先是入队
    static void enque(String msg){//入队
         Jedis jedis=new Jedis("192.168.126.130",6379);
         jedis.auth("123456");//没有认证不需要写这个语句
         jedis.lpush("queue",msg);
         jedis.close();
    }

    //底层异步出队(基于这个消息,生成订单,扣减库存,...)
    static String deque(){//出队
        Jedis jedis=new Jedis("192.168.126.130",6379);
        jedis.auth("123456");//没有认证不需要写这个语句
        String result=jedis.rpop("queue");
        jedis.close();
        return result;
    }
    public static void main(String[] args){
        //1.多次抢购(模拟在界面上多次点击操作)
        new Thread(){
            @Override
            public void run() {
                for(int i=1;i<=10;i++){//模拟页面上按钮点击
                    enque(String.valueOf(i));
                    try{Thread.sleep(100);}catch(Exception e){}
                }
            }
        }.start();

        //2.从队列取内容(模拟后台从队列取数据)
        new Thread(){
            @Override
            public void run() {
                for(;;){
                    String msg=deque();
                    if(msg==null)continue;
                    System.out.print(msg);
                }
            }
        }.start();

    }
}
3.4.4 简易投票系统

业务描述

在很多系统中设计中,都会有一个活动设计,开启一个活动之前,可以对这个活动的支持力度先进行一个调查,例如基于这个活动设计一个投票系统,例如:

关键代码实现

package com.jt;

import redis.clients.jedis.Jedis;

import java.util.Set;

/*
 * 基于某个活动id的简易投票系统设计及实现
 * 1)定义活动
 * 2)基于活动id进行投票(投票结果存储到redis中)
 * 3)同一个人基于同一个活动id不能重复投票,假如已参与过投票,再点击投票则取消投票
 * 4)可以获取投票人数
 */
public class SSODemo02 {

    public static void main(String[] args) {
        //1.定义活动
        String activityId = "201";
        String user1 = "1001";
        String user2 = "1002";
        //2.基于活动Id进行投票
        boolean flag = doVote(activityId, user1);
        flag = doVote(activityId, user2);
        flag = doVote(activityId, user2);
        //3.获取票数
        Long count = doCount(activityId);
        //产看那些人参与了投票
        Set<String> users = doGetUsers(activityId);
        System.out.println(users);
    }

    /*
    * 执行投票逻辑
    * activityId : 活动Id
    * userId : 用户Id
    * 返回值 : 是否投票成功
    * */
    private static boolean doVote(String activityId, String userId) {
        //1.参数校验
        //2.执行投票逻辑
        //2.1 连接redis
        Jedis jedis = JedisDataSource.getConnection();
        //2.2 检查是否已经参与过投票.基于检查结果执行投票或取消投票
        Boolean flag = jedis.sismember(activityId, userId);//set类型
        if(flag){//true标识已经参与过投票,此时可以取消投票
            jedis.srem(activityId, userId);
            jedis.close();
            return false;
        }
        //假如没有参与过投票,则直接投票
        jedis.sadd(activityId, userId);
        jedis.close();
        return true;
    }

    /*
    * 基于活动Id统计投票总数
    * */
    static Long doCount(String activityId){
        //1.参数校验
        //2.获取投票总数
        //2.1 连接redis
        Jedis jedis = JedisDataSource.getConnection();
        //2.2 获取总数
        Long count = jedis.scard(activityId);
        //2.3 释放资源
        jedis.close();
        //3 返回投票总数
        return count;
    }

    /*
    * 获取参与投票的用户
    * */
    static Set<String> doGetUsers(String activityId){
        //1.参数校验
        //2.获取投票的用户
        Jedis jedis = JedisDataSource.getConnection();
        Set<String> users = jedis.smembers(activityId);
        jedis.close();
        return users;
    }
}
3.4.5 简易购物车系统

业务描述

简易购物车业务设计如图所示:

基础指令操作,例如:

1)向购物车添加商品
hset  cart:101  2001  1
hset  cart:101  2002  1
hset  cart:101  2003  2
2)查看购物车商品
hgetall cart:101
3)删除购物车商品
hdel cart:101 2003
4)改变购物车某个商品的购买数量
hincrby cart:101 2002 2

关键代码实现

package com.jt.demos;

import redis.clients.jedis.Jedis;

import java.util.Map;

/**
 * 作业:基于redis存储商品购物车信息
 */
public class CartDemo01 {

    public static void addCart(Long userId,Long productId,int num){
        //1.建立redis链接
        Jedis jedis=new Jedis("192.168.126.130",6379);
        jedis.auth("123456");
        //2.向购物车添加商品
        //hincrBy这个函数在key不存在时会自动创建key
        jedis.hincrBy("cart:" + userId, String.valueOf(productId),num);
        //3.释放redis链接
        jedis.close();
    }
    //查看我的购物车
    public static Map<String, String> listCart(Long userId){
        //1.建立redis链接
        Jedis jedis=new Jedis("192.168.126.130",6379);
        jedis.auth("123456");
        //2.查看购物车商品
        Map<String, String> map = jedis.hgetAll("cart:" + userId);
        //3.释放redis链接
        jedis.close();
        return map;
    }
    public static void main(String[] args) {
        //1.向购物车添加商品
        addCart(101L,201L,1);
        addCart(101L,202L,1);
        addCart(101L,203L,2);
        //2.查看购物车商品
        Map<String, String> map = listCart(101L);
        System.out.println(map);
    }

}
3.5 SpringBoot工程中Redis与Aop技术的整合
3.5.1 业务描述

基于AOP与Redis技术实现mysql,redis数据库中数据操作.

3.5.2 项目准备工作

第一步:打开sca-template工程,添加访问MySql数据库的依赖(两个)

<!--mysql依赖-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!--mybatis plus (简化mybatis操作)-->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.4.2</version>
    </dependency>

第二步:修改sca-template工程的配置文件,添加连接mysql数据库的配置

spring:
  datasource:
    url: jdbc:mysql:///jt-sso?serverTimezone=Asia/Shanghai&characterEncoding=utf8
    username: root
    password: root
3.5.3 Pojo逻辑对象定义

定义一个Menu对象,用户封装tb_menus表中的数据,例如:

package com.jt.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import org.springframework.data.annotation.Id;

import java.io.Serializable;

@TableName(value = "tb_menus")
public class Menu implements Serializable {
    private static final long serialVersionUID = -577747732166248365L;
    @TableId(type = IdType.AUTO)
    private Long id;
    private String name;
    private String permission;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPermission() {
        return permission;
    }

    public void setPermission(String permission) {
        this.permission = permission;
    }

    @Override
    public String toString() {
        return "Menu{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", permission='" + permission + '\'' +
                '}';
    }
}
3.5.4 Dao逻辑对象设计及实现

创建用于操作数据库中tb_menus表中数据的Mapper对象,例如:

package com.jt.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jt.com.jt.pojo.Menu;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface MenuMapper extends BaseMapper<Menu> {
    //这里先使用BaseMapper中提供的方法即可
}
3.5.5 Service逻辑对象设计及实现

第一步:定义用于处理菜单业务的业务接口,例如:

package com.jt.service;
import com.jt.com.jt.pojo.Menu;

public interface MenuService {
    /*
     * 基于id查找菜单
     * */
    Menu selectMenu(Long id);
    /*
    * 新增菜单信息
    * */
    Menu insertMenu(Menu menu);
    /*
    * 修改菜单信息
    * */
    Menu updateMenu(Menu menu);
}

第二步:定义用于处理菜单业务的业务接口实现类,
在这个实现类中自己基于RedisTemplate对象操作Redis缓存,例如:

package com.jt.service.impl;

import com.jt.com.jt.pojo.Menu;
import com.jt.dao.MenuMapper;
import com.jt.service.MenuService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.time.Duration;

@Service
public class MenuServiceImpl implements MenuService {
    private final Logger logger= LoggerFactory.getLogger(MenuService.class);

    @Autowired
    MenuMapper menuMapper;
//    @Autowired
//    private RedisTemplate redisTemplate;

    /*
    * 升级写法:
    * 说明:ValueOperations是一个接口,底下有一个DefaultValueOperations默认实现类
    *     该类只有一个构造函数,且参数需要一个RedisTemplate对象,所以spring在创建
    *     ValueOperations对对象的时候,会根据 @Resource(name="redisTemplate")注解
    *     中的name里的redisTemplate值,取容器中取对应的对象注入构造方法中,创建ValueOperations对象
    *     前提是Spring容器中有bean对象名为redisTemplate的对象.
    * */
    //此写法可参考官方
    @Resource(name="redisTemplate")
    private ValueOperations valueOperations;

    @Override
    public Menu selectMenu(Long id) {
        //1.从Redis缓存查询数据,假如有,则直接返回缓存中数据
//        ValueOperations valueOperations = redisTemplate.opsForValue();
        String key = String.valueOf(id);
        Object obj = valueOperations.get(key);
        if(obj!=null) {
            logger.info("data from redis");
            return (Menu)obj;
        }
        //2.redis缓存没有则查询关系型数据库
        Menu menu = menuMapper.selectById(id);
        logger.info("data from rdbms");
        //3.将数据存到redis缓存
        valueOperations.set(key,menu, Duration.ofSeconds(180));
        return menu;
    }

    @Override
    public Menu insertMenu(Menu menu) {
        logger.debug("insert before {}",menu);
        menuMapper.insert(menu);
        logger.debug("insert after {}",menu);
        //写redis
//        ValueOperations valueOperations = redisTemplate.opsForValue();
        valueOperations.set(String.valueOf(menu.getId()), menu, Duration.ofSeconds(180));
        return menu;//这里返回的参数会相对于参数多了个id值
    }

    @Override
    public Menu updateMenu(Menu menu) {
        //更新Mysql
        menuMapper.updateById(menu);
        //更新Redis
//        ValueOperations valueOperations = redisTemplate.opsForValue();
        valueOperations.set(String.valueOf(menu.getId()), menu, Duration.ofSeconds(180));
        return menu;
    }
}

第三步:定义用于处理菜单业务的业务接口实现类,基于AOP方式操作redis缓存,比较
与第二步写的Redis操作方式的不同,例如:

package com.jt.service.impl;

import com.jt.com.jt.pojo.Menu;
import com.jt.dao.MenuMapper;
import com.jt.service.MenuService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class AopCacheMenuServiceImpl implements MenuService {

    private static Logger log = LoggerFactory.getLogger(AopCacheMenuServiceImpl.class);

    @Autowired
    MenuMapper menuMapper;
    /*
     * @Cacheable 注解描述的方法为spring中的缓存切入点方法
     * 方法执行之前会先查缓存,缓存没有查询mysql,然后将查询
     * 结果存储到缓存。
     * @param id
     * @return
     */
    @Cacheable(value = "menuCache",key = "#id")
    @Override
    public Menu selectMenu(Long id) {
        log.info("Get Data From RDBMS");
        return menuMapper.selectById(id);
    }

    @CachePut(value = "menuCache",key = "#menu.id")//值是方法的返回值
    @Override
    public Menu insertMenu(Menu menu) {
        log.info("insert before : {}",menu);
        menuMapper.insert(menu);
        log.info("insert after : {}",menu);
        return menu;
    }

    /*
    * @CachePut 注解描述的方法同样是一个缓存切入点方法,主要用于更新缓存
    * value 模块名 相当于前缀  一般是模块名+key(id)作为缓存的key 拼接方式:menuCache::id
    * key一般是id
    * 数据库存储的值是方法的返回值
    * */
    @CachePut(value = "menuCache",key = "#menu.id")
    @Override
    public Menu updateMenu(Menu menu) {
        menuMapper.updateById(menu);
        return menu;
    }
}

说明,启动AOP方式的缓存应用,需要在启动类上添加@EnableCaching注解:

第四步:定义单元测试类,基于单元测试类测试缓存应用.例如:

package com.jt;

import com.jt.com.jt.pojo.Menu;
import com.jt.service.MenuService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.util.Assert;

@SpringBootTest
public class MenuServerTests {
    //写三个单元测试方法,对menuService中的方法进行单元测试
    @Autowired
    @Qualifier("aopCacheMenuServiceImpl")
    private MenuService menuService;


    @Test
    void testSelectMenu(){
        Menu menu = menuService.selectMenu(5L);
        Assert.notNull(menu, "menu is null");
        System.out.println(menu);
    }

    @Test
    void testInsertMenu(){
        Menu menu = new Menu();
        menu.setName("test resource").setPermission("sys:res:test");
        Menu menu1 = menuService.insertMenu(menu);
        System.out.println(menu1);
    }
    @Test
    void testUpdateMenu(){
        Menu menu = menuService.selectMenu(4L);
        menu.setName("export resource").setPermission("sys:res:export");
        Menu menu1 = menuService.updateMenu(menu);
        System.out.println(menu1);
    }
}

第五步:改变AOP方式中redis数据存储时的序列化方式(假如业务上需要).其实现上要借助
CacheManager对象,例如:

package com.jt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;

/**
 * 重构CacheManager对象,其目的是改变AOP方式应用redis的序列化和反序列化的方式.
 */
@Configuration
public class CacheManagerConfig {
    /**
     * 重构CacheManager对象
     * @return
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        //定义RedisCache配置
         RedisCacheConfiguration cacheConfiguration =RedisCacheConfiguration.defaultCacheConfig();

        //指定key的序列化方式(默认是String方式)
        cacheConfiguration.serializeKeysWith(RedisSerializationContext
                                .SerializationPair
                                .fromSerializer(RedisSerializer.string()));
        //指定值的序列化方式(默认是jdk的序列化方式)
        cacheConfiguration.serializeValuesWith(RedisSerializationContext
                                .SerializationPair
                                .fromSerializer(RedisSerializer.json()));

        return  RedisCacheManager.builder(connectionFactory)
               .cacheDefaults(cacheConfig)
               .build();//建造者模式(复杂对象的创建,建议使用这种方式,封装了对象的创建细节)
    }
}

写好这个对象后,可以再次基于MenuService中的方法进行单元测试,检测redis数据的存储.

3.5.6 Controller逻辑对象设计及实现

第一步:定义Controller处理,处理客户端对菜单数据的请求操作,例如:

package com.jt.controller;

import com.jt.com.jt.pojo.Menu;
import com.jt.service.MenuService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/menu")
public class MenuController {
    /*
    * 这种方式@Autowired会有黄线警告,官方推荐是正确娃玩法是通过构造方法给属性赋值
    * */
//    @Autowired 
//    @Qualifier("menuServiceImpl")
//    private MenuService menuService;

    /*
    * 使用构造方法给属性赋值还可以将属性设为final类型的
    * */
    private final MenuService menuService;
    @Autowired
    public MenuController(@Qualifier("menuServiceImpl") MenuService menuService) {
        this.menuService = menuService;
    }

    @GetMapping("/{id}")
    public Menu doSelectById(@PathVariable("id") Long id) {
        return menuService.selectMenu(id);
    }

    @PutMapping
    public String doUpdate(@RequestBody Menu menu) {
        menuService.updateMenu(menu);
        return "update ok";
    }

    @PostMapping
    public String doInsert(@RequestBody Menu menu) {
        menuService.insertMenu(menu);
        return "insert ok";
    }

}

第二步:打开postman进行访问测试.检测redis数据存储与更新

四. Redis 数据持久化实践

4.1 简介
4.1.1 背景

​ Redis是一种内存数据库,在断电时数据可能会丢失。比如你redis整个挂了,然后redis不可用了,如果没有持久化的话,redis就会丢失所有的数据,如果通过持久化将数据搞一份儿到磁盘上去,然后再定期同步到一些云存储服务上去,那么就可以保证一些数据不丢失,保证数据的可靠性。

4.1.2 持久化方式

​ Redis中为了保证在系统宕机(类似进程被杀死)情况下,能更快的进行故障恢复,设计了两种数据持久化方案,分别为rdb和aof方式。

4.1.3 配置准备工作

第一步:从redis.io官方下载对应版本的redis.conf文件,地址如下(假如下载不下来从code上去取或者从同学那拿):

https://redis.io/topics/config/

第二步:停止redis并删除挂载目录下(/usr/local/docker/redis01/conf)的redis.conf配置文件.
第三步:将下载的redis.conf文件拷贝到redis挂载目录(/usr/local/docker/redis01/conf)

第四步:基于vim打开redis.conf文件,然后注释 bind 127.0.0.1这一行,并修改protected-mode的值修改为no.(java连接redis需要改这两项目)
第五步:重启redis服务,并检查启动日志(docker logs 容器id)

4.2 Rdb方式持久化
4.2.1 概述

Rdb方式是通过手动(save-阻塞式,bgsave-异步)或周期性方式保存redis中key/value的一种机制,Rdb方式一般为redis的默认数据持久化方式.系统启动时会自动开启这种方式的持久化机制。

4.2.2 RDB方式配置

RDB方式的持久化是默认开启的,也可按规则自己配置,例如,打开redis.conf文件,例如

# 这里表示每隔60s,如果有超过1000个key发生了变更,那么就生成一个新的dump.rdb文件,就是当前redis内存中完整的数据快照,这个操作也被称之为snapshotting(快照)。

save 60 1000

# 持久化 rdb文件遇到问题时,主进程是否接受写入,yes 表示停止写入,如果是no 表示redis继续提供服务。
stop-writes-on-bgsave-error yes
    
# 在进行快照镜像时,是否进行压缩。yes:压缩,但是需要一些cpu的消耗。no:不压缩,需要更多的磁盘空间。
rdbcompression yes
# 一个CRC64的校验就被放在了文件末尾,当存储或者加载rbd文件的时候会有一个10%左右的性能下降,为了达到性能的最大化,你可以关掉这个配置项。
rdbchecksum yes

# 快照的文件名
dbfilename dump.rdb

# 存放快照的目录
dir /var/lib/redis
4.2.3 Rdb方式持久化实践

试验一

​ 在redis中保存几条数据,然后执行shutdown关闭redis,然后再重启redis,看看刚才插入的数据是否还在?假如数据还在,为什么?
因为,通过redis-cli shutdown这种方式去停掉redis,其实是一种安全退出的模式,redis在退出的时候会将内存中的数据立即生成一份完整的rdb快照,例如

127.0.0.1:6379> set phone 11111111
OK
127.0.0.1:6379> shutdown   #默认也会进行持久化
[root@centos7964 ~]#  docker start redis01
[root@centos7964 ~]# docker exec -it redis01 redis-cli
127.0.0.1:6379> keys *
1) "pone"

试验二

在redis中再保存几条新的数据,用kill -9粗暴杀死redis进程,模拟redis故障异常退出,导致内存数据丢失的场景?
这次就发现,redis进程异常被杀掉,几条最新的数据就丢失了。例如:

首先,打开第一个客户端,先清除redis内存和磁盘对应的数据

[root@centos7964 data]# docker exec -it redis01 redis-cli
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> exit
[root@centos7964 data]# ls
dump.rdb
[root@centos7964 data]# rm –f dump.rdb
[root@centos7964 data]# ls

然后,打开并登录第二个客户端,并向redis存储一些数据,例如

[root@centos7964 ~]# docker exec -it redis01 redis-cli
127.0.0.1:6379> set one mybatis
OK
127.0.0.1:6379> set two spring
OK
127.0.0.1:6379> keys *
1) "one"
2) "two"

接下来,再次回到第一个客户端,杀掉redis进程(带端口号的那个进程),例如

[root@centos7964 data]# ps -ef | grep redis
polkitd    6995   6974  0 14:44 ?        00:00:00 redis-server *:6379
root       7064   6974  0 14:44 pts/0    00:00:00 redis-cli
root       7111   6467  0 14:47 pts/1    00:00:00 docker exec -it redis01 redis-cli
root       7130   6974  0 14:47 pts/1    00:00:00 redis-cli
root       7278   7180  0 14:51 pts/0    00:00:00 grep --color=auto redis
[root@centos7964 data]# kill -9 6995
[root@centos7964 data]# docker start redis01

最后,打开第一个客户端,登录redis,检查key是否还存在.

[root@centos7964 ~]# docker exec -it redis01 redis-cli
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379>
[root@centos7964 ~]#

试验三

手动调用save(同步保存)或者bgsave(异步保存)执行rdb快照生成.然后杀掉redis进程,再重启检测是否还有刚刚保存的数据.

127.0.0.1:6379> set id 100
OK
127.0.0.1:6379> set name jack
OK
127.0.0.1:6379> save  #阻塞式持久化
OK
127.0.0.1:6379> set address beijing
OK
127.0.0.1:6379> bgsave  #异步方式持久化
Background saving started
4.2.4 小节面试分析

Redis中的save和bgsave有什么不同?

  • Redis Save 命令执行一个同步保存操作,将当前 Redis 实例的所有数据快照(snapshot)以 RDB 文件的形式保存到硬盘。

  • BGSAVE 命令执行之后立即返回 OK ,然后 Redis fork 出一个新子进程,原来的 Redis 进程(父进程)继续处理客户端请求,而子进程则负责将数据保存到磁盘,然后退出。

RDB持久化机制有哪些优点?

​ 第一:RDB会生成多个数据文件,每个数据文件都代表了某一个时刻中redis的数据,这种多个数据文件的方式,非常适合做冷备,可以将这种完整的数据文件发送到一些远程云服务上去,在国内可以是阿里云的ODPS分布式存储上,以预定好的备份策略来定期备份redis中的数据.
​ 第二:RDB对redis对外提供的读写服务,影响非常小,可以让redis保持高性能,因为redis主进程只需要fork一个子进程,让子进程执行磁盘IO操作来进行RDB持久化即可。
​ 第三:相对于AOF持久化机制来说,直接基于RDB数据文件来重启和恢复redis进程,更加快速。

RDB持久化机制有哪些缺点?

​ 假如redis故障时,要尽可能少的丢失数据,那么RDB方式不太好,它都是每隔5分钟或更长时间做一次快照,这个时候一旦redis进程宕机,那么会丢失最近几分钟的数据。

4.3 Aof方式数据持久化
4.3.1 概述

Aof方式是通过记录写操作日志的方式,记录redis数据的一种持久化机制,这个机制默认是关闭的。

4.3.2 AOF方式配置
# 是否开启AOF,默认关闭
appendonly yes
# 指定 AOF 文件名
appendfilename appendonly.aof
# Redis支持三种刷写模式:
# appendfsync always #每次收到写命令就立即强制写入磁盘,类似MySQL的sync_binlog=1,是最安全的。但该模式下速度也是最慢的,一般不推荐使用。
appendfsync everysec #每秒钟强制写入磁盘一次,在性能和持久化方面做平衡,推荐该方式。
# appendfsync no     #完全依赖OS的写入,一般为30秒左右一次,性能最好但是持久化最没有保证,不推荐。
    
#在日志重写时,不进行命令追加操作,而只是将其放在缓冲区里,避免与命令的追加造成DISK IO上的冲突。
#设置为yes表示rewrite期间对新写操作不fsync,暂时存在内存中,等rewrite完成后再写入,默认为no,建议yes
no-appendfsync-on-rewrite yes
#当前AOF文件大小是上次日志重写得到AOF文件大小的二倍时,自动启动新的日志重写过程。
auto-aof-rewrite-percentage 100
#当前AOF文件启动新的日志重写过程的最小值,避免刚刚启动Reids时由于文件尺寸较小导致频繁的重写。
auto-aof-rewrite-min-size 64mb
4.3.3 AOF方式持久化实践

第一:打开AOF的开关,启用AOF持久化

第二:写入一些数据,观察AOF文件(appendonly.aof)中的日志内容

第三:kill -9杀掉redis进程,重新启动redis进程,发现数据被恢复回来了,就是从AOF文件中恢复回来的,redis进程启动的时候,直接就会从appendonly.aof中加载所有的日志,把内存中的数据恢复回来。

4.3.4 小节面试分析
  • 如何理解AOF方式中的rewrite操作?

​ redis中的可以存储的数据是有限的,很多数据可能会自动过期,也可能会被用户删除或被redis用缓存清除的算法清理掉。也就是说redis中的数据会不断淘汰掉旧的,只有一部分常用的数据会被自动保留在redis内存中,所以可能很多之前的已经被清理掉的数据,对应的写日志还停留在AOF中,AOF日志文件就一个,会不断的膨胀,最好导致文件很大。
​ 所以,AOF会自动在后台每隔一定时间做rewrite操作,比如日志里已经存放了针对100w数据的写日志了,但redis内存现在10万数据; 于是,基于内存中当前的10万数据构建一套最新的日志,然后到AOF文件中; 覆盖之前的老日志,从而,确保AOF日志文件不会过大,保持跟redis内存数据量一致.

  • AOF持久化机制有哪些优点?

​ 第一:AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据.

​ 第二:AOF日志文件通常以append-only模式写入,所以没有任何磁盘寻址的开销,写入性能非常高,并且文件不容易破损,即使文件尾部破损,也很容易修复。

​ 第三:AOF日志文件过大的时候,出现后台重写操作,也不会影响客户端的读写。因为在rewrite log的时候,会对其中的日志进行压缩,创建出一份需要恢复数据的最小日志出来。再创建新日志文件的时候,老的日志文件还是照常写入。当新的merge后的日志文件ready的时候,再交换新老日志文件即可。
​ 第四:AOF日志文件的命令通过易读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据.

  • AOF持久化机制有哪些缺点?

​ 第一:对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大。
​ 第二:AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,当然,每秒一次fsync,性能也还是很高的。

​ 第三:AOF这种基于命令日志方式,比基于RDB每次持久化一份完整的数据快照文件的方式,更加脆弱一些,容易有bug。不过AOF为了避免rewrite过程导致的bug,因此每次rewrite并不是基于旧的指令日志进行merge的,而是基于当时内存中的数据进行指令的重新构建,这样健壮性会好很多。

  • 如何选择redis的持久化方式?

    第一:不要仅仅使用RDB,因为那样会导致你丢失很多数据。

    第二:也不要仅仅使用AOF,因为AOF做冷备没有RDB做冷备进行数据恢复的速度快,并且RDB简单粗暴的数据快照方式更加健壮。

    第三:综合使用AOF和RDB两种持久化机制,用AOF来保证数据不丢失,作为数据恢复的第一选择; 用RDB来做不同程度的冷备。

五. Redis 事务处理实践

5.1 Redis事务简介
5.1.1 概述

​ 事务是一个业务,也可以看成是一个逻辑工作单元,是为了保证业务的完整,数据的正确而推出的一种控制机制,原则上来讲,事务必须要满足ACID四个特性(原子性,一致性,隔离性,持久性),在多个事务并发执行,为更好保证事务的四个特性的实现,通常会对事务加锁,Redis为了性能,采用了乐观锁方式进行事务控制,它使用watch命令监视给定的key,当exec(提交事务)的时候,如果监视的key从调用watch后发生过变化,则整个事务会失败。也可以调用watch多次监视多个key。注意watch的key是对整个连接有效的,如果连接断开,监视和事务都会被自动清除。当然exec,discard,unwatch命令都会清除连接中的所有监视。

5.1.2 基本指令

redis进行事务控制时,通常是基于如下指令进行实现,例如:

  • multi 开启事务

  • exec 提交事务

  • discard 取消事务

  • watch 监控,如果监控的值发生变化,则提交事务时会失败

  • unwatch 去掉监控

​ Redis保证一个事务中的所有命令要么都执行,要么都不执行(原子性)。如果在发送EXEC命令前客户端断线了,则Redis会清空事务队列,事务中的所有命令都不会执行。而一旦客户端发送了EXEC命令,所有的命令就都会被执行,即使此后客户端断线也没关系,因为Redis中已经记录了所有要执行的命令。

5.2 Redis事务控制实践
5.2.1 exec提交事务

例如:模拟转账,tony 500,jack 200,tony转给jack100。过程如下:

127.0.0.1:6379> set tony 500
OK
127.0.0.1:6379> set jack 200
OK
127.0.0.1:6379> mget tony jack
1) "500"
2) "200"
127.0.0.1:6379> multi #开启事务
OK
127.0.0.1:6379(TX)> decrby tony 100 #所有指令操作会进入到队列
QUEUED
127.0.0.1:6379(TX)> incrby jack 100
QUEUED
127.0.0.1:6379(TX)> mget tony jack
QUEUED 
127.0.0.1:6379(TX)> exec  #提交事务
1) (integer) 400
2) (integer) 300
3) 1) "400"
   2) "300"
   127.0.0.1:6379> mget tony jack
1) "400"
2) "300"
127.0.0.1:6379>
5.2.2 discard取消事务

注意redis事务太简单,没有回滚,而只有取消。

127.0.0.1:6379> mget tony jack
1) "400"
2) "300"
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incrby jack 100
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> get jack
"300"
127.0.0.1:6379> exec
(error) ERR EXEC without MULTI

当出现错误指令时,事务也会自动取消。

127.0.0.1:6379> mget tony jack
1) "400"
2) "300"
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incrby jack 100
QUEUED
127.0.0.1:6379(TX)> abcd
(error) ERR unknown command `abcd`, with args beginning with:
127.0.0.1:6379(TX)> get jack
QUEUED
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get jack
"300"
127.0.0.1:6379>
5.2.3 秒杀抢票事务处理

基于一个秒杀,抢购案例,演示redis乐观锁方式,例如

第一步:打开客户端1,执行如下操作

127.0.0.1:6379> set ticket 1
OK
127.0.0.1:6379> set money 0
OK
127.0.0.1:6379> watch ticket		#乐观锁,对值进行观察,改变则事务失败
OK
127.0.0.1:6379> multi				#开启事务
OK
127.0.0.1:6379> decr ticket
QUEUED
127.0.0.1:6379> incrby money 100
QUEUED

第二步:打开客户端2,执行如下操作,演示还没等客户端1提交事务,此时客户端2把票买到了。

127.0.0.1:6379> get ticket
"1"
127.0.0.1:6379> decr ticket
(integer) 0

第三步,回到客户端1:提交事务,检查ticket的值

127.0.0.1:6379> exec
(nil) #执行事务,失败
127.0.0.1:6379> get ticket
“0”
127.0.0.1:6379> unwatch #取消监控
5.2.4 Jedis 客户端事务操作

基于Jedis进行事务测试,代码如下:

package com.jt;
import org.junit.Test;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

public class JedisTransactionTests {

    @Test
    public void testTransaction(){
        Jedis jedis=new Jedis("192.168.126.130",6379);
        jedis.auth("123456");
        jedis.set("tony","300");
        jedis.set("jack","500");
        //实现操作,tony转账100给jack
        //开启事务
        Transaction multi = jedis.multi();
        //执行业务操作
        try {
            multi.decrBy("tony", 100);
            multi.incrBy("jack", 100);
            int n=100/0;//模拟异常
            //提交事务
            multi.exec();
        }catch(Exception e) {
            //出现异常取消事务
            multi.discard();
        }
        String tonyMoney=jedis.get("tony");
        String jackMoney=jedis.get("jack");
        System.out.println("tonyMoney="+tonyMoney);
        System.out.println("jackMoney="+jackMoney);
        jedis.close();
    }
}
5.2.5 Jedis 客户端秒杀操作实践
package com.jt.demos;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Response;
import redis.clients.jedis.Transaction;

import java.util.List;

/**
 * redis秒杀练习:
 * 模拟两个线程都去抢购同一张票(考虑乐关锁)
 */
public class SecondKillDemo02 {

      public static void secKill(){
          Jedis jedis=new Jedis("192.168.126.130",6379);
          jedis.auth("123456");
          jedis.watch("ticket","money");
          String ticket = jedis.get("ticket");
          if(ticket==null||Integer.valueOf(ticket)==0)
              throw new RuntimeException("已无库存");
          Transaction multi = jedis.multi();
          try {
              multi.decr("ticket");
              multi.incrBy("money", 100);
              List<Object> exec = multi.exec();
              System.out.println(exec);
          }catch (Exception e){
              e.printStackTrace();
              multi.discard();
          }finally {
              jedis.unwatch();
              jedis.close();
          }
      }
      public static void main(String[] args) {
          Jedis jedis=new Jedis("192.168.126.130",6379);
          jedis.auth("123456");
          jedis.set("ticket","1");
          jedis.set("money","0");

          Thread t1=new Thread(()->{
              secKill();
          });
          Thread t2=new Thread(()->{
              secKill();
          });
          t1.start();
          t2.start();
      }
}

六. Redis架构设计应用实践

6.1 Redis主从复制
6.1.1 简介

​ 单个Redis支持的读写能力还是有限的,此时我们可以使用多个redis来提高redis的并发处理能力,这些redis如何协同,就需要有一定的架构设计,这里我们首先从主从(Master/Slave)架构进行分析和实现.

6.1.2 基本架构

redis主从架构如图所示:

其中,master负责读写,并将数据同步到salve,从节点负责读操作.

6.1.3 快速入门实践

基于redis,设计一主从架构,一个Master,两个Slave,其中Master负责Redis读写操作,并将数据同步到Slave,Slave只负责读.,其步骤如下:
第一步:删除所有原有的redis容器,例如:

docker rm -f  redis容器名

第二步:进入你的宿主机docker目录,然后将redis01拷贝两份,例如:

cp -r redis01/ redis02
cp -r redis01/ redis03

第三步:启动三个新的redis容器,例如:

docker run -p 6379:6379 --name redis6379 \
-v /usr/local/docker/redis01/data:/data \
-v /usr/local/docker/redis01/conf/redis.conf:/etc/redis/redis.conf \
-d redis redis-server /etc/redis/redis.conf \
--appendonly yes
docker run -p 6380:6379 --name redis6380 \
-v /usr/local/docker/redis02/data:/data \
-v /usr/local/docker/redis02/conf/redis.conf:/etc/redis/redis.conf \
-d redis redis-server /etc/redis/redis.conf \
--appendonly yes
docker run -p 6381:6379 --name redis6381 \
-v /usr/local/docker/redis03/data:/data \
-v /usr/local/docker/redis03/conf/redis.conf:/etc/redis/redis.conf \
-d redis redis-server /etc/redis/redis.conf \
--appendonly yes

第四步 检测redis服务角色

启动三个客户端,分别登陆三台redis容器服务,通过info指令进行角色查看,默认新启动的三个redis服务角色都为master.

127.0.0.1:6379> info replication
\# Replication
role:master
connected_slaves:0
master_repl_offset:3860
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:2
repl_backlog_histlen:3859

第五步:检测redis6379的ip设置

docker inspect redis6379
……
"Networks": {
                "bridge": {
                    "IPAMConfig": null,
                    "Links": null,
                    "Aliases": null,
                    "NetworkID": "c33071765cb48acb1efed6611615c767b04b98e6e298caa0dc845420e6112b73",
                    "EndpointID": "4c77e3f458ea64b7fc45062c5b2b3481fa32005153b7afc211117d0f7603e154",
                    "Gateway": "172.17.0.1",
                    "IPAddress": "172.17.0.2",
                    "IPPrefixLen": 16,
                    "IPv6Gateway": "",
                    "GlobalIPv6Address": "",
                    "GlobalIPv6PrefixLen": 0,
                    "MacAddress": "02:42:ac:11:00:02",
                    "DriverOpts": null
                }
            }

第六步:设置Master/Slave架构

分别登陆redis6380/redis6381,然后执行如下语句

slaveof 172.17.0.2 6379 

说明,假如master有密码,需要在slave的redis.conf配置文件中添加"masterauth 你的密码"这条语句,然后重启redis再执行slaveof 指令操作.

第七步:再次登陆redis6379,然后检测info

[root@centos7964 ~]# docker exec -it redis6379 redis-cli
127.0.0.1:6379> info replication
\# Replication
role:master
connected_slaves:2
slave0:ip=172.17.0.3,port=6379,state=online,offset=2004,lag=1
slave1:ip=172.17.0.4,port=6379,state=online,offset=2004,lag=1
master_failover_state:no-failover
master_replid:5baf174fd40e97663998abf5d8e89a51f7458488
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:2004
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:2004

第八步: 登陆redis6379测试,master读写都可以

[root@centos7964 ~]# docker exec -it redis6379 redis-cli
127.0.0.1:6379> set role master6379
OK
127.0.0.1:6379> get role
"master6379"
127.0.0.1:6379>

第九步: 登陆redis6380测试,slave只能读。

[root@centos7964 ~]# docker exec -it redis6380 redis-cli
127.0.0.1:6379> get role
"master6379"
127.0.0.1:6379> set role slave6380
(error) READONLY You can't write against a read only replica.
127.0.0.1:6379>

Java中的读写测试分析,代码如下:

@SpringBootTest
public class MasterSlaveTests {
    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    void testMasterReadWrite(){//配置文件端口为6379
        ValueOperations valueOperations = redisTemplate.opsForValue();
        valueOperations.set("role", "master6379");
        Object role = valueOperations.get("role");
        System.out.println(role);
    }

    @Test
    void testSlaveRead(){//配置文件端口为6380
        ValueOperations valueOperations = redisTemplate.opsForValue();
        Object role = valueOperations.get("role");
        System.out.println(role);
    }

}
6.1.4 主从同步原理分析

Redis的主从结构可以采用一主多从结构,Redis主从复制可以根据是否是全量分为全量同步和增量同步。

  • Redis全量同步:

Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。具体步骤如下:1)从服务器连接主服务器,发送SYNC命令;
2)主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
3)主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
4)从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
5)主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
6)从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;

  • Redis增量同步

Redis增量复制是指Slave初始化后,开始正常工作时主服务器发生的写操作同步到从服务器的过程。 增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。

6.1.5 小节面试分析
  • 如果redis要支持10万+的的并发你会怎么做?

单机的redis几乎不太可能说QPS超过10万+,除非一些特殊情况,比如你的机器性能特别好,配置特别高,物理机,维护做的特别好,而且你的整体的操作不是太复杂,一般的单机也就在几万。真正实现redis的高并发,需要读写分离。对缓存而言,一般都是用来支撑读高并发的,写的请求是比较少的,可能写请求也就一秒钟几千。读的请求相对就会比较多,例如,一秒钟二十万次读。所以redis的高并发可以基于主从架构与读写分离机制进行实现。

  • Redis的replication机制是怎样的?

(1)redis采用异步方式复制数据到slave节点。
(2)一个master node是可以配置多个slave node的。
(3)slave node做复制的时候,是不会block master node的正常工作的。

(4)slave node在做复制的时候,也不会block对自己的查询操作,它会用旧的数据集来提供服务; 但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了。
(5)slave node主要用来进行横向扩容,做读写分离,扩容的slave node可以提高读的吞吐量。

6.2 Redis哨兵模式
6.2.1 简介

​ 哨兵(Sentinel)是Redis的主从架构模式下,实现高可用性(high availability)的一种机制。
由一个或多个Sentinel实例(instance)组成的Sentinel系统(system)可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。

6.2.1 基本架构

6.2.2 哨兵快速入门

第一步:打开三个redis客户端窗口,分别进入3台redis容器内部,在容器(Container)指定目录/etc/redis中执行如下语句:

cat <<EOF > /etc/redis/sentinel.conf 
sentinel monitor redis6379 172.17.0.2 6379 1
EOF

其中, 如上指令表示要的监控的master, redis6379为服务名, 172.17.0.2和6379为master的ip和端口,1表示多少个sentinel认为一个master失效时,master才算真正失效.

第二步:在每个redis容器内部的/etc/redis目录下执行如下指令,启动哨兵服务

redis-sentinel sentinel.conf

第三步:打开一个新的客户端连接窗口,关闭redis6379服务(这个服务是master服务)

docker stop redis6379

在其它客户端窗口,检测日志输出,例如

410:X 11 Jul 2021 09:54:27.383 # +switch-master redis6379 172.17.0.2 6379 172.17.0.4 6379
410:X 11 Jul 2021 09:54:27.383 * +slave slave 172.17.0.3:6379 172.17.0.3 6379 @ redis6379 172.17.0.4 6379
410:X 11 Jul 2021 09:54:27.383 * +slave slave 172.17.0.2:6379 172.17.0.2 6379 @ redis6379 172.17.0.4 6379

第四步:登陆ip为172.17.0.4对应的服务进行info检测,例如:

127.0.0.1:6379> info replication
\# Replication
role:master
connected_slaves:1
slave0:ip=172.17.0.3,port=6379,state=online,offset=222807,lag=0
master_failover_state:no-failover
master_replid:3d63e8474dd7bcb282ff38027d4a78c413cede53
master_replid2:5baf174fd40e97663998abf5d8e89a51f7458488
master_repl_offset:222807
second_repl_offset:110197
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:29
repl_backlog_histlen:222779
127.0.0.1:6379>

从上面的信息输出发现,redis6381服务现在已经变为master。

6.2.3 Sentinel 配置进阶

对于sentinel.conf文件中的内容,我们还可以基于实际需求,进行增强配置,例如:

sentinel monitor redis6379 172.17.0.2 6379 1 
daemonize yes #后台运行
logfile "/var/log/sentinel_log.log" #运行日志
sentinel down-after-milliseconds redis6379 30000 #默认30秒

其中:
1)daemonize yes表示后台运行(默认为no)
2)logfile 用于指定日志文件位置以及名字

3)sentinel down-after-milliseconds 表示master失效了多长时间才认为失效

例如: 基于cat指令创建sentinel.conf文件,并添加相关内容.

cat <<EOF > /etc/redis/sentinel.conf
sentinel monitor redis6379 172.17.0.2 6379 1
daemonize yes 
logfile "/var/log/sentinel_log.log"
sentinel down-after-milliseconds redis6379 30000 
EOF
6.2.4 哨兵工作原理分析

1):每个Sentinel以每秒钟一次的频率向它所知的Master,Slave以及其他 Sentinel 实例发送一个 PING 命令。

2):如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值(这个配置项指定了需要多少失效时间,一个master才会被这个sentinel主观地认为是不可用的。 单位是毫秒,默认为30秒), 则这个实例会被 Sentinel 标记为主观下线。

3):如果一个Master被标记为主观下线,则正在监视这个Master的所有 Sentinel 要以每秒一次的频率确认Master的确进入了主观下线状态。

4):当有足够数量的 Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认Master的确进入了主观下线状态, 则Master会被标记为客观下线 。

5):在一般情况下, 每个 Sentinel 会以每 10 秒一次的频率向它已知的所有Master,Slave发送 INFO 命令 。

6):当Master被 Sentinel 标记为客观下线时,Sentinel 向下线的 Master 的所有 Slave 发送 INFO 命令的频率会从 10 秒一次改为每秒一次 。

7):若没有足够数量的 Sentinel 同意 Master 已经下线, Master 的客观下线状态就会被移除。
8): 若 Master 重新向 Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除。

6.3 Redis集群高可用
6.3.1 简述

​ Redis单机模式可靠性保证不是很好,容易出现单点故障,同时其性能也受限于CPU的处理能力,实际开发中Redis必然是高可用的,所以单机模式并不是我们的终点,我们需要对目前redis的架构模式进行升级。
​ Sentinel模式做到了高可用,但是实质还是只有一个master在提供服务(读写分离的情况本质也是master在提供服务),当master节点所在的机器内存不足以支撑系统的数据时,就需要考虑集群了。
​ Redis集群架构实现了对redis的水平扩容,即启动N个redis节点,将整个数据分布存储在这N个redis节点中,每个节点存储总数据的1/N。redis集群通过分区提供一定程度的可用性,即使集群中有一部分节点失效或无法进行通讯,集群也可以继续处理命令请求。

6.3.2 基本架构

对于redis集群(Cluster),一般最少设置为6个节点,3个master,3个slave,其简易架构如下:

6.3.3 创建集群

第一步:准备网络环境

创建虚拟网卡,主要是用于redis-cluster能于外界进行网络通信,一般常用桥接模式。

docker network create redis-net

查看docker的网卡信息,可使用如下指令

docker network ls

查看docker网络详细信息,可使用命令

docker network inspect redis-net

第二步:准备redis配置模板

mkdir -p /usr/local/docker/redis-cluster
cd /usr/local/docker/redis-cluster
vim redis-cluster.tmpl

在redis-cluster.tmpl中输入以下内容

port ${PORT}
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-announce-ip 192.168.126.129
cluster-announce-port ${PORT}
cluster-announce-bus-port 1${PORT}
appendonly yes

其中:

各节点解释如下所示:

  1. port:节点端口,即对外提供通信的端口
  2. cluster-enabled:是否启用集群
  3. cluster-config-file:集群配置文件
  4. cluster-node-timeout:连接超时时间
  5. cluster-announce-ip:宿主机ip
  6. cluster-announce-port:集群节点映射端口
  7. cluster-announce-bus-port:集群总线端口
  8. appendonly:持久化模式

第三步:创建节点配置文件

在redis-cluser中执行以下命令

for port in $(seq 8010 8015); \
do \
  mkdir -p ./${port}/conf  \
  && PORT=${port} envsubst < ./redis-cluster.tmpl > ./${port}/conf/redis.conf \
  && mkdir -p ./${port}/data; \
done

其中:

  • for 变量 in $(seq var1 var2);do …; done为linux中的一种shell 循环脚本, 例如:
[root@centos7964 ~]# for i in $(seq 1 5);
> do echo $i;
> done;
1
2
3
4
5
[root@centos7964 ~]#
  • 指令envsubst <源文件>目标文件,用于将源文件内容更新到目标文件中.

通过cat指令查看配置文件内容

cat /usr/local/docker/redis-cluster/801{0..5}/conf/redis.conf

第四步:创建集群中的redis节点容器

for port in $(seq 8010 8015); \
do \
   docker run -it -d -p ${port}:${port} -p 1${port}:1${port} \
  --privileged=true -v /usr/local/docker/redis-cluster/${port}/conf/redis.conf:/usr/local/etc/redis/redis.conf \
  --privileged=true -v /usr/local/docker/redis-cluster/${port}/data:/data \
  --restart always --name redis-${port} --net redis-net \
  --sysctl net.core.somaxconn=1024 redis redis-server /usr/local/etc/redis/redis.conf; \
done

其中, --privileged=true表示让启动的容器用户具备真正root权限, --sysctl net.core.somaxconn=1024 这是一个linux的内核参数,用于设置请求队列大小,默认为128,后续启动redis的启动指令需要先放到这个请求队列中,然后依次启动.
创建成功以后,通过docker ps指令查看节点内容。

第五步:创建redis-cluster集群配置

docker exec -it redis-8010 bash
redis-cli --cluster create 192.168.126.129:8010 192.168.126.129:8011 192.168.126.129:8012 192.168.126.129:8013 192.168.126.129:8014 192.168.126.129:8015 --cluster-replicas 1

如上指令要尽量放在一行执行,其中最后的1表示主从比例,当出现选择提示信息时,输入yes即可。当集群创建好以后,可以通过一些相关指令查看集群信息,例如

cluster nodes   #查看集群节点数
cluster info #查看集群基本信息

第六步:连接redis-cluster,并添加数据到redis

redis-cli -c -h 192.168.126.129 -p 8010

其中,这里-c表示集群(cluster),-h表示host(一般写ip地址),-p为端口(port)

其它:
在搭建过程,可能在出现问题后,需要停止或直接删除docker容器,可以使用以下参考命令:

批量停止docker 容器,例如:

docker ps -a | grep -i "redis-801*" | awk '{print $1}' | xargs docker stop

批量删除docker 容器,例如

docker ps -a | grep -i "redis-801*" | awk '{print $1}' | xargs docker rm -f

批量删除文件,目录等,例如:

rm -rf 801{0..5}/conf/redis.conf
rm -rf 801{0..5}

以上就是基于docker搭建redis-cluster的简单步骤,实际应用中可能还要更复杂一些,该文仅用于参考。

6.4 Jedis读写数据测试
@Test
void testJedisCluster()throws Exception{
      Set<HostAndPort> nodes = new HashSet<>();
      nodes.add(new HostAndPort("192.168.126.129",8010));
      nodes.add(new HostAndPort("192.168.126.129",8011));
      nodes.add(new HostAndPort("192.168.126.129",8012));
      nodes.add(new HostAndPort("192.168.126.129",8013));
      nodes.add(new HostAndPort("192.168.126.129",8014));
      nodes.add(new HostAndPort("192.168.126.129",8015));
      JedisCluster jedisCluster = new JedisCluster(nodes);
      //使用jedisCluster操作redis
      jedisCluster.set("test", "cluster");
      String str = jedisCluster.get("test");
      System.out.println(str);
      //关闭连接池
      jedisCluster.close();
 }
6.5 RedisTemplate读写数据测试

第一步:配置application.yml,例如:

spring:
  redis:
    cluster: #redis 集群配置
      nodes: 192.168.126.129:8010,192.168.126.129:8011,192.168.126.129:8012,192.168.126.129:8013,192.168.126.129:8014,192.168.126.129:8015
      max-redirects: 3 #最大跳转次数
    timeout: 5000 #超时时间
    database: 0
    jedis: #连接池
      pool:
        max-idle: 8
        max-wait: 0 #

第二步:编写单元测试类,代码如下:

package com.cy.redis;
@SpringBootTest
public class RedisClusterTests {
    @Autowired
    private RedisTemplate redisTemplate;
    @Test
    void testMasterReadWrite(){
        //1.获取数据操作对象
        ValueOperations valueOperations = redisTemplate.opsForValue();
        //2.读写数据
        valueOperations.set("city","beijing");
        Object city=valueOperations.get("city");
        System.out.println(city);
    }
}
6.6 小节面试分析
  • Redis的高并发跟整个系统的高并发是什么关系?

第一:Redis你要搞高并发的话,不可避免,要把底层的缓存搞得很好。例如,mysql的高并发,是通过一系列复杂的分库分表,订单系统,事务要求的,QPS到几万,比较高了。
第二:要做一些电商的商品详情页,真正的超高并发,QPS上十万,甚至是百万,一秒钟百万的请求量,只有redis是不够的,但是redis是整个大型的缓存架构中,支撑高并发的架构里面,非常重要的一个环节。
第三:你的底层的缓存中间件,缓存系统,必须能够支撑的起我们说的那种高并发,其次,再经过良好的整体的缓存架构的设计(多级缓存架构、热点缓存),支撑真正的上十万,甚至上百万的高并发。

七. 总结

7.1 核心知识点
  • Redis技术简介(是什么,诞生背景,版本,基本架构)
  • Redis服务的基本操作(登录redis,基础指令,退出redis)
  • Redis中常用数据类型(string,hash,list,set)
  • 工程结构分析及项目创建
  • Jedis及JedisPool基本应用实践
  • RedisTemplate对象应用实践分析
  • StringRedisTemplate对象应用实践分析
  • 基于业务定制RedisTemplate对象?
  • 基于redis实现单点登录系统?(在redis记录用户状态)
  • 基于redis实现某个活动的投票系统?
  • SpringBoot工程中整合redis应用实践?
  • SpringBoot 中AOP方式应用Redis技术
  • Redis 持久化机制以及持久化方式
  • Redis 事务处理机制和常用指令应用。
  • Redis 主从架构设计及实践(有备无患,薪火相传)
  • Redis 哨兵机制及应用实践 (谋权篡位,反客为主)
  • Redis 集群架构设计及实践 (众志成城,齐心合力)
7.2 常见问题分析
  • Redis是什么?(分布式Key/Value结构的缓存数据库,非关系型数据,NoSql数据库)
  • Redis数据库诞生的背景?(关系型数据库的访问压力比较大,本地内存不支持多服务实例共享)
  • Redis数据库的基本架构?(C/S,redis-cli,redis-server)
  • 你了解Redis有哪些基础指令?(redis-cli,redis-server,exit,clear,type,expire,shutdown,help,?,keys,flushall,flushdb)
  • 字符串类型有什么特点?(所有值都是字符串,空间动态分配,可以实现整数值的递增,递减,实现日志记录)
  • 操作字符串类型(string)的常用指令?(set,get,strlen,append,mset,mget,incr,incrby,decr,decrby)
  • 哈希类型(hash)数据有什么特性?(就是值还可以使用key/value结构存储,key无序,key相同值覆盖,存储对象方便)
  • 操作哈希类型(hash)的常用指令?(hset,hget,hgetall,hexits,hdel,hkeys,hvals,hincrby,hmget)
  • 列表类型(list)数据有什么特性?(链表,会记录添加的元素的顺序,元素允许重复,可以实现FIFO,FILO这些特性)
  • 操作列表类型(list)类型的常用指令?(lpush,rpop,rpush,lpop,lrem,lindex,ltrim,lset,linsert,lrange,rpoplpush,lpos)
  • Set类型数据的特性?(散列,不记录元素添加顺序,不允许元素重复)
  • 操作set类型的常用指令?(sadd,smembers,spop,smove,scard,sunion)
  • Redis中各种数据类型的应用场景?
  • Jedis常用API有哪些?(Jedis,JedisPool,JedisPoolConfig)
  • Jedis如何存储POJO对象到redis?(将对象转换为json串,将对象以Hash形式进行存储)
  • RedisTemplate 对象默认的序列化方式是什么?(JDK方式)
  • RedisTemplate 对象按默认规则序列化时,我们的对象需要实现序列化接口吗?(需要)
  • StringRedisTemplate对象与RedisTemplate对象最本质的区别是什么?(默认序列化方式)
  • RedisTemplate对象基于默认的json方式序列化时会存储对象类型吗?(会)
  • 如何修改RedisTemplate对象的序列化方式?(通过setXxx等方法设置序列化)
  • 为什么要定制RedisTemplate对象?
  • 基于redis存储用户状态时,你使用的数据类型是什么?(hash类型)
  • 通过redis存储登录状态,你觉得有什么弊端?(检查用户登录状态需要查询redis)
  • 基于redis实现某个活动的投票系统,你使用的数据类型是什么?(Set类型)
  • 在我们的业务对象中如何使用redis?(注入RedisTemplate或其子类,AOP方式)
  • 为什么AOP方式应用redis?(优势-简单,劣势-不灵活)
  • 如何AOP方式应用redis?(说说步骤)
  • Aop方式应用Redis时如何进行序列化等方式定制?(修改CacheManager的配置)
  • Spring工程中用于描述应用缓存的切入点注解你了解哪些?(@Cacheable,@CachePut,…)
  • Spring工程中假如需要基于AOP方式整合缓存应用需要在配置上加什么注解吗?(@EnableCaching)
  • Spring工程中CacheManager作用是什么?(管理缓存,定义缓存配置及实现,也可以定制)
  • 为什么要持久化?(更好的保证数据的可靠性,防止数据断电丢失)
  • Redis中持久化的方式有哪些?(RDB,AOF)
  • 你是否了解AOF中的Rewrite操作?(重写AOF日志文件)
  • 你了解Redis中的哪些持久化配置?
  • 说说Redis中Rdb和Aof方式持久化数据的优势和劣势?(Rdb性能影响小,数据更准确,但是数据可能会丢,AOF更可靠一些,但是性能影响大一些)
  • 生产环境下如何应用Rdb和Aof的持久化方式?(两种都要配置)
  • Redis主从架构,哨兵,集群架构诞生的一个背景?
  • 单纯的Redis主从架构存在什么问题?(主机点宕机,整个主从不再支持写操作)
  • Redis哨兵(sentinel)做用是什么?(监控主从节点工作状态,主节点宕机,自动选择新的主节点)
  • Redis 从节点下还可有从节点吗?(可以的,薪火相传)
  • Redis主从加哨兵还存在什么明显缺陷?(主节点只有一个,支持可存储的数据量受限)
  • 集群架构要解决的主要问题是什么?(横向扩容,单个主节点不能支持更大并发的写操作,且容量有限)
  • 当架构设计中的redis服务有问题时怎么办?(一定要看容器日志)
7.3 常见Bug分析
  • redis服务不能启动?(看容器日志)
  • redis指令应用错误
  • 连不上远程redis服务?(redis服务是否启动,是否开启了防火墙,ip和账号是否正确,redis.conf配置问题)
  • 基于RedisTemplate对象的incrment方法实现key值的递增时,注意值的结构类型?(Long)
  • 对象序列化和反序列化异常?(一定要注意序列化规则)
  • 依赖注入异常?(检查对象是否交给了Spring管理,一个接口有多个实现时如何实现对象的注入)
  • 启动AOP方式的缓存实现,在启动类或配置上类没有加@EnableCaching注解
  • AOP方式应用Redis不生效?(检查启动类或配置类上是否开启了缓存配置)
  • redis配置文件配置出错导致redis服务无法启动?(vim指令不熟,容器服务启动医用要看日志)
  • Redis主从加哨兵还存在什么明显缺陷?(主节点只有一个,支持可存储的数据量受限)
  • 集群架构要解决的主要问题是什么?(横向扩容,单个主节点不能支持更大并发的写操作,且容量有限)
  • 当架构设计中的redis服务有问题时怎么办?(一定要看容器日志)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值