百度指数: http://index.baidu.com/v2/index.html#/

Redis

# key 命令

Redis 支持 5 中数据类型:

  • string(字符串)
  • hash(哈希)
  • list(列表)
  • set(集合)
  • zset(sorted set:有序集合)
  • 等(新版会增加类型,如 5.0,后面会讲到)

常用命令 key 管理

keys * :返回满足的所有键,可以模糊匹配,比如 keys abc* 代表 abc 开头的 key
exists key : 是否存在指定的 key,存在返回1 , 不存在返回 0
expire key second : 设置 某个 key 的过期时间 时间单位 秒
del key : 删除某个 key
ttl key : 查看剩余时间,
		 当 key 不存在时,返回 -2 ;
		 存在但没有设置剩余生存时间时,返回 -1 ,
		 否则,以秒单位返回 key 的剩余生存时间
persist key : 取消过时时间
PEXPIRE key milliseconds : 修改 key 的过期时间为毫秒
select : 选择数据库 数据库为 0~15(默认一共16个数据库)
				设计成多个数据库实际上是为了数据库安全和备份
move key dbindex : 将当前数据库中的 key 转移到其他数据库
randomkey:随机返回一个key
rename key key2 : 重命名 key
echo : 打印命令
dbsize : 查看数据库的 key 数量
info: 查看数据库信息
config get * : 实时传存储收到的请求,返回相关的配置
flushdb : 清空当前数据库
flushall:清空所有数据库

应用场景

EXPIRE key seconds、EXISTS key

  1. 显示的优惠活动信息
  2. 网站数据存储(对于一些需要定时更新的数据,如:积分排行榜)
  3. 手机验证码
  4. 限制网站访客访问频率(例如:1 分钟最多访问 10 次)(流量攻击)
    ip 得到 ==> 计数 key 的生存时间为 1 秒

# key 命令规范(建议)

为什么要有命名规范?
非关系数据库(Redis):数据与数据之间没有关联关系。
避免回看时候的混乱

命名规范:

  • <mark>key 不要太长</mark>,尽量不要超过 1024 字节,这不仅消耗内存,而且会<mark>降低查找的效率</mark>;
    redis 单个 key 允许存入 512M 大小

  • <mark>key 也不要太短</mark>,太短的话,key 的<mark>可读性差</mark>

  • 在一个项目中,key 最好<mark>使用同一的命名模式</mark>。
    例如:user:123:password

  • key 名称<mark>区分大小写</mark>

比较好行的命名模式 ⇒ 冒号模式分割

user:id:name
如
user:1:zhangshang
user:2:lisi


不建议:
user_id_name(某些情况无法区分)

另外,如果遵循 user:id:name 的命名规范,上面的可视化工具会自动帮我们分组管理




# 数据类型

String 类型

String 类型是 Redis 最基本的数据类型,一个键最大存储 512MB
String 数据结构是简单的 key-value 类型,<mark>value 其不仅是 string、也可以是数字,是包含很多种类型额的特殊类型</mark>

<mark>string 类型是二进制安全的,意思是 redis 的 string 可以包含任何数据</mark>

比如序列化的对象进行存储,比如一张图片进行二进制存储,比如简单的字符串,数值等等。

String 命令

!赋值语法
------------------------------------------------------------------
SET KEY value
	多次设置KEYVALUE会覆盖
	(即如果KEY已经存储值,SET就会覆写旧值,且无视类型)

MSET K1 V1 K2 V2 ...
	批量写入

SETNX KEY value
	(NX = not exists)
	如果KEY不存在,则设值,并且返回 1
	如果KEY存在,则不设值,并且返回 0
	(解决分布式锁 方案之一,	只有在KEY不存在时设置KEY的值。)
	(类似 MYSQLSET if not exists)

SETEX KEY time value
	(字符串类型专用过,设置过期时间)

SETRANGE KEY offset value
	替换字符串
	如:
	set name hhhh
	SETRANGE name 0 11
	get name
	得到:11hh

SETBIT KEY offset value 
	设置指定 key 对应偏移量上的 bit 值,value 只能是1或者0
	(String 作为 Bitmaps 类型的 api,后面讲bitmaps时再提)


