1. Redis基础数据结构与应用场景

概要
1)Redis的常用指令及业务应用场景
2)使用Java客户端操作Redis

简介:redis是一个开源的使用C语言编写、支持网络、可基于内存也可以持久化的日志型Key-Value数据库,并提供多种语言的API。
本质:客户端-服务端应用程序
特点:使用简单、性能强大、应用场景丰富

1.1 通用指令

指令 说明
del key key存在时,将key删除
dump key 序列化给定key,并返回被序列化的值
exists key 检查给定key是否存在
expire key seconds 为给定key设置过期时间,以秒计
ttl key 以秒为单位,返回给定key的剩余生存时间(TTL-time to live)
type key 返回key所存储的值得类型
dbsize 返回对应DB的大小
help @对应的数据类型 查看对应的数据类型的所有指令
help cmd 查看对应的指令的用法

示例
连接到服务端:

[docker@docker-node1 redis-all]$ ./redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379>

简单指令演示:

127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> set name vander
OK
127.0.0.1:6379[1]> exists name
(integer) 1
127.0.0.1:6379[1]> ttl name
(integer) -1
127.0.0.1:6379[1]> type name
string
127.0.0.1:6379[1]> set name vander ex 10
OK
127.0.0.1:6379[1]> ttl name
(integer) 5
127.0.0.1:6379[1]> get name
"vander"
127.0.0.1:6379[1]> ttl name
(integer) -2
127.0.0.1:6379[1]> get name
(nil)
127.0.0.1:6379[1]> set name1 jason
OK
127.0.0.1:6379[1]> keys *
1) "name1"
127.0.0.1:6379[1]>

1.2 五种基本数据结构

Redis的数据结构的命令可以从官方文档查找:https://redis.io/commands

redis官网
Redis的5种基本数据结构.png

1.2.1 数据结构-String

简介:string数据结构是简单的key-value类型,value不仅是string,还可以是数字
应用场景:微博数、粉丝数等常规计数、分布式锁(Redis执行Lua脚本保证原子性)、Session共享、全局序列号

字符串常用操作

指令 说明
set key value 设置指定key的值
get key 获取指定key的值
mset key value [ ...] 批量存储多个键值对
mget key [key ...] 获取所有给定key的值

原子加减

指令 说明
incr key 将key中存储的数字值增1
decr key 将key中存储的数字值减1
incrby key increment 将key存储的数字值加上increment
decrby key decrement 将key存储的数字值减去decrement

设置Key-Value的有效时间:

127.0.0.1:6379[1]> set name vander ex 10
OK
127.0.0.1:6379[1]> get name
"vander"
127.0.0.1:6379[1]> ttl name
(integer) 5
127.0.0.1:6379[1]> get name
"vander"
127.0.0.1:6379[1]> ttl name
(integer) -2
127.0.0.1:6379[1]> get name
(nil)

设置对Value进行递增或者递减

127.0.0.1:6379[1]> set counter 10
OK
127.0.0.1:6379[1]> incr counter
(integer) 11
127.0.0.1:6379[1]> decr counter
(integer) 10
127.0.0.1:6379[1]>

应用场景

## 单值缓存或者是使用对象序列化器存储
127.0.0.1:6379> set name jason
OK
127.0.0.1:6379> get name
"jason"
127.0.0.1:6379>
## 分布式锁的应用
# 假设是第一个线程执行这个指令
127.0.0.1:6379> setnx product:iPhone true 
(integer) 1
# 另一个线程执行此指令就失败了
127.0.0.1:6379> setnx product:iPhone true 
(integer) 0
# 第一个线程获取设置成功后表示获取到了锁,等用完之后把锁释放掉
127.0.0.1:6379> del product:iPhone 
(integer) 1
## 计数器场景
# 例如记录文章被读取的次数
127.0.0.1:6379> incr article:read_count:001
(integer) 1
127.0.0.1:6379> incr article:read_count:001
(integer) 2
127.0.0.1:6379> incr article:read_count:001
(integer) 3
127.0.0.1:6379> get article:read_count:001
"3"
## Web集群的Session共享
Spring Session + Redis 实现Session共享
## 分布式系统全局序列号
#为了保证性能每个客户端批量获取一批序列号,用完之后再到Redis里重新取一批
incrby orderid 1000

