Redis学习

1.NoSQL数据库:

  1. 定义:
    NoSQL,表示“Not Only SQL”,泛指非关系型数据库,表示其数据的存储不是按照业务的逻辑来的,而是简单的以 key-value 的形式存储,因此加大了数据库的扩展性能。

  2. 作用:
    1.在分布式开发中,一个请求可能会被分配到不同的服务器中,但是用户在不同服务器中的session不能被共享。为解决这个问题:首先可以将session存在cookie中,但是这样安全性不是很好;然后可以复制session,但是这样会有很多的重复冗余;最优的解决办法是将数据存在nosql中,如果另一个服务器需要就直接在nosql中取出。来缓解cpu或内存的压力
    2.NoSQL可作为缓存数据库,减少io的读取,可以将一些经常被读取的数据存放进NoSQL中。

  3. 特色:
    不遵循SQL标准
    不支持ACID(但不代表不支持事务操作)
    性能高效

  4. 适用场景:
    对数据高并发的读写、海量数据的读写。

  5. 常用的NoSQL数据库:
    Redis、MongoDB

2.Redis

1.安装:

参考文章:https://www.linuxprobe.com/centos-redis.html

  • 先到官网进行下载:但是官网只提供了linux版本的,不提供Windows版本的
  • 下载后使用xftp,将下载好的压缩包传到linux虚拟机中。
  • 使用命令对压缩包进行解压,再将配置文件移动到一个方便的目录下,并修改配置文件中的 daemxxx 将 false 改为 true
  • 启动 redis,命令: redis-server /etc/redis.conf (加上后面的conf就是后台启动,是推荐的启动方式;如果只写 redis-server 则是前台启动,但是一旦关闭了终端后程序也关闭了,所以不推荐使用这种方法 )
  • 连接客户端,命令:redis-cli
  • 关闭 redis,命令:shutdown
    在这里插入图片描述
  1. 特点:
    1.默认端口号为6379。
    2.redis有16个数据库,下标从0开始,初始默认使用0号库。
    3.数据是存在内存中的
    4.与最原始的NoSQL数据库 memcached 相比,redis 支持多数据类型,支持持久化,是单线程+多路IO复用的模式。
    (多路IO复用的理解:就像黄牛卖票,多个人找黄牛买票,黄牛买票是一个单线程的操作,但是他可以在多个时间内对接不同的客户实现多路复用)
    在这里插入图片描述

2.1 常用数据类型:

  1. 对key的操作:
    keys * : 展示所有的key (如果没有key则显示 empty)
    set key value:添加key
    在这里插入图片描述
    exists key:查看key是否存在,存在为 1,不存在为 0
    type key:查看key的类型
    expire key time :为key设置存活时间,其中 time 的单位是秒
    ttl key:查看key的剩余存活时间:-1为永久存活 -2为已过期
    del key:删除指定的key数据
    unlink key:也是删除,但是是非阻塞删除,效果也是一样的,但是实际上真正的删除是在后续异步操作的。
    flushdb:清空当前库
    flushall:通杀全部库

2.1.1 常用数据类型 —— String字符串:

String 是 Redis 最基本的类型,是一个key对应一个value;并且他是二进制安全的,说明他的string可以包含任何的数据,比如 jpg图片或者序列化对象;一个redis中字符串的value最多可以是512M。
常用命令:
set key value :添加键值对(如果添加的key已经有value值了则直接覆盖)
get key:通过 key 寻找值
append key value:将指定的value添加到指定的key的value后面
strlen key:获得键所对应的值的长度
setnx key value:只为不存在的key设置value值
incr key:将指定key中存储的数字加1(仅限数字类型)
decr key:将指定key中存储的数字减1(仅限数字类型)
incrby/decrby key 步长:将key中存储的值增减,自定义步长。
这里的增减操作是原子性的,因为redis是单线程的,对值的修改结果是相互不影响的。
mset、mget:批量添加key-v、获得批量key对应的值
msetnx:批量设置不存在的key
在这里插入图片描述
getrange key 起始位置 结束位置:截取指定key中的值的范围。
setex key 过期时间 value :添加一个kv,并给其设定过期时间。
getset key value:新值替换旧值,并会返回旧值。
数据结构:
string的数据结构是 简单动态字符串(缩写 SDS),是可以修改的字符串,内部结构类似ArrayList,分配的空间要比实际字符串长度大。当字符串长度小于 1M 时,扩容是加倍现有的空间,如果超过1M,扩容一次只会多扩1M空间。并且字符串最大长度是512M。

2.1.2 List列表:

redis中的list是一个key对应了多个value。
常用命令:
lpush / rpush key value value:从左边/右边插入一个值(有点像栈的思想,例:从左边插入一个值,那最后插入的值就在最左边;如果从右边插入,则最后插入的值就在最右边)
lpop / rpop key :从左边 / 右边弹出一个值
lrange key 开始位置 结束位置:从最左边的开始展示 list 中的值,如果写成 lrange key 0 -1,则表示展示所有的值。
rpoplpush key1 key2:将k2最右边的值弹出到k1最左边。
lindex key index:按照索引下标获得元素(从左到右)
llen key:获得长度
linsert key before value newvalue:在value后面插入newvalue插入值
lrem key n value:从左边删除 n 个value(从左到右)
lset key index value 将列表key下标为index的值替换成value
数据结构:
list的数据结构为快速链表 quickList。
首先在列表元素较少的时候回使用一块连续的内存存储地址,这个结果是ziplist,也是压缩列表,它是所有元素紧挨着存储,分配的是一块连续的内存。
但元素数据多时改为 quicklist
redis将链表和ziplist结合起来变成 了quicklist,将多个ziplist使用双向指针串起来使用。

2.1.3 set集合:

与 list功能类似,不同的地方在set可以自动排重,无序且不重复的。底层是一个value为null的hash表,所以操作的复杂度都是O(1).
常用命令:
SADD key member1 [member2] :向集合添加一个或多个成员
SCARD key :获取集合的成员数
SDIFF key1 [key2]:返回第一个集合与其他集合之间的差异。
SDIFFSTORE destination key1 [key2]:返回给定所有集合的差集并存储在 destination 中
SINTER key1 [key2]:返回给定所有集合的交集
SINTERSTORE destination key1 [key2]:返回给定所有集合的交集并存储在 destination 中
SISMEMBER key member:判断 member 元素是否是集合 key 的成员
SMEMBERS key:返回集合中的所有成员
SMOVE source destination member:将 member 元素从 source 集合移动到 destination 集合
SPOP key:移除并返回集合中的一个随机元素
SRANDMEMBER key [count]:返回集合中一个或多个随机数
SREM key member1 [member2]:移除集合中一个或多个成员
SUNION key1 [key2]:返回所有给定集合的并集
SUNIONSTORE destination key1 [key2]:所有给定集合的并集存储在 destination 集合中
SSCAN key cursor [MATCH pattern] [COUNT count]:迭代集合中的元素

2.1.3 hash:

redis hash是一个键值对的集合,是一个string类型的 field(字段) 和 value(值) 的映射表,它特别适合存储对象,因为其value也是一个类似键值对的形式。
在这里插入图片描述
常用命令:
HDEL key field1 [field2]:删除一个或多个哈希表字段
HEXISTS key field:查看哈希表 key 中,指定的字段是否存在。
HGET key field:获取存储在哈希表中指定字段的值。
HGETALL key:获取在哈希表中指定 key 的所有字段和值
HINCRBY key field increment:为哈希表 key 中的指定字段的整数值加上增量 increment 。
HINCRBYFLOAT key field increment:为哈希表 key 中的指定字段的浮点数值加上增量 increment 。
HKEYS key:获取所有哈希表中的字段
HLEN key:获取哈希表中字段的数量
HMGET key field1 [field2]:获取所有给定字段的值
HMSET key field1 value1 [field2 value2 ]:同时将多个 field-value (域-值)对设置到哈希表 key 中。
HSET key field value:将哈希表 key 中的字段 field 的值设为 value 。
HSETNX key field value:只有在字段 field 不存在时,设置哈希表字段的值。
HVALS key:获取哈希表中所有值。
HSCAN key cursor [MATCH pattern] [COUNT count]:迭代哈希表中的键值对。

2.1.4 Zset:

有序集合,就是在set的基础上有了顺序。每一个元素都会关联一个double类型的分数,redis通过用户为其设定的分数来进行由小到大的排序。并且分数是可以重复的。
常用命令:
ZADD key score1 member1 [score2 member2]:向有序集合添加一个或多个成员,或者更新已存在成员的分数
ZCARD key:获取有序集合的成员数
ZCOUNT key min max:计算在有序集合中指定区间分数的成员数
ZINCRBY key increment member:有序集合中对指定成员的分数加上增量 increment
ZINTERSTORE destination numkeys key [key …]:计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 destination 中
ZLEXCOUNT key min max:在有序集合中计算指定字典区间内成员数量
ZRANGE key start stop [WITHSCORES]:通过索引区间返回有序集合指定区间内的成员
ZRANGEBYLEX key min max [LIMIT offset count]:通过字典区间返回有序集合的成员
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT]:通过分数返回有序集合指定区间内的成员
ZRANK key member:返回有序集合中指定成员的索引
ZREM key member [member …]:移除有序集合中的一个或多个成员
ZREMRANGEBYLEX key min max:移除有序集合中给定的字典区间的所有成员
ZREMRANGEBYRANK key start stop:移除有序集合中给定的排名区间的所有成员ZREMRANGEBYSCORE key min max:移除有序集合中给定的分数区间的所有成员
ZREVRANGE key start stop [WITHSCORES]:返回有序集中指定区间内的成员,通过索引,分数从高到低
ZREVRANGEBYSCORE key max min [WITHSCORES]:返回有序集中指定分数区间内的成员,分数从高到低排序
ZREVRANK key member:返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序
ZSCORE key member:返回有序集中,成员的分数值
ZUNIONSTORE destination numkeys key [key …]:计算给定的一个或多个有序集的并集,并存储在新的 key 中
ZSCAN key cursor [MATCH pattern] [COUNT count]:迭代有序集合中的元素(包括元素成员和元素分值)
数据结构:
1.hash
2.跳跃表

