【中间件】Redis从入门到精通-黑马点评综合实战

文章目录

一:Redis基础

1.Redis是什么

Redis是一种key-value结构的数据库。

key value
id 1001
name 张三
age 21
1001 {“id”:1001,“name”:“张三”,“age”:21}

这就是NoSql数据库

2.初识Redis

  • 认识NoSQL

    • SQL:关系型数据库

    • NoSql:非关系型数据库:key-value/document(每个文件都是一个json)/graph(每个节点都是一个插入的内容)

    • S:(Structured结构化)有表的约束:比如用户表id name age三个字段,有Primarykey unique unsigned等约束,以及字段类型。我们插入的数据要符合表的约束,这种数据库就是结构化的.

    • Q:(Relational关联的):SQL有外键约束不同的表,如果想修改子表的结构,数据库不允许破坏结构。而NoSql数据库不会帮你维护这种关系

    • L:(SQL查询)select id,name,age from tb_user where id=1在关系型数据库中sql语言结构通用,但是在不同的nosql数据库中。redis会用get user:1 MongoDB会用 db.users.find({_id:1}),elasticsearch会用GET http://localhost:9200/users/1

    • 事务:关系型需要实现(ACID特性)Nosql无法全部满足ACID

    • 键值类型 Redis
      文档类型 MongoDB
      列类型 HBase
      Graph类型 Neo4j
      SQL NoSQL
      数据结构 结构化的 非结构化的
      数据关联 关联的 无关联的
      查询方式 SQL查询 非SQL查询
      事务特性 ACID BASE
      存储方式 磁盘 内存
      扩展性 垂直(不考虑分布式 数据都存在本机) 水平(哈希运算实现数据拆分 数据分配不同节点 分布式)
      使用场景 1.数据结构固定 2.相关业务对数据安全性和一致性要求高 1.数据结构不固定 2.对安全些和一致性要求不高 3.对性能要求高
  • 认识Redis

    • Remote Dictionary Server 远程词典服务器。是基于内存的键值型NoSql数据库
    • 键值(key-value) value支持多种不同数据结构
    • 单线程:每个命令有原子性(命令执行单线程,网络连接多线程)
    • 低延迟,速度快(基于内存,IO多路复用,良好编码)
    • 支持数据持久化(数据存到硬盘里)
    • 支持主从集群,分片集群
    • 支持多语言客户端(Java/Python/C都可以操作redis)
  • 安装Redis

    houor@redis:~$ sudo apt update
    houor@redis:~$ sudo apt upgrade
    houor@redis:~$ sudo apt install redis-server
    houor@redis:~$ redis-cli --version
    

3.Redis的数据结构

A.通用命令

Redis是一个key-value的数据库,key一般是String类型,但是value的类型多种多样

String hello world
Hash {name:“Jack”,age:23}
List [A -> B -> C -> C]
Set {A,B,C}
SortedSet {A:1,B:2,C:3}
GEO {A:(120.3,30.5)}
BitMap 0110110101110101011
HyperLog 0110110101110101011
help @generic //查询所有 我们看到一个指令不熟悉用法 就help 指令名
help keys//查询keys的解析
keys * //查询所有的key 生产环境不要用->Redis单线程->会阻塞服务
keys a*//查询所有以A开头的Key 

help del //查询del解析
del name //删除key为name

mset k1 v1 k2 v2 k3 v3//在redis中插入三个键值对 分别是k1 k2 k3
del k1 k2 k3 k4//但是返回值是3 因为只有三个键存在 所以只删除存在的key

exists k1//检测k1是否存在
exists k1 k2 k3//检测k1 k2 k3是否存在

expire age 20//给key(是对一个已经创建的Key)设置有效期 有效期到期该key会被自动删除 例如短信验证码 单位为秒
ttl age //查询某个key的剩余有效期 -1为永久有效 -2为key已经删除
B.String类型

Redis中最简单的储存类型

其value是字符串,根据字符串格式不同,分为三类

  • string:普通字符串
  • int:整数类型:可以自增和自减
  • float:浮点类型:可以做自增和自减

不管是哪种格式,底层都是 字节数组储存,编码方式不同。字符串最大空间不超过512m

KEY VALUE
msg hello world
num 10
score 92.5
SET:添加或者修改已经存在的一个String类型键值对
set name '喜羊羊'
GET:根据key获取String类型的value
get name
MSET:批量添加多个String类型的键值对
mset k1 v1 k2 v2 k3 v3
MGET:根据多个key和获取多个String类型的value
MGET k1 k2 k3
INCR:令key对应的value+1,前提是其value为Integer编码
incr count 
INCRBY:令整型的key自增并且指定步长
incrby count 2
INCRBYFLOAT:令浮点数自增并且指定步长
incrbyfloat countf 2.0
SETNX:添加一个String类型键值对 前提key不存在, 否则不执行
setnx valid 'valid'
SETEX:添加一个String类型键值对 并且指定有效期
setex ex 500 100 //后面的参数分别是:键 有效时间 值