1.2.2 数据结构-List

简介:即链表

数据结构-list.png

可实现的数据结构:
Stack(栈) = LPUSH+LPOP = FILO
Queue(队列)= LPUSH+RPOP = FIFO
Blocking Queue(阻塞队列)= LPUSH + BRPOP

使用场景:微博的关注列表,粉丝列表

指令 说明
lpush key value1 value2 ... 将一个或多个值插入到列表头部
rpush key value1 value2 ... 在列表中添加一个或多个值
lpop key 移除并获取列表的第一个元素,返回值为移除的元素
rpop key 移除并获取列表的最后一个元素,返回值为移除的元素
lrange 获取所有给定key的值

实现栈(后进先出)的功能:

127.0.0.1:6379[1]> rpush teachers Miss.Xu Miss.Yu
(integer) 2
127.0.0.1:6379[1]> rpop teachers
"Miss.Yu"
127.0.0.1:6379[1]> rpop teachers
"Miss.Xu"
127.0.0.1:6379[1]> rpush teachers Miss.Xu
(integer) 1
127.0.0.1:6379[1]> rpush teachers Miss.Yu
(integer) 2
127.0.0.1:6379[1]> rpop teachers
"Miss.Yu"
127.0.0.1:6379[1]> rpop teachers
"Miss.Xu"
127.0.0.1:6379[1]>

实现队列(先进先出)功能:

127.0.0.1:6379[1]> lpush students stu1 stu2
(integer) 2
127.0.0.1:6379[1]> rpop students
"stu1"
127.0.0.1:6379[1]> rpop students
"stu2"
127.0.0.1:6379[1]>

场景模拟

指令 说明
lpush key value1 value2 ... 将一个或多个值插入到列表头部
rpush key value1 value2 ... 在列表中添加一个或多个值
lpop key 移除并获取列表的第一个元素,返回值为移除的元素
rpop key 移除并获取列表的最后一个元素,返回值为移除的元素
lrange 获取所有给定key的值
blpop key [key ...] timeout 从key列表头部弹出一个元素,若列表没有元素,阻塞等待timeout秒,如果timeout=0,则会一直阻塞等待
brpop key [key ...] timeout 从key列表尾部弹出一个元素,若列表没有元素,阻塞等待timeout秒,如果timeout=0,则会一直阻塞等待
##微博或微信公众号消息,如Vander关注了"财经旗舰"、“大众点评”两个公众号
#001为用户Vander的UserID
#模拟财经旗舰发送了一个消息,消息编号为1001
127.0.0.1:6379> lpush msg:001 msg_1001
(integer) 1
#模拟大众点评发送了一个消息,消息编号为1005
127.0.0.1:6379> lpush msg:001 msg_1005
(integer) 2
#查看待阅列表
127.0.0.1:6379> lrange msg:001 0 2
1) "msg_1005"
2) "msg_1001"

1.2.3 数据结构-Set

简介:即集合,存储一堆不重复值的组合
使用场景:抽奖、微信微博点赞、收藏、标签

Set常用操作

指令 说明
sadd key member [member ...] 将一个或多个值插入到列表头部
srem key member [member ...] 从集合key中删除元素
spop key [count] 移除并获取集合中随机获取count个元素,返回值为移除的元素
srandmember key [count] 获取集合中随机获取count个元素,不会移除元素
sismembers key member 判断member元素是否存在集合key中
smembers key 返回集合中的所有成员
sunion 返回给定集合的并集

往集合中添加删除元素:

127.0.0.1:6379[1]> sadd students a_stu b_stu
(integer) 2
127.0.0.1:6379[1]> spop students
"b_stu"
127.0.0.1:6379[1]> sadd students c_stu
(integer) 1
127.0.0.1:6379[1]> spop students
"c_stu"
127.0.0.1:6379[1]> sadd students 0_stus
(integer) 1
127.0.0.1:6379[1]> spop students
"a_stu"
127.0.0.1:6379[1]> sadd students a_stu b_stu
(integer) 2
127.0.0.1:6379[1]> smembers students
1) "b_stu"
2) "0_stus"
3) "a_stu"
127.0.0.1:6379[1]> spop students 3
1) "b_stu"
2) "0_stus"
3) "a_stu"