!取值语法
------------------------------------------------------------------
GET KEY
	如果KEY不存在,则返回 nil
	如果KEY存储的值不是字符串类型,返回一个错误

GET K1 K2 K3 ...
	批量读

GETRANGE KEY offset end
	字符量截取
	(包括offset和end)
	如:
	set name 123456
	GETRANGE name 1 3
	得到:23

GETBIT KEY offset
	对KEY所存储字符串,获取指定的偏移量上的位(bit)
	如:value=2 => (bit)10  => 1(offset=0),0(offset=1)


GETSET KEY value
	设置KEY上新的值value,并返回旧的值
	当key不存在时,依然存入新的值,并返回nil

STRLEN KEY
	返回KEY所存储的字符串的长度


!删除语法
------------------------------------------------------------------
DEL KEY
	删除指定的KEY,如果存在,返回数字类型


!自增、自减
------------------------------------------------------------------
INCR KEY
	自增
	如果KEY不存在,那么结果为 1KEY会先被初始化为 0,然后马上自增为 1)
	如果类型不对:("ERR value is not an integer or out of range"INCRBY KEY increment
	指定增量值

DECR KEY
	自减
	如果KEY不存在,那么结果为 -1KEY会先被初始化为 0,然后马上自减为 -1)
	如果类型不对:("ERR value is not an integer or out of range"DECRBY KEY decrement
	指定减量值

APPEND KEY VALUE
	字符串拼接
	(追加至末尾)

String 应用场景

  1. String 通常用于保存单个字符串或 JSON 字符串数据
  2. 因为 String 是二进制安全的,所以你完全可以把一个图片文件的内容作为字符串来存储
  3. 计数器(常规 Key-valu 缓存应用。常规计数器:微博数、粉丝数)

INCR 等指令本身就具有原子操作的特性
<mark>所以我们完全可以利用 redis 的 INCR、INCRBY、DECR、DECRBY 等指令来实现原子计数的效果</mark>
(简单理解原子性)假如:

  • 在某种场景下有 3 个客户端同时读取了 mynum 的值(值为 2)
  • 然后对其同时进行了 加 1 的操作。
  • 那么,最后 mynum 的值一定是 6



Hash 类型 = Object (Map)

  • <mark>Hash 类型是 String 类型的 field 和 value 的映射表,或者说是一个 String 的集合。</mark>
  • <mark>相比于 String,hash 存储对象所占空间比 String 类型少,hash 特别适合于存储对象</mark>
  • <mark>相比于 json , hash 存储在磁盘中的空间也很小</mark>
    (Redis 中,每个 hash 可以存储 2 的 32 次方-1 键值对 = 40 多亿)

一句话: 特别适合存储对象 Object

Hash 命令

!赋值语法
------------------------------------------------------------------
HSET KEY FIELD value
	存储单个属性
	如:
	hset user:1 id 1
	Json 视角:
	'user:1' : {
		id:1
	}

HMSET KEY FIELD value
	存储多个属性(批量存储属性)
	如:
	hmset user:1 id 1 name alan age 18
	Json 视角:
	'user:1' : {
		id:1 ,
		name : "alan" ,
		age : 18
	}


!取值语法
------------------------------------------------------------------
HGET KEY FIELD
	获取 KEY 对象的 FIELD 属性值

HMGET KEY F1  F2  ...
	批量获取 KEY 对象的 FIELD 属性值

HGETALL KEY
	获取KEY中全部属性值

HKEYS KEY
	获取KEY对象的所有属性名

HLEN KEY
	获取KEY对象属性的个数

!删除语法
------------------------------------------------------------------
HDEL KEY F1 F2 ...
	删除一个或多个KEY对象的属性


!其他语法
------------------------------------------------------------------
HSETNX KEY FIELD value
	只有在字段FIELD不存在时,设置值

HINCRBY KEY FIELD increment
	为KEY对象(整数类型的)FIELD属性设置自增量

HINCRBYLOAT KEY FIELD increment
	为KEY对象(浮点类型的)FIELD属性设置自增量

HEXISTS KEY FIELD
	查看KEY对象的FIELD是否存在

Hash 应用场景

常用语存储一个对象

为什么不用 String 存储一个对象?

  • <mark>Hash 是最接近关系数据库结构的数据结构</mark>,可以将数据库一条记录或者程序中一个对象转换成hashmap存放在redis中。
    <mark>可以理解成:用hash类型存储的,直接就是对象</mark>
  • 而用 String 类型存储对象,有两种方案:
    1. 对象系列化成String存储
      <mark>这样添加了序列化、反序列化的开销</mark>
      进而导致在修<mark>改时候需要整个对象</mark>
      再进而导致需要<mark>对并发保护</mark>,和引入 CAS 等复杂问题。
    2. Key=用户id+属性名,value=属性值 方式存储
      这方案问题很明显,<mark>浪费内存,效率低</mark>
      假如:一个用户10个属性,创建10个键值对。
      10个用户100个键值对
      <mark>内存消耗成几何倍数增长</mark>

List 类型

特点:

  • 有序
  • 可重复

L 开头

命令

如:
rpush key value1 value2 ...
	从列表右端插入N个数据


linsert


lrem

ltrim
保留1到4,其他删除




blpop、brpop

应用场景

微博

整理

  • LPUSH + LPOP (后进先出) = Stack 栈结构
  • LPUSH + RPOP (先进先出)= Queue 队列结构
  • LPUSH + LTRIM = Capped Collection 限制大小的集合
  • LPUSH + BRPOP = Message Queue 消息队列

Set 类型

和 Java 一样

  • 元素无序(不记录插入顺序)、唯一

在 Redis 中 set 是通过 hashtable 实现的

  • 集合中最大的成员数 2的32次方 -1 个(40多亿)
  • 类似于 Hashtable 集合

命令

赋值:
	SADD KEY member1 member2 ...
	向集合添加一个或多个成员

取值:
	SCARD KEY
	获取集合的成员数

	SMEMBERS KEY
	返回集合中的所有成员

	SISMEMBER KEY member
	判断 member 元素是否是集合 KEY 的成员
	(开发中,用于判断是否存在)

	SRANDMEMBER key [count]
	返回集合中一个或多个随机数

删除:
	SREM KEY member1 member2 ...
	移除集合中一个或多个成员
	
	SPOP KEY [count]
	移除并返回集合中的一个或多个随机元素

	SMOVE source destination member
	将 member 元素从 source 集合移动到 destination 集合
	
差集:
	SDIFF key1 key2 ...
	返回给定所有集合的差集(左侧)

	SDIFFSTORE destination key1 key2 ... 
	返回给定所有集合的差集,并存储在 destination 中

交集:
	SINTER key1 key2 ...
	返回给定所有集合的交集(共有数据)

并集:
	SUNION key1 key2 ...
	返回所有给定集合的并集

	SUNIONSTORE destination key1 key2 ...
	所有给定集合的并集存储在 destination 集合中

应用场景

常应用于:对两个集合间的数据(计算)进行交集、并集、差集运算

  1. 共同关注、共同喜好、二度好友等功能。同时还能生成新的集合
  2. 判断唯一性,可以统计网站的所有独立IP、存取当天[或某天]的活跃用户列表

ZSet 类型

有序集合 (sorted set)

简介

  1. 不允许重复
  2. 不同的是,每个元素都会关联一个 double类型 的分数。
    <mark>redis 正是通过分数来为集合中的成员进行从小到大的排序。</mark>
  3. 有序集合的成员是唯一的,但分数(score)却可以重复
  4. 集合是通过hashtable 实现的。
    最大成员数 2的32次方-1 (40多亿)

<mark>在redis中,有序集合相关的操作命令都是z开头的</mark>

很多时候,也会将redis中的 有序集合 叫做 zsets

命令

赋值:
	ZADD KEY score1 member1 score2 member2 ....
	向有序集合添加一个或多个成员
	或更新已存在成员的分数

取值:
	ZCARD KEY
	获取有序集合的成员数

	ZCOUNT KEY min max 
	计算在有序集合中,指定取件分数的成员数

	ZRANK KEY member
	返回有序集合中,指定成员的索引

	ZRANGE KEY start stop [WITHSCORES]
	通过索引区间,返回有序集合指定区间内的成员
	(低到高)

	ZREVRANGE KEY start stop [WITHSCORES]
	通过索引区间,返回有序集合指定区间内的成员
	(高到低)

	ZRANGEBYSCORE KEY min max [WITHSOCRES] [LIMIT]
	通过分数,返回有序集合指定区间内的成员
	(低到高)

	ZREVRANGEBYSCORE KEY max min [WITHSCORES] 
	同上
	(高到低)

删除:
	DEL KEY
	移除集合

	ZREM KEY member member ...
	移除有序集合中一个或多个成员

	ZREMRANGEBYSCORE KEY start stop 
	移除有序集合中给定的排名区间的所有成员
	(第一名是0)
	(低到高排序)

	ZRERANGEBYSCORE KEY min max 
	移除有序集合中,给定的分数区间的所有成员
	
	ZINCBY KEY increment member
	增加member元素的分数increment,返回值是更改后的分数	

应用场景

  • 排行榜
  • 带权重的队列(重要的任务先执行)

Bitmaps 类型

存储需求:存储位的状态
应用场景:统计有/无两种状态的数据
如:

  • 某电影一周内是否有没点播
  • 某账号一年内是否有被使用

这个类型实际上就是 String,不过我们会操作 String 的作为 bitmaps 的 api

GETBIT KEY offset 
	获取指定KEY对应偏移量上的bit值

SETBIT KEY offset value
	设置指定KEY对应偏移量上的bit值,value只能是1或者0

bitop op destkey key1 key2 ...
# op可以是:
# and 交
# not 非
# or 并
# xor 异或
	对指定的KEY按位进行交、并、非、异或操作,并将结果保存到 destKey 中
bitcount KEY [start end]
	统计指定KEY中1的数量

例子

HyperLogLog 类型

存储需求: 独立信息统计(基数统计)
应用场景:
如:
统计独立UV

  • 原始方案:set(存储吗每个用户的 id 字符串)
  • 改进方案:bitmaps(存储每个用户状态 bit)
  • 全新方案:Hyperloglog(存储基数)

特点:<mark>高效、节省空间</mark>
必要说明

  • 用于进行基数统计,不是集合,不保存数据,只记录数量而不是统计数据

  • 核心是基数估算算法,最终数值存在一定误差

  • 耗费空间小,每个hyperloglog key 最大占用了 12k (<mark>最大值不随统计数量变化</mark>)的内存用于标记基数
    (也就是说,实际数据远大于12k时,才考虑使用)

  • pfadd 命令不是一次性分配 12k 内存,会随基数的增加内存逐渐增大

  • pfmerge 命令合并后占用的存储空间为12k,无论合并之前数据是多少

原理:

  • HyperLogLog 是用来做基数统计的,运用了loglog算法
  • 基数:是数据集去重后元素个数
  • loglog 算法:(<mark>懵逼就对了</mark>,看 专门的文章 了解吧)


简介

Redis 在 2.8.9 版本添加了 HyperLogLog 结构。

Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,<mark>计算基数所需的空间总是固定的,并且是很小的</mark>。

  • 在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,可以计算接近 2的64次方个不同元素的基数。
    <mark>这和计算基数时,元素越多耗费内存养护耳朵的集合形成鲜明对比</mark>。
  • 但是,因为 HyperLogLog 只会根据输入的元素来计算基数,而不会存储输入元素本身,所以,<mark>HyperLogLog 不能像集合那样,返回输入的各个元素</mark>

基数
一堆数据中,不重复的数的个数
如: [1,2,3,4,5,6,7,8,1]
==>
不重复: [1,2,3,4,5,6,7,8]
基数:8


基数估计
在误差可接受范围内,快速计算基数。

常用命令


PFADD KEY element element ... 
	添加指定元素到 HyperLogLog 中

PFCOUNT key1 key2 ...
	返回给定 HyperLogLog 的基数估算值

PFMERGE destkey sourcekey sourcekey ...
	将多个 HyperLogLog 合并为一个 HyperLogLog

应用场景

  • 基数不大,数据量不大就用不上,会有点大材小用浪费空间
  • 有局限性,就是只能统计基数数量,而没办法去知道具体的内容是什么
  • 统计注册 IP 数
  • 统计每日访问 IP 数
  • 统计页面实时 UV 数
  • 统计在线用户数
  • 统计用户每天搜索不同词条的个数
  • 统计真实文章阅读数

GEO

静止地球轨道(Geostationary Earth Orbit,GEO)
<mark>专门存储和处理地理位置信息</mark>

  • 经度纬度

应用场景
生活服务类软件

基本操作

geoadd  key longitude latitude member  [longitude latitude member  ...]
	添加坐标点

geopos key member [member ... ]
	获取坐标点

geodist key member1 member2 [unit]
	计算坐标点距离

georaidus key longitude latitude radius m|km|ft|mi [withcoord] [withdist] [withhash] [count count]
	根据坐标求范围内的数据

georadiusbymember key member radius m|km|ft|mi [withcoord] [withdist] [withhash] [count count]
	根据点求范围内数据

geohash key member [member...]
	获取指定点对应的坐标hash值







# redis客户端对比(Jedis、Redisson、lettuce)

<mark>在 Spring Boot 2.0 之前,主流的,对 redis 连接的支持,有以下多种:</mark>

<mark>spring boot 2.0 之后,(默认)采用了 lettuce 对 redis 做支持</mark>

概念

  • Jedis :老牌的 Redis 的 Java 实现客户端,提供了比较全面的 Redis 命令支持。
  • Redisson:实现了可<mark>分布式和可扩展</mark>的Java数据结构
  • Lettuce:<mark>高级Redis客户端,用于线程安全同步,异步和响应使用,支持集群,Sentinel,管道和编码器</mark>

优点

  • Jedis :比较全面的提供了 Redis 的操作特性
  • Redisson :促使使用者对Redis的关注分离,提供很多分布式相关操作服务
    例如:<mark>分布式锁、分布式集合、可通过Redis支持延迟队列</mark>
  • Lettuce: <mark>基于 Netty 框架的事件驱动的通信层,其方法调用时异步的。Lettuce 的 API 是线程安全的。所以可以是作单个 Lettuce 连接来完成各种操作</mark>

三方对比:可伸缩性

  • Jedis
    使用阻塞的I/O,且其 <mark>方法调用都是同步的</mark>,程序流需要等到sockets处理完I/O才能执行
    不支持异步。
    Jedis客户端实例不是线程安全的,所以<mark>需要通过连接池来使用Jedis</mark>。

  • Redisson
    <mark>基于Netty</mark>框架的事件驱动的通信层,其方法调用是<mark>异步</mark>的
    其连接实例(StatefulRedisConnection)可以在多个线程间并发访问。
    Redisson的API是<mark>线程安全</mark>的,所以可以操作单个Redisson连接来完成各种操作
    <mark>同时,当一个连接实例不够的情况下,可以按需增加连接实例。(体现了可伸缩的设计)</mark>
    但和 Jedis 相比,功能较为简单,<mark>不支持字符串操作,不支持排序、事务、管道、分区等Redis特性</mark>
    因为其宗旨是促进使用者对Redis的关注分离,从而将精力放在业务逻辑处理上

  • Lettuce
    <mark>基于Netty</mark>框架的事件驱动的通信层,其方法调用是异步的。
    Lettuce的API是<mark>线程安全</mark>的,所以可以操作单个Lettuce连接来完成各种操作

lettuce 能够支持 redis4,需要 java8 以上。

总结

  • redisson 分布式
  • lettuce 对字符串支持好,线程安全
  • jedis 对字符串支持好(功能全),<mark>但线程不安全</mark>
  • <mark>开发中,优先使用 Lettuce,如果需要分布式锁,分布式集合等分布式特性,添加Redisson结合使用</mark>
    (不单独使用 Redisson ,因为redisson 本身对字符串的操作支持很差)

下面,分别使用 Jedis 和 Lettuce 作为演示案例

SpringBoot 整合 Jedis

新建springboot项目

# 创建 JedisPool

引入依赖
SpringBoot 内默认引入了Jedis版本,所以这里不需要写版本

        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

application.yml

spring:
  redis:
    port: 6379
    host: 192.168.64.33
    password: root
    jedis:
      pool:
        max-idle: 6 #最大空闲数
        min-idle: 2 #最小空闲数
        max-active: 10 #最大连接数
    timeout: 2000 #连接超时

编写Config

package com.example.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/** * @author alan smith * @version 1.0 * @date 2020/3/26 8:57 */
@Slf4j
@Configuration // 相当于一个XML
public class JedisConfig {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int port;

    @Value("${spring.redis.password}")
    private String password;

    @Value("${spring.redis.timeout}")
    private int timeout;

    @Value("${spring.redis.jedis.pool.max-active}")
    private int maxActive;

    @Value("${spring.redis.jedis.pool.max-idle}")
    private int maxIdle;

    @Value("${spring.redis.jedis.pool.min-idle}")
    private int minIdle;

    // SpringBoot2.2.2版本
    @Bean
    public JedisPool jedisPool() {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxIdle(maxIdle);
        config.setMinIdle(minIdle);
        config.setMaxTotal(maxActive);

        JedisPool pool = new JedisPool(config, host, port, timeout, password);

        log.info("JedisPool连接成功:{}\t{}", host, port);

        return pool;
    }

}

启动

# 测试String类型操作

接口

package com.example.service;

/** * @author alan smith * @version 1.0 * @date 2020/3/26 9:56 */
public interface UserService {
    /** * Redis String 类型 * 需求:用户输入一个key,先判断Redis中是否存在该数据 * 如果存在,在Redis中进行查询,并返回 * 如果不存在,在MySQL数据库查询,将结果交给Redis,并返回 */
    String getString(String key);

    /** * 清空缓存 */
    void clear();

}

接口实例

package com.example.service.impl;

import com.example.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

/** * @author alan smith * @version 1.0 * @date 2020/3/26 9:59 */
@Slf4j
@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private JedisPool jedisPool;

    @Override
    public String getString(String key) {
        String val = null;
        // 得到Jedis对象
        Jedis jedis = jedisPool.getResource();
        // 判断key是否存在
        if (jedis.exists(key)) {
            log.info("查询Redis中的数据");
            val = jedis.get(key);
        } else {
            // 如果不存在,
            log.info("查询的是MySQL数据库");
            val = "key from MySQL";
            jedis.set(key, val);
        }
        // 关闭连接
        jedis.close();
        return val;
    }

    @Override
    public void clear() {
        Jedis jedis = jedisPool.getResource();
        jedis.flushDB();
        log.info("清空当前数据库");
        jedis.close();
    }
}