C.Key的层级格式

Redis中,没有类似Mysql的Table概念,如何区别不同的类型的key呢?

储存用户和商品信息到redis,有一个用户id是1 一个商品id也是1

Redis的key允许有多个单词形成层级结构,多个单词用:隔开

项目名:业务名:类型:id

我们项目叫proName,有user和product两种不同类型的数据,可以定义key

  • user相关的key: proName:user:1
  • product相关的key: proName:product:1
KEY VALUE
proName:user:1 {“id”:1,“name”:“Jack”,“age”:21}
proName:product:1 {“id”:1,“name”:“小米11”,“price”:4999}
D.Hash类型

Hash类型,也叫散列,其value是一个无序字典,类似Java中的HashMap结构

Key field value
proName:user:1 name Jack
age 21
proName:user:2 name Rose
age 18

String结构将对象序列化为json字符串后存储,当需求修改对象某个字段时不方便

Hash结构可以将对象中的每个字段独立存储,可以针对单个字段做crud

hset key field value:针对key的field赋值为value:
hset proName:user:1 name 'Jack'
hset proName:user:1 age 21

hget key field:针对某个key去取值
hget proName:user:1 name
hget proName:user:1 age 21

hmset:针对键k 给字段f赋值v hmset k  f1 v1 k2 v2 f3 v3
hmset proName:user3 name Tom age 20

hmget 批量获取多个hash类型key的field hmget key f1 f2 f3
hmget proName:user3 name age

hgetall:获取一个hash类型的key中的所有值
hgetall proName:user3 

Hkeys key:获取一个hash类型的key中所有的field
hkeys proName:user3

Hvals key:获取一个hash类型的的key所有的value
hvals proName:user3 

hincrby:让一个hash类型key的字段值自增并且指定步长
hincrby proName:user3 age 3

hsetnx :添加一个hash类型的key 的field值,前提是该filed不存在,否则不执行
hsetnx proName:user3 address london
E.List类型

Redis中的List类型与Java中的LinkedList类似,看做是双向链表结构。支持正向检索,也支持反向检索

  • 有序
  • 元素可以重复
  • 插删快 查询慢
lpush key element 向列表左边插入一个或者多个元素
lpush testage 18

lpop key:移除并且返回列表左侧第一个元素,如果没有就返回nil
lpop testage

rpush key element 向列表右边插入一个或者多个元素
rpop key 移除并且返回列表右侧第一个元素

lrange key star end
lpush agegroup 16 17 18 19 20 21
lrange agegroup 1 6 

blpop brpop 与lpop和rpop类似 只是在没有元素时等待指定时间 而非直接返回nil
blpop agegroup
brpop agegroup


  • 如何用List结构模拟一个栈
    • 入口和出口在同一边
  • 如何用List结构模拟一个队列
    • 入口和出口在不同边
  • 如何用List结构模拟阻塞队列
    • 入口出口不在同一边
    • 出队列采用blpop或者brpop
F.Set类型

Redis中的Set结构与java中的HashSet类似,可以看成value为null的HashMap,因为其也是hash表,因此具备和HashSet类似的特征

  • 无序
  • 元素不可重复
  • 查找快
  • 支持交集,并集,差集查询。
    • 在社交性应用中普遍使用
Set集合常见命令:

SADD key member:向set中添加一个或多个元素
sadd set1 apple banana cherry
sadd set2 banana cherry date

SREM key member:移除set中的指定元素
srem set1 apple
srem set2 banana

SCARD key:返回set中元素的个数
scard set1

SIsMember key member:判断元素是否存在于set中
SisMember set1 apple

Smembers 获取set中所有元素
smembers set1

SInter key1 key2 求key1和key2的交集
sInter set1 set2

SDiFF key1 key2 求key1和key2的差集
sDiff set1 set2

场景题:

  • 张三的好友:李四,王五,赵六

  • 李四的好友:王五,麻子,二狗

    • 计算张三的好友数量
    • 计算张三和李四的共同好友
    • 查询哪些人是张三的好友,不是李四的好友
    • 查询张三和李四好友总共有哪些人
    • 判断李四是否为张三的好友
    • 判断张三是否是李四的好友
    • 在张三的好友列表中移除李四
    sadd zhangsan '李四' '王五' '赵六'
    sadd lisi '王五' '麻子' '二狗'
    
    scard zhangsan
    
    sinter zhangsan lisi
    
    sdiff zhangsan lisi
    
    smembers lisi
    smembers zhangsan
    
    sismember zhangsan '李四'
    sismember lisi '张三'
    
    srem zhangsan '李四'
    
G.SortedSet类型

Redis的SortedSet是一个可排序的set集合,与Java的TreeSet相似,但底层数据结构差别很大。SortedSet每一个元素都有score属性,可以基于score进行排序。底层是用跳表实现的(SkipList)

  • 可排序
  • 元素不重复
  • 查询速度快

所以我们用SortedSet可排序特性实现排行榜功能

注意:这里的key可以理解成sql中的表