将三个集合取并集:

127.0.0.1:6379[1]> sadd stuGroup1 stu_a stu_b stu_c
(integer) 3
127.0.0.1:6379[1]> sadd stuGroup2 stu_k stu_a stu_b
(integer) 3
127.0.0.1:6379[1]> sadd stuGroup3 stu_x stu_c stu_b
(integer) 3
127.0.0.1:6379[1]> sunion stuGroup1 stuGroup2 stuGroup3
1) "stu_x"
2) "stu_c"
3) "stu_k"
4) "stu_a"
5) "stu_b"
127.0.0.1:6379[1]>

场景模拟

##模拟抽奖
# 添加抽奖用户
127.0.0.1:6379> sadd draw_user Vander Jason Panda Susan
(integer) 4
# 查看所有参与抽奖用户
127.0.0.1:6379> smembers draw_user
1) "Susan"
2) "Jason"
3) "Panda"
4) "Vander"
# 抽取一个用户放回的(不放回的用spop draw_user 1)
127.0.0.1:6379> srandmember draw_user 1
1) "Susan"
127.0.0.1:6379> srandmember draw_user 1
1) "Panda"
127.0.0.1:6379> srandmember draw_user 1
1) "Jason"

##模拟点赞取消点赞、收藏、标签
# 点赞,001-消息ID、1001-用户ID,说明1001用户对001消息点赞
127.0.0.1:6379> sadd like:001 1001
(integer) 1
# 1001用户对002消息点赞
127.0.0.1:6379> sadd like:002 1001
(integer) 1
# 1002用户对001消息点赞
127.0.0.1:6379> sadd like:001 1002
(integer) 1
# 检查1002用户是否对001消息点赞
127.0.0.1:6379> sismember like:001 1002
(integer) 1
# 获取对001消息点赞的用户列表
127.0.0.1:6379> smembers like:001
1) "1001"
2) "1002"
# 获取对001消息点赞的用户数
127.0.0.1:6379> scard like:001
(integer) 2
# 1001用户取消对001消息点赞
127.0.0.1:6379> srem like:001 1001
(integer) 1

Set集合操作

指令 说明
sinter key [key ...] 交集运算
sinterstore dest key [key...] 交集运算并把结果存到新的集合dest中
sunion key [key ...] 并集运算
sunionstore dest key [key ...] 并集运算并把结果存到新的集合dest中
sdiff key [key ...] 差集运算
sdiffstore dest key [key ...] 差集运算并把结果存到新的集合dest中
set-集合运算.png

集合运算应用场景:共同关注、共同喜好、电商商品筛选

假设上图中的集合Set1是A同学的关注列表,Set2是B同学的关注列表,Set3是C同学的关注列表

A同学和B同学的共用关注:Set1∩Set2 -> sinter set1 set2

A同学关注的人关注了他:即A同学关注了B同学,A同学关注了C同学,B、C同学都关注了d
SISMEMBER Set2 d(即A同学关注的B同学关注了d)
SISMEMBER Set3 d(即A同学关注的C同学关注了d)

A可能认识的人:SDIFF Set2 Set1 (即从B同学的关注列表里列出A同学没有的)

场景模拟

# 添加华为p40手机
127.0.0.1:6379> sadd brand:huawei p40
(integer) 1
# 添加小米11Pro手机
127.0.0.1:6379> sadd brand:xiaomi "xiaomi 11 pro"
(integer) 1
# 添加iPhone 12 Pro手机
127.0.0.1:6379> sadd brand:apple "iPhone 12 Pro"
(integer) 1
# 添加安卓系统的手机
127.0.0.1:6379> sadd os:android p40 "xiaomi 11 pro"
(integer) 2
# 添加内存8G的手机
127.0.0.1:6379> sadd ram:8G p40 "xiaomi 11 pro" "iPhone 12 Pro"
(integer) 3
# 获取安卓系统且内存为8G的手机
127.0.0.1:6379> sinter os:android ram:8G
1) "p40"
2) "xiaomi 11 pro"

1.2.4 数据结构-Sorted Set(ZSet)