测试类

package com.example;

import com.example.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import redis.clients.jedis.JedisPool;

@Slf4j
@SpringBootTest
class DemoBootJedisApplicationTests {

    @Autowired
    private JedisPool jedisPool;
    @Autowired
    private UserService userService;


    @Test
    void contextLoads() {
        String result = null;
        log.info("jedisPool:{}", jedisPool.toString());
        userService.clear();
        result = userService.getString("name");
        log.info("第一次:{}", result);
        result = userService.getString("name");
        log.info("第二次:{}", result);
    }

}

# 测试Hash类型操作

编写PO类

package com.example.po;

import lombok.*;

import java.io.Serializable;

/** * @author alan smith * @version 1.0 * @date 2020/3/26 8:35 */
@ToString
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
    private String id;
    private String name;
    private Integer age;
}

接口添加抽象方法

    /** * 获取一个对象 * 需求:传入一个ID,根据ID查询对象 * 如果Redis中存在,直接返回给用户结果 * 如果Redis中不存在,查询MySQL,结果存入Redis并返回 */
    public User selectById(String id);

实例添加方法实现

 @Override
    public User selectById(String id) {
        String key = "user:" + id;
        Jedis jedis = jedisPool.getResource();

        //实现业务逻辑
        User user = null;
        if (jedis.exists(key)) {
            log.info("查询的是Redis的数据");
            Map<String, String> map = jedis.hgetAll(key);
            // 可以引入工具类简化
            user = new User();
            user.setId(map.get("id"));
            user.setName(map.get("name"));
            user.setAge(Integer.valueOf(map.get("age")));

        } else {
            log.info("查询的是MySQL的数据");
            user = new User();
            user.setId(id);
            user.setName("我妻由乃");
            user.setAge(16);

            //存入redis
            Map map = new HashMap();
            map.put("id", user.getId());
            map.put("name", user.getName());
            map.put("age", user.getAge());
            jedis.hmset(key, map);
        }

        jedis.close();
        return user;
    }