注意:rankscore不一样

ZADD key score member:添加一个或者多个元素到sortedset 如果存在就更新score值
zadd user 1 Tom
zadd user 2 Bob
zadd user 3 Peter
zadd age 1 1
zadd ss1 1 test1 2 test2 3 test3 4 test4
zadd ss2 5 test5 6 test6 7 test7 8 test8

ZREM key member 删除sorted set中一个指定元素
zrem user Tom

zscore key member 获取sorted set中指定元素的score值
zscore user Tom

zrank key member 获取指定元素的排名
zrank user Tom


zcard key 统计key这个sortedset中元素的个数
zcard user


zcount key min max 统计score在某个范围内所有元素的个数
zcount user 1 3


zincrby key number member 对key的member对象自增
zincrby hash 2 1



zrange key min max:按照score排序后 获取指定排名范围内的元素
zrange user 1 4


Zrangebyscore key min max 按照score排序后 获取指定score范围内的元素
zrangebyscore user 1 4


ZDIFF差集
zdiff ss1 ss2
Zinter交集
zinter ss1 ss2
Zunion并集。
zunion ss1 ss2

二:Redis的Java客户端

1.Jedis

A.引入依赖

<dependency>
	<groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.7.0</version>
</dependency>

B.建立连接

private Jedis jedis
@BeforeEach//单元测试类
void setUp(){
   
   
    //建立连接
    jedis = new Jedis("192.168.150.101",6379);//虚拟机ip与端口号
    //设置密码
    jedis.auth("123321");
    //选择库
    jedis.select(0);
}

C.测试Jedis

@Test
void testString(){
   
   
    //插入数据 方法名称就是redis命令名称
    String result = jedis.set("name","张三");
    System.out.println("Result="+result);
    //获取数据
    String name = jedis.get("name");
    System.out.println("name="+name);
}

D.释放资源

@AfterEach
void tearDown(){
   
   
    if(jedis!=null){
   
   
        jedis.close();
    }
}

2.Jedis连接池

Jedis对象本身线程不安全,频繁创建和销毁连接存在性能损耗。因此推荐使用Jedis连接池替代Jedis的直接连接方式

public class JedisConnectFactory{
   
   
    private static final JedisPool jedisPool;
    static{
   
   
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        
        //最大连接
        jedisPoolConfig.setMaxTotal(8);
        //最大空闲连接
        jedisPoolConfig.setMaxIdle(8);
        //最小空闲连接
        jedisPoolConfig.setMinIdle(0);
        //设置最长等待时间
        jedisPoolConfig.setMaxWaitMillis(200);
        jedisPool = new JedisPool(jedisPoolConfig,"192.168.150.101",6379,1000,"123321");
    }
    //获取Jedis对象
    public static Jedis getJedis(){
   
   
        return jedisPool.getResource;
    }
}

3.SpringDataRedis

SpringData是Spring中数据操纵的模块,包含对各种数据库的集成。对Redis操作的模块,就叫SpringDataRedis.

  • 提供了对不同客户端的整合:Lettuce与Jedis
  • 提供了RedisTemplate统一API来操作Redis
  • 支持Redis的发布订阅模型
  • 支持Redis哨兵与Redis集群
  • 支持基于Lettuce的响应式编程
  • 支持基于JDK,JSON,字符串,Spring对象的数据序列化与反序列化
  • 支持基于Redis的JDKCollection的实现

A.redisTemplate

API 返回值类型 说明
redisTemplate.opsForValue() ValueOperations 操作String类型数据
redisTemplate.opsForHash() HashOperations 操作Hash类型数据
redisTemplate.opsForList() ListOperations 操作List类型数据
redisTemplate.opsForSet() SetOperations 操作Set类型数据
redisTemplate.opsForZSet() ZSetOperations 操作SortedSet类型数据
redisTemplate 通用命令

依赖导入

<!--Redis依赖-->
<dependency>
	<groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-redis</artifactId>
</dependency>
<!--连接池依赖-->
<dependency>
	<groupId>org.apache.commons</groupId>
    <aritifactId>commons.pool2</aritifactId>
</dependency>

配置文件(application.properties)

# 服务器ip
spring.redis.host=192.168.11.128
# 端口
spring.redis.port=6379
# 第一个数据库存储
spring.redis.database=0
# 最大活跃数
spring.redis.jedis.pool.max-active=20
# 最大空闲数
spring.redis.jedis.pool.max-idle=10
# 最小空闲数
spring.redis.jedis.pool.min-idle=5

注入redisTemplate

@Autowired
private RedisTemplate redisTemplate;

编写测试

@SpringbootTest
public class RedisTest{
   
   
    @Autowired
    private RedisTemplate redisTemplate;
    @Test
    void testString(){
   
   
        //插入一条String类型数据
        redisTemplate.opsForValue.set("name","李四");
        Object name = redisTemplate.opsForValue().get("name");
        Systen.out.println("name"+name);
    }
}

B.redisSerializer