简介:即有序集合,存储一堆不重复值的组合,与Set类似,区别在于Set不是自动有序,Sorted Set则可以通过用户额外提供一个优先级(score)来为成员排序,并且是插入有序(会自动进行排序)

数据结构-zset.png

使用场景:排行榜、按用户投票和进行时间排序

ZSet常用操作

指令 说明
zadd key [ ...] 往有序集合key中加入带分值的元素
zrem key member [member ...] 从有序集合key中删除元素
zscore key member 返回有序集合key中元素member的分值
zincrby key incr member 为有序集合key中元素member的分支加上incr
zcard key 返回有序集合key中的元素个数
zrange key start stop [withscores] 正序获取有序集合Key,从start下标到stop下标的元素
zrevrange key start stop [withscores] 倒序获取有序集合Key,从start下标到stop下标的元素

往有序集中添加元素:

127.0.0.1:6379[1]> del stuGroup1
(integer) 1
127.0.0.1:6379[1]> zadd stuGroup1 0 "stu_a" 1 "stu_b" 2 "stu_c"
(integer) 3
127.0.0.1:6379[1]> del stuGroup1
(integer) 1
127.0.0.1:6379[1]> zadd stuGroup1 0 stu_a 1 stu_b 2 stu_c
(integer) 3
127.0.0.1:6379[1]> zrange stuGroup1 0 -1 withscores
1) "stu_a"
2) "0"
3) "stu_b"
4) "1"
5) "stu_c"
6) "2"

移除并显示有序集的元素:

127.0.0.1:6379[1]> zrem stuGroup1 stu_b
(integer) 1
127.0.0.1:6379[1]> zrange stuGroup1 0 -1 withscores
1) "stu_a"
2) "0"
3) "stu_c"
4) "2"
127.0.0.1:6379[1]> zcard stuGroup1
(integer) 2

场景模拟

##使用ZSet实现排行榜,假设当前百度热榜是赵丽颖结婚、郑爽弃养、乔妹离婚
# 使用此方式登录,否则显示乱码
./redis-cli --raw -h 127.0.0.1 -p 6379
# 模拟点击新闻
127.0.0.1:6379> zincrby HotNews:20210201 1 郑爽弃养
"1"
127.0.0.1:6379> zincrby HotNews:20210201 1 郑爽弃养
"2"
127.0.0.1:6379> zincrby HotNews:20210201 1 郑爽弃养
"3"
127.0.0.1:6379> zincrby HotNews:20210201 1 郑爽弃养
"4"
127.0.0.1:6379> zincrby HotNews:20210201 1 赵丽颖结婚
"1"
127.0.0.1:6379> zincrby HotNews:20210201 1 赵丽颖结婚
"2"
127.0.0.1:6379> zincrby HotNews:20210201 1 乔妹离婚
"1"
127.0.0.1:6379> zincrby HotNews:20210201 1 小米11发布
"1"
127.0.0.1:6379> zincrby HotNews:20210201 1 苹果发布会
"1"
# 展示当日点击量排行前3的新闻,并将点击量输出
127.0.0.1:6379> zrevrange HotNews:20210201 0 2 withscores
郑爽弃养
4
赵丽颖结婚
2
苹果发布会
1

#新增其它两天的点击量
127.0.0.1:6379> zincrby HotNews:20210202 1 小米11发布
1
127.0.0.1:6379> zincrby HotNews:20210202 1 乔妹离婚
1
127.0.0.1:6379> zincrby HotNews:20210203 1 郑爽弃养
1
127.0.0.1:6379> zincrby HotNews:20210203 1 小米11发布
1
# 3天内的榜单计算
127.0.0.1:6379> zunionstore HotNews:20210201-20210203 3 HotNews:20210201 HotNews:20210202 HotNews:20210203
5
127.0.0.1:6379> zrange HotNews:20210201-20210203 0 -1 withscores
苹果发布会
1
乔妹离婚
2
赵丽颖结婚
2
小米11发布
3
郑爽弃养
5
# 展示3天排行前3名点击量的事件
127.0.0.1:6379> zrevrange HotNews:20210201-20210203 0 2
郑爽弃养
小米11发布
赵丽颖结婚

ZSet集合操作

