JAVA入门基础_从零开始的培训_Redis

Redis能够为我们解决什么问题

  • 减轻CPU和内存压力
  • 减轻 IO压力
  • 访问redis数据库是直接 从内存中读取数据,比直接进行IO读取速度要快的多
  • 适用场景
  • 对数据高并发的读写
  • 海量数据的读写
  • 对数据高扩展的
  • 不适用场景
  • 需要事物支持的
  • 处理复杂的关系需要即时查询的
  • *用不着SQL和用了SQL也解决不了的问题时可以考虑使用NOSQL的数据库,例如Redis

Redis的下载与安装

  • 下载地址
  • redis-6.2.1.tar.gz压缩包放在Linux系统的 /opt目录下,并完成解压
  • 需要安装gcc环境
    yum install gcc
  • 进入到解压目录,使用 make命令进行编译,编译完成后使用 make install命令安装
  • 如果中途出现了 —Jemalloc/jemalloc.h:没有那个文件 问题,可以make distclean
  • 尝试安装gcc后再次尝试
  • 不出问题的话就已经安装好了,其安装目录在: /usr/local/bin

前台启动(不推荐)与后台启动

  • 前台启动,就是直接进入/usr/local/bin下执行redis-server即可(不推荐)
  • 复制一个 /opt/redis解压目录/下的一个redis.conf文件到 /etc目录
  • 修改/etc/redis.conf配置文件,将 daemonize no 的参数设置为 yes,大概250行左右
  • 进入到 /usr/local/bin目录下,执行 redis-server /etc/redis.conf即可完成后台启动

常用五大数据类型

Redis键常用命令(key)

命令 作用 示例 keys * 查看所有的key keys *

exists key 判断某个key是否存在 exists k1

type key 查看某个key的类型 type k1

del key 删除某个key del k1

unlink key 删除某个key,但是会调用异步线程 unlink k2

expire key second 设置某个key的过期时间 expire k1 10

ttl key 查看某个key的过期时间,-1用不过期,-2已过期 ttl k1

4个数据库操作命令

命令 作用 示例 select 数据库编号 切换到指定的数据库默认为0号数据库 select 10 dbsize 查看当前数据库有多少个key dbsize

flushdb 清空当前数据库 flushdb

flushall 清空所有数据库 flushall

String字符串命令

命令 作用 示例 set key value 设置键值 set k1 v1

get key 获取某个key get k1

setnx key value 设置键值、如果key已存在则设置失败 setnx k1 v1

mset key1 value1 key2 value2 批量设置键值 mset k3 v3 k4 v4 k5 v5

msetnx 批量设置键值,任意一个key已存在则全部设置失败 msetnx k8 v8 k1 v1

setex key second value 设置键值并指定过期时间,单位为秒 setex k1 10 v1

append key value 在原有字符串中追加value append k1 nihao

setrange key startIndex value 在字符串指定位置设置value,会覆盖原有字符串的范围内容 setrange k1 1 abc

getrange key startIndex endIndex 获取指定范围内的value(包含头和尾) getrange k1 1 3

getset key value 获取原有的值,并设置新的值 getset k1 zhangsan

strlen key 获取值得长度 strlen k1

incr key 将key中存储到数字+1 incr k1

incr by key 步长 将key中存储到数字加步长 incrby k1 10

decr key 将key中存储到数字-1 decr k2

decr by key 步长 将key中存储到数字减步长 decrby k1 10

String的内存结构

  • SDS(Simple Dynamic String)简单动态字符串,结构上类似于Jva的ArrayList,预分配一些内存空间,避免频繁扩容

List类型命令(单键多值)

命令 作用 示例 lpush key v1 v2 v3 从左边插入一个或多个值 lpush k1 a b c d e

rpush key v1 v2 v3 从右边插入一个或多个值 rpush k2 a b c d e

lpop key n 从左边取出n个值并删除 lpop k1 2

rpop key n 从右边取出n个值并删除 rpop k1 2

rpoplpush source dest 从一个列表的右边取出一个值添加到另一个列表的左边 rpoplpush k1 k2

lrange key 0 -1 按照索引下标获得元素,0,-1代表获取全部 lrange k2 0 -1

lindex key index 按照索引下标获取元素,索引从0开始 lindex k2 1

llen 获得列表长度 llen k2

linsert key before value newValue 在某个value值的前或后添加一个元素(若有多个,则插入到从左边开始找到的第一个) linsert k2 before c zhangsan

lrem key n value 从左边删除n个相同的元素value lrem k2 2 value3

lset key index value 将列表中索引位置的值替换成指定的值 lset k2 0 wangwu

List列表类型数据结构

JAVA入门基础_从零开始的培训_Redis

Set集合命令(member指的是Set集合key中的元素)

命令 作用 示例 sadd key value1 value2 valve3 将一个或多个member值添加到集合当中 sadd k1 v1 v2 v3

smembers key 取出一个集合的所有值 smembers k1

sismember key value 判断集合key中是否还有某个value值,0为没有,1为有 sismember k1 v1

scard key 返回该集合的元素个数 scard k1

srem key value1 value2 删除集合中的某些元素 scard k1

spop key n 随机从该集合中取出n个值并删除 spop k1 2

srandmember key n 随机从集合中取出多个值但不删除 srandmember k1 2

smove key1 key2 value 把集合中一个值移动到另一个集合中 smove k1 k2 v3

sinter key1 key2 返回2个集合的交集 sinter k1 k2

sunion key1 key2 返回2个集合的并集 sunion k1 k2

sdiff key1 key2 返回2个集合的差集 sdiff k1 k2

Set集合数据结构

  • 底层其实是一个value为null(内部值)的hash表,所以 *增删改的时间复杂度为O(1)

hash数据类型常用命令

命令 作用 示例 hset key fieldname fieldValue 将一个或多个值添加到hash当中 hset k1 name zhangsan age 18

hsetnx key fieldname fieldValue 将一个值添加到hash当中,如果field已存在则添加失败 hsetnx k1 birthday 2000-10-10

hget key fieldname 取出hash中的一个元素,通过字段名 hget k1 name

hexist key fieldname 查看hash中,指定字段是否存在 hexists k1 age

hkeys key 列出hash中所有的fieldname hkeys k1

hvals key 列出hash中所有的fieldvalue hvals k1

hincrby key field increment 为hash中的某个字段进行数值增加或减少 hincrby k1 age -5

hash数据结构

  • field-name的长度较短并且字段较少时,使用ziplist
  • 否则使用hashtable,也就是哈希表
  • 其内存结构跟Java中的HashMap差不多

Zset常用命令(带分数排序的Set集合)

命令 作用 示例 zadd key score1 value1 score2 value2 将一个或多个member值添加到zset集合中 zadd k1 100 java 200 c++ 300 c#

zrange key 0 -1 [withscores] 返回下标在start_end之间的元素(默认不包括分数) zrange k1 0 1 withscores

zrangebyscore key min max [withscores] 返回分数在min ~ max之间的元素 zrangebyscore k1 200 300 withscores

zrevrangebyscore key max min [withscores] 同上,但是需要降序排列,而且是max~min zrevrangebyscore k1 300 200 withscores

zincrby key increment value 为元素的score加分或减分 zincrby k1 -300 java

zrem key value 删除该集合下指定值的一个或多个元素 zrem k1 c++ c#

zcount key min max 统计该集合分数区间内的元素个数 zcount k1 100 300

zrand key value 返回一个元素在集合中的排名,从0开始 zrank k1 java

zrandmember key n 返回zset中指定个数的元素(按照排名) zrandmember k1 2

Zset数据结构

  • 底层首先是一个hash表(Map
  • 并且还存在 跳跃表
  • 生成的文件与命令中:运行命令的路径有关

Redis配置文件介绍

###Units

  • 配置单位大小,开头定义了一些基本的 度量单位,只支持 bytes,并且 大小写不敏感,不支持bit(一个字节8位)
1k => 1000 bytes
1kb => 1024 bytes
1m => 1000000 bytes
1mb => 1024*1024 bytes
1g => 1000000000 bytes
1gb => 1024*1024*1024 bytes

###INCLUDES

  • 包含,类似于JSP或者Thymeleaf中的include。例如可以包含一些配置文件
include /path/to/local.conf
include /path/to/other.conf

###NETWORK

  • 网络配置,其中常用的几个配置如下(如下配置均为默认,暂未修改)
标志当前可以访问的IP地址,如果想要IP都能访问,可以直接将其注释掉
bind 127.0.0.1 -::1

保护模式,如果开启了,那么在没有bind,redis连接也没有密码时,redis将会只接收本机的响应
protected-mode yes

服务的端口号
port 6379

是完成了TCP三次握手以及未完成TCP三次握手的连接队列
Linux内核会将这个值减少到vim /proc/sys/net/core/somaxconn的值(128)
如果真的需要增加连接队列数量,则需要修改vim /proc/sys/net/core/somaxconn
和 /proc/sys/net/ipv4/tcp_max_syn_backlog
tcp-backlog 511

超时时间,如果客户端连接到redis超过这个时间没有进行过任何操作(空闲时间),
则中断连接,0表示永不超时
timeout 0

对访问客户端的心跳检测,单位为秒,(判断客户端是否存活),建议设置为60
tcp-keepalive 300

###GENERAL

  • 一些通用的配置
是否以后台运行redis
daemonize yes

记录当前redis启动的线程ID,只会存储当前运行redis服务的线程ID
如果redis服务关闭,会将该文件删除
pidfile /var/run/redis_6379.pid

日志级别,debug -> verbose -> notice -> warning
loglevel notice

日志文件的名称
logfile ""

数据库个数,从0开始
databases 16

###SECURITY

  • 与安全相关的配置,设置密码(永久设置)
设置当前redis的密码
requirepass foobared
  • 设置密码后需要授权才能操作redis数据库
    JAVA入门基础_从零开始的培训_Redis
  • 临时设置密码
查看当前密码
config get requirepass

设置密码
config set requirepass "123456"

授权
auth 123456

###CLIENTS

  • 最大的客户端连接数量,如果超过了此数量,会返回:max number of clients reached(已达到最大连接数)
maxclients 10000

###MEMORY MANAGEMENT

  • 内存管理常用配置
设置Redis可以使用的内存容量,建议**必须设置**,**否则内存占满后将会造成服务器宕机**
maxmemory <bytes>

&#x8FBE;&#x5230;&#x6700;&#x5927;&#x5185;&#x5B58;&#x5BB9;&#x91CF;&#x7684;&#x79FB;&#x9664;key&#x7B56;&#x7565;&#x3002;
volatile-lru&#xFF1A;&#x4F7F;&#x7528;LRU&#x7B97;&#x6CD5;&#x79FB;&#x9664;key&#xFF0C;&#x53EA;&#x5BF9;&#x8BBE;&#x7F6E;&#x4E86;&#x8FC7;&#x671F;&#x65F6;&#x95F4;&#x7684;&#x952E;&#xFF1B;&#xFF08;&#x6700;&#x8FD1;&#x6700;&#x5C11;&#x4F7F;&#x7528;&#xFF09;
allkeys-lru&#xFF1A;&#x5728;&#x6240;&#x6709;&#x96C6;&#x5408;key&#x4E2D;&#xFF0C;&#x4F7F;&#x7528;LRU&#x7B97;&#x6CD5;&#x79FB;&#x9664;key
volatile-random&#xFF1A;&#x5728;&#x8FC7;&#x671F;&#x96C6;&#x5408;&#x4E2D;&#x79FB;&#x9664;&#x968F;&#x673A;&#x7684;key&#xFF0C;&#x53EA;&#x5BF9;&#x8BBE;&#x7F6E;&#x4E86;&#x8FC7;&#x671F;&#x65F6;&#x95F4;&#x7684;&#x952E;
allkeys-random&#xFF1A;&#x5728;&#x6240;&#x6709;&#x96C6;&#x5408;key&#x4E2D;&#xFF0C;&#x79FB;&#x9664;&#x968F;&#x673A;&#x7684;key
volatile-ttl&#xFF1A;&#x79FB;&#x9664;&#x90A3;&#x4E9B;TTL&#x503C;&#x6700;&#x5C0F;&#x7684;key&#xFF0C;&#x5373;&#x90A3;&#x4E9B;&#x6700;&#x8FD1;&#x8981;&#x8FC7;&#x671F;&#x7684;key
noeviction&#xFF1A;&#x4E0D;&#x8FDB;&#x884C;&#x79FB;&#x9664;&#x3002;&#x9488;&#x5BF9;&#x5199;&#x64CD;&#x4F5C;&#xFF0C;&#x53EA;&#x662F;&#x8FD4;&#x56DE;&#x9519;&#x8BEF;&#x4FE1;&#x606F;
maxmemory-policy noeviction

&#x8BBE;&#x7F6E;&#x6837;&#x672C;&#x6570;&#x91CF;&#xFF0C;LRU&#x7B97;&#x6CD5;&#x548C;&#x6700;&#x5C0F;TTL&#x7B97;&#x6CD5;&#x90FD;&#x5E76;&#x975E;&#x662F;&#x7CBE;&#x786E;&#x7684;&#x6570;&#x91CF;
&#x4E00;&#x822C;&#x8BBE;&#x7F6E;3&#x5230;7&#x7684;&#x6570;&#x5B57;&#xFF0C;&#x6570;&#x503C;&#x8D8A;&#x5C0F;&#x6837;&#x672C;&#x8D8A;&#x4E0D;&#x7CBE;&#x786E;&#xFF0C;&#x4F46;&#x6027;&#x80FD;&#x6D88;&#x8017;&#x8D8A;&#x5C0F;
maxmemory-samples 5
</bytes>

Redis的订阅与发布

什么是订阅与发布(频道)

  • 想想生活中的例子,我们订阅了一个频道,那么这个频道有消息的时候就会通知到我们
  • 其实程序中的订阅与发布也是如此。
  • 需要接收到消息的一方(订阅者) 订阅某个通道,发送消息(发布者)的一方就 通过这个通道来发送消息,因此订阅者就可以接收到消息

开启一个Redis客户端,成为订阅者订阅一个或多个频道

&#x8FDE;&#x63A5;redis
redis-cli

&#x8BA2;&#x9605;&#x591A;&#x4E2A;&#x9891;&#x9053;
subscribe channel1 channel2

开启一个Redis客户端,成为发布者,在某个频道发布消息

publish channel1 helloredis

订阅者收到消息

JAVA入门基础_从零开始的培训_Redis

Redis的新数据类型

Bitmaps

  • 作用:统计用户活跃量
  • setbit
  • getbit
  • bitcount
  • bitop

HyperLogLog

  • 作用:基数统计,例如独立访客,不允许重复
  • pfadd
  • pfcount
  • pfmerge

Geospatial

  • 作用:统计经纬度,还能计算距离
  • geoadd 添加
  • geopos 获取指定地区的坐标值
  • geodist 获取直线距离
  • georadius 在距离范围内的

Redis_Jedis连接Redis进行操作

修改redis.conf并且开放Linux的端口号

  • 修改redis.conf
&#x6CE8;&#x91CA;&#x6389;bind
#bind 127.0.0.1 -::1

&#x5173;&#x95ED;&#x4FDD;&#x62A4;&#x6A21;&#x5F0F;
protected-mode no
  • 开放端口6379
&#x6C38;&#x4E45;&#x5F00;&#x653E;&#x7AEF;&#x53E3;6379
firewall-cmd --permanent --add-port=6379/tcp

&#x91CD;&#x542F;&#x9632;&#x706B;&#x5899;
systemctl restart firewalld.service
  • 可以使用telnet ip地址 端口 来测试是否可以连接上
    telnet 192.168.22.100 6379

创建一个Maven工程,引入Jedis的依赖

    <dependency>
        <groupid>redis.clients</groupid>
        <artifactid>jedis</artifactid>
        <version>3.2.0</version>
    </dependency>

简单测试一下,其实方法跟命令行的差不多

public class JedisTest {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.22.100", 6379);

        // 1&#x3001;&#x64CD;&#x4F5C;string&#x7684;&#x5E38;&#x7528;&#x65B9;&#x6CD5;
        jedis.set("k1","v1");
        jedis.get("k1");
        jedis.setnx("k1","v1");
        jedis.mset("k1","v1","k2","v2");
        jedis.msetnx("k1","v1","k2","v2");
        jedis.setex("k1",  3, "v1");
        jedis.append("k1", "111");
        jedis.setrange("k1", 1, "zhang");
        jedis.getrange("k1", 0 , 3);
        jedis.getSet("k1","v1");
        jedis.strlen("k1");
        jedis.incr("k1");
        jedis.incrBy("k1", 3);
        jedis.decr("k1");
        jedis.decrBy("k1", 3);

        // 2&#x3001;&#x64CD;&#x4F5C;List&#x3001;Set&#x3001;Hash&#x3001;Zset&#x7684;&#x65B9;&#x5F0F;&#x5747;&#x4E0E;&#x547D;&#x4EE4;&#x884C;&#x6572;&#x547D;&#x4EE4;&#x65F6;&#x4E00;&#x81F4;

        // 3&#x3001;&#x5173;&#x95ED;&#x8FDE;&#x63A5;
        jedis.close();
    }
}