SpringBoot 整合 Lettuce

编写缓存配置类 RedisConfig 用于调优缓存默认配置,RedisTemplate 类型 <mark>兼容性更高</mark>

大家可以看到 redisTemplate() 这个方法中用 JacksonJsonRedisSerializer 换掉了 Redis 默认的序列化方法: JdkSerializationRedisSerializer
类比理解

  • jdbcTemplate 是对 JDBC 的进一步封装
  • RedisTemplate 是对 Redis 进行了进一步封装(早期:jedis如今:lettuce


Spring-data-redis 中序列化类有以下几个:

  • GenericToStringSerializer:可以将 <mark>任何对象</mark> 泛化为字符串并 <mark>序列化</mark>
  • Jackson2JsonRedisSerializer:序列化 <mark>Object对象</mark> 为 <mark>Json字符串</mark> (与jacksonJsonRedisSerializer相同)
  • JdkSerializationRedisSerilizer:序列化 <mark>java对象</mark>
    被序列化对象必须实现 Serializable 接口,被序列化除属性内容还有其他内容,长度长且不容易阅读
  • StringRedisSerializer:<mark>简单的字符串序列化</mark>

# RedisConfig 配置类

新建springboot项目
依赖

 <!--默认是lettuce客户端-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--redis依赖commons-pool,这个依赖一定要添加,否则会丢失资源-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

<mark>spring-boot-starter-data-redis 即 Spring Data</mark>
.
Spring Data概述:
Spring Data 是 Spreing 的一个子项目,<mark>用于简化数据库访问</mark>,支持NoSQL和关系数据库存储
.
封装链:(后者对前者封装)
hibernate ⇒ JPA ⇒ SpringData

application.yml

spring:
  redis:
    port: 6379
    password: root
    host: 192.168.64.33
    lettuce:
      pool:
        max-active: 8 #连接池最大连接数(使用负值表示没有限制)
        max-idle: 8 #连接池中最大空闲连接
        min-idle: 0 #连接池中最小空闲连接
        max-wait: 1000 #连接池最大阻塞等待时间(使用负值表示没有限制)
      shutdown-timeout: 100 #关闭超时时间

RedisConfig

RedisTemplate 介绍
Spring Data 提供了 RedisTemplate 模板
.
封装了 Redis 连接池管理的逻辑,业务代码无需关心获取和释放的连接逻辑。
.
Spring Redis 同时支持了 Jedis、Jredis、rjc 客户端操作
在RedisTemplate 中提供了几个常用的接口方法的使用,分别是:

  • valueOps
  • listOps
  • setOps
  • zSetOps
  • list
package com.example.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.lang.reflect.Method;

/** * Redis的配置类 * <p> * 暂时只需要 RestTemplate * <p> * “非必要” 和继承 CachingConfigurerSupport内容后面讲 * * @author alan smith * @version 1.0 * @date 2020/3/26 14:26 */
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    /** * (非必要) * 自定义缓存key的生成策略。 * 默认的生成策略是看不懂的(乱码内容) * 通过Spring的依赖注入特性,进行自定义的配置,并且此类是一个配置类,可以更多程度的自定义配置 * * @return */
    @Bean
    @Override
    public KeyGenerator keyGenerator() {
        return new KeyGenerator() {
            // 类名+方法名+参数1+参数2+...
            @Override
            public Object generate(Object target, Method method, Object... params) {
                StringBuilder sb = new StringBuilder();
                // 类名
                sb.append(target.getClass().getName());
                // 类名+方法名
                sb.append(method.getName());
                // 类名+方法名+参数1+参数2+...
                for (Object obj : params) {
                    sb.append(obj.toString());
                }
                return sb.toString();
            }
        };
    }

    // redis的startter提供
    @Autowired
    private LettuceConnectionFactory factory;

    /** * (非必要) * 缓存配置管理器 * * @return */
    @Override
    public CacheManager cacheManager() {
        // 以锁写入的方式创建RedisCacheWriter对象
        RedisCacheWriter writer = RedisCacheWriter.lockingRedisCacheWriter(factory);
        // 创建默认缓存配置对象
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        RedisCacheManager cacheManager = new RedisCacheManager(writer, config);
        return cacheManager;
    }

    /** * 把任何数据保存到redis中时,都需要进行序列化 * 默认使用 JdkSerializationRedisSerializer 进行数据序列化。 * 所有的key和value还有hashkey和hashvalue的原始支付前,都加了一串字符。 * <p> * 而下面改为 Jackson2JsonRedisSerializer * * @return */
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        // (下面)在使用注解@Bean返回RedisTemplate的时候,同时配置hashKey与hashValue的序列化方式

        /*可忽略(start)*/
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        // 需要依赖:jackson-databind
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // value序列化采用 jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        /*可忽略(end)*/
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);

        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();

        return template;
    }

}

