马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
一: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中的表
留意:rank和score不一样
- 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例子:
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的企业应用
- [/code] [size=4]1.短信登录(Redis的存与取)[/size]
- [size=3]A.导入黑马点评项目[/size]
- [size=3]B.基于Session实现登录[/size]
- [size=2]B.1业务逻辑与概念扫盲[/size]
-
- [list]
- [*]发送短信验证码
- [list]
- [*]开始
- [*]提比武机号
- [*]校验手机号
- [list]
- [*]符合:天生验证码
- [*]不符合:提比武机号
- [/list]
- [*]天生验证码
- [*]生存验证码在[b]Session[/b]
- [*]发送验证码
- [/list]
- [*]短信验证码登录,注册
- [list]
- [*]提比武机号与验证码
- [*]校验验证码:
- [list]
- [*]符合:根据手机号查询用户
- [*]不符合:提比武机号与验证码
- [/list]
- [*]根据手机号查询用户信息
- [*]用户是否存在
- [list]
- [*]存在->生存用户到Session
- [*]不存在->创建新的用户,填入手机号等信息,填入数据库
- [/list]
- [*]生存用户到Session
- [/list]
- [*]校验登录状态
- [list]
- [*]哀求并携带Cookie
- [*]从Session中获取用户
- [*]判定用户是否存在
- [list]
- [*]存在:生存用户到[b]ThreadLocal[/b]并[b]放行[/b]
- [*]不存在:拦截
- [/list]
- [/list]
- [/list] Session基于Cookie,每一个Session都有一个SessionID生存在欣赏器的cookie中
- [size=3]1. 基于Session实现登录的案例[/size]
- 以下是一个简化的基于Session的登录流程案例,利用伪代码体现:
- [code]# 用户提交登录表单
- 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相干:
- 线程安全:ThreadLocal为每个线程提供了一个独立的变量副本,从而制止了多线程并发访问共享资源时的线程安全标题。在Web应用中,服务器通常为每个哀求创建一个线程,通过ThreadLocal可以包管每个哀求的用户信息是独立的。
- 数据隔离:由于ThreadLocal包管了变量的线程独立性,因此它可以确保每个线程(即每个用户哀求)的数据不会互干系扰。这对于保持用户会话数据和状态好坏常告急的。
- 方便获取:将用户信息存储在ThreadLocal中后,在任何地方都可以方便地获取到当火线程对应的用户信息,而不必要通过参数通报的方式,简化了代码逻辑。
- 性能优化:与全局变量相比,ThreadLocal镌汰了锁的利用,由于每个线程访问的是自己独立的变量副本,从而进步了体系的性能。
以下是详细的应用场景分析:
- 用户会话管理:当一个用户登录体系后,其登录信息(如用户ID、权限等)可以存储在ThreadLocal中。在随后的哀求处理处罚过程中,体系可以从ThreadLocal中直接读取这些信息,而不必要每次都去查询数据库或访问外部存储。
- 事件管理:在事件处理处罚中,ThreadLocal可以用来存储事件的上下文信息,确保事件的连贯性和划一性。
- 日志
记载:在记载日志 时,ThreadLocal可以用来生存日志 相干的上下文信息,如用户利用轨迹等。
B.2实现-发送短信验证码
- 分析点击[发送验证码]后发出的哀求
- RequestURL:http://localhost:8080/api/user/code?phone=13456789001
- RequestMethod:POST
复制代码 分析 哀求方式 POST 哀求路径 /user/code 哀求参数 phone,电话号码 返回值 无
- 分析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)
- }
复制代码
- 实现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:验证码 返回值 无
- 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);
- }
复制代码
- 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()如许的方法调用通常来自以下几种环境:
- save(user):
- 来自继承的基类或接口: 在许多框架中,比如MyBatis-Plus,存在一个通用的Service基类,此中包罗了CRUD(创建、读取、更新、删除)利用的方法。save(user)大概是继承自如许的基类或实现了某个接口,此中界说了save方法。
- 来自注入的服务: 有大概save方法是某个服务类中的方法,该服务类被注入到当前类中,以便举行数据长期化利用。
- 来自工具类: 偶然,这些方法大概来自于一个工具类,专门用于处理处罚数据库利用。
示例代码如下:
- // 假设存在一个BaseService类
- public class UserService extends BaseService<User> {
-
-
- // ... 其他方法 ...
- public boolean save(User user) {
-
-
- // 实现保存用户的逻辑
- return true;
- }
- }
复制代码
- 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 的一部分)共同实现的。以下是这个过程的工作原理:
- 会话创建:
- 当用户初次访问一个必要跟踪会话的 Web 应用步伐时,Web 服务器会创建一个新的 HttpSession 对象。
- 这个 HttpSession 对象会被分配一个唯一的标识符,称为 sessionId。
- 发送 sessionId 到客户端:
- 服务器会在 HTTP 相应中通过 Set-Cookie 头部将 sessionId 发送给客户端。
- 客户端(通常是欣赏器)会存储这个 sessionId,并在后续的哀求中通过 Cookie 头部将其发送回服务器。
- 哀求中的 sessionId 提取:
- 当客户端发送哀求时,Web 服务器会查抄哀求头中的 Cookie 字段,以查找与该会话关联的 sessionId。
- 如果找到了 sessionId,服务器会利用这个标识符在服务器的会话管理器中查找对应的 HttpSession 对象。
- 绑定 HttpSession 到 HttpServletRequest:
- 一旦找到 HttpSession 对象,服务器会将其绑定到 HttpServletRequest 对象上。
- 如许,Servlet 可以通过调用 HttpServletRequest 的 getSession() 方法来访问 HttpSession。
以下是这个过程的关键参加者:
- Web 服务器:负责处理处罚 HTTP 哀求和相应,维护会话状态,以及发送和吸取 sessionId。
- Servlet 容器:实现了 Java Servlet API,提供了 HttpServletRequest 和 HttpSession 等接口,以及它们之间的交互逻辑。
- Servlet:当哀求到达时,Servlet 容器会调用 Servlet 的方法(如 doGet、doPost 等),并提供 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中数据。标题:存在耽误,因此不接纳
衍生思索:
我们找到了版本答案——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(
复制代码 |