或许,你在redis中发现从本地储存到redis中的value为"\xac\xed\x00\x05t\x00\aagetest"似乎有一些乱码!我们引入序列化工具

实际上:redisTemplate 可以接收任意Object作为值写入Redis,但是会在写入前将Object序列化为字节形式,默认是采用JDK序列化,因此会形成"\xac\xed\x00\x05t\x00\aagetest"

存在缺点:

  • 可读性差
  • 内存占用大

解决策略:

  • 不使用JDK的默认序列化方式,我们要重写序列化方式

自定义RedisTemplate序列化方式

@Configuration
public class RedisConfig {
   
   
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
   
   
      //创建RedisTemplate对象
        RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
      //设置连接工厂
        redisTemplate.setConnectionFactory(redisConnectionFactory);

      //设置Json序列化工具
        GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
      //设置key的序列化
        redisTemplate.setKeySerializer(RedisSerializer.string());
      //设置value的序列化
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
        return redisTemplate;
    }

}

C.StringRedisTemplate

JSON序列化可以满足一定需求,但是存在问题,例如:

{
   
   
    "@class":"com.heima.redis.pojo.User",
    "name":"测试id",
    "age":21
}

为了在反序列化时知道对象类型,json序列化器会将类的class类型写入json结果,存入Redis,会带来额外内存开销

为了节省内存空间,我们并不会使用json序列化器来处理value

而是统一使用String序列化器,要求只能存储String类型的key和value。当需求存储java对象,手动完成序列化与反序列化

流程:pojo类 ->手动序列化 成json 然后redisTemplate.opsForValue().set(“user”,jsonStr)

然后从redis拿 redisTemplate.opsForValue().get(“user”)然后得到json,之后把json反序列化为字节码中的javaben类

javabean类例子:

public class User{
   
   
    private String name;
    private Integer age;
}

jsonstr例子:

{
   
   
    name:"Jack",
    age:21
}

Spring默认提供了一个StringRedisTemplate类,它的key和value的序列化方式默认就是String方式。省去了我们自定义RedisTemplate的过程:

@Autowired
private StringRedisTemplate stringRedisTemplate;
//JSON工具
private static final ObjectMapper mapper = new ObjectMapper();
@Test
void testStringTemplate() throws JsonProcessingException{
   
   
    //准备对象
    User user = new User("测试id",19);
    //手动序列化
    String json = mapper.writeValueAsString(user);
    //写入一条数据到redis
    stringredisTemplate.opsForValue().set("user:200",json);
    //读取数据
    String val = stringRedisTemplate.opsForValue.get("user:200");
    //反序列化
    User user1 = mapper.readValue(val,User.class);
    System.out.println(user1);
}
//哈希化
@Test
void testHash(){
   
   
    stringRedisTemplate.opsForHash().put("user:400","name","测试id");
    stringRedisTemplate.opsForHash().put("user:400","age","19");
    Map<Object,Object> entries = stringRedisTemplate.opsForHash().entries("user:400");
    System.out.println(entries);
}

三:Redis的企业应用


1.短信登录(Redis的存与取)

A.导入黑马点评项目

B.基于Session实现登录

B.1业务逻辑与概念扫盲
  • 发送短信验证码
    • 开始
    • 提交手机号
    • 校验手机号
      • 符合:生成验证码
      • 不符合:提交手机号
    • 生成验证码
    • 保存验证码在Session
    • 发送验证码
  • 短信验证码登录,注册
    • 提交手机号与验证码
    • 校验验证码:
      • 符合:根据手机号查询用户
      • 不符合:提交手机号与验证码
    • 根据手机号查询用户信息
    • 用户是否存在
      • 存在->保存用户到Session
      • 不存在->创建新的用户,填入手机号等信息,填入数据库
    • 保存用户到Session
  • 校验登录状态
    • 请求并携带Cookie
    • 从Session中获取用户
    • 判断用户是否存在
      • 存在:保存用户到ThreadLocal放行
      • 不存在:拦截

Session基于Cookie,每一个Session都有一个SessionID保存在浏览器的cookie中

1. 基于Session实现登录的案例

以下是一个简化的基于Session的登录流程案例,使用伪代码表示:

# 用户提交登录表单
POST /login
# 请求包含用户名和密码
{
    
    
"username": "user123",
"password": "password123"
}
# 服务器端处理登录请求
def handle_login_request(request):
 username = request.POST['username']
 password = request.POST['password']

 # 验证用户名和密码(这里应该查询数据库并使用加密验证)
 if authenticate_user(username, password):
     # 创建一个新的会话
     session_id = create_session(username)

     # 设置Session ID到Cookie中
     response = set_cookie("session_id", session_id)

     # 登录成功,返回响应
     return response_with_status_code(200, "Login successful")
 else:
     # 登录失败,返回错误信息
     return response_with_status_code(401, "Invalid credentials")
# 用户验证函数(伪代码)
def authenticate_user(username, password):
 # 这里应该是数据库查询和密码加密验证的过程
 return True