2.2 持久化:

redis是将数据存在了内存中,内存只是临时储存器,断电就什么都没有了,所以持久化操作就是将数据存在硬盘上,同时redis官方也提供了两种不同的持久化方法来将数据存在硬盘中:

  • 快照
  • AOF(Append Only File)只追加日志文件。

2.2.1 快照:

快照,就是在某一时刻将所有的数据写进硬盘中,就像拍一张照片一样将所有数据保存下来,这也是 redis 默认开启的持久化方法,所以我们在退出程序后下一次再打开 redis 依旧可以看到上次操作的数据。
保存的文件是以 .rdb 形式结尾的文件,所以这种形式也被称为RDB方式。我的这个文件是存在 /usr/local/bin 目录下。

快照的生成方式:
1.客户端方式:BGSAVE 和 SAVE 指令
区别:

  • BGSAVE: 当接收到客户端的BGSAVE命令时,redis会调用fork来创建一个子线程,然后子线程负责将快照写入到磁盘中,主线程依旧处理其他的请求命令。所以我们也能看见,使用了这个命令后的返回值是 Background saving started:后台保存启动。
    在刚开始,主线程和子线程共享内存,这样确保了快照的执行速度;当主进程对内存进行了写操作后,子线程对内存的共享才会结束。
  • SAVE :可以发现,在进行SAVE 操作后,返回的值是ok,说明此时redis服务器在快照结束前是不再响应其他指令的。此时redis服务器是处于堵塞的状态。
127.0.0.1:6379> BGSAVE
Background saving started
127.0.0.1:6379> save
OK

2.服务器主动快照:
在我们使用 shutdown命令或者使用 ctrl + c 关闭redis时,redis会主动给我们做一次快照,形式是SAVE。
但是如果是突然断电宕机了,则不会生成快照,这也是快照的一个弊端,因为快照只能记录某一时刻的数据,如果突然宕机了,可能后面的数据就没有记录下来。

2.2.2 AOF:

这种方法会使用一个aof文件记录每一次redis的写命令,将每一次的写命令都写到aop文件的末端,因此redis只要从头到尾的执行一遍aof中的写命令,就可以恢复原来的aof文件对应的数据值。
这个操作需要在conf文件中的APPEND ONLY MODE 自行打开,默认是关闭的 appendonly no,我们需要将 no 设置为 yes。

appendonly yes
appendfilename "appendonly.aof"
appenddirname "appendonlydir"

这里可以设置aof文件的读取设置:
1.always:每个redis写命令是都会被写进硬盘,从而当系统发送崩溃时的数据丢失会减到最少,但因为这种同步策略需要对硬盘进行大量的读写,所以redis的处理命令的速度会受到硬盘性能的限制。【谨慎使用!】
2.everysec:每秒执行一次同步显式的将多个命令同步到磁盘,这里是以每一秒的频率对aof文件进行同步,对于redis的性能影响也不是很大,同时可以保证即使系统崩溃,用户最多也就丢失一秒之内产生的数据。
3.no:由操作系统决定何时同步【不推荐】
**加粗样式**
如果两种持久化方式都被选择,则redis会优先选择aof方法,因为这种方法更加的安全。
但是使用aof方式也是有问题的,因为aof会记录每一次写的命令,但是随着时间的积累,aof文件的体积会变的越来越大。

aof的重写:

3.操作redis:

3.1 java连接redis:

  1. 添加 maven 依赖:
 	 <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>
  1. 测试连接redis的代码:
    这里我们通过ip地址和端口号连接redis的服务
	public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.17.128",6379);
        //如果设置了密码就需要加这句
        jedis.auth("xxxx");
        jedis.close();
    }

注:在连接过程中遇到了问题,就是显示连接超时了,解决步骤如下:
1.进入redis.conf文件,将bind xxxx 注释掉,然后将下面的protectedxxx 设置为no
2.将6379端口放开:用命令 firewall-cmd --zone=public --add-port=6379/tcp --permanent 将对应端口放开。
参考文章:https://www.cnblogs.com/lirhbky/p/15182657.html
安全考虑,还可以给redis添加一个密码:
参考文章:https://blog.csdn.net/weixin_55229531/article/details/125218284