练习使用Jedis完成一个手机验证码功能

  • 模拟如下功能
    1、输入手机号,点击发送后随机生成6位数字码,2分钟有效
    2、输入验证码,点击验证,返回成功或失败
    3、每个手机号每天只能输入3次
  • 思路:
  • 首先方法中都必须做的事情
    • (1)定义好手机号、验证码所对应的key名
  • 发送验证码的思路(接收手机号)
    • (1)判断当前手机号是否已经在redis中存在
    • (1.1)如果不存在,则进行存储,将其数量设置为1,并设置过期时间为次日凌晨,也就是 24小时减当前时间
    • (1.2)如果已存在,判断是否小于3,如果满足则代表发送验证码没有超过3次,为其发送验证码,否则告知每日发送验证码的次数不能超过3次
    • (2)获取验证码
    • (3)将验证码存储到Redis当中并设置超时时间为2分钟
  • 检验验证码的思路(接收手机号、验证码)
    • (1)根据当前的手机号、验证码拼接到对应的key
    • (2)通过对应的key去Redis中获取数据并进行响应判断即可
  • 定义发送验证码的方法
    • (1)6次for循环
    • (2)每次都拼接一个1~9的随机数即可
    • (3)随机数由Random类生成
  • 实际编码
public class RedisCode {
    public static void main(String[] args) throws Exception{
        // &#x53D1;&#x9001;&#x9A8C;&#x8BC1;&#x7801;
        sendCode("15577778888");

        // &#x6821;&#x9A8C;&#x9A8C;&#x8BC1;&#x7801;
        boolean b = verifyCode("15577778888", "735315");
        System.out.println(b);
    }

    public static boolean verifyCode(String phone, String code) {
        Jedis jedis = new Jedis("192.168.22.100", 6379);

        // 1&#x3001;&#x5B9A;&#x4E49;&#x624B;&#x673A;&#x53F7;&#x5BF9;&#x5E94;&#x7684;&#x9A8C;&#x8BC1;&#x7801;&#x7684;key
        String codeKey = "Verify:" + phone + ":code";

        // 2&#x3001;&#x4ECE;redis&#x4E2D;&#x83B7;&#x53D6;&#x9A8C;&#x8BC1;&#x7801;
        String redisCode = jedis.get(codeKey);

        // 3&#x3001;&#x6821;&#x9A8C;&#x9A8C;&#x8BC1;&#x7801;&#x662F;&#x5426;&#x5DF2;&#x7ECF;&#x5931;&#x6548;
        if (redisCode == null || "".equals(redisCode)) {
            System.out.println("&#x5F53;&#x524D;&#x9A8C;&#x8BC1;&#x7801;&#x5931;&#x6548;&#xFF0C;&#x8BF7;&#x91CD;&#x65B0;&#x83B7;&#x53D6;");
            jedis.close();
            return false;
        }

        // 4&#x3001;&#x8FDB;&#x884C;&#x9A8C;&#x8BC1;&#x7801;&#x7684;&#x6821;&#x9A8C;
        if(redisCode.equals(code)) {
            System.out.println("&#x9A8C;&#x8BC1;&#x7801;&#x6821;&#x9A8C;&#x6210;&#x529F;");
            jedis.close();
            return true;
        }

        // 5&#x3001;&#x90FD;&#x8D70;&#x5230;&#x8FD9;&#x4E86;&#xFF0C;&#x8BF4;&#x660E;&#x6CA1;&#x6709;&#x6821;&#x9A8C;&#x6210;&#x529F;
        System.out.println("&#x9A8C;&#x8BC1;&#x7801;&#x6821;&#x9A8C;&#x5931;&#x8D25;");
        jedis.close();
        return false;
    }

    public static void sendCode(String phone) {
        Jedis jedis = new Jedis("192.168.22.100", 6379);
        // 1&#x3001;&#x5B9A;&#x4E49;&#x624B;&#x673A;&#x53F7;&#x5BF9;&#x5E94;&#x7684;key
        String phoneKey = "Verify:" + phone + ":qt";
        String codeKey = "Verify:" + phone + ":code";

        // 2&#x3001;&#x53BB;Redis&#x4E2D;&#x67E5;&#x8BE2;&#x662F;&#x5426;&#x542B;&#x6709;&#x8BE5;key&#x5BF9;&#x5E94;&#x7684;value
        String phoneValue = jedis.get(phoneKey);

        // 3&#x3001;&#x6821;&#x9A8C;&#x662F;&#x5426;&#x4E3A;null
        if(phoneValue == null) {
            // 3.1 &#x5728;redis&#x4E2D;&#x8BBE;&#x7F6E;&#x503C;&#xFF0C;&#x660E;&#x5929;&#x51CC;&#x6668;&#x91CD;&#x7F6E;
                // &#x83B7;&#x53D6;&#x5230;&#x5F53;&#x524D;&#x7684;&#x65F6;&#x95F4;&#x6233;
            long nowTimeStamp = Instant.now().toEpochMilli();
                // &#x83B7;&#x53D6;&#x5230;&#x660E;&#x5929;&#x51CC;&#x6668;&#x7684;&#x65F6;&#x95F4;&#x6233;
            long tomorrowTimeStamp = LocalDateTime.now().plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0)
                    .toInstant(ZoneOffset.of("+8")).toEpochMilli();
            // &#x8BA1;&#x7B97;&#x83B7;&#x5F97;&#x73B0;&#x5728;&#x5230;&#x660E;&#x5929;&#x51CC;&#x6668;&#x7684;&#x79D2;&#x6570;
            int second = (int) ((tomorrowTimeStamp - nowTimeStamp) / 1000);

            // &#x5F80;redis&#x5F53;&#x4E2D;&#x5B58;&#x50A8;&#x6570;&#x636E;
            jedis.setex(phoneKey,second,"1");
        }else if (Integer.parseInt(phoneValue) < 3) {
            // 3.2 &#x8FD9;&#x4E2A;&#x65F6;&#x5019;&#x4EE3;&#x8868;&#x4ECA;&#x5929;&#x5DF2;&#x7ECF;&#x7ED9;&#x4ED6;&#x53D1;&#x9001;&#x8FC7;&#x9A8C;&#x8BC1;&#x7801;&#x4E86;&#xFF0C;&#x5E76;&#x4E14;&#x6B21;&#x6570;&#x6CA1;&#x6709;&#x8FBE;&#x5230;3&#x6B21;&#xFF0C;&#x6B21;&#x6570;&#x52A0;&#x4E00;
            jedis.incr(phoneKey);
        }else {
            // 3.3 &#x8BF4;&#x660E;&#x6B21;&#x6570;&#x5DF2;&#x7ECF;&#x8FBE;&#x5230;3&#x6B21;&#x4E86;
            System.out.println("&#x4ECA;&#x65E5;&#x53D1;&#x9001;&#x9A8C;&#x8BC1;&#x7801;&#x7684;&#x6B21;&#x6570;&#x5DF2;&#x7ECF;&#x8FBE;&#x5230;3&#x6B21;&#xFF0C;&#x660E;&#x5929;&#x518D;&#x6765;&#x5427;");
            jedis.close();
            return;
        }

        // 4&#x3001;&#x53D1;&#x9001;&#x9A8C;&#x8BC1;&#x7801;&#xFF0C;&#x5047;&#x88C5;&#x5DF2;&#x7ECF;&#x53D1;&#x9001;&#x4E86;
        String code = getCode();
        System.out.println("&#x5F53;&#x524D;&#x9A8C;&#x8BC1;&#x7801;&#x662F;&#xFF1A;" + code);

        // 5&#x3001;&#x5B58;&#x50A8;&#x5230;redis&#x4E2D;&#xFF0C;&#x8BBE;&#x7F6E;&#x8FC7;&#x671F;&#x65F6;&#x95F4;&#x4E3A;2&#x5206;&#x949F;
        jedis.setex(codeKey, 60 * 2 ,code);

        jedis.close();
    }

    public static String getCode() {
        StringBuilder sb = new StringBuilder();
        Random random = new Random();

        for (int i = 0; i < 6; i++) {
            sb.append(random.nextInt(10));
        }

        return sb.toString();
    }
}

Redis整合SpringBoot

创建一个SpringBoot工程并引入redis启动器和所需的pool2

    <!-- 引入一个web模块,用于测试RedisTemplate -->
    <dependency>
        <groupid>org.springframework.boot</groupid>
        <artifactid>spring-boot-starter-web</artifactid>
    </dependency>

    <!-- Redis启动器 -->
    <dependency>
        <groupid>org.springframework.boot</groupid>
        <artifactid>spring-boot-starter-data-redis</artifactid>
    </dependency>

    <!-- spring2.X集成redis所需common-pool2-->
    <dependency>
        <groupid>org.apache.commons</groupid>
        <artifactid>commons-pool2</artifactid>
    </dependency>

修改配置文件

#Redis&#x670D;&#x52A1;&#x5668;&#x5730;&#x5740;
spring.redis.host=192.168.22.100
#Redis&#x670D;&#x52A1;&#x5668;&#x8FDE;&#x63A5;&#x7AEF;&#x53E3;
spring.redis.port=6379
#Redis&#x6570;&#x636E;&#x5E93;&#x7D22;&#x5F15;&#xFF08;&#x9ED8;&#x8BA4;&#x4E3A;0&#xFF09;
spring.redis.database= 0
#&#x8FDE;&#x63A5;&#x8D85;&#x65F6;&#x65F6;&#x95F4;&#xFF08;&#x6BEB;&#x79D2;&#xFF09;&#xFF0C;&#x8FD9;&#x91CC;&#x662F;30&#x5206;&#x949F;
spring.redis.timeout=1800000
#&#x8FDE;&#x63A5;&#x6C60;&#x6700;&#x5927;&#x8FDE;&#x63A5;&#x6570;&#xFF08;&#x4F7F;&#x7528;&#x8D1F;&#x503C;&#x8868;&#x793A;&#x6CA1;&#x6709;&#x9650;&#x5236;&#xFF09;
spring.redis.lettuce.pool.max-active=20
#&#x6700;&#x5927;&#x963B;&#x585E;&#x7B49;&#x5F85;&#x65F6;&#x95F4;(&#x8D1F;&#x6570;&#x8868;&#x793A;&#x6CA1;&#x9650;&#x5236;)
spring.redis.lettuce.pool.max-wait=-1
#&#x8FDE;&#x63A5;&#x6C60;&#x4E2D;&#x7684;&#x6700;&#x5927;&#x7A7A;&#x95F2;&#x8FDE;&#x63A5;
spring.redis.lettuce.pool.max-idle=5
#&#x8FDE;&#x63A5;&#x6C60;&#x4E2D;&#x7684;&#x6700;&#x5C0F;&#x7A7A;&#x95F2;&#x8FDE;&#x63A5;
spring.redis.lettuce.pool.min-idle=0

添加Redis的配置类(自动配置的不够我们用,所以需要自行扩展一下)

@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
    @Bean
    public RedisTemplate<string, object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<string, object> template = new RedisTemplate<>();
        RedisSerializer<string> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setConnectionFactory(factory);
        //key&#x5E8F;&#x5217;&#x5316;&#x65B9;&#x5F0F;
        template.setKeySerializer(redisSerializer);
        //value&#x5E8F;&#x5217;&#x5316;
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //value hashmap&#x5E8F;&#x5217;&#x5316;
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        return template;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<string> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        //&#x89E3;&#x51B3;&#x67E5;&#x8BE2;&#x7F13;&#x5B58;&#x8F6C;&#x6362;&#x5F02;&#x5E38;&#x7684;&#x95EE;&#x9898;
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // &#x914D;&#x7F6E;&#x5E8F;&#x5217;&#x5316;&#xFF08;&#x89E3;&#x51B3;&#x4E71;&#x7801;&#x7684;&#x95EE;&#x9898;&#xFF09;,&#x8FC7;&#x671F;&#x65F6;&#x95F4;600&#x79D2;
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }

}
</string></string></string,></string,>