# 创建会话函数(伪代码)
def create_session(username):
 session_id = generate_unique_session_id()
 session_data = {
    
    
     "user_id": get_user_id_by_username(username),
     "username": username,
     "last_login": get_current_time()
 }
 store_session(session_id, session_data)
 return session_id
# 存储会话函数(伪代码)
def store_session(session_id, session_data):
 # 这里将会话数据存储到服务器的内存、数据库或分布式缓存中
 pass
# 生成唯一会话ID函数(伪代码)
def generate_unique_session_id():
 return "unique_session_id"
# 设置Cookie函数(伪代码)
def set_cookie(name, value):
 response = create_response_object()
 response.set_cookie(name, value)
 return response

2. 具体的Session内容

假设用户登录成功,以下是一个具体的Session内容示例:

{
    
    
"session_id": "unique_session_id",
"data": {
    
    
 "user_id": 123,
 "username": "user123",
 "last_login": "2023-12-01T12:00:00Z",
 "is_authenticated": true
}
}

3. Session存储在哪里

在这个案例中,Session可以存储在以下位置:

  • 服务器内存:服务器为每个会话分配一块内存区域。
  • 数据库:服务器将Session信息存储在数据库的会话表中。
  • 分布式缓存:如Redis或Memcached,适用于需要横向扩展的分布式系统。

4. 返回给前端的内容

当用户登录成功后,服务器返回给前端的内容可能如下:

  • HTTP响应状态码:200 OK
  • HTTP响应头:包含Set-Cookie头,用于设置Session ID的Cookie
  • HTTP响应体:通常是一个JSON对象,包含登录成功的信息
HTTP/1.1 200 OK
Set-Cookie: session_id=unique_session_id; Path=/; HttpOnly; Secure
Content-Type: application/json
{
  "message": "Login successful",
  "user": {
    "username": "user123"
  }
}

客户端(通常是浏览器)会存储这个session_id Cookie,并在随后的请求中自动将其发送给服务器,以维持会话状态。

ThreadLocal相关:

  1. 线程安全:ThreadLocal为每个线程提供了一个独立的变量副本,从而避免了多线程并发访问共享资源时的线程安全问题。在Web应用中,服务器通常为每个请求创建一个线程,通过ThreadLocal可以保证每个请求的用户信息是独立的。
  2. 数据隔离:由于ThreadLocal保证了变量的线程独立性,因此它可以确保每个线程(即每个用户请求)的数据不会互相干扰。这对于保持用户会话数据和状态是非常重要的。
  3. 方便获取:将用户信息存储在ThreadLocal中后,在任何地方都可以方便地获取到当前线程对应的用户信息,而不需要通过参数传递的方式,简化了代码逻辑。
  4. 性能优化:与全局变量相比,ThreadLocal减少了锁的使用,因为每个线程访问的是自己独立的变量副本,从而提高了系统的性能。

以下是具体的应用场景说明:

  • 用户会话管理:当一个用户登录系统后,其登录信息(如用户ID、权限等)可以存储在ThreadLocal中。在随后的请求处理过程中,系统可以从ThreadLocal中直接读取这些信息,而不需要每次都去查询数据库或访问外部存储。
  • 事务管理:在事务处理中,ThreadLocal可以用来存储事务的上下文信息,确保事务的连贯性和一致性。
  • 日志记录:在记录日志时,ThreadLocal可以用来保存日志相关的上下文信息,如用户操作轨迹等。
B.2实现-发送短信验证码
  1. 分析点击[发送验证码]后发出的请求

    RequestURL:http://localhost:8080/api/user/code?phone=13456789001
    RequestMethod:POST
    
    
    说明
    请求方式 POST
    请求路径 /user/code
    请求参数 phone,电话号码
    返回值
  2. 分析UserController节选验证码部分

    //修改前
    @PostMapping("code")
    public Result sendCode(@RequestParam("phone")String phone,HttpSession session){
         
         
        //TODO 发送短信验证码并且保存验证码
        return Result.fail("功能未完成")
    }
    
    //修改后
    @PostMapping("code")
    public Result sendCode(@RequestParam("phone")String phone,HttpSession session){
         
         
        //发送短信验证码并且保存验证码
        return userService.sendCode(phone,session)
    }
    
  3. 实现UserService及其实现类

    public interface IUserService extends IService<User>{
         
         
        Result sendCode(String phone,HttpSession session);
    }
    
    public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements IUserService{
         
         
        @Override
        public Result sendCode(String phone,HttpSession session){
         
         
            //1.校验手机号
            if(RegexUtils.isPhoneInvalid(phone)){
         
         
            //2.如果不符合,返回错误信息
                return Result.fail("手机号格式错误");
            }
            //3.符合,生成验证码(6位)
            String code = RandomUtil.randomNumbers(6);//
            //4.保存验证码到session
            session.setAttribute("code",code);
            //5.发送验证码
            log.debug("发送验证码成功,验证码:{}",code);
            //6.返回Ok
            return Resut.ok();//这里ok本质是我们重新封装的
        }
    }
    