指令 说明
zunionstore dest numkeys key [key ...] 并集运算,并存储结果到dest
zinterstore dest numkeys key [key ...] 交集运算,并存储结果到dest

1.2.5 数据结构-Hash

简介:Hash是一个string类型的field和value的映射表
使用场景:存储部分变更数据,如用户信息

指令 说明
hset key field value 将哈希表中key的字段field的值设置为value
hget key field 获取存储在哈希表中指定field的值
hmset key field value [field value ...] 批量获取哈希表key中对应多个field的键值
hmget key field [field ...] 批量获取哈希表key中多个field键值
hsetnx key field value 存储一个不存在与hash表key的键值对
hdel key field [field ...] 删除hash表key中的field简直
hlen key 返回hash表key中field的数量
hget all 获取在哈希表中指定key的所有字段和值
hincrby key field increment 为hash表key中filed键的值上增量increment

往Hash表中插入元素:

127.0.0.1:6379[1]> hset Miss.Xu age 25
(integer) 1
127.0.0.1:6379[1]> hset Miss.Xu height 170
(integer) 1
127.0.0.1:6379[1]> hget Miss.Xu age
"25"
127.0.0.1:6379[1]> hgetall Miss.Xu
1) "age"
2) "25"
3) "height"
4) "170"
127.0.0.1:6379[1]>

场景模拟

##对象缓存
127.0.0.1:6379> hmset user:2010130110 name Vander age 18
OK
127.0.0.1:6379> hmget user:2010130110 name age
1) "Vander"
2) "18"
##电商购物车,key-用户ID field-商品ID value-商品数量
# 添加商品
127.0.0.1:6379> hset cart:2010130110 1001 1
(integer) 1
127.0.0.1:6379> hset cart:2010130110 1002 1
(integer) 1
127.0.0.1:6379> hset cart:2010130110 1003 2
(integer) 1
127.0.0.1:6379> hincrby cart:2010130110 1001 3
(integer) 4
# 获取商品种类的个数
127.0.0.1:6379> hlen cart:2010130110
(integer) 3
# 删除商品
127.0.0.1:6379> hdel cart:2010130110 1002
(integer) 1
# 获取购物车的所有商品
127.0.0.1:6379> hgetall cart:2010130110
1) "1001"
2) "4"
3) "1003"
4) "2"

Hash VS string

优势:
1)同类数据归类整合存储,方便数据管理
2)相比string操作消耗内存与CPU更小
3)相比string存储更省空间

劣势:

1)过期功能不能使用在field上,只能用在key上
2)Redis集群架构下不适合大规模使用

1.3 Java代码实操

下面我们通过Spring演示以上的数据,细节请查看Spring官方文档:

https://docs.spring.io/spring-data/redis/docs/2.1.8.RELEASE/reference/html/

官方文档中有这么一句话:
Spring Redis requires Redis 2.6 or above and Spring Data Redis integrates with Lettuce and Jedis, two popular open-source Java libraries for Redis(说明Spring的Redis客户端有Lettuce和Jedis两种)

1.3.1 Example :使用Jedis连接单实例的Redis服务端

@Profile(“jedis”)—@ActiveProfiles(“jedis”)成对使用,ActiveProfiles会选择对应的Profile进行注入
测试类:JedisTests.java——使用Jedis操作set、hash、zset、list(模拟队列)

package szu.vander.test.standalone;

import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import redis.clients.jedis.Jedis;

import java.util.HashMap;
import java.util.List;
import java.util.Set;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
@ActiveProfiles("single") // 设置profile
@Slf4j
public class JedisTests {

    @Autowired
    private Jedis jedis;

    // ------------------------ jedis 工具直连演示
    // jedis和redis命令名称匹配度最高,最为简洁,学习难度最低

    // 列表~ 集合数据存储~ java.util.List,java.util.Stack
    // 生产者消费者(简单MQ)
    @Test
    public void list() {

        // 插入数据1 --- 2 --- 3
        jedis.rpush("queue_1", "1");
        jedis.rpush("queue_1", "2", "3");

        List strings = jedis.lrange("queue_1", 0, -1);
        for (String string : strings) {
            log.info(String.format("往队列queue_1中写入:%s", string));
        }

        // 消费者线程简例
        while (true) {
            String item = jedis.lpop("queue_1");
            if (item == null) break;
            log.info(String.format("从队列queue_1中取出:%s", item));
        }

        jedis.close();
    }