# 测试 String 类型操作

沿用 Jedis 的 service 类

这里直接写实例了

实例

package com.example.service.impl;

import com.example.po.User;
import com.example.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.Set;

/** * @author alan smith * @version 1.0 * @date 2020/3/26 15:24 */
@Service
@Slf4j
public class UserServiceImpl implements UserService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Override
    public String getString(String key) {

        log.info("RedisTemplate测试:");

        if (redisTemplate.hasKey(key)) {
            return (String) redisTemplate.opsForValue().get(key);
        } else {
            String val = "RedisTemplate模板学习 lettuce 客户端";
            log.info("MySQL中查询出来的:{}", val);
            log.info("结果存入redis");
            // ops operation
            // value 就是 String
            redisTemplate.opsForValue().set(key, val);
            return val;
        }
    }

    @Override
    public void clear() {
        Set<String> keys = redisTemplate.keys("*");
        redisTemplate.delete(keys);
    }

    @Override
    public User selectById(String id) {
        return null;
    }
}

测试类

package com.example;

import com.example.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@Slf4j
@SpringBootTest
class DemoBootLuttuceApplicationTests {

    @Autowired
    private UserService userService;

    @Test
    void contextLoads() {
        userService.clear();
        log.info("清空数据");
        String val1 = userService.getString("name");
        log.info("val1={}", val1);
        String val2 = userService.getString("name");
        log.info("val2={}", val2);
    }

}