jedis在java中的操作和原来的命令差不多。

		jedis.set("k1","wyh");
        jedis.set("k2","wqj");
        Set<String> keys = jedis.keys("*");
        for (String key : keys) {
            System.out.println(key);
        }
        System.out.println(jedis.get("k2"));

3.2 springboot整合redis:

Spring Boot Data(数据) Redis 中提供了RedisTemplate和StringRedisTemplate
StringRedisTemplate是RedisTemplate的子类,两个方法基本一致,不同之处主要体现在操作的数据类型不同。
RedisTemplate中的两个泛型都是Object,意味着存储的key和value都可以是一个对象,而StringRedisTemplate的两个泛型都是String,意味着StringRedisTemplate的key和value都只能是字符串。

  1. 引用的jar包是:spring-boot-starter-data-redis
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.7.1</version>
        </dependency>
  1. 编写redis的配置文件:
server.port=1126
spring.redis.port=7000
spring.redis.host=192.168.17.130

spring.redis.timeout=60000
  1. 编写一个测试类:
@SpringBootTest()
public class WyhTest {
    @Resource
    private RedisTemplate RedisTemplate;

    @Test
    public void wyhRedisTest(){
        System.out.println(RedisTemplate.opsForValue().get("k1"));
    }
}

遇到的问题:
1.包导入问题:
老是报 junit 找不到的错,
java.lang.ClassNotFoundException: org.junit.jupiter.api.ClassOrderer
在这里插入图片描述

遇到报错:明明导了包但是还是报错:程序包org.springframework.data.redis.core不存在
在这里插入图片描述
反正就是不停的刷新maven等,最后不知道为啥就解决了。

  1. 连接不上redis的问题:
    不知道为什么怎么都连不上redis服务器,我看防火墙端口也开放了,配置文件中也修改成功了,但还是有问题。
    最后的解决办法是切换了一个端口为7000,我怀疑问题出在我的linux网络有点问题,我必须要重启一下1和8的适配器后才能访问linux网络。

参考:https://blog.csdn.net/chengtry/article/details/121694025?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_title~default-4-121694025-blog-90751976.pc_relevant_aa&spm=1001.2101.3001.4242.3&utm_relevant_index=6

# bind 192.168.1.100 10.0.0.1     # listens on two specific IPv4 addresses
# bind 127.0.0.1 ::1              # listens on loopback IPv4 and IPv6
# bind * -:: *   

相关操作:
redis在存入相关数据前会对数据进行序列化后再将数据存入redis中
1.对 StringRedisTemplate的默认序列化的方法是 StringRedisSerializer,序列化是写在了构造方法中。

public class StringRedisTemplate extends RedisTemplate<String, String> {
	public StringRedisTemplate() {
		setKeySerializer(RedisSerializer.string());
		setValueSerializer(RedisSerializer.string());
		setHashKeySerializer(RedisSerializer.string());
		setHashValueSerializer(RedisSerializer.string());
	}

RedisSerializer的string()方法 源码

	static RedisSerializer<String> string() {
		return StringRedisSerializer.UTF_8;
		}

在这里插入图片描述
在这里插入图片描述
2.对于RedisTemplate的默认序列化的方法是JdkSerializationRedisSerializer,因为redisTemplate处理的是对象相关的数据。
如果想要存储key是string,value是object的数据的话可以对序列化方式进行设置,将key的序列化方式改为string。

redisTemplate.setKeySerializer(new StringRedisSerializer());
	redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.opsForValue().set("book1",new Books("1q84","村上春树"));
        System.out.println(redisTemplate.opsForValue().get("1q84"));

示例:
如果键和值都不是对象的形式,那就是字符串的形式,应该用StringRedisTemplate 来获取对应的值;如果键是string,值是对象类型的,就给RedisTemplate 设置key的序列化方法;如果键是string、值也是string,那就直接使用RedisTemplate 不用修改其他

@Resource
    private RedisTemplate redisTemplate;
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Test
    public void wyhRedisTest() {
        stringRedisTemplate.opsForValue().set("wyh","want to sleep");
        System.out.println(stringRedisTemplate.opsForValue().get("wyh"));

        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.opsForValue().set("book1",new Books("1q84","村上春树"));
        System.out.println(redisTemplate.opsForValue().get("book1"));
        //总是报错
        redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
    }

如果使用 RedisTemplate.opsForValue().get(“wyh”) 这一行,用redistemplate 去拿键值都是string的类型的数据,则会报错。

如果我存储的Books对象类中没有做序列化的操作,在放入redis中就会出现以下错误。