B.3实现短信验证码登录和注册

经过B2.在前端点击发送验证码,可以在NetWork中观察到

RequestPayLoad有Json 下面的code和phone都是我输入的内容
{
   
   
    code:"123321",
    phone:13456789001
}
说明
请求方式 POST
请求路径 /user/login
请求参数 phone:电话号码;code:验证码
返回值
  1. UserController中完善Login功能

    //完善功能前
    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginFormDTO,HttpSession session){
         
         
        //TODO 实现登录功能
        return Result.fail("功能未完成")}
    
    @Data
    public class LoginFormDTO{
         
         
        private String phone;
        private String code;
        private String password;//考虑密码登录情况
    }
    
    //完善功能后
    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm,HttpSession session){
         
         
        //实现登录功能
        return UserService.login(loginForm,session);
    }
    
  2. UserService与其实现类

    public interface IUserService extends IService<User>{
         
         
        
        Result sendCode(String phone,HttpSession session);
        
        Result login(LoginFormDTO loginForm,HttpSession session);
    }
    
    @Override
    public Result login(LoginFormDTO loginForm,HttpSession session){
         
         
        //1.校验手机号
        String phone = loginForm.getPhone();
        if(RegexUtils.isPhoneInvalid(phone)){
         
         
            return Result.fail("手机号格式错误");
            //1.2:如果不一致返回错误信息
        }
        //2.校验验证码
        Object cacheCode = session.getAttribute("code");
        String code = loginForm.getCode();
        if(cacheCode==null||!cacheCode.toString().equals(code)){
         
         
            //3.不一致报错
            return Result.fail("验证码错误");
        }
        //4.一致,根据手机号查询用户
        User user = query().eq("phone",phone).one();
        //5.判断用户是否存在
        if(user==null){
         
         
                //6.不存在,创建新用户并且保存
            user = createUserWithPhone(phone);
        }
    
        //7.保存用户信息到session
        session.setAttribute("user",user);
        return Result.ok();
    }
    
    private User createUserWithPhone(String phone){
         
         
        //1.创建用户
        User user = new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));
        save(user)
        return user;
    }
    

    在Java代码中,save(user)query()这样的方法调用通常来自以下几种情况:

    1. save(user):

      • 来自继承的基类或接口: 在很多框架中,比如MyBatis-Plus,存在一个通用的Service基类,其中包含了CRUD(创建、读取、更新、删除)操作的方法。save(user)可能是继承自这样的基类或实现了某个接口,其中定义了save方法。
      • 来自注入的服务: 有可能save方法是某个服务类中的方法,该服务类被注入到当前类中,以便进行数据持久化操作。
      • 来自工具类: 有时,这些方法可能来自于一个工具类,专门用于处理数据库操作。
        示例代码如下:
      // 假设存在一个BaseService类
      public class UserService extends BaseService<User> {
              
              
          // ... 其他方法 ...
      
          public boolean save(User user) {
              
              
              // 实现保存用户的逻辑
              return true;
          }
      }
      
    2. query().eq(“phone”, phone).one():

      • 来自MyBatis-Plus等ORM框架: MyBatis-Plus是一个MyBatis的增强工具,它提供了很多链式操作方法,例如query()通常是对Mapper接口方法的增强调用,用于构建查询条件。eq是等于(Equal)的缩写,用于添加等于条件,one表示查询并返回单个结果。
      • 来自自定义的查询工具: 在某些情况下,query()可能是一个自定义的查询工具类或方法,它内部构建SQL语句或调用数据库API来执行查询。
        示例代码如下:
      // 假设存在一个Query类用于构建查询
      public class Query {
              
              
          // 构建查询条件
          public Query eq(String column, Object value) {
              
              
              // 添加等于条件
              return this;
          }
      
          // 执行查询并返回单个结果
          public User one() {
              
              
              // 实现查询逻辑
              return new User();
          }
      }
      

      下面是MyBatis-Plus中query()方法的一般用法:

    // 假设UserMapper继承了BaseMapper接口
    public interface UserMapper extends BaseMapper<User> {
          
          
        // ... 其他方法 ...
    }
    // 在服务层中,可以这样使用
    @Service
    public class UserService {
          
          
        @Autowired
        private UserMapper userMapper;
        public User queryUserByPhone(String phone) {
          
          
            QueryWrapper<User> queryWrapper = new QueryWrapper<>();
            queryWrapper.eq("phone", phone);
            return userMapper.selectOne(queryWrapper);
        }
    }
    

    在这个例子中,QueryWrapper是MyBatis-Plus提供的一个条件构造器,用于生成SQL的where条件,selectOne是MyBatis-Plus提供的方法,用于查询并返回单个结果。
    总之,save(user)query().eq("phone", phone).one()是典型的数据持久化操作方法,它们通常来源于ORM框架提供的接口或自定义的数据库操作类。在具体实现中,需要查看该类是如何与数据库交互的,以及这些方法的具体实现细节。