    // 类似:在redis里面存储一个hashmap
    // 推荐的方式,无特殊需求是,一般的缓存都用这个
    @Test
    public void hashTest() {
        String key = "2010130110";
        jedis.hset(key, "name", "Vander");
        jedis.hset(key, "age", "18");

        jedis.hget(key, "name");
        log.info(String.format("获取Key=%s的所有相关属性:%s", key, jedis.hgetAll(key).toString()));
        jedis.close();
    }

    // 用set实现(交集 并集)
    // 交集示例: 共同关注的好友
    // 并集示例:
    @Test
    public void setTest() {
        // 取出两个人共同关注的好友
        // 每个人维护一个set
        jedis.sadd("userA", "userC", "userD", "userE");
        jedis.sadd("userB", "userC", "userE", "userF");
        // 取出共同关注
        Set intersection = jedis.sinter("userA", "userB");
        log.info(String.format("获取userA{%s}和userB{%s}的交集:%s", jedis.smembers("userA"), jedis.smembers("userB"), intersection));

        // 取出共同人群
        Set unionSet = jedis.sunion("userA", "userB");
        log.info(String.format("获取userA{%s}和userB{%s}的并集:%s", jedis.smembers("userA"), jedis.smembers("userB"), unionSet));

        jedis.close();
    }

    // 游戏排行榜
    @Test
    public void zsetTest() {
        String ranksKeyName = "exam_rank";
        jedis.zadd(ranksKeyName, 100.0, "stu1");
        jedis.zadd(ranksKeyName, 82.0, "stu2");
        jedis.zadd(ranksKeyName, 90, "stu3");
        jedis.zadd(ranksKeyName, 96, "stu4");
        jedis.zadd(ranksKeyName, 89, "stu5");
        jedis.zadd(ranksKeyName, 29, "stu6");

        Set stringSet = jedis.zrevrange(ranksKeyName, 0, 2);
        System.out.println("返回前三名:");
        for (String s : stringSet) {
            System.out.println(s);
        }

        long zcount = jedis.zcount(ranksKeyName, 85, 100);
        System.out.println("超过85分的数量 " + zcount);

        jedis.close();
    }
}

JedisConfig

package szu.jason.redis.standalone.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import redis.clients.jedis.Jedis;
import szu.jason.redis.standalone.config.common.RedisStandaloneProperties;

/**
 * @author : Vander
 * @date :   2021/3/7
 * @description : 注入连接单节点的Jedis客户端
 */
@Profile("jedis")
@Slf4j
@Configuration
public class JedisConfig {

    @Autowired
    private RedisStandaloneProperties properties;

    @Bean
    public Jedis jedis() {
        log.info("注入Jedis!");
        Jedis jedis = new Jedis(properties.getHostname(), properties.getPort());
        jedis.select(properties.getDb());
        return jedis;
    }

}

User

package szu.jason.redis.standalone.model;

import com.sun.xml.internal.ws.developer.Serialization;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * @author : Vander
 * @date :   2021/3/7
 * @description : 此对象需要实现序列化接口,否则JDK序列化器无法将其序列化
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {

    private String userId;

    private String username;

}

AppConfig

package szu.jason.redis.standalone.config.common;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import szu.jason.redis.standalone.model.User;

import java.util.HashMap;
import java.util.Map;

/**
 * @author : Vander
 * @date :   2021/3/7
 * @description :
 */
@ComponentScan("szu.jason.redis.standalone")
@Configuration
public class AppConfig {
    @Bean
    public Map userRepository() {
        Map userRepository = new HashMap<>();
        userRepository.put("2010130110", new User("2010130110", "Jason"));
        userRepository.put("2013130128", new User("2013130128", "Panda"));
        return userRepository;
    }
}

PropertiesConfig

package szu.jason.redis.standalone.config.common;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
 * @author : Vander
 * @date :   2021/3/7
 * @description :
 */
@Configuration
@EnableConfigurationProperties(RedisStandaloneProperties.class)
public class PropertiesConfig {
}