	org.springframework.data.redis.serializer.SerializationException: Cannot serialize; 
	nested exception is org.springframework.core.serializer.support.SerializationFailedException: Failed to serialize object using DefaultSerializer; nested exception is java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type [com.redis2.entity.Books]

在类中添加继承序列号的部分,就成功了。

public class Books implements Serializable {
    private String name;
    private String author;
}

特别的:
因为 hash 比较特别,它的值也是一个键值对,所以对于hash,可以用 setHashKeySerializer 来设置hash函数值中的key的序列化方法。同理也可以设置hash的值中的value的序列化方法。

	redisTemplate.setHashKeySerializer(new StringRedisSerializer());

绑定key:
在之前我们对一个key进行操作时,可能需要写很多遍这个key值,我们可以使用boundValueOps方法来绑定一个key,如下列代码所示,就不用一直输入key值了。

//        stringRedisTemplate.opsForValue().set("bf","0602");
//        stringRedisTemplate.opsForValue().append("bf","好人");

		BoundValueOperations<String, String> bf = stringRedisTemplate.boundValueOps("bf");
        bf.set("wqj");
        bf.append("臭狗屎");
        System.out.println(stringRedisTemplate.opsForValue().get("bf"));

4.redis的应用:

  1. 利用redis中字符串类型的时效性,实现手机验证码功能。
  2. 实现具有时效性的任务,如:订单下单后有一定期限的付款时间,有效时间过后会自动取消订单。
  3. 在分布式集群中实现session的共享。
  4. redis的zset类型可以实现排行榜之类的功能,因为它的有序不可重复的特性。
  5. 利用redis实现缓存效果。
  6. 使用redis存储认证的token信息。
  7. 解决分布式集群中的分布锁的问题。(利用redis的单线程的特性)

5. redis分布式缓存:

1.缓存:计算机内存中的一段数据。
2.内存特点:读写快,断电立即丢失。
3.缓存解决的问题:将常用的数据存储在缓存中,大大提高了效率,同时也可以减轻被访问程序(如数据库)的访问压力。
4.使用场景:缓存中适合存储不常变化的数据。
5.本地缓存和分布式缓存的区别:
本地缓存:存放在应用服务器内存中的数据。
分布式缓存:存放在当前应用服务器内存之外的数据称为分布式缓存。
集群:将同一种服务的多个节点放在一起共同对系统提供服务的过程称为集群
分布式:有多个不同服务集群共同对系统提供服务这个系统称为分布式系统。

5.1 myBatis二级缓存:

在mapper配置文件中添加 < cache/>,即可开启缓存,mybatis中的二级缓存是sqlsessionfactory级别的缓存,并且是所有会话共享的。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.redis2.dao.AdminDao">
    <cache/>
    <select id="findAll" resultType="Admin">
    select * from books_system.admin
    </select>
</mapper>

可看见在没有开启缓存时进行操作时,是查询了对应的数据库的。
在这里插入图片描述

@SpringBootTest
public class MysqlCacheTest {
    @Autowired
    private AdminServiceImpl adminService;
    @Test
    public void MysqlCache(){
        adminService.getAllAdmin();
        System.out.println("=====================================");
        adminService.getAllAdmin();
        System.out.println("=====================================");
        adminService.getAllAdmin();
    }
}

在开启了缓存,并且我们进行多次的查询可以发现,第一次是走了数据库的,但是第二次和第三次是没有走数据库的,是从缓存中获得数据的。
在这里插入图片描述

报了一个序列化的错,提示实体类admin没有序列化,这也证明了mysql进行缓存时也需要对象进行序列化。要让实体类继承序列化接口。(这也解释了一些代码生成器生成的entity都是继承了序列化接口的)
在这里插入图片描述
总结:mybatis 也可以实现缓存,是本地缓存,一旦jvm关闭时缓存也不复存在。
源码分析:
mybatis中的缓存提供了一个单独的cache接口,其中的 perpetualcache 实现类就是我们使用到的。
在这里插入图片描述
可以看出,mybatis中的缓存底层是map的形式,通过键值对存放缓存值。

 private final Map<Object, Object> cache = new HashMap();
 
 public void putObject(Object key, Object value) {
        this.cache.put(key, value);
    }