使用示例

@RestController
public class RedisTestController {
    @Autowired
    private RedisTemplate redisTemplate;

    @GetMapping("/testRedis")
    public Object testRedis() {
        // 1&#x3001;string&#x7684;&#x64CD;&#x4F5C;&#x7C7B;
        ValueOperations strOperations = redisTemplate.opsForValue();

        // 2&#x3001;list&#x7684;&#x64CD;&#x4F5C;&#x7C7B;
        ListOperations listOperations = redisTemplate.opsForList();

        // 3&#x3001;Set&#x7684;&#x64CD;&#x4F5C;&#x7C7B;
        SetOperations boundSetOperations = redisTemplate.opsForSet();

        // 4&#x3001;hash&#x7684;&#x64CD;&#x4F5C;&#x7C7B;
        HashOperations hashOperations = redisTemplate.opsForHash();

        // 5&#x3001;zset&#x7684;&#x64CD;&#x4F5C;&#x7C7B;
        ZSetOperations zSetOperations = redisTemplate.opsForZSet();

        return "hello spring-boot-starter-redis";
    }
}

Redis的事物

Redis事物的定义

  • Redis的事物并 不支持ACID
  • Redis事物是一个单独的隔离操作:事物中所有的命令都会序列化、按顺序的执行。事物在执行的过程中,不会被其他客户端发送来的请求所打断。
  • Redis中事物的主要作用就是 串联多个命令防止别的命令插队

Multi、Exec、Discard

  • 在执行了 Multi命令之后,输入的命令都会被加入到一个队列(组)当中,但是不会执行。(组队阶段)
  • 当执行Exec命令后,队列中的命令将会按照顺序执行,执行的过程中不会被其他客户端的请求所打断。因为这是一个单独的隔离操作。(执行阶段)
  • 在Multi命令的 中途如果想要放弃当前的队列,则可以 执行discard命令。

组队阶段与执行阶段出现错误时

  • 组队阶段出现了错误时,那么只要执行了exec进入执行阶段时,所有组队的命令都不会被执行。
  • 执行阶段出现错误时:指令依然会按照顺序执行,成功的就成功,失败的就失败,并 没有原子性

Redis对于事物冲突的解决方案(乐观锁)

  • Redis中使用watch来监视某个key,依次来达到乐观锁的效果,在修改被监视的key后,key的版本号会发生改变,因此当再次修改时,若没有获取到最新的数据,则会导致更新失败。
  • watch的监视需要 *执行在 multi之前

使用示例(2台客户端)

  • 客户端1号
&#x5F00;&#x542F;&#x4E50;&#x89C2;&#x9501;&#x76D1;&#x89C6;k1
watch k1

&#x5F00;&#x542F;&#x4E8B;&#x7269;
multi
  • 客户端2号
&#x5F00;&#x542F;&#x4E50;&#x89C2;&#x9501;&#x76D1;&#x89C6;k1
watch k1

&#x5F00;&#x542F;&#x4E8B;&#x7269;
multi
  • 客户端1号修改了k1的值并执行
&#x4FEE;&#x6539;k1&#x7684;&#x503C;
set k1 clientOne

&#x6267;&#x884C;
exec
  • 客户端2号也尝试修改k1的值并执行
set k1 clientTwo
exec

&#x8FD4;&#x56DE;&#x7684;&#x7ED3;&#x679C;&#xFF0C;&#x8868;&#x793A;&#x4FEE;&#x6539;&#x5931;&#x8D25;
(nil)

Watch配合Redis事物的总结

  • 当一个客户端使用wath开始了key的监视(可以监视1个或多个key)后
  • 那么当前客户端开启的事物,只要在最后exec执行之前
  • unwatch: 取消对key的监控。(如果已经执行了exec或discard后则会自动取消)
  • 发现 watch监视的任何一个key发生了变化后,则会导致当前的 事物失效所有的指令全都无法执行

Redis事物的三特性

  • 单独的隔离操作
  • 当执行了exec后,将会把队列中的指令序列化,按照顺序的执行这些指令,在此期间不会被其他客户端的请求打断。
  • 没有隔离级别的概念
  • 队列中的命令在没有进行exec之前都不会被执行,只是放在队列当中。
  • 不保证原子性
  • 事物中如果有一条命令执行失败,并不会导致其他命令回滚