RedisStandaloneProperties

package szu.jason.redis.standalone.config.common;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * @author : Vander
 * @date :   2021/3/7
 * @description : 根据前缀注入属性值
 */
@Getter
@Setter
@ConfigurationProperties(prefix = "standalone.redis")
public class RedisStandaloneProperties {

    private String hostname;

    private int port;

    private int db;

}

application-redis.properties——配置redis服务

standalone.redis.hostname=192.168.118.8
standalone.redis.port=6379
standalone.redis.db=1

Pom



    
        cache-redis-demo
        szu.jason
        0.0.1-SNAPSHOT
    
    4.0.0

    redis-standalone

    
        
            redis.clients
            jedis
            3.1.0
        
        
            io.lettuce
            lettuce-core
            5.2.2.RELEASE
        
        
            org.springframework
            spring-test
            5.2.4.RELEASE
        
        
            ch.qos.logback
            logback-classic
            1.2.3
        
        
            junit
            junit
            4.12
        
        
            org.springframework.data
            spring-data-redis
            2.2.5.RELEASE
        
        
            org.springframework.boot
            spring-boot-configuration-processor
            2.2.5.RELEASE
        
        
            org.springframework.boot
            spring-boot-starter
            2.2.5.RELEASE
        
        
            org.springframework.boot
            spring-boot-starter-test
            2.2.5.RELEASE
        
    


1.3.2 Example :使用Lettuce连接单实例的Redis服务端

测试类:使用Lettuce客户端操作Hash类型的数据

package test.redis.standalone;

import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import szu.jason.redis.standalone.config.common.AppConfig;
import szu.jason.redis.standalone.model.User;
import szu.jason.redis.standalone.service.LettuceExampleService;


@ActiveProfiles("lettuce")
@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@PropertySource("classpath:application-redis.properties")
@ImportAutoConfiguration(classes= AppConfig.class)
public class LettuceTests {

    @Autowired
    private LettuceExampleService exampleService;

    @Test
    public void testGet() throws Exception {
        User user = exampleService.findUser("2010130110");
        System.out.println(user);
    }

}

LettuceConfig

package szu.jason.redis.standalone.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import szu.jason.redis.standalone.config.common.RedisStandaloneProperties;

@Profile("lettuce")
@Slf4j
@EnableCaching
@Configuration
public class LettuceConfig {


    @Autowired
    private RedisStandaloneProperties properties;

    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        log.info(String.format("注入LettuceConnectionFactory,当前Host:%s:%s database:%s!",
                properties.getHostname(), properties.getPort(), properties.getDb()));
        RedisStandaloneConfiguration redisStandaloneConfiguration =
                new RedisStandaloneConfiguration(properties.getHostname(), properties.getPort());
        redisStandaloneConfiguration.setDatabase(properties.getDb());
        return new LettuceConnectionFactory(redisStandaloneConfiguration);
    }

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        log.info("注入RedisTemplate!");
        RedisTemplate redisTemplate = new RedisTemplate();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        // 可以配置对象的转换规则,比如使用json格式对object进行存储。
        // Object --> 序列化 --> 二进制流 --> redis-server存储
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
        return redisTemplate;
    }

    // 配置Spring Cache注解功能
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        log.info("注入CacheManager!");
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
        RedisCacheManager cacheManager = new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
        return cacheManager;
    }

}

LettuceExampleService

package szu.jason.redis.standalone.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import szu.jason.redis.standalone.model.User;

import java.util.Map;


@Profile("lettuce")
@Slf4j
@Service
public class LettuceExampleService {
    // 参数可以是任何对象,默认由JDK序列化
    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private Map userRepository;

    /**
     * 对象缓存功能
     */
    public User findUser(String userId) throws Exception {
        User user;
        // 1、 判定缓存中是否存在
        user = (User) redisTemplate.opsForValue().get(userId);
        if (user != null) {
            log.info("从缓存中读取到值:" + user);
            return user;
        }

        // TODO 2、不存在则读取数据库或者其他地方的值
        user = userRepository.get(userId);
        log.info("从数据库中读取到值:" + user);

        // 3、 同步存储value到缓存。
        redisTemplate.opsForValue().set(userId, user);
        return user;
    }

}