    public Object getObject(Object key) {
        return this.cache.get(key);
    }

并且我们可以在mapper的 cache 标签中设置 type 为指定的 cache 类型

<cache type="org.apache.ibatis.cache.impl.PerpetualCache"/>

5.2 redis缓存:

我们了解mybatis缓存的执行流程后,可将缓存换为redis缓存。
首先创建一个rediscache 继承一下cache接口,然后将xml配置文件中的缓存类型改为我们自定义的缓存。

public class RedisCache implements Cache

<cache type="com.redis2.cache.RedisCache"/>

运行时发现报了错,提示缺少一个id

Invalid base cache implementation (class com.redis2.cache.RedisCache).  Base cache implementations must have a constructor that takes a String id as a parameter.

我们按照 perpetualcache 实现类的写法,将id添加进去,并打印id,发现id就是我们当前放入缓存的 mapper 的 namespace,cache标签是写在一个mapper标签中的,就说明一个cache是对应一个 namespace 的。

id:com.redis2.dao.AdminDao

因为还没有重写get和put的方法,所以当前缓存还不生效,但因为我们开启缓存是在mybatis的xml配置文件中写的,并不是在spring的容器中写的,所以没法利用自动装配启动 redis,我们可以通过一个工具类 SpringUtil 来获得 redisTemplate 的 bean 对象。
使用的是 ApplicationContextAware 类:

@Component
public class SpringUtil implements ApplicationContextAware {
    private static ApplicationContext applicationContextUtil;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        applicationContextUtil = applicationContext;
    }

    public static Object getBean(String name){
        return applicationContextUtil.getBean(name);
    }
}

写好工具类后我们来完善redis缓存的功能:
因为机制是根据id就是命名空间来存储缓存,所以使用了redis的hash来存储,最大的key就是存储id,hash的key就存储缓存的key,value存储缓存的value。

    @Override
    public void putObject(Object key, Object value) {
        RedisTemplate redisTemplate = (RedisTemplate) SpringUtil.getBean("redisTemplate");
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());

        //通过一个redis,hash进行存储,我们获得的key
        redisTemplate.opsForHash().put(id,key.toString(),value);
        System.out.println("key: "+key);
        System.out.println("value: "+key);
    }

    @Override
    public Object getObject(Object key) {
        RedisTemplate redisTemplate = (RedisTemplate) SpringUtil.getBean("redisTemplate");
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        return redisTemplate.opsForHash().get(id,key.toString());
    }

结果显示:可以看见所有的数据都是从缓存中拿到的。
在这里插入图片描述
问题:在对数据进行增删改的时候,我们发现需要修改更新缓存,因为缓存的获取是根据key进行获取,在对数据更新时,key不会更新,只会更新value,所以不会更新redis中的数据。
删除数据:清空redis中对应id的数据,全部清空

    //这个方法没有使用
    @Override
    public Object removeObject(Object o) {
        return null;
    }

    @Override
    public void clear() {
        getRedisTemplate().delete(id);
        System.out.println("我执行了");
    }
    
	@Override
    public int getSize() {
        return getRedisTemplate().opsForHash().size(id).intValue();
    }

在向表中添加数据时,可以发现默认调用了 clear 方法,在添加和修改数据时,会默认调用clear方法,删除相应id下的缓存,这样做是为了刷新数据,避免数据错误。

	public void addAdmin(){
        adminService.addAdmin(new Admin("wqj",2000));
    }

在这里插入图片描述
总结
redis作为缓存适合查询多修改少的数据,因为每次修改数据(增改删)就会删除一次原来的缓存,他是利用了mybatis的缓存机制,将原来的mybatis的缓存替换为了redis的缓存。

同时也存在一个问题:这种情况适用于单表查询,数据互不关联;当我们面对多表查询,查出的数据具有关联性时:当一个的数据做了改动,缓存中的其他id中也缓存了这个表的相关信息,此时其他缓存中的此表的相关信息就无法进行更新,此时可以使用 cache-ref 来关联其他表。

	<cache-ref = />

5.3 其他

  1. 关于key过长的问题:
    可以看见,每次的生成的hashkey的长度都很长,为了方便,可以先对存入的key添加MD5加密。
    MD5加密的特点:md5的加密方法是根据文件内容进行加密的,内容相同的文件md5是一样的。并且都会生成一串32位16进制的字符串。
    所以建议将存入redis的key进行md5优化。

  2. 面试相关问题:

    • 缓存穿透、击穿:客户端查询了一个数据库中没有的数据,导致缓存在这种情况下无法利用,需要每次都查询数据库。
      mybatis中的cache解决方法:将数据库中没有的值也存入缓存,值为一个空值。这样即使是空值也只需要查询一次数据库。
    • 缓存雪崩:在系统运行的某一刻,redis缓存中的数据突然全部失效,恰好这一时刻有大量客户请求需要进行操作,缓存内容不可用,全部访问数据库导致数据库程序堵塞。
      原因:不同的业务可能设置了相同的缓存时间(比如说7天,时间一到,相应的缓存就会失效)
      解决方法:1.将缓存的存活时间设置为永久 2、不同的业务设置不同的缓存时间。

6. redis 架构相关:

6.1 主从复制:

7. 实战篇:

7.1 短信登录:

  1. 基于session的验证码登录功能:
    在这里插入图片描述
  • 用户输入手机号码,校验手机号后发送验证码,并将验证码存储在session中。
	/**
     * 发送手机验证码
     */
    @PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
        // TODO 发送短信验证码并保存验证码
        return userService.sendCode(phone,session);
    }