Redis事物秒杀案例

  • 要求
  • 同一个用户最多只能秒杀成功一次
  • 实现思路( 库存使用string存,秒杀成功的人数使用set存储
  • (1)接收到客户端发来的 用户名与商品ID
  • (2)根据商品ID去Redis中 查询是否为null
    • (2.1)如果为null代表秒杀还没有开始
    • (2.2)如果不为null,则 判断当前的用户是否已经秒杀过了,若已经秒杀过则直接提示后结束当前方法
  • (3)根据第二步查询到的商品库存 判断当前库存容量
    • (3.1)如果库存容量够,则秒杀成功的列表中加入当前用户ID,然后库存 -1 ,告知秒杀成功
    • (3.2)如果库存不够,则提示秒杀已结束
  • 编码实现
    @PostMapping("/secKill")
    public void secKill(String productId) {
        Jedis jedis = new Jedis("192.168.22.100", 6379);

        // &#x6A21;&#x62DF;&#x4E0D;&#x540C;&#x7684;&#x7528;&#x6237;&#xFF0C;&#x968F;&#x673A;4&#x4F4D;&#x7528;&#x6237;ID
        String userId = getRandomUserId();
        // 1&#x3001;&#x62FC;&#x63A5;&#x5F53;&#x524D;&#x7684;key
        String productCountKey = "sk:" + productId + ":qt";
        String successSetKey = "sk:" + productId + ":user";

        // 2&#x3001;&#x5224;&#x65AD;&#x5F53;&#x524D;&#x662F;&#x5426;&#x5F00;&#x59CB;&#x4E86;&#x79D2;&#x6740;
        Integer productCount = Integer.parseInt(jedis.get(productCountKey));

        // 2.1 &#x5224;&#x65AD;&#x5F53;&#x524D;&#x79D2;&#x6740;&#x662F;&#x5426;&#x5F00;&#x59CB;
        if (productCount == null) {
            System.out.println(new Result(false, "&#x79D2;&#x6740;&#x8FD8;&#x672A;&#x5F00;&#x59CB;"));
            return;
        }

        // 3&#x3001;&#x5224;&#x65AD;&#x5F53;&#x524D;&#x7528;&#x6237;&#x662F;&#x5426;&#x5DF2;&#x7ECF;&#x79D2;&#x6740;&#x8FC7;&#x4E86;
        if (jedis.sismember(successSetKey, userId)) {
            System.out.println(new Result(false, "&#x60A8;&#x5DF2;&#x7ECF;&#x79D2;&#x6740;&#x6210;&#x529F;&#x8FC7;&#x4E86;&#xFF0C;&#x4E0D;&#x80FD;&#x518D;&#x79D2;&#x6740;&#x4E86;"));
            return;
        }

        // 4&#x3001;&#x5224;&#x65AD;&#x5F53;&#x524D;&#x5E93;&#x5B58;&#x662F;&#x5426;&#x5DF2;&#x7ECF;&#x6CA1;&#x6709;&#x4E86;
        if(productCount <= 0) { system.out.println(new result(false, "非常抱歉,秒杀已经结束了")); return; } 5、库存 - 1,秒杀成功的用户列表加上当前用户 jedis.decr(productcountkey); jedis.sadd(successsetkey, userid); jedis.close(); result(true, "恭喜你秒杀成功")); private string getrandomuserid() stringbuilder sb="new" stringbuilder(); for (int i="0;" < 4; i++) sb.append(new random().nextint(10)); return sb.tostring(); code></=>

Linux系统中安装压力测试工具httpd-tools

yum install httpd-tools

ab命令的使用示例

  • 在任意目录创建一个需要传递的参数文件: vim /opt/postfile
  • 修改其中的内容,放上需要传递的参数,以&结尾
productId=1010&
  • 输入如下指令完成压力测试
2000&#x4E2A;&#x7EBF;&#x7A0B;&#xFF0C;&#x5B58;&#x5728;200&#x4E2A;&#x5E76;&#x53D1;&#x3002;&#x6CE8;&#x610F;ip&#x5730;&#x5740;&#x548C;&#x7AEF;&#x53E3;&#x53F7;&#x522B;&#x5199;&#x9519;&#x4E86;
ab -n 2000 -c 200 -k -p /opt/postfile -T application/x-www-form-urlencoded http://192.168.31.71:8080/secKill

如上编码出现的问题

连接超时问题

  • 采用连接池,之后用连接池来获取Jedis
  • 连接池编码
public class JedisPoolUtils {
    private volatile static JedisPool jedisPool = null;

    public static JedisPool getInstance() {
        if(jedisPool == null) {
            synchronized (JedisPoolUtils.class) {
                if(jedisPool == null) {
                    JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
                    // &#x6700;&#x5927;&#x8FDE;&#x63A5;
                    jedisPoolConfig.setMaxTotal(200);
                    // &#x6700;&#x5927;&#x7A7A;&#x95F2;
                    jedisPoolConfig.setMaxIdle(32);
                    // &#x6700;&#x5927;&#x7B49;&#x5F85;&#x65F6;&#x95F4;
                    jedisPoolConfig.setMaxWaitMillis(100 * 1000);
                    // &#x8868;&#x793A;&#x5F53;pool&#x4E2D;&#x7684;jedis &#x5B9E;&#x4F8B;&#x90FD;&#x88AB;&#x5206;&#x914D;&#x5B8C;&#x65F6;&#xFF0C;&#x662F;&#x5426;&#x8981;&#x8FDB;&#x884C;&#x963B;&#x585E;
                    jedisPoolConfig.setBlockWhenExhausted(true);
                    // &#x6BCF;&#x6B21;&#x83B7;&#x53D6;&#x8FDE;&#x63A5;&#x65F6;&#x5019;&#x90FD;&#x8981;&#x5230;&#x6570;&#x636E;&#x5E93;&#x9A8C;&#x8BC1;&#x8FDE;&#x63A5;&#x6709;&#x6548;&#x6027;
                    jedisPoolConfig.setTestOnBorrow(true);

                    jedisPool = new JedisPool(jedisPoolConfig, "192.168.22.100");
                }
            }
        }

        return jedisPool;
    }
}

超卖问题

  • 原因:由于整个流程判断,以及对数据的操作都是多线程的,会导致多个线程同时判断到了库存大于0而往下执行了,这多个线程往往超出实际的库存量,因此出现超卖问题
  • 解决思路:使用Redis的乐观锁机制来解决事物问题
  • 修改后的代码
    @PostMapping("/secKill2")
    public void secKillNew(String productId) {
        Jedis jedis = new Jedis("192.168.22.100", 6379);

        // &#x6A21;&#x62DF;&#x4E0D;&#x540C;&#x7684;&#x7528;&#x6237;&#xFF0C;&#x968F;&#x673A;4&#x4F4D;&#x7528;&#x6237;ID
        String userId = getRandomUserId();

        // 1&#x3001;&#x62FC;&#x63A5;&#x5F53;&#x524D;&#x7684;key
        String productCountKey = "sk:" + productId + ":qt";
        String successSetKey = "sk:" + productId + ":user";

        // -- &#x89E3;&#x51B3;&#x8D85;&#x5356;1&#xFF1A; &#x76D1;&#x89C6;&#x5E93;&#x5B58;&#x662F;&#x5426;&#x53D1;&#x751F;&#x53D8;&#x5316;
        jedis.watch(productCountKey);

        // 2&#x3001;&#x5224;&#x65AD;&#x5F53;&#x524D;&#x662F;&#x5426;&#x5F00;&#x59CB;&#x4E86;&#x79D2;&#x6740;
        Integer productCount = Integer.parseInt(jedis.get(productCountKey));

            // 2.1 &#x5224;&#x65AD;&#x5F53;&#x524D;&#x79D2;&#x6740;&#x662F;&#x5426;&#x5F00;&#x59CB;
        if (productCount == null) {
            System.out.println(new Result(false, "&#x79D2;&#x6740;&#x8FD8;&#x672A;&#x5F00;&#x59CB;"));
            return;
        }

        // 3&#x3001;&#x5224;&#x65AD;&#x5F53;&#x524D;&#x7528;&#x6237;&#x662F;&#x5426;&#x5DF2;&#x7ECF;&#x79D2;&#x6740;&#x8FC7;&#x4E86;
        if (jedis.sismember(successSetKey, userId)) {
            System.out.println(new Result(false, "&#x60A8;&#x5DF2;&#x7ECF;&#x79D2;&#x6740;&#x6210;&#x529F;&#x8FC7;&#x4E86;&#xFF0C;&#x4E0D;&#x80FD;&#x518D;&#x79D2;&#x6740;&#x4E86;"));
            return;
        }

        // 4&#x3001;&#x5224;&#x65AD;&#x5F53;&#x524D;&#x5E93;&#x5B58;&#x662F;&#x5426;&#x5DF2;&#x7ECF;&#x6CA1;&#x6709;&#x4E86;
        if(productCount <= 0) { system.out.println(new result(false, "非常抱歉,秒杀已经结束了")); return; } 解决超卖2: 开启事物,使如下命令编程串行执行 transaction multi="jedis.multi();" 5、库存 - 1,秒杀成功的用户列表加上当前用户 multi.decr(productcountkey); multi.sadd(successsetkey, userid); 解决超卖3: 执行组队好了的指令 list<object> execResult = multi.exec();

        if (execResult == null || execResult.size() == 0) {
            System.out.println("&#x79D2;&#x6740;&#x5931;&#x8D25;&#x4E86;");
            jedis.close();
            return;
        }

        jedis.close();
        System.out.println(new Result(true, "&#x606D;&#x559C;&#x4F60;&#x79D2;&#x6740;&#x6210;&#x529F;"));
    }
</=>

如上虽解决了超卖问题,但是又出现了库存遗留问题

  • 明明已经显示秒杀结束了,但是却还有库存
  • 出现的原因:因为乐观锁问题,导致大部分线程在抢到了商品而进行库存减少时,都失败了。
  • 使用lua脚本来实现事务的控制,当使用lua脚本时,redis执行单个脚本是无法被其他客户端的请求所中断的。
  • 脚本如下
local productId=KEYS[1];
local userId=KEYS[2];
local productCountKey="sk:"..productId..":qt";
local successSetKey="sk:"..productId..":user";
local userExists=redis.call("sismember",successSetKey,userId);
if tonumber(userExists)==1 then
  return 2;
end
local num= redis.call("get" ,productCountKey);
if tonumber(num)<=0 then return 0; else redis.call("decr",productcountkey); redis.call("sadd",successsetkey,userid); end 1; < code></=0>
  • 修改后的代码如下
    static String str = "local productId=KEYS[1];\n" +
            "local userId=KEYS[2]; \n" +
            "local productCountKey=\"sk:\"..productId..\":qt\";\n" +
            "local successSetKey=\"sk:\"..productId..\":user\"; \n" +
            "local userExists=redis.call(\"sismember\",successSetKey,userId);\n" +
            "if tonumber(userExists)==1 then \n" +
            "  return 2;\n" +
            "end\n" +
            "local num= redis.call(\"get\" ,productCountKey);\n" +
            "if tonumber(num)<=0 then \n" + " return 0; "else redis.call(\"decr\",productcountkey);\n" redis.call(\"sadd\",successsetkey,userid);\n" "end\n" "return 1;\n"; @postmapping(" seckill3") public void seckillnew3(string productid) { jedis 模拟不同的用户,随机4位用户id string userid="getRandomUserId();" 加载lua脚本并执行 sha1="jedis.scriptLoad(str);" object obj="jedis.evalsha(sha1," 2, productid, userid); result="String.valueOf(obj);" if("2".equals(result)) system.out.println("您已经秒杀过了,不能再次秒杀"); }else if("0".equals(result)) system.out.println("库存已经没有了,秒杀结束了~"); system.out.println("秒杀成功~"); } 一定要关闭连接 jedis.close(); < code></=0>

Redis之持久化

RDB持久化

RDB文件备份的流程

  • Redis在进行RDB文件备份时,会单独创建一个 Fork进程来进行数据的持久化
  • Fork进程会先将数据写到一个 临时的文件中, 当持久化结束后,再将该临时文件替换掉原本的dump.rdb文件(又称为 写时复制技术),这是出于数据的完整性考虑,不然如果直接往磁盘上备份,突然宕机将会导致数据的不完整性。( 一般情况父进程和子进程会共用同一段物理内存
  • 在这个过程中,redis的主进程是不会进行任何IO操作的,因此如果进行大规模的数据恢复,RDB是很不错的选择
  • 不过也有个弊端,RDB最后一次持久化后的数据可能会丢失。

配置文件中关于RDB的问题、如何触发RDB快照(保持策略)

rdb&#x5907;&#x4EFD;&#x7684;&#x6587;&#x4EF6;&#x540D;
dbfilename dump.rdb

&#x5907;&#x4EFD;&#x6587;&#x4EF6;&#x7684;&#x8DEF;&#x5F84;&#xFF0C;&#x4E0E;AOF&#x5171;&#x4EAB;&#x3002; &#x8BE5;&#x8DEF;&#x5F84;&#x6307;&#x7684;&#x662F;&#x542F;&#x52A8;redis-server &#x7684;&#x8DEF;&#x5F84;
&#x5982;&#x679C;&#x5728;&#x5F53;&#x524D;&#x8DEF;&#x5F84;&#x76F4;&#x63A5;&#x6267;&#x884C;redis-server&#xFF0C;&#x5219;&#x662F;&#x5F53;&#x524D;&#x8DEF;&#x5F84;
&#x5982;&#x679C;&#x662F; /local/usr/bin/redis-server&#x7684;&#x8BDD;&#xFF0C;&#x5219;&#x4E3A;/local/usr/bin&#x76EE;&#x5F55;&#x4E0B;
dir ./

&#x9ED8;&#x8BA4;&#x5F53; 3600&#x79D2;&#x4E4B;&#x5185;&#x6709;&#x4E00;&#x4E2A;key&#x6539;&#x53D8;&#x4E86;&#xFF0C;&#x5219;&#x8FDB;&#x884C;&#x4E00;&#x6B21;&#x6301;&#x4E45;&#x5316;
&#x5F53;300&#x79D2;&#x4E4B;&#x5185;&#x6709;100&#x4E2A;key&#x6539;&#x53D8;&#x4E86;&#xFF0C;&#x5219;&#x8FDB;&#x884C;&#x4E00;&#x6B21;&#x6301;&#x4E45;&#x5316;
&#x5F53;60&#x79D2;&#x4E4B;&#x5185;&#x6709;10000&#x4E2A;key&#x6539;&#x53D8;&#x4E86;&#xFF0C;&#x5219;&#x8FDB;&#x884C;&#x4E00;&#x6B21;&#x6301;&#x4E45;&#x5316;&#xFF0C;&#x6309;&#x7167;&#x95F4;&#x9694;&#x65F6;&#x95F4;&#x6765;
Redis&#x4F1A;&#x5728;&#x540E;&#x53F0;&#x5F02;&#x6B65;&#x8FDB;&#x884C;&#x5FEB;&#x7167;&#x5DE5;&#x4F5C;&#xFF0C;&#x5FEB;&#x7167;&#x7684;&#x540C;&#x65F6;&#x8FD8;&#x80FD;&#x54CD;&#x5E94;&#x5BA2;&#x6237;&#x7AEF;&#x8BF7;&#x6C42;
&#x5F53;&#x5468;&#x671F;&#x7684;&#x95F4;&#x9694;&#x65F6;&#x95F4;&#x5230;&#x4E86;&#x65F6;&#x4F1A;&#x81EA;&#x52A8;&#x89E6;&#x53D1;bgsave&#x81EA;&#x52A8;&#x4FDD;&#x5B58;
save 3600 1
save 300 100
save 60 10000

&#x5F53;redis&#x65E0;&#x6CD5;&#x5199;&#x5165;&#x78C1;&#x76D8;&#x7684;&#x65F6;&#x5019;&#xFF0C;&#x76F4;&#x63A5;&#x5173;&#x95ED;redis&#x7684;&#x5199;&#x64CD;&#x4F5C;&#xFF0C;&#x63A8;&#x8350;yes
stop-writes-on-bgsave-error yes

redis&#x4F1A;&#x91C7;&#x7528;LZF&#x7B97;&#x6CD5;&#x8FDB;&#x884C;&#x538B;&#x7F29;
rdbcompression yes

&#x68C0;&#x67E5;&#x5FEB;&#x7167;&#x7684;&#x5B8C;&#x6574;&#x6027;&#xFF0C;&#x63A8;&#x8350;yes
rdbchecksum yes

RDB是如何完成数据恢复的 ###SNAPSHOTTING

  • 当redis服务启动时,则会按照配置文件中所 设置的备份文件的路径、文件名自动完成数据恢复的工作

2个命令(停止和查看最后一次备份时间)

  • 通过lastsave命令可以查看最后一次快照时间
  • redis-cli config set save “”,禁用保存策略

RDB的优势与劣势

  • 优势
  • 恢复数据快
  • 节省磁盘空间
  • 适合大规模的数据恢复
  • 对数据完整性要求不高更适合使用
  • 劣势
  • 每次fork都会写一次临时文件,导致2倍的膨胀性
  • 虽然redis在fork时使用了写时复制技术,但是如果数据库庞大还是比较消耗性能
  • RDB是在备份周期的间隔时间做一次备份,如果redis意外的down掉的话,将会损失最后一次持久化后的数据。

AOF持久化

AOF持久化的流程

  • 客户端只要执行了写的命令,那么该命令就会被追加到AOF缓冲区中
  • AOF的缓冲区根据AOF的持久化策略来决定何时写入到磁盘的AOF备份文件当中
  • 当AOF文件的大小超过重写策略或手动重写时,会对AOF文件进行rewrite重写,压缩AOF文件容量
  • Redis启动时,会自动的加载AOF文件,执行AOF文件中的写指令,以达到数据恢复的目的

配置文件中的AOF文件 ###APPEND ONLY MODE

&#x662F;&#x5426;&#x5F00;&#x542F;AOF&#x529F;&#x80FD;
appendonly no

AOF&#x6587;&#x4EF6;&#x7684;&#x540D;&#x79F0;
appendfilename "appendonly.aof"

AOF&#x6587;&#x4EF6;&#x7684;&#x8DEF;&#x5F84;&#x8DDF;RDB&#x6587;&#x4EF6;&#x7684;&#x8DEF;&#x5F84;&#x4E00;&#x81F4;
dir ./

AOF&#x5728;&#x7F13;&#x51B2;&#x533A;&#x65F6;&#x7684;&#x540C;&#x6B65;&#x7B56;&#x7565;, always &#x6BCF;&#x6B21;&#x5199;&#x5165;&#x65F6;&#x90FD;&#x76F4;&#x63A5;&#x540C;&#x6B65;&#x5230;&#x78C1;&#x76D8;&#x3002;everysec &#x6BCF;&#x79D2;&#x3002;
no &#x4E0D;&#x4E3B;&#x52A8;&#x8FDB;&#x884C;&#x540C;&#x6B65;&#x64CD;&#x4F5C;&#xFF0C;&#x7531;&#x64CD;&#x4F5C;&#x7CFB;&#x7EDF;&#x51B3;&#x5B9A;&#x4F55;&#x65F6;&#x540C;&#x6B65;
appendfsync everysec

与重写相关的配置

&#x5982;&#x679C;&#x8BBE;&#x7F6E;&#x4E3A;yes&#xFF0C;&#x91CD;&#x5199;&#x65F6;&#x6570;&#x636E;&#x5C06;&#x53EA;&#x5199;&#x5165;&#x7F13;&#x5B58;&#xFF0C;&#x4E0D;&#x5199;&#x5165;aof&#x6587;&#x4EF6;&#xFF0C;&#x6027;&#x80FD;&#x66F4;&#x9AD8;&#x4F46;&#x53EF;&#x80FD;&#x5BFC;&#x81F4;&#x6570;&#x636E;&#x4E22;&#x5931;
&#x8BBE;&#x7F6E;&#x4E3A;no&#xFF0C;&#x5219;&#x4F1A;&#x628A;&#x6570;&#x636E;&#x5F80;&#x78C1;&#x76D8;&#x91CC;&#x5237;&#xFF0C;&#x5C06;&#x4F1A;&#x5BFC;&#x81F4;&#x4E3B;&#x7EBF;&#x7A0B;&#x5904;&#x4E8E;&#x963B;&#x585E;&#x72B6;&#x6001;
no-appendfsync-on-rewrite=yes

&#x91CD;&#x5199;&#x7684;&#x57FA;&#x51C6;&#x503C;&#xFF0C;&#x6587;&#x4EF6;&#x8FBE;&#x5230;100%&#x65F6;&#x5F00;&#x59CB;&#x91CD;&#x5199;&#xFF08;&#x6587;&#x4EF6;&#x662F;&#x539F;&#x6765;&#x91CD;&#x5199;&#x540E;&#x6587;&#x4EF6;&#x7684;2&#x500D;&#x65F6;&#x89E6;&#x53D1;&#xFF09;
auto-aof-rewrite-percentage 100

&#x8BBE;&#x7F6E;&#x91CD;&#x5199;&#x7684;&#x57FA;&#x51C6;&#x503C;&#xFF0C;&#x5F53;&#x6587;&#x4EF6;&#x5927;&#x5C0F;&#x8FBE;&#x5230;&#x8BE5;&#x503C;&#x540E;&#x5F00;&#x59CB;&#x91CD;&#x5199;
auto-aof-rewrite-min-size 64mb

&#x4F8B;&#x5982;&#xFF1A;&#x6587;&#x4EF6;&#x8FBE;&#x5230;70MB&#x5F00;&#x59CB;&#x91CD;&#x5199;&#xFF0C;&#x964D;&#x5230;50MB&#xFF0C;&#x4E0B;&#x6B21;&#x4EC0;&#x4E48;&#x65F6;&#x5019;&#x5F00;&#x59CB;&#x91CD;&#x5199;&#xFF1F;100MB
&#x7CFB;&#x7EDF;&#x8F7D;&#x5165;&#x65F6;&#x6216;&#x8005;&#x4E0A;&#x6B21;&#x91CD;&#x5199;&#x5B8C;&#x6BD5;&#x65F6;&#xFF0C;Redis&#x4F1A;&#x8BB0;&#x5F55;&#x6B64;&#x65F6;AOF&#x5927;&#x5C0F;&#xFF0C;&#x8BBE;&#x4E3A;base_size,
&#x5982;&#x679C;Redis&#x7684;AOF&#x5F53;&#x524D;&#x5927;&#x5C0F;>= base_size +base_size*100% (&#x9ED8;&#x8BA4;)&#x4E14;&#x5F53;&#x524D;&#x5927;&#x5C0F;>=64mb(&#x9ED8;&#x8BA4;)&#x7684;&#x60C5;&#x51B5;&#x4E0B;&#xFF0C;Redis&#x4F1A;&#x5BF9;AOF&#x8FDB;&#x884C;&#x91CD;&#x5199;&#x3002;

当AOF与RDB同时存在时,优先选用谁?

  • 当AOF与RDB同时存在时,Redis会使用AOF作为数据恢复的文件

AOF文件修复命令

redis-check-aof--fix appendonly.aof

AOF的优势和劣势

  • 优势
  • 备份机制更加文件,丢失数据的概率更低
  • 可读的日志文本,通过操作AOF文件,可以处理误操作
  • 劣势
  • 比起RDB更加耗费磁盘空间
  • 恢复、备份的速度较慢
  • 每次读写都进行同步的话,有一定的性能压力,因为不断的IO
  • 存在个别bug将会导致恢复不能

使用建议

  • 当更追求速度且对数据完整性要求不高,可以考虑使用RDB
  • 对数据完整性要求高时,可以考虑AOF
  • 官方推荐是2个都开启
  • 不建议单独使用AOF,可能会出现bug

主从复制

  • 主机负责读,从机负责写,一般都是一主多从

能够为我们解决的问题

  • 容灾快速恢复
  • 读写分离,性能扩展

采用模拟的方式完成主从复制,实现一主多从

在/opt文件下创建一个myredis文件夹,将redis.conf文件复制到这修改成公共的配置文件

  • mkdir /opt/myredis
  • cp /etc/redis.conf /opt/myredis/
  • 修改配置文件
  • 1、bind ip地址绑定注释掉
  • 2、protected no 关闭保护模式
  • 3、daemonize yes 开启后台进程

创建3个配置文件,分别为redis6379.conf、redis6380.conf、redis6381.conf

  • redis6379.conf
include /opt/myredis/redis.conf
pidfile /opt/myredis/redis6379.pid
port 6379
dbfilename dump6379.rdb
  • redis6380.conf
include /opt/myredis/redis.conf
pidfile /opt/myredis/redis6380.pid
port 6380
dbfilename dump6380.rdb
  • redis6381.conf
include /opt/myredis/redis.conf
pidfile /opt/myredis/redis6381.pid
port 6381
dbfilename dump6381.rdb

启动3个redis并查看当前进程

&#x6267;&#x884C;&#x8FD9;&#x4E9B;&#x547D;&#x4EE4;&#x7684;&#x65F6;&#x5019;&#xFF0C;&#x90FD;&#x662F;&#x5728;/opt/myredis&#x4E0B;&#x8FDB;&#x884C;&#x7684;
redis-server redis6379.conf
redis-server redis6380.conf
redis-server redis6381.conf

JAVA入门基础_从零开始的培训_Redis

使用3个xshell分别连接到不同的redis服务,并查看当前的服务器状态

redis-cli -p 6379
redis-cli -p 6380
redis-cli -p 6381

&#x67E5;&#x770B;&#x670D;&#x52A1;&#x5668;&#x7684;&#x4E3B;&#x4ECE;&#x590D;&#x5236;&#x4FE1;&#x606F;
info replication

JAVA入门基础_从零开始的培训_Redis

操作2个从机,使用命令使其连接上主机后再查看3台从机的主从信息

  • 在2个从机分别执行如下命令,由于我3台都在本机,所以是127.0.0.1
slaveof 127.0.0.1 6379
  • 主机6379
    JAVA入门基础_从零开始的培训_Redis
  • 从机6380
    JAVA入门基础_从零开始的培训_Redis
  • 从机6381
    JAVA入门基础_从零开始的培训_Redis

主从复制的几个特点

一主二仆

  • 主机可以进行读写的操作
  • 从机只能进行读的操作
  • 当从机与主机建立上关系后,会向主机发送一个sync命令,让主机同步数据文件过来,完成数据的同步
  • 之后就是主机自动发请求过来,从机接收。
  • 只有第一次连接时,是从机主动发请求让主机发送数据
  • 综上所述,主机只要进行了写的操作,从机也可以拿到数据

薪火相传

  • 由于每次主机写数据的时候,都需要向所有的从机发送数据进行同步,那么主机的压力就会不断增大
  • 因此可以这样:主机底下永远只有几个从机,但是 从机又是其他服务器的主机
  • 类似于领导下有2个直系管理的人员,而这2个人员又是其他人的管理者
  • 这里演示一下,6381从机将其主机设置为6380
  • 127.0.0.1:6381> slaveof localhost 6380
  • 此时我们看看6380的主从复制信息
    JAVA入门基础_从零开始的培训_Redis
  • 此时当6379再进行写操作时,就只会向6380发送数据了。而6380再向6381发送数据来进行同步
  • 有一个问题需要注意:就是当6380宕机了,竟会导致6381也无法同步到数据。

反客为主

  • 在一主一从、或者一主多从的时候,如果主机宕机了,那么从机可以反客为主 晋升为主机
  • 从机手动执行: slaveof no one命令,晋升为主机
  • 如果从机不反客为主,那么当挂掉的主机重启时,他们的关系依然是主从关系。
  • 而当之前的主机再次开机时,会发现自己还是主机,但是那台反客为主的从机已经没了。

复制原理

  • 当从机连接上主机后,会向主机发送一个sync命令,然后主机将会被整个的数据文件发送给从机以此来完成数据同步
  • 而仅仅是第一次连接时,是从机向主机发送同步命令,之后就全都由主机来主动完成同步
  • 全量复制: 每次重新连接上主机时,都会进行一次全量复制
  • 增量复制: 已经连接上了主机后,由主机主动发送过来的同步数据为增量复制

哨兵模式

  • 可以理解为反客为主的升级版
  • 可以监视主从服务器的状态,当主机宕机时,会根据策略选取一名从机来充当主机
  • 而当主机再次上线时,会发现自己变成了从机。

创建哨兵启动时所需的配置文件

  • 在/opt/myredis/下创建一个sentinel.conf配置文件
  • 修改其中的内容
mymaster&#x4E3A;&#x76D1;&#x63A7;&#x5BF9;&#x8C61;&#x8D77;&#x7684;&#x670D;&#x52A1;&#x5668;&#x540D;&#x79F0;&#xFF0C;1 &#x4E3A;&#x81F3;&#x5C11;&#x6709;&#x591A;&#x5C11;&#x4E2A;&#x54E8;&#x5175;&#x540C;&#x610F;&#x8FC1;&#x79FB;&#x7684;&#x6570;&#x91CF;
&#x5565;&#x610F;&#x601D;&#x5462;&#xFF1F;&#x610F;&#x601D;&#x662F;&#x5982;&#x679C;&#x9700;&#x8981;&#x8FC1;&#x79FB;&#x4E3B;&#x673A;&#xFF0C;&#x53EA;&#x9700;&#x8981;&#x4E00;&#x4E2A;&#x53CA;&#x4E00;&#x4E2A;&#x4EE5;&#x4E0A;&#x54E8;&#x5175;&#x540C;&#x610F;&#x5373;&#x53EF;
&#x5F00;&#x542F;&#x4E86;&#x54E8;&#x5175;&#x6A21;&#x5F0F;&#xFF0C;&#x90A3;&#x4E48;&#x6240;&#x6709;&#x7684;&#x670D;&#x52A1;&#x5668;&#x90FD;&#x6210;&#x4E86;&#x54E8;&#x5175;
sentinel monitor mymaster 127.0.0.1 6379 1

启动哨兵,开启后的默认端口号为26379

redis-sentinel /opt/myredis/sentinel.conf

JAVA入门基础_从零开始的培训_Redis
  • 此时当主机宕机时,比如关闭掉6379这台主机
  • 此时发现我们的哨兵进程已经监控到,并且完成了容灾的处理
    JAVA入门基础_从零开始的培训_Redis

哨兵选举新主机的策略

  • (1)根据每台服务器配置文件中的 slave-priority配置来决定,数值越小优先级越高
  • (2)选择偏移量最大的(指的是哪个从机复制的数据最多最全的)
  • (3)选择runid最小的从机,每个redis服务在运行时都会随机生成一个40位的runid

哨兵模式小总结

  • 当监控线程发现了主机宕机后,会选取一个从机晋升为主机
  • 并且将之前主机之下的从机切换成新晋升的主机
  • 当之前的主机再次上线时,发现自己也变成了新主机的从机

在Java程序中使用哨兵模式

  • 修改一下之前配置的线程池,改成JedisSetinelPool
public class JedisPoolUtils {
    private volatile static JedisSentinelPool jedisPool = null;

    public static JedisSentinelPool getInstance() {
        if(jedisPool == null) {
            synchronized (JedisPoolUtils.class) {
                if(jedisPool == null) {
                    // &#x521B;&#x5EFA;&#x4E00;&#x4E2A;set&#x96C6;&#x5408;&#xFF0C;&#x4FDD;&#x5B58;&#x54E8;&#x5175;&#x7EBF;&#x7A0B;&#x7684; ip&#x548C;&#x7AEF;&#x53E3;&#x53F7;
                    Set<string> sentinelSet=new HashSet<>();
                    sentinelSet.add("192.168.22.100:26379");

                    JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
                    // &#x6700;&#x5927;&#x8FDE;&#x63A5;
                    jedisPoolConfig.setMaxTotal(200);
                    // &#x6700;&#x5927;&#x7A7A;&#x95F2;
                    jedisPoolConfig.setMaxIdle(32);
                    // &#x6700;&#x5927;&#x7B49;&#x5F85;&#x65F6;&#x95F4;
                    jedisPoolConfig.setMaxWaitMillis(100 * 1000);
                    // &#x8868;&#x793A;&#x5F53;pool&#x4E2D;&#x7684;jedis &#x5B9E;&#x4F8B;&#x90FD;&#x88AB;&#x5206;&#x914D;&#x5B8C;&#x65F6;&#xFF0C;&#x662F;&#x5426;&#x8981;&#x8FDB;&#x884C;&#x963B;&#x585E;
                    jedisPoolConfig.setBlockWhenExhausted(true);
                    // &#x6BCF;&#x6B21;&#x83B7;&#x53D6;&#x8FDE;&#x63A5;&#x65F6;&#x5019;&#x90FD;&#x8981;&#x5230;&#x6570;&#x636E;&#x5E93;&#x9A8C;&#x8BC1;&#x8FDE;&#x63A5;&#x6709;&#x6548;&#x6027;
                    jedisPoolConfig.setTestOnBorrow(true);

                    jedisPool = new JedisSentinelPool("mymaster", sentinelSet,jedisPoolConfig);
                }
            }
        }

        return jedisPool;
    }
}
</string>

Redis集群

集群提供了什么好处

  • Redis集群实现了对Redis的水平扩容,实现多台服务器分担 客户端请求压力
  • Redis中的集群将会有一个 slots插槽值,集群中的每个节点都刚好能 存储数据的 1/N,这个N指的是节点的数量。而插槽slots则规定了整个集群能够存储数据的插槽范围, 每个节点都会有对应的那一段范围。 在存储一个key时,会计算该key对应的插槽位置, 存储到指定的节点。
  • 并且Redis为我们提供了 无中心化集群 配置,使得集群中的任何一个节点都能够访问到其他的节点,也就是客户端请求服务时,即便请求的服务是节点B提供的,访问节点A也可以获取到节点B的提供的服务。

制作6个实例,分别实现刚好能让他们一主一从,3个节点

  • (1)复制原先/conf/redis.conf 文件到 /opt/myredis/ 目录下
  • (2)修改该配置文件
  • (2.1)开启后台启动
  • (2.2)关闭ip绑定
  • (2.3)关闭保护模式
  • (3)创建6个配置文件,分别为redis6379.conf、redis6380.conf、redis6381.conf、redis6389.conf、redis6390.conf、redis6391.conf
  • (3.1)配置基本信息(pidfile、port、dump.rdb、log日志文件名、aof功能)
  • (3.2)配置集群信息(打开集群模式、设置节点配置文件名称、设置节点失联时间,超时自动进行主从切换)
  • 提示:可以使用 %s/6379/6380这样的命令来一次性替换
&#x5F15;&#x5165;redis&#x914D;&#x7F6E;&#x6587;&#x4EF6;
include /opt/myredis/redis.conf
&#x5F53;&#x524D;&#x670D;&#x52A1;&#x7684;&#x8FDB;&#x7A0B;id&#x5B58;&#x653E;&#x5730;&#x5740;
pidfile "/opt/myredis/redis6379.pid"
port 6379
rdb&#x5907;&#x4EFD;&#x6587;&#x4EF6;&#x540D;&#x79F0;
dbfilename "dump6379.rdb"
&#x540E;&#x53F0;&#x542F;&#x52A8;
daemonize yes
&#x5173;&#x95ED;&#x4FDD;&#x62A4;&#x6A21;&#x5F0F;
protected-mode no
&#x5F00;&#x542F;&#x96C6;&#x7FA4;&#x529F;&#x80FD;
cluster-enabled yes
&#x8BBE;&#x7F6E;&#x96C6;&#x7FA4;&#x7684;&#x8282;&#x70B9;&#x914D;&#x7F6E;&#x6587;&#x4EF6;&#x540D;
cluster-config-file nodes-6379.conf
&#x914D;&#x7F6E;&#x8282;&#x70B9;&#x5931;&#x8054;&#x65F6;&#x95F4;&#xFF0C;&#x8FD9;&#x91CC;&#x662F;&#x6BEB;&#x79D2;&#xFF0C;&#x6362;&#x7B97;&#x6210;&#x79D2;&#x5C31;&#x662F;15&#x79D2;
cluster-node-timeout 15000

启动6个redis服务并查看进程

redis-server redis6379.conf
redis-server redis6380.conf
redis-server redis6381.conf
redis-server redis6389.conf
redis-server redis6390.conf
redis-server redis6391.conf

JAVA入门基础_从零开始的培训_Redis

将6个节点合并成一个集群

  • 进入到之前解压redis的目录: cd /opt/redis-6.2.1/src/
  • 执行如下命令
--replicas 1&#x91C7;&#x7528;&#x6700;&#x7B80;&#x5355;&#x7684;&#x65B9;&#x5F0F;&#x914D;&#x7F6E;&#x96C6;&#x7FA4;
redis-cli --cluster create --cluster-replicas 1 192.168.22.100:6379 192.168.22.100:6380 192.168.22.100:6381 192.168.22.100:6389 192.168.22.100:6390 192.168.22.100:6391

中途会有点提示,询问是否按照默认分配的主机与从机进行集群配置。

JAVA入门基础_从零开始的培训_Redis
JAVA入门基础_从零开始的培训_Redis
  • 最后还能看到插槽的数量是16384个,下面最大是16383是因为,插槽是从0开始算的
  • 6379这个节点的插槽是:0~5460
  • 6380节点:5461~10922
  • 6381节点:10923~16383

连接客户端开始使用(采用集群方式连接)

  • 如果不采用集群方式连接,存储数据时,如果key计算的插槽不在当前节点,则会导致出错
    JAVA入门基础_从零开始的培训_Redis
  • 因此应该采用集群的方式连接客户端
    redis-cli -c -p 6379
  • 此时由于 无中心化集群配置的原因,设置key和获取key对应的值时, *都会根据key所对应的插槽使当前连接的客户端连接到指定节点

集群的故障恢复以及相关配置

  • 综上的配置,当前的集群一共有3个主机,3个从机
  • 如果一个主机宕机,那么15秒内从机将会晋升为主机,当主机再次上线时,就变成了从机
  • redis的配置文件中有如下配置
&#x5982;&#x679C;&#x8BE5;&#x914D;&#x7F6E;&#x8BBE;&#x7F6E;&#x4E3A;yes&#xFF0C;&#x90A3;&#x4E48;&#x5982;&#x679C;&#x67D0;&#x4E2A;&#x8282;&#x70B9;&#x7684;&#x4E3B;&#x673A;&#x548C;&#x4ECE;&#x673A;&#x5168;&#x90E8;&#x6302;&#x6389;&#x65F6;&#xFF0C;&#x6574;&#x4E2A;&#x96C6;&#x7FA4;&#x90FD;&#x4F1A;&#x76F4;&#x63A5;&#x6302;&#x6389;
&#x5982;&#x679C;&#x8BBE;&#x7F6E;&#x4E3A;no&#xFF0C;&#x90A3;&#x4E48;&#x5269;&#x4E0B;&#x7684;&#x4E3B;&#x4ECE;&#x8282;&#x70B9;&#x5C06;&#x4F1A;&#x7EE7;&#x7EED;&#x63D0;&#x4F9B;&#x5B83;&#x4EEC;&#x63D2;&#x69FD;&#x8303;&#x56F4;&#x5185;&#x7684;&#x670D;&#x52A1;
cluster-require-full-coverage yes

Jedis的集群开发

  • 使用JedisCluster工具来完成集群操作(别忘了关闭Linux的防火墙,或者开放端口)
public class JedisClusterTest {
    public static void main(String[] args) {
        //  &#x6CE8;&#x610F;&#xFF1A;&#x8BBF;&#x95EE;&#x4EFB;&#x4F55;&#x4E00;&#x4E2A;&#x8282;&#x70B9;&#x90FD;&#x662F;&#x53EF;&#x4EE5;&#x7684;&#xFF0C;&#x56E0;&#x4E3A;&#x662F;&#x65E0;&#x4E2D;&#x5FC3;&#x5316;&#x96C6;&#x7FA4;
        JedisCluster jedisCluster = new JedisCluster(
                new HostAndPort("192.168.22.100", 6391));

        jedisCluster.set("k3", "wangming");

        String result = jedisCluster.get("k3");

        System.out.println(result);
    }
}

集群的好处和不足

  • 好处
  • 减轻了单台服务器的压力
  • 实现扩容
  • 无中心化配置比较简单
  • 不足
  • 多键操作是不被支持的,虽然可以靠着分组完成,但是并不方便
  • 多键的Redis事务是不被支持的,lua脚本不被支持

Redis应用问题及解决方案

缓存穿透

  • 问题描述
  • (1)服务器的压力突然剧增
  • (2)访问的接口全部都无法靠缓存处理, key对应的数据源并不存在
  • (3)访问的请求甚至是无效请求
  • (4)一般遇到这种情况都是黑客攻击,可以考虑直接报网警
  • 解决方案
  • (1) 对空值进行缓存。如果查询返回的数据为null,将其缓存到redis数据库中,设置极短的过期时间,一般不超过5分钟(应急方案)
  • (2) 设置可以访问的白名单。使用bitMaps定义一个可以访问的名单,名单id作为bitMaps的偏移量,每次访问时都通过bitMaps来查询是否在白名单范围内
  • (3)采用布隆过滤器
  • (4) 进行实时监控,当发现Redis的缓存命中率急剧下降时,需要排查访问对象,与运维人员配合。
  • (5)直接报警。

缓存击穿

  • 问题描述
  • (1)服务器的压力突然增加
  • (2)此时缓存服务器中的某个热门key突然过期了
  • (3)而大量的请求都在访问这个热门key,最终导致大量请求都在一瞬间访问服务器,导致服务器压力过大直接宕机。
  • 解决方案
  • (1)预先设置好一些热门数据
  • (2)实时调整:现场监控哪些热门数据,实时调整key的过期时间
  • (3)使用锁的方式。
    • 当缓存失效时,不是立即去访问服务器
    • 使用某些操作成功会带返回值的操作,例如redis的setnx,来设置一个互斥的key
    • 当操作返回成功的时候,再进行访问服务器的操作,并回头设置缓存,最后删除互斥key
    • 当操作返回失败,证明已经有线程正在load db,可以当线程睡眠一会后直接get整个缓存

缓存雪崩

  • 问题描述
  • (1)服务器的压力突然增加
  • (2)缓存服务器中的多个key在同一时间过期
  • (3)而刚好在这时有大量的客户端请求发送过来,并且大部分都没有办法命中缓存
  • (4)因此全都会去访问服务器,最终服务器不堪重负宕机
  • 解决方案
  • (1)构建多级缓存结构: nginx缓存 + redis缓存 + 其他缓存(ehcache等)
  • (2)使用锁或队列:不适用大量并发情况
  • (3)设置过期标识更新缓存
    • 记录缓存是否过期(设置提前量),如果过期会触发通知另外的线程完成对缓存的更新。
  • (4)将缓存的过期时间分散开来。
    • 比如我们可以在原有缓存的失效时间基础上添加一个随机值,比如1~5分钟,这样就可以减少大量的缓存在同一时间过期的概率。

分布式锁

  • 指的是在不同的机器提供的服务,需要实现一把锁对所有机器提供服务的控制,为了解决这个问题,就需要一种技术来实现跨JVM的互斥机制来控制 共享资源的访问。上锁之后对所有机器都有效,这就叫做分布式锁
  • 解决方案
  • (1)基于数据库实现分布式锁
  • (2)基于缓存(Redis等)
  • (3)给予Zookeeper
  • 优缺点
  • (1)性能: redis性能最高
  • (2)可靠性: Zookeeper可靠性最高

使用Redis实现分布式锁

redis命令: # set sku:1:info "OK" NX PX 10000

  • EX second&#xFF1A;设置键的过期时间为秒
  • PX millisecond:设置键的过期时间为毫秒
  • NX:只有当键不存在时,才能对键进行操作
  • XX :只有当键已经存在时,才对键进行操作

实现redis中num数字的增加,靠setnx完成

@RestController
public class NumRedisLockController {
    @Autowired
    private RedisTemplate redisTemplate;

    // 1&#x3001;&#x538B;&#x529B;&#x6D4B;&#x8BD5;&#x524D;&#xFF1A; &#x5148;&#x5728;redis&#x5F53;&#x4E2D;&#x8BBE;&#x7F6E;string&#x7C7B;&#x578B;&#x7684; num = 0
    @RequestMapping("numLockTest")
    public void numLockTest() {
        // 2&#x3001;&#x4E0A;&#x9501;
        if (redisTemplate.opsForValue().setIfAbsent("numLock", "ok")){
            // &#x6267;&#x884C;num + 1
            redisTemplate.opsForValue().increment("num");
            // 3&#x3001;&#x89E3;&#x9501;
            redisTemplate.delete("numLock");
        }else {
            try { TimeUnit.NANOSECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace(); }
            // &#x91CD;&#x8BD5;
            numLockTest();
        }
    }
}
优化之添加锁的过期时间
  • 为什么要这么做?
  • 因为如果一个线程上了锁之后,在里面发生了异常,可能最终导致都无法解锁,那么其他线程将会一直等待。
  • 实现代码
@RestController
public class NumRedisLockController {
    @Autowired
    private RedisTemplate redisTemplate;

    // 1&#x3001;&#x538B;&#x529B;&#x6D4B;&#x8BD5;&#x524D;&#xFF1A; &#x5148;&#x5728;redis&#x5F53;&#x4E2D;&#x8BBE;&#x7F6E;string&#x7C7B;&#x578B;&#x7684; num = 0
    @RequestMapping("numLockTest")
    public void numLockTest() {
        // 2&#x3001;&#x4E0A;&#x9501; &#xFF0C;&#x8BBE;&#x7F6E;&#x8FC7;&#x671F;&#x65F6;&#x95F4;
        if (redisTemplate.opsForValue().setIfAbsent("numLock", "ok", 1, TimeUnit.SECONDS)){
            // &#x6267;&#x884C;num + 1
            redisTemplate.opsForValue().increment("num");
            // 3&#x3001;&#x89E3;&#x9501;
            redisTemplate.delete("numLock");
        }else {
            try { TimeUnit.NANOSECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace(); }
            // &#x91CD;&#x8BD5;
            numLockTest();
        }
    }
}
优化之添加UUID防止误删除
  • 为什么会出现这种问题
  • 线程A刚要执行解锁操作时,锁的过期时间到了,导致锁过期了。
  • 这个时候如果线程B拿到锁进入了方法体,将会导致A释放了线程B的锁。
  • 实现方案
@RestController
public class NumRedisLockController {
    @Autowired
    private RedisTemplate redisTemplate;

    // 1&#x3001;&#x538B;&#x529B;&#x6D4B;&#x8BD5;&#x524D;&#xFF1A; &#x5148;&#x5728;redis&#x5F53;&#x4E2D;&#x8BBE;&#x7F6E;string&#x7C7B;&#x578B;&#x7684; num = 0
    @RequestMapping("numLockTest")
    public void numLockTest() {
        // &#x83B7;&#x53D6;UUID
        String uuid = UUID.randomUUID().toString();
        // 2&#x3001;&#x4E0A;&#x9501; &#xFF0C;&#x8BBE;&#x7F6E;&#x8FC7;&#x671F;&#x65F6;&#x95F4;
        if (redisTemplate.opsForValue().setIfAbsent("numLock", uuid, 1, TimeUnit.SECONDS)){
            // &#x6267;&#x884C;num + 1
            redisTemplate.opsForValue().increment("num");
            // 3&#x3001;&#x89E3;&#x9501;
            if (uuid.equals((String)redisTemplate.opsForValue().get("numLock"))) {
                redisTemplate.delete("numLock");
            }
        }else {
            try { TimeUnit.NANOSECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace(); }
            // &#x91CD;&#x8BD5;
            numLockTest();
        }
    }
}
优化之使用lua脚本保证的原子性
  • 问题分析
  • 线程A判断成功了当前锁对应的value值确实是自己的uuid
  • 刚准备执行删除操作的时候,锁过期时间到了
  • 与此同时,线程B拿到了锁
  • 此时线程A将会把线程B的锁给释放。
  • 因此得出结论,需要让进行删除标识判断以及删除的操作保持原子性
  • 创建一个lua脚本
if redis.call('get', KEYS[1]) == ARGV[1]
then
return redis.call('del', KEYS[1])
else
return 0
end
  • 使用示例
@RestController
public class NumRedisLockController {
    @Autowired
    private RedisTemplate redisTemplate;

    // 1&#x3001;&#x538B;&#x529B;&#x6D4B;&#x8BD5;&#x524D;&#xFF1A; &#x5148;&#x5728;redis&#x5F53;&#x4E2D;&#x8BBE;&#x7F6E;string&#x7C7B;&#x578B;&#x7684; num = 0
    @RequestMapping("numLockTest")
    public void numLockTest() {
        // &#x83B7;&#x53D6;UUID
        String uuid = UUID.randomUUID().toString();

        String lockKey = "numLock";

        // &#x83B7;&#x53D6;&#x9501;
        Boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, 1, TimeUnit.SECONDS);

        if (lock) {
            // &#x4F7F;num &#x6BCF;&#x6B21;+1 &#x653E;&#x5165;&#x7F13;&#x5B58;
            redisTemplate.opsForValue().increment("num");
            /*&#x4F7F;&#x7528;lua&#x811A;&#x672C;&#x6765;&#x9501;*/
            // &#x5B9A;&#x4E49;lua &#x811A;&#x672C;
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

            // &#x521B;&#x5EFA;&#x4E00;&#x4E2A;Redis&#x811A;&#x672C;&#x6587;&#x4EF6;
            DefaultRedisScript<long> redisScript = new DefaultRedisScript<>();
            redisScript.setScriptText(script);
            // &#x8BBE;&#x7F6E;&#x4E00;&#x4E0B;&#x8FD4;&#x56DE;&#x503C;&#x7C7B;&#x578B; &#x4E3A;Long &#xFF0C;&#x6307;&#x7684;&#x662F;redisTemplate.execute&#x8FD4;&#x56DE;&#x7684;&#x5B9E;&#x9645;&#x7C7B;&#x578B;
            redisScript.setResultType(Long.class);

            // &#x7B2C;&#x4E00;&#x4E2A;&#x8981;&#x662F;script &#x811A;&#x672C; &#xFF0C;&#x7B2C;&#x4E8C;&#x4E2A;&#x9700;&#x8981;&#x5224;&#x65AD;&#x7684;key&#xFF0C;&#x7B2C;&#x4E09;&#x4E2A;&#x5C31;&#x662F;key&#x6240;&#x5BF9;&#x5E94;&#x7684;&#x503C;&#x3002;
            Long execute = (Long)redisTemplate.execute(redisScript, Arrays.asList(lockKey), uuid);
            System.out.println(execute == 1 ? "&#x6267;&#x884C;&#x6210;&#x529F;": "&#x6267;&#x884C;&#x5931;&#x8D25;");

        }else {
            try { TimeUnit.NANOSECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace(); }
            // &#x91CD;&#x8BD5;
            numLockTest();
        }
    }
}
</long>

内存淘汰策略

8种淘汰策略(分为两类,一类为设置了过期时间的,一类为没有设置过期时间的)

  • 1、noeviction
    第一种淘汰策略是 noeviction,它是 Redis 的默认策略。在内存超过阀值后,Redis 不做任何清理工作,然后对所有写操作返回错误,但对读请求正常处理。noeviction 适合数据量不大的业务场景,将关键数据存入 Redis 中,将 Redis 当作 DB 来使用。
  • 2、 volatile-lru
    第二种淘汰策略是 volatile-lru,它对带过期时间的 key 采用最近最少访问算法来淘汰。使用这种策略,Redis 会从 redisDb 的 expire dict 过期字典中,首先随机选择 N 个 key,计算 key 的空闲时间,然后插入 evictionPool 中,最后选择空闲时间最久的 key 进行淘汰。这种策略适合的业务场景是,需要淘汰的key带有过期时间,且有冷热区分,从而可以淘汰最久没有访问的key。
  • 3、volatile-lfu
    第三种策略是 volatile-lfu,它对带过期时间的 key 采用最近最不经常使用的算法来淘汰。使用这种策略时,Redis 会从 redisDb 中的 expire dict 过期字典中,首先随机选择 N 个 key,然后根据其 value 的 lru 值,计算 key 在一段时间内的使用频率相对值。对于 lfu,要选择使用频率最小的 key,为了沿用 evictionPool 的 idle 概念,Redis 在计算 lfu 的 Idle 时,采用 255 减去使用频率相对值,从而确保 Idle 最大的 key 是使用次数最小的 key,计算 N 个 key 的 Idle 值后,插入 evictionPool,最后选择 Idle 最大,即使用频率最小的 key,进行淘汰。这种策略也适合大多数 key 带过期时间且有冷热区分的业务场景。
  • 4、volatile-ttl
    第四种策略是 volatile-ttl,它是对带过期时间的 key 中选择最早要过期的 key 进行淘汰。使用这种策略时,Redis 也会从 redisDb 的 expire dict 过期字典中,首先随机选择 N 个 key,然后用最大无符号 long 值减去 key 的过期时间来作为 Idle 值,计算 N 个 key 的 Idle 值后,插入evictionPool,最后选择 Idle 最大,即最快就要过期的 key,进行淘汰。这种策略适合,需要淘汰的key带过期时间,且有按时间冷热区分的业务场景。
  • 5、volatile-random
    第五种策略是 volatile-random,它是对带过期时间的 key 中随机选择 key 进行淘汰。使用这种策略时,Redis 从 redisDb 的 expire dict 过期字典中,随机选择一个 key,然后进行淘汰。如果需要淘汰的key有过期时间,没有明显热点,主要被随机访问,那就适合选择这种淘汰策略。
  • 6、allkey-lru
    第六种策略是 allkey-lru,它是对所有 key,而非仅仅带过期时间的 key,采用最近最久没有使用的算法来淘汰。这种策略与 volatile-lru 类似,都是从随机选择的 key 中,选择最长时间没有被访问的 key 进行淘汰。区别在于,volatile-lru 是从 redisDb 中的 expire dict 过期字典中选择 key,而 allkey-lru 是从所有的 key 中选择 key。这种策略适合,需要对所有 key 进行淘汰,且数据有冷热读写区分的业务场景。
  • 7、allkeys-lfu
    第七种策略是 allkeys-lfu,它也是针对所有 key 采用最近最不经常使用的算法来淘汰。这种策略与 volatile-lfu 类似,都是在随机选择的 key 中,选择访问频率最小的 key 进行淘汰。区别在于,volatile-flu从expire dict 过期字典中选择 key,而 allkeys-lfu 是从主 dict 中选择 key。这种策略适合的场景是,需要从所有的 key 中进行淘汰,但数据有冷热区分,且越热的数据访问频率越高。
  • 8、allkeys-random
    第八种策略是 allkeys-random,它是针对所有 key 进行随机算法进行淘汰。它也是从主 dict 中随机选择 key,然后进行删除回收。如果需要从所有的 key 中进行淘汰,并且 key 的访问没有明显热点,被随机访问,即可采用这种策略。

配置redis内存淘汰策略

  • 设置Redis 内存大小的限制,我们可以设置maxmemory
  • 在配置文件中搜索maxmemory-policy,设置Redis的淘汰策略
    JAVA入门基础_从零开始的培训_Redis

SpringBoot整合redis 并整合Spring提供的CacheManager

引入依赖并修改依赖

<!--redis依赖-->
<dependency>
    <groupid>org.springframework.boot</groupid>
    <artifactid>spring-boot-starter-data-redis</artifactid>
</dependency>
  • 修改application.yml配置
spring:
  redis:
    port: 6379
    host: 192.168.50.10

添加一个redis配置类(使用的时候可以使用@Resource注入)

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.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import org.springframework.cache.CacheManager;
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.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

@Configuration
public class RedisConfig {
    @Bean
    public Jackson2JsonRedisSerializer<object> jackson(){
        Jackson2JsonRedisSerializer<object>  jackson2Serializer =
                new Jackson2JsonRedisSerializer<>(Object.class);
        //&#x5E8F;&#x5217;&#x5316;&#x65F6;&#x6DFB;&#x52A0;&#x5BF9;&#x8C61;&#x4FE1;&#x606F;&#xFF1A;&#x9632;&#x6B62;&#x51FA;&#x73B0;&#x628A;Object&#x8F6C;&#x6362;&#x6210;LinkedHashMap
        ObjectMapper om = new ObjectMapper();
        /*
        &#x6307;&#x5B9A;&#x5E8F;&#x5217;&#x5316;&#x7684;&#x5C5E;&#x6027;&#xFF0C;
            PropertyAccessor.ALL&#xFF1A;&#x8868;&#x793A;&#x5C5E;&#x6027;&#x3001;get&#x548C;set
            JsonAutoDetect.Visibility.ANY:&#x8868;&#x793A;&#x5305;&#x62EC;private&#x548C;public
        */
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        /*
        &#x6307;&#x5B9A;&#x5E8F;&#x5217;&#x5316;&#x8F93;&#x5165;&#x7C7B;&#x578B;&#xFF0C;&#x4E5F;&#x5C31;&#x662F;&#x503C;&#x7C7B;&#x578B;
        */
        om.activateDefaultTyping(
            //&#x53EA;&#x5E8F;&#x5217;&#x5316;&#x975E;final&#x7684;&#x5C5E;&#x6027;&#xFF08;&#x4F8B;&#x5982;&#x4E0D;&#x4F1A;&#x628A;String,Integer&#x7B49;&#x7C7B;&#x578B;&#x7684;&#x6570;&#x636E;&#x6309;&#x7167;&#x6211;&#x4EEC;&#x7684;&#x5E8F;&#x5217;&#x5316;&#x65B9;&#x5F0F;&#xFF09;
            LaissezFaireSubTypeValidator.instance ,
            /*&#x5BF9;&#x4E8E;&#x9664;&#x4E86;&#x4E00;&#x4E9B;&#x81EA;&#x7136;&#x7C7B;&#x578B;(String&#x3001;Double&#x3001;Integer&#x3001;Double)&#x7C7B;&#x578B;&#x5916;&#x7684;&#x975E;&#x5E38;&#x91CF;(non-final)&#x7C7B;&#x578B;&#xFF0C;&#x7C7B;&#x578B;&#x5C06;&#x4F1A;&#x7528;&#x5728;&#x503C;&#x7684;&#x542B;&#x4E49;&#x4E0A;&#x3002;&#x4EE5;&#x4FBF;&#x53EF;&#x4EE5;&#x5728;JSON&#x4E32;&#x4E2D;&#x6B63;&#x786E;&#x7684;&#x63A8;&#x6D4B;&#x51FA;&#x503C;&#x6240;&#x5C5E;&#x7684;&#x7C7B;&#x578B;&#x3002;*/
            ObjectMapper.DefaultTyping.NON_FINAL,
            //&#x5C06;&#x591A;&#x6001;&#x4FE1;&#x606F;&#x4F5C;&#x4E3A;&#x6570;&#x636E;&#x7684;&#x5144;&#x5F1F;&#x5C5E;&#x6027;&#x8FDB;&#x884C;&#x5E8F;&#x5217;&#x5316;
            JsonTypeInfo.As.PROPERTY);
         //&#x5BF9;&#x8C61;&#x5C5E;&#x6027;&#x4E3A;&#x7A7A;&#x65F6;&#xFF0C;&#x4E0D;&#x8FDB;&#x884C;&#x5E8F;&#x5217;&#x5316;&#x50A8;&#x5B58;
        om.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        //&#x628A;long&#x7C7B;&#x578B;&#x7684;id&#x5DF2;&#x5B57;&#x7B26;&#x4E32;&#x5F62;&#x5F0F;&#x683C;&#x5F0F;&#x5316;
        SimpleModule simpleModule = new SimpleModule();
        simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
        simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
        om.registerModule(simpleModule);

        //&#x683C;&#x5F0F;&#x5316;&#x65E5;&#x671F;java.util.date
        // om.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

        // &#x5904;&#x7406;java8&#x65E5;&#x671F;&#x683C;&#x5F0F;&#x7684;&#x6A21;&#x5757;
        JavaTimeModule javaTimeModule = getTimeModule();
        om.registerModule(javaTimeModule);

        // &#x5982;&#x679C;&#x6709;&#x672A;&#x77E5;&#x5C5E;&#x6027;&#xFF0C;&#x5219;&#x76F4;&#x63A5;&#x4E0D;&#x53CD;&#x89E3;&#x6790;&#x672A;&#x77E5;&#x5C5E;&#x6027;&#x5230;&#x5B9E;&#x4F53;&#x7C7B;&#x4E2D;&#x5373;&#x53EF;&#xFF0C;&#x4E0D;&#x629B;&#x51FA;&#x5F02;&#x5E38;&#x3002;
        om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

        jackson2Serializer.setObjectMapper(om);
        return jackson2Serializer;
    }

    /**
     * &#x5904;&#x7406;java8&#x4E2D;&#xFF0C;&#x65F6;&#x95F4;&#x65E5;&#x671F;&#x683C;&#x5F0F;&#x7684;Module
     * @return
     */
    private JavaTimeModule getTimeModule() {
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        String STANDARD_PATTERN = "yyyy-MM-dd HH:mm:ss";
        String DATE_PATTERN = "yyyy-MM-dd";
        String TIME_PATTERN = "HH:mm:ss";
        //&#x5904;&#x7406;LocalDateTime
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(STANDARD_PATTERN);
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(dateTimeFormatter));
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(dateTimeFormatter));

        //&#x5904;&#x7406;LocalDate
        DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(DATE_PATTERN);
        javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(dateFormatter));
        javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(dateFormatter));

        //&#x5904;&#x7406;LocalTime
        DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern(TIME_PATTERN);
        javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(timeFormatter));
        javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(timeFormatter));
        return javaTimeModule;
    }

    /**
     * &#x4E3B;&#x8981;&#x914D;&#x7F6E;&#x4E86;redis&#x5728;&#x5E8F;&#x5217;&#x5316;&#x4E0E;&#x53CD;&#x5E8F;&#x5217;&#x5316;&#x65F6;&#x7684;&#x4E00;&#x4E2A;&#x5E8F;&#x5217;&#x5316;&#x5668;
     * @param redisConnectionFactory
     * @param sedisSerializer
     * @return
     */
    @Bean
    public RedisTemplate<string,object> redisTemplate(RedisConnectionFactory redisConnectionFactory,Jackson2JsonRedisSerializer<object> sedisSerializer){
        RedisTemplate<string, object> redisTemplate = new RedisTemplate();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        // &#x521B;&#x5EFA;key&#x7684;&#x5E8F;&#x5217;&#x5316;&#x65B9;&#x5F0F;
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // &#x6307;&#x5B9A;string&#x7C7B;&#x578B;&#x7684;key&#x7684;&#x5E8F;&#x5217;&#x5316;&#x65B9;&#x5F0F;: string
        redisTemplate.setKeySerializer(stringRedisSerializer);
        // hash&#x7C7B;&#x578B;key&#x7684;&#x5E8F;&#x5217;&#x5316;&#x65B9;&#x5F0F;
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        // &#x521B;&#x5EFA;value&#x7684;&#x5E8F;&#x5217;&#x5316;&#x65B9;&#x5F0F;
        // &#x6307;&#x5B9A;string&#x7C7B;&#x578B;&#x7684;value&#x7684;&#x5E8F;&#x5217;&#x5316;&#x65B9;&#x5F0F;:json
        redisTemplate.setValueSerializer(sedisSerializer);
        // hash&#x7C7B;&#x578B;value&#x7684;&#x5E8F;&#x5217;&#x5316;&#x65B9;&#x5F0F;
        redisTemplate.setHashValueSerializer(sedisSerializer);
        return redisTemplate;
    }
    /**
     * &#x6307;&#x5B9A;&#x4F7F;&#x7528;redis&#x8FDB;&#x884C;&#x7F13;&#x5B58;&#xFF0C;
     * &#x7ED9;&#x5BB9;&#x5668;&#x6CE8;&#x518C;&#x4E00;&#x4E2A;Bean&#xFF0C;&#x8FD4;&#x56DE;&#x7F13;&#x5B58;&#x7BA1;&#x7406;&#x5668;,&#x5982;&#x679C;&#x5BB9;&#x5668;&#x4E2D;&#x6709;redisTemplate&#xFF0C;&#x4F1A;&#x81EA;&#x52A8;&#x6CE8;&#x5165;
     * @return
     */
    @Bean
    public CacheManager MyRedisCacheConfig(RedisConnectionFactory redisConnectionFactory,Jackson2JsonRedisSerializer<object> sedisSerializer) {
        //1.&#x521B;&#x5EFA;RedisCacheWriter
        /**
         * &#x975E;&#x9501;&#x65B9;&#x5F0F;&#xFF1A;nonLockingRedisCacheWriter(RedisConnectionFactory connectionFactory);
         * &#x6709;&#x9501;&#x65B9;&#x5F0F;&#xFF1A;lockingRedisCacheWriter(RedisConnectionFactory connectionFactory);
         */
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
        //3.&#x4F20;&#x5165; Jackson&#x5BF9;&#x8C61; &#x5E76;&#x83B7;&#x53D6; RedisSerializationContext&#x5BF9;&#x8C61;
        RedisSerializationContext<object, object> serializationContext = RedisSerializationContext.fromSerializer(sedisSerializer);
        //4.&#x914D;&#x7F6E;RedisCacheConfiguration
        /**
         * RedisCacheConfiguration.defaultCacheConfig()
         * &#x8BBE;&#x7F6E; value &#x7684;&#x5E8F;&#x5217;&#x5316; serializeValuesWit(SerializationPari<?> valueSerializationPari)
         * &#x8BBE;&#x7F6E; key &#x7684;&#x5E8F;&#x5217;&#x5316; serializeKeysWith(SerializationPari valueSerializationPari)
         */
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(serializationContext.getValueSerializationPair());
        return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
    }
}
</object,></object></string,></object></string,object></object></object>