B.4登录校验:

在B.3实现后,当验证码正确时,并且账号没有注册时候,会在数据库中注册新的账号。但是:我们的前台页面没有跳转,登录校验就是解决这个问题的

黑马点评服务,有若干个Controller,如果每一个Controller都做一次用户登录校验,那么冗余代码。我们在访问Controller前,加入拦截器。让所有请求经过拦截器后才能访问Controller。

但也带来问题:用户信息交互在拦截器,那我Controller拿不到Session,如何对传递数据到mapper去增删查改呢。

策略:拦截器会向不同的Controller发送ThreadLocal,ThreadLocal就储存了Session等信息

1.拦截器:处于Utils包下
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中的用户
      Object user = session.getAttribute("user");
        //3.判断用户是否存在
        if(user==null){
   
   
        //4.不存在就拦截
            response.setStatus(401);
            return fasle;
        }

        //5.存在就保存信息到ThreadLocal中
        UserHolder.saveUser((User) user)
        //6.放行
        return true;
    }
    // 在业务处理器处理请求完成之后,生成视图之前执行
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
      throws Exception{
   
   
        //不工作 在本项目中
    }
    // 在DispatcherServlet完全处理完请求之后被调用,可用于清理资源
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
      throws Exception{
   
   
        //移除用户
        UserHolder.removeUser();
    }
}
2.配置拦截器

拦截器写好了,但是没有生效,因此需要配置拦截器

public class MvcConfig implements WebMvcConfigurer{
   
   
    @Override
    public void addInterceptors(InterceptorRegistery registry){
   
   
        registry.addInterceptor(new LoginInterceptor())
            .excludePathPatterns(
        	"/shop/**",
            "/voucher/**",
            "/shop-type/**",
            "/upload/**",
            "/blog/hot",
            "/user/code",//获取验证码的接口当然不能拦截
            "/user/login",//登录的接口拦截了,那怎么登录呢
            //上述为不应该被拦截的RESTful接口
        );
    }
}

从请求中提取 sessionId 并在 HttpServletRequest 中携带 HttpSession 的功能是由 Web 服务器(如 Apache Tomcat、Jetty、Undertow 等)和 Servlet 容器(通常是 Java Servlet API 的一部分)共同实现的。以下是这个过程的工作原理:

  1. 会话创建
    • 当用户首次访问一个需要跟踪会话的 Web 应用程序时,Web 服务器会创建一个新的 HttpSession 对象。
    • 这个 HttpSession 对象会被分配一个唯一的标识符,称为 sessionId
  2. 发送 sessionId 到客户端
    • 服务器会在 HTTP 响应中通过 Set-Cookie 头部将 sessionId 发送给客户端。
    • 客户端(通常是浏览器)会存储这个 sessionId,并在后续的请求中通过 Cookie 头部将其发送回服务器。
  3. 请求中的 sessionId 提取
    • 当客户端发送请求时,Web 服务器会检查请求头中的 Cookie 字段,以查找与该会话关联的 sessionId
    • 如果找到了 sessionId,服务器会使用这个标识符在服务器的会话管理器中查找对应的 HttpSession 对象。
  4. 绑定 HttpSessionHttpServletRequest
    • 一旦找到 HttpSession 对象,服务器会将其绑定到 HttpServletRequest 对象上。
    • 这样,Servlet 可以通过调用 HttpServletRequestgetSession() 方法来访问 HttpSession
      以下是这个过程的关键参与者:
  • Web 服务器:负责处理 HTTP 请求和响应,维护会话状态,以及发送和接收 sessionId
  • Servlet 容器:实现了 Java Servlet API,提供了 HttpServletRequestHttpSession 等接口,以及它们之间的交互逻辑。
  • Servlet:当请求到达时,Servlet 容器会调用 Servlet 的方法(如 doGetdoPost 等),并提供 HttpServletRequest 对象。
    以下是代码示例中可能涉及的几个关键步骤:
// 服务器创建会话并设置sessionId
HttpSession session = request.getSession();
String sessionId = session.getId();
// 服务器通过Set-Cookie发送sessionId到客户端
response.addCookie(new Cookie("JSESSIONID", sessionId));
// 客户端在后续请求中携带sessionId
// 以下是请求头中可能包含的Cookie字段
Cookie: JSESSIONID=sessionIDValue
// 服务器从请求中提取sessionId,并在请求处理期间绑定HttpSession
HttpSession session = request.getSession(); // 这会查找与sessionId关联的会话

总的来说,这个功能是由 Web 服务器和 Servlet 容器自动处理的,不需要开发者手动实现 sessionId 的发送和接收。开发者只需要通过 HttpServletRequest 的 API 来使用会话功能。

B.5实现对用户信息的隐藏

在B.4中,我们可以找到访问./me时的json包 很明显:此处暴露了用户的个人信息

后端返回给前端的内容:不必要包含密码等信息,如何做删去这些不必的内容呢?