这里的是模拟的验证码,使用随机数生成代替;验证手机号码是导入的一个工具类进行验证的。

@Override
    public Result sendCode(String phone, HttpSession session) {
        //校验手机号
        if (RegexUtils.isPhoneInvalid(phone)){
            return Result.fail("输入的手机号格式不正确");
        }
        String code = RandomUtil.randomNumbers(6);
        session.setAttribute("code",code);
        log.debug("发送验证码成功,验证码:{}"+code);
        return Result.ok();
    }
  • 用户输入验证码,点击登录按钮;首先,先根据用户输入的code和session中的code进行比较,如果不符,则报错;然后根据手机号在数据库中查找用户信息,如果用户不存在,就注册一个用户,将新用户的信息存入数据库(最重要的主键就是手机号,其他可以为默认值或为空);然后将用户信息存入session中。
    /**
     * 登录功能
     * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
     */
    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
        // TODO 实现登录功能
        return userService.login(loginForm,session);
    }

可以看见,这里操作单表数据使用的是mybatisplus,

    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //1.校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)){
            return Result.fail("手机号码格式有误,请重新输入一个手机号!");
        }
        Object cacheCode = session.getAttribute("code");
        //2.校验验证码
        if (!loginForm.getCode().equals(cacheCode.toString())||cacheCode == null) {
            return Result.fail("验证码有误");
        }
        //3.使用mybatisplus根据手机号查询用户
        HmUser user = query().eq("phone", phone).one();
        //4.验证用户是否存在,如果不存在就创建一个新的用户
        if (user == null){
            //5.用户不存在,需要新注册一个用户
            user = createUserByPhone(phone);
        }
        //6.保存用户到session
        session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
        log.debug("用户信息为: user{}"+user);
        return Result.ok();
    }

    private HmUser createUserByPhone(String phone) {
        HmUser user = new HmUser();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomNumbers(10));
        save(user);
        return user;
    }

特别的,我们在将用户信息存入session中时做了一个转换,为什么呢?

	session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));

这里将 user 转换为了 UserDTO,原因有俩:一是user中信息太多,全部存储进去会给session带来负担,第二是信息会显示出来,用户的隐私受到影响。
使用 userDTO 优化后:
在这里插入图片描述

  • 记录登录状态:因为很多地方我们都需要去获得登录状态,所以写一个拦截器去判断登录状态
/**
 * 添加一个拦截器做登录状态的判断
 * @author wyh
 */
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1.获得session
        HttpSession session = request.getSession();
        //2.获得session中的user
        Object user = session.getAttribute("user");
        //3.判断user是否存在
        if (user == null){
            //4.如果不存在或为空就拦截,401表示权限不足
            response.setStatus(401);
            return false;
        }
        //5.不为空就将user信息存在threadLocation中
        UserHolder.saveUser((UserDTO)user);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

写一个配置类:

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    /**
     * 在mvc中添加一个拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor()).excludePathPatterns(
                "/shop/**",
                "/voucher/**",
                "/shop-type/**",
                "/upload/**",
                "/blog/hot",
                "/user/code",
                "/user/login"
        );
    }
}

这里我们还用到了一个工具类userHandle:我们的用户信息是存储在threadlocation中的。

	public class UserHolder {
    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();

    public static void saveUser(UserDTO user){
        tl.set(user);
    }

    public static UserDTO getUser(){
        return tl.get();
    }

    public static void removeUser(){
        tl.remove();
    }
}
  1. 基于redis的登录功能:
    在这里插入图片描述
    redis是多个服务器所共享的,所以此时的key不能再为code了,因为会有很多的code;可以使用手机号作为key,但手机号毕竟作为隐私信息,所以我们使用token来唯一标识存储用户的信息。
  • 将手机号和对应的验证码存入到redis中,以phone作为key也是唯一标识,并给这个验证码设置有效时间两分钟(利用redis可设置有效时间的特性)
	public Result sendCode(String phone, HttpSession session) {
        //校验手机号
        if (RegexUtils.isPhoneInvalid(phone)){
            return Result.fail("输入的手机号格式不正确");
        }
        String code = RandomUtil.randomNumbers(6);
        //将存入session改为存入redis
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
        log.debug("发送验证码成功,验证码:{}",code);
        return Result.ok();
    }
  • 将登陆和注册获得的用户信息存入redis中:
    先从redis中拿到验证码,利用uuid生成一个token,并以token为key存入redis中,value就是一个hash的形式,存入我们的用户信息。注意,这里要将 user 对象手动转换为一个map的形式,并且要注意map的kv都必须是string类型的,因为我们使用的是 stringredistemplate 。
public Result login(LoginFormDTO loginForm, HttpSession session) {
        //1.获得手机号
        String phone = loginForm.getPhone();
        //2.校验验证码
        String code = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        if (!loginForm.getCode().equals(code)){
            return Result.fail("验证码有误");
        }
        //3.根据手机号查询用户
        HmUser user = query().eq("phone", phone).one();
        //4.验证用户是否存在,如果不存在就创建一个新的用户
        if (user == null){
            //5.用户不存在,需要新注册一个用户
            user = createUserByPhone(phone);
        }
        //6.保存用户到redis
        //6.1 生成一个token
        String token = UUID.randomUUID().toString(true);
        //6.2 将我们需要存储的对象转换为map
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        Map<String, String> userMap = new HashMap<>();
        userMap.put("id",userDTO.getId().toString());
        userMap.put("nickName",userDTO.getNickName());
        userMap.put("icon",userDTO.getIcon());
        //6.3 将对象存储进redis并赋予有效时间
        //但是这里的有效期指的是自登录以来过了30分钟后不管用户是否操作都失效,但是我们想拥有的效果是在用户不操作的30分钟后token失效。
        stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,userMap);
        stringRedisTemplate.expire(LOGIN_USER_KEY+token,30,TimeUnit.MINUTES);
        log.info("用户信息为: user{}", user);
        return Result.ok(token);
    }

注意,我们这里利用了redis为token设置了一个有效时间:30min,就是说30min后,不管用户是否操作token就会失效,登录状态也会失效;但是我们想要达到的效果是用户30min不操作后token才会失效,所以当用户进行登录状态的判断时,如果用户是登录的状态,则刷新用户token的时间。

/**
 * 添加一个拦截器做登录状态的判断
 * @author 吴雅慧
 */