AppConfig、PropertiesConfig、RedisStandaloneProperties、User、application-redis.properties、pom均与上个example一致

1.3.3 Example :使用Spring Cache实现自动移除缓存、添加缓存、更新缓存

测试类:模拟查找、更新、删除数据库记录的操作,Spring Cache会自动更新缓存

package test.redis.standalone;

import lombok.extern.slf4j.Slf4j;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.MethodSorters;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import szu.jason.redis.standalone.config.common.AppConfig;
import szu.jason.redis.standalone.model.User;
import szu.jason.redis.standalone.service.SpringCacheService;

@ActiveProfiles("lettuce")
@Slf4j
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@RunWith(SpringJUnit4ClassRunner.class)
@PropertySource("classpath:application-redis.properties")
@ImportAutoConfiguration(classes= AppConfig.class)
public class SpringCacheTests {

    private final static String USER_ID = "2010130110";

    @Autowired
    private SpringCacheService springCacheService;

    @Test
    public void test0FindUserById() {
        User user = springCacheService.findUserById(USER_ID);
        System.out.println("\n");
        log.info("查找到userId为{}的用户:{}", USER_ID, user.toString());
        System.out.println("\n");
    }

    @Test
    public void test1UpdateUser() {
        springCacheService.updateUser(new User(USER_ID, "Panda"));
        User user = springCacheService.findUserById(USER_ID);
        System.out.println("\n");
        log.info("userId为{}的用户:{}", USER_ID, user.toString());
        System.out.println("\n");
    }

    @Test
    public void test2DeleteUserById() {
        User user = springCacheService.deleteUserById(USER_ID);
        System.out.println("\n");
        log.info("删除userId为{}的用户:{}", USER_ID, user.toString());
        System.out.println("\n");
    }
}

SpringCacheService

package szu.jason.redis.standalone.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import szu.jason.redis.standalone.model.User;

import java.util.Map;

@Profile("lettuce")
@Slf4j
@Service
public class SpringCacheService {

    @Autowired
    private Map userRepository;

    /**
     * value的作用:写入Redis时会自动添加缓存前缀
     * key:支持SpEL表达式
     * @param userId
     * @return
     */
    @Cacheable(cacheManager = "cacheManager", value = "user", key = "#userId")
    public User findUserById(String userId) {
        // 读取数据库
        User user = userRepository.get(userId);
        System.out.println("\n");
        log.info("从数据库中读取到数据:{}", user);
        System.out.println("\n");
        return user;
    }

    @CacheEvict(cacheManager = "cacheManager", value = "user", key = "#userId")
    public User deleteUserById(String userId) {
        User user = userRepository.remove(userId);
        System.out.println("\n");
        log.info(String.format("用户从数据库删除成功,请检查缓存ID:%s是否已经清除!", user.getUserId()));
        System.out.println("\n");
        return user;
    }

    // 如果数据库更新成功,更新redis缓存
    @CachePut(cacheManager = "cacheManager", value = "user", key = "#user.userId", condition = "#result ne null")
    public User updateUser(User user) {
        // 更新数据库
        userRepository.put(user.getUserId(), user);
        System.out.println("\n");
        log.info("数据库进行了更新,检查缓存是否一致");
        System.out.println("\n");
        return user; // 返回最新内容,代表更新成功
    }

}

AppConfig、PropertiesConfig、RedisStandaloneProperties、User、application-redis.properties、pom均与上个example一致

运行结果:
test0FindUserById:观察到对应的User被加载到了缓存

test1UpdateUser:观察到对应的User的名称被改为了Panda

test2DeleteUserById:观察到对应的Key已经被删除了

附录:建议安装教程

下载Redis,解压后编译

$ wget http://download.redis.io/releases/redis-5.0.4.tar.gz
$ tar xzf redis-5.0.4.tar.gz
$ cd redis-5.0.4
$ make

运行Redis服务端

$ src/redis-server
You can interact with Redis using the built-in client:

运行Redis客户端

$ src/redis-cli
redis> set foo bar
OK
redis> get foo
"bar"
···

你可能感兴趣的:(1. Redis基础数据结构与应用场景)