{
    
    
 createTime:"",
 icons:"",
 id:5,
 nickName:"user_xxxxx",
 password:"",
 phone:"165489465",
 updateTime:"2024-7-22-1"
}
1.分析无法实现隐藏的根源

在UserService的Login部分:

session.setAttribute("user",user);

我们将整个user都放入了session.而整个user中包含了密码,因此返回给前端结果包含了密码

2.解决策略

因此需要设计一个部分对象避免返回敏感数据,仅仅返回基本数据。

session.setAttribute("user",BeanUtil.copyProperties(user,UserDTO.class))
//BeanUtil.copyProperties是将前者的属性拷贝到后者的bean中取
//接下来需要将项目中其他返回user的部分,替换成UserDTO
    
@Data
public class UserDTO{
   
   
    private Long id;
    private String nickName;
    private String icon;
    //发现了:这个bean里面没有密码等对象 因此我们用BeanUtil拷贝user过来,发现密码是没有对应字段的,自然无法拷贝,被保留的就是非敏感字段了
}

C.集群的Session共享问题

session共享问题:多台Tomcat不共享session储存空间,当请求切换到不同Tomcat服务,会导致数据丢失的问题。

**最初解决策略:**让tomcat支持拷贝功能,让其他的Tomcat能够拷贝已存在数据的Tomcat中数据。问题:存在延迟,因此不采用

衍生思考:

  • 新的方案要满足:
    • 数据共享
    • 内存存储
    • key-value结构

我们找到了版本答案——Redis

  • Redis优势
    • Redis是中间件,因此多台Tomcat可以访问我们的Redis集群,解决数据共享问题
    • Redis是基于内存存储的数据库,解决内存存储问题
    • Redis还是key-value结构的非关系型数据库,满足key-value需求

D.基于Redis实现共享Session登录

D.1业务逻辑梳理
  • 发送短信验证码

    • 开始
    • 提交手机号
    • 校验手机号
      • 符合:生成验证码
      • 不符合:提交手机号
    • 生成验证码
    • 保存验证码在:Redis:注意(phone:138797984 )
    • 发送验证码
    key value
    phone:138384111438 9527

    保存验证码步骤,不再将验证码存在Session中,而是存在Redis中,且key为 Phone:手机号 value为生成的验证码

    验证码和手机号都应该是String类型

  • 短信验证码登录,注册

    • 提交手机号与验证码
    • 校验验证码:
      • 符合:根据手机号查询用户
      • 不符合:提交手机号与验证码
    • 根据手机号查询用户信息
    • 用户是否存在
      • 存在->保存用户到Redis(用户对象用hash类型->方便对 对象具体的值进行修改)
      • 不存在->创建新的用户,填入手机号等信息,填入Redis (要用随机token去给用户的字段赋值),并且存到数据库,
    • 保存用户到Redis,返回token给客户端(为用token去给Redis对象中字段赋值做前提)
KEY Value Value
field value
heima:user:1 name Jack
age 21
heima:user:2 name Rose
age 18
  • 校验登录状态
    • 请求并携带Token(注意:不是Session)
    • 从Redis(用Token去取Redis中的值)中获取用户
    • 判断用户是否存在
      • 存在:保存用户到ThreadLocal放行
      • 不存在:拦截
D.2实现-发送短信验证码
public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements IUserService{
   
   
    @Resource
    private StringRedisTemplate stringRedisTempalte; //我们要引入Redis替代Session,就得引入这个集成操作Redis的对象
    
    @Override
    public Result sendCode(String phone,HttpSession session){
   
   
        //1.校验手机号
        if(RegexUtils.isPhoneInvalid(phone)){
   
   
        //2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误");
        }
        //3.符合,生成验证码(6位)
        String code = RandomUtil.randomNumbers(6);//
        //4.保存验证码到session
        /*session.setAttribute("code",code);*/
        //4.保存验证码到Redis
        stringRedisTemplate.opsForValue().set("login:code:"+phone,code,LOGIN_CODE_TTL,TimeUnit.MINUTES);
        /*注意 login:code是显示层级结构 
        2(LOGIN_CODE_TTL)是值 TimeUnit.MINUTES是单位 表示这个验证码在redis中的有效期为2分钟 类似于setex的命令 或 set key value ex 120 
        login:code:可以替换为LOGIN_CODE_KEY 前提是你定义RedisConstants*/
        
        //5.发送验证码
        log.debug("发送验证码成功,验证码:{}",code);
        //6.返回Ok
        return Resut.ok();//这里ok本质是我们重新封装的
    }
}
public class RedisConstants{
   
   
    public static final String LOGIN_CODE_KEY="login:code:";
    public static final Long LOGIN_CODE_TTL = 2L;
    public static final Long LOGIN_USER_KEY ="login:token:";
    public static final Long LOGIN_USER_TTL = 30L;
}
D.3实现短信验证码登录和注册
@Override
public Result login(LoginFormDTO loginForm,HttpSession session){
   
   
    //1.校验手机号
    String phone = loginForm.getPhone(
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值