public class LoginInterceptor implements HandlerInterceptor {
    private StringRedisTemplate stringRedisTemplate;

    public LoginInterceptor(StringRedisTemplate RedisTemplate) {
        this.stringRedisTemplate = RedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1.从header中获得token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            response.setStatus(401);
            return false;
        }
        String key = LOGIN_USER_KEY + token;
        //2.通过token获得用户信息
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        if (userMap.isEmpty()) {
            response.setStatus(401);
            return false;
        }
        //2.1 将获得的map转为对象
        UserDTO user = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        //3.判断user是否存在
        if (user == null){
            //4.如果不存在或为空就拦截,401表示权限不足
            response.setStatus(401);
            return false;
        }
        //5.不为空就将user信息存在threadLocation中
        UserHolder.saveUser(user);
        //6.刷新redis:
        stringRedisTemplate.expire(key,30, TimeUnit.MINUTES);
        return true;
    }

前端是将token信息存储在 request header的 authorization 字段中的!
在这里插入图片描述
写到这里,其实还有一个隐藏的问题,就是我们的刷新token是写在登录状态校验的方法里的,但是不是所有的网页都走这个拦截器,假象用户在没有加入过滤的首页操作了30min,他的token还是会过期。
针对这一问题,可以另写一个过滤器,专门用于获取token和token的刷新:

/**
 * 获取token并刷新的拦截器
 * @author wyh
 */
public class TokenInterceptor implements HandlerInterceptor {
    private StringRedisTemplate stringRedisTemplate;

    public TokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1.从header中获得token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
        //为什么这里直接返回了true呢,返回了true说明token为空,即还未登录注册,此时也不需要刷新token,直接放行。
            return true;
        }
        String key = LOGIN_USER_KEY + token;
        //2.通过token获得用户信息
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        if (userMap.isEmpty()) {
            return true;
        }
        //2.1 将获得的map转为对象
        UserDTO user = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        //3.判断user是否存在
        if (user == null){
            return true;
        }
        //5.不为空就将user信息存在threadLocation中
        UserHolder.saveUser(user);
        //6.刷新redis:
        stringRedisTemplate.expire(key,30, TimeUnit.MINUTES);
        return true;
    }

因为我们的用户信息是存储在 threadlocation 中的,所以我们对登录状态的判断就是直接对threadlocation 中是否存在用户进行判断。

public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (UserHolder.getUser() == null){
            response.setStatus(401);
            return false;
        }
        return true;
    }

最后添加我们的拦截器添加到 webConfig 中:
在web中拦截器的加载顺序和添加顺序有关,所以可以将token相关的拦截器放在前面,不放心也可以设置一下order优先级,越小优先级越高。

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 在mvc中添加一个拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
        registry.addInterceptor(new LoginInterceptor()).excludePathPatterns(
                "/shop/**",
                "/voucher/**",
                "/shop-type/**",
                "/upload/**",
                "/blog/hot",
                "/user/code",
                "/user/login"
        ).order(1);
    }

redis中的结果如图所示:token的有效时间有在更新。
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值