非常值得注意的一件事,Jackson解析与反解析是根据实体类中的什么来决定的

  • 需要被序列化的类必须实现Serializable接口
  • 进行序列化时,不仅会根据get方法,还会根据任何有返回值的方法的返回结果进行序列化
  • 而在反序列化时,只会根据set方法,如果没有指定的set方法完成反序列化,则会报错
  • 因此需要添加如下配置
        // &#x5982;&#x679C;&#x6709;&#x672A;&#x77E5;&#x5C5E;&#x6027;&#xFF0C;&#x5219;&#x76F4;&#x63A5;&#x4E0D;&#x53CD;&#x89E3;&#x6790;&#x672A;&#x77E5;&#x5C5E;&#x6027;&#x5230;&#x5B9E;&#x4F53;&#x7C7B;&#x4E2D;&#x5373;&#x53EF;&#xFF0C;&#x4E0D;&#x629B;&#x51FA;&#x5F02;&#x5E38;&#x3002;
        om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

添加一个Redis的工具类

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.TimeUnit;

@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
    @Autowired
    public RedisTemplate redisTemplate;

    /**
     * &#x7F13;&#x5B58;&#x57FA;&#x672C;&#x7684;&#x5BF9;&#x8C61;&#xFF0C;Integer&#x3001;String&#x3001;&#x5B9E;&#x4F53;&#x7C7B;&#x7B49;
     *
     * @param key &#x7F13;&#x5B58;&#x7684;&#x952E;&#x503C;
     * @param value &#x7F13;&#x5B58;&#x7684;&#x503C;
     */
    public <t> void setCacheObject(final String key, final T value)
    {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * &#x7F13;&#x5B58;&#x57FA;&#x672C;&#x7684;&#x5BF9;&#x8C61;&#xFF0C;Integer&#x3001;String&#x3001;&#x5B9E;&#x4F53;&#x7C7B;&#x7B49;
     *
     * @param key &#x7F13;&#x5B58;&#x7684;&#x952E;&#x503C;
     * @param value &#x7F13;&#x5B58;&#x7684;&#x503C;
     * @param timeout &#x65F6;&#x95F4;
     * @param timeUnit &#x65F6;&#x95F4;&#x9897;&#x7C92;&#x5EA6;
     */
    public <t> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
    {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    /**
     * &#x8BBE;&#x7F6E;&#x6709;&#x6548;&#x65F6;&#x95F4;
     *
     * @param key Redis&#x952E;
     * @param timeout &#x8D85;&#x65F6;&#x65F6;&#x95F4;
     * @return true=&#x8BBE;&#x7F6E;&#x6210;&#x529F;&#xFF1B;false=&#x8BBE;&#x7F6E;&#x5931;&#x8D25;
     */
    public boolean expire(final String key, final long timeout)
    {
        return expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * &#x8BBE;&#x7F6E;&#x6709;&#x6548;&#x65F6;&#x95F4;
     *
     * @param key Redis&#x952E;
     * @param timeout &#x8D85;&#x65F6;&#x65F6;&#x95F4;
     * @param unit &#x65F6;&#x95F4;&#x5355;&#x4F4D;
     * @return true=&#x8BBE;&#x7F6E;&#x6210;&#x529F;&#xFF1B;false=&#x8BBE;&#x7F6E;&#x5931;&#x8D25;
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit)
    {
        return redisTemplate.expire(key, timeout, unit);
    }

    /**
     * &#x83B7;&#x5F97;&#x7F13;&#x5B58;&#x7684;&#x57FA;&#x672C;&#x5BF9;&#x8C61;&#x3002;
     *
     * @param key &#x7F13;&#x5B58;&#x952E;&#x503C;
     * @return &#x7F13;&#x5B58;&#x952E;&#x503C;&#x5BF9;&#x5E94;&#x7684;&#x6570;&#x636E;
     */
    public <t> T getCacheObject(final String key)
    {
        ValueOperations<string, t> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    /**
     * &#x5220;&#x9664;&#x5355;&#x4E2A;&#x5BF9;&#x8C61;
     *
     * @param key
     */
    public boolean deleteObject(final String key)
    {
        return redisTemplate.delete(key);
    }

    /**
     * &#x5220;&#x9664;&#x96C6;&#x5408;&#x5BF9;&#x8C61;
     *
     * @param collection &#x591A;&#x4E2A;&#x5BF9;&#x8C61;
     * @return
     */
    public long deleteObject(final Collection collection)
    {
        return redisTemplate.delete(collection);
    }

    /**
     * &#x7F13;&#x5B58;List&#x6570;&#x636E;
     *
     * @param key &#x7F13;&#x5B58;&#x7684;&#x952E;&#x503C;
     * @param dataList &#x5F85;&#x7F13;&#x5B58;&#x7684;List&#x6570;&#x636E;
     * @return &#x7F13;&#x5B58;&#x7684;&#x5BF9;&#x8C61;
     */
    public <t> long setCacheList(final String key, final List<t> dataList)
    {
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }

    /**
     * &#x83B7;&#x5F97;&#x7F13;&#x5B58;&#x7684;list&#x5BF9;&#x8C61;
     *
     * @param key &#x7F13;&#x5B58;&#x7684;&#x952E;&#x503C;
     * @return &#x7F13;&#x5B58;&#x952E;&#x503C;&#x5BF9;&#x5E94;&#x7684;&#x6570;&#x636E;
     */
    public <t> List<t> getCacheList(final String key)
    {
        return redisTemplate.opsForList().range(key, 0, -1);
    }

    /**
     * &#x7F13;&#x5B58;Set
     *
     * @param key &#x7F13;&#x5B58;&#x952E;&#x503C;
     * @param dataSet &#x7F13;&#x5B58;&#x7684;&#x6570;&#x636E;
     * @return &#x7F13;&#x5B58;&#x6570;&#x636E;&#x7684;&#x5BF9;&#x8C61;
     */
    public <t> BoundSetOperations<string, t> setCacheSet(final String key, final Set<t> dataSet)
    {
        BoundSetOperations<string, t> setOperation = redisTemplate.boundSetOps(key);
        Iterator<t> it = dataSet.iterator();
        while (it.hasNext())
        {
            setOperation.add(it.next());
        }
        return setOperation;
    }

    /**
     * &#x83B7;&#x5F97;&#x7F13;&#x5B58;&#x7684;set
     *
     * @param key
     * @return
     */
    public <t> Set<t> getCacheSet(final String key)
    {
        return redisTemplate.opsForSet().members(key);
    }

    /**
     * &#x7F13;&#x5B58;Map
     *
     * @param key
     * @param dataMap
     */
    public <t> void setCacheMap(final String key, final Map<string, t> dataMap)
    {
        if (dataMap != null) {
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }

    /**
     * &#x83B7;&#x5F97;&#x7F13;&#x5B58;&#x7684;Map
     *
     * @param key
     * @return
     */
    public <t> Map<string, t> getCacheMap(final String key)
    {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * &#x5F80;Hash&#x4E2D;&#x5B58;&#x5165;&#x6570;&#x636E;
     *
     * @param key Redis&#x952E;
     * @param hKey Hash&#x952E;
     * @param value &#x503C;
     */
    public <t> void setCacheMapValue(final String key, final String hKey, final T value)
    {
        redisTemplate.opsForHash().put(key, hKey, value);
    }

    /**
     * &#x83B7;&#x53D6;Hash&#x4E2D;&#x7684;&#x6570;&#x636E;
     *
     * @param key Redis&#x952E;
     * @param hKey Hash&#x952E;
     * @return Hash&#x4E2D;&#x7684;&#x5BF9;&#x8C61;
     */
    public <t> T getCacheMapValue(final String key, final String hKey)
    {
        HashOperations<string, string, t> opsForHash = redisTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }

    /**
     * &#x5220;&#x9664;Hash&#x4E2D;&#x7684;&#x6570;&#x636E;
     *
     * @param key
     * @param hkey
     */
    public void delCacheMapValue(final String key, final String hkey)
    {
        HashOperations hashOperations = redisTemplate.opsForHash();
        hashOperations.delete(key, hkey);
    }

    /**
     * &#x83B7;&#x53D6;&#x591A;&#x4E2A;Hash&#x4E2D;&#x7684;&#x6570;&#x636E;
     *
     * @param key Redis&#x952E;
     * @param hKeys Hash&#x952E;&#x96C6;&#x5408;
     * @return Hash&#x5BF9;&#x8C61;&#x96C6;&#x5408;
     */
    public <t> List<t> getMultiCacheMapValue(final String key, final Collection<object> hKeys)
    {
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }

    /**
     * &#x83B7;&#x5F97;&#x7F13;&#x5B58;&#x7684;&#x57FA;&#x672C;&#x5BF9;&#x8C61;&#x5217;&#x8868;
     *
     * @param pattern &#x5B57;&#x7B26;&#x4E32;&#x524D;&#x7F00;
     * @return &#x5BF9;&#x8C61;&#x5217;&#x8868;
     */
    public Collection<string> keys(final String pattern)
    {
        return redisTemplate.keys(pattern);
    }
}
</string></object></t></t></string,></t></t></string,></t></string,></t></t></t></t></string,></t></string,></t></t></t></t></t></string,></t></t></t>

通过spring配置开启CacheManager,以及在需要进行缓存的ServiceImpl方法上,添加注解

  • 修改启动类
//&#x5F00;&#x542F;&#x7F13;&#x5B58;
@EnableCaching
  • 在service上添加@Cacheable注解
    JAVA入门基础_从零开始的培训_Redis

@Cacheable的常用属性以及sqEL编写key的元数据

  • Cacheable的常用属性
  • cacheNames/value :用来指定缓存组件的名字
  • key :缓存数据时使用的 key,可以用它来指定。默认是使用方法参数的值。(这个 key 你可以使用 spEL 表达式来编写)
  • keyGenerator :key 的生成器。 key 和 keyGenerator 二选一使用
  • cacheManager :可以用来指定缓存管理器。从哪个缓存管理器里面获取缓存。
  • condition :可以用来指定符合条件的情况下才缓存
  • unless :否定缓存。当 unless 指定的条件为 true ,方法的返回值就不会被缓存。当然你也可以获取到结果进行判断。(通过 #result 获取方法结果)
  • sync :是否使用异步模式。
  • cacheNames
    用来指定缓存组件的名字,将方法的返回结果放在哪个缓存中,可以是数组的方式,支持指定多个缓存。
    JAVA入门基础_从零开始的培训_Redis
  • key
    缓存数据时使用的key,默认使用的是方法参数的值。可以使用 spEL 表达式去编写。
    JAVA入门基础_从零开始的培训_Redis
  • keyGenerator(与key进行二选一)
    key 的生成器,可以自己指定 key 的生成器,通过这个生成器来生成 key。
    JAVA入门基础_从零开始的培训_Redis
    JAVA入门基础_从零开始的培训_Redis
  • condition
    符合条件的情况下才缓存
    JAVA入门基础_从零开始的培训_Redis
  • unless(刚好与condition相反)
    否定缓存。当 unless 指定的条件为 true ,方法的返回值就不会被缓存。
    JAVA入门基础_从零开始的培训_Redis
  • sync
    是否使用异步模式。默认是方法执行完,以同步的方式将方法返回的结果存在缓存中。
  • sqlEl编写key时的元数据
    JAVA入门基础_从零开始的培训_Redis

@CacheEvict

JAVA入门基础_从零开始的培训_Redis

@CachePut

JAVA入门基础_从零开始的培训_Redis

Original: https://www.cnblogs.com/itdqx/p/16688133.html
Author: code_Stars
Title: JAVA入门基础_从零开始的培训_Redis

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/594006/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 第三周

    第三周 1.测试成功的接口再次测试报错 原因:之前在查询时更改了方法,由Mybatis Plus 查询的方式改为了xml,同时在实体类中添加了字段做连表查询,导致之前所有用Myba…

    Java 2023年6月7日
    095
  • Mall商城的高级篇的开发(二)性能压测和性能监控

    Mall商城的高级篇的开发(二) 性能压测–压力测试 压力测试考察当前软件硬件环境下系统所能承受的最大负荷并帮助找出系统的瓶颈所在。压测都是为了系统在上线的处理能力和稳…

    Java 2023年6月5日
    098
  • Fork/Join框架

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    Java 2023年6月7日
    068
  • java AWT 图片查看器

    java AWT 图片查看器 1 package io.guanghe; 2 3 import java.awt.*; 4 import java.awt.event.*; 5 i…

    Java 2023年5月29日
    096
  • Kafka 消费者解析

    一、消费者相关概念 1.1 消费组&消费者 消费者: 消费者从订阅的主题消费消息,消费消息的偏移量保存在Kafka的名字是 __consumer_offsets的主题中 消…

    Java 2023年6月5日
    073
  • Java中ArrayList和LinkedList的区别

    在Java中虽然ArrayList和LinkedList都实现了List接口,但是其底层原理不相同。 ArrayList的底层是一个数组,LinkedList的底层是链表。 Arr…

    Java 2023年5月29日
    094
  • 2022-8-23 css

    ✏️CSS 一个标签可以有多个css样式浏览器处理冲突的能力,如果一个属性通过两个相同的选择器设置到这个元素上,会根据样式的层叠规则样式的层叠规则——按照样式的声明顺序来层叠的【就…

    Java 2023年6月13日
    082
  • 数据结构(C++)–学习单链表时发现的一些小坑

    #include<bits stdc++.h> using namespace std; const int MaxSize = 10; template <cl…

    Java 2023年6月7日
    082
  • HashMap源码,看我这篇就够了

    HashMap源码深度剖析 * HashMap底层数据结构(为什么引入红黑树、存储数据的过程、哈希碰撞相关问题) * HashMap成员变量(初始化容量是多少、负载因子、数组长度为…

    Java 2023年6月15日
    054
  • JAVA的环境配置以及多版本配置及详解

    一、概述 二、实践(以java8和java12为例,配置环境变量) 1.打开环境变量窗口 2.新建JAVA_HOME 3.修改Path 变量 4.新建CLASSPATH变量 5.验…

    Java 2023年6月15日
    063
  • Aop-Transaction事物

    事物注解:@Transactional 事物举例: sql: undefined DROP TABLE IF EXISTS account;CREATE TABLE account…

    Java 2023年6月5日
    069
  • 为什么不建议使用自定义Object作为HashMap的key?

    此前部门内的一个线上系统上线后内存一路飙高、一段时间后直接占满。协助开发人员去分析定位,发现内存中某个Object的量远远超出了预期的范围,很明显出现内存泄漏了。 结合代码分析发现…

    Java 2023年6月7日
    083
  • Javaweb学习-HTML

    重新开始HTML,之前学的都忘了 posted @2022-03-24 21:27 HelloHui 阅读(7 ) 评论() 编辑 Original: https://www.cn…

    Java 2023年6月9日
    090
  • 工作中常用Linux命令组合

    1、find命令查询当前路径递归下所有文件并展示 find ./ -type f -exec ls -l {} \; Original: https://www.cnblogs.c…

    Java 2023年6月5日
    072
  • 65.走散

    sdfsf posted @2022-09-28 08:37 随遇而安== 阅读(5 ) 评论() 编辑 Original: https://www.cnblogs.com/55z…

    Java 2023年6月7日
    097
  • 面试必问之 CopyOnWriteArrayList,你了解多少?

    一、摘要 在介绍 CopyOnWriteArrayList 之前,我们一起先来看看如下方法执行结果,代码内容如下: public static void main(String[]…

    Java 2023年6月9日
    096
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球