# 测试Hash类型操作

同样,沿用上面 User 实体类
和 service 接口 的 select 方法

直接添加实例方法

	@Override
    public User selectById(String id) {
        //redisTemplate.hasKey(key) 判断整个key是否存在
        //如 user:id , user:1 , user:2
        if(redisTemplate.opsForHash().hasKey("user", id )) {
            return (User) redisTemplate.opsForHash().get("user", id);
        }else {
            // 查询数据库
            log.info("查询MySQL数据库");
            User u = new User();
            u.setId(id);
            u.setName("三上悠亚");
            u.setAge(18);

            // 存入缓存
            /* h 用户实体 "user" hk 用户主键 id hv 整个对象 u */
            redisTemplate.opsForHash().put("user", id , u);

            return u ;
        }
    }

测试

    @Test
    void testHash() {
        userService.clear();
        log.info("清空数据");
        User user1 = userService.selectById("0010");
        log.info("user1={}", user1);
        User user2 = userService.selectById("0010");
        log.info("user2={}", user2);
    }

<mark>注意,这里 key、value 之所以是“明文”存储</mark>
是因为我们配置类里面重新制定了 序列化的引擎: Jackson2JsonRedisSerializer

案例

# 限制登录功能

github - https://github.com/LawssssCat/login-trylimit

需求描述:

  • 用户在2分钟内,仅允许输入错误密码5次
  • 如果超过次数,限制登录1小时(需要每登录失败,都给相应的提示)