LOADING...

加载过慢请开启缓存(浏览器默认开启)

loading

P-luminary

奋斗的开始——词形+词性+介词

2022/3/27

主谓表

主谓表的谓语一定是系动词

  • 表状态:be动词(连接主语和表语,无意义,用于构成句子)
  • 表感官:look,sound,taste,smell,feel (感官动词后只能加形容词)
  • 表变化:get,become,turn,grow,fall
  • 表保持:keep,stay,remain,stand
  • 表现象:seem,appear
  • 表终止或结果:prove

谓语

谓语只能有一个,要么是实义动词(词组),要么是系动词,谓语的本质就是具有时态的实义动词或者系动词

谓语就是具有时态(是表示动作发生的时间)和语态(表明主语和谓语的关系)的动词{实义动词(词组)、系动词}>,有时候主语自己不能发出动作,此时就需要使用被动语态

一个句子中一般只有一个谓语,我们需要把所有不作谓语的动词(词组)变成非谓语结构(V.-ing、V.-ed、V.-todo)

当需要动词(词组),而又没有动词(词组)时,永远可以加be动词,并且be动词一般不译出意思

主语

动词不定式短语做主语时,常用it做形式主语,而把真正的主语不定式后置,动词不定式放在句末

能做主语的成分有:代词名词(短语)非谓语结构从句(引导词+句子)

尽量多想该用什么句型或其他结构代替人称代词做主语

主语缺失(如何填补)
  • 用it做形式主语 => 和”天气温度时间“有关
  • Three be句型(只有一个谓语动词,多余的动词变为非谓语) => 需要句子带”“的含义
  • 主动语态变被动语态
  • 加人称代词
被动语态
  • 原主动句中的宾语变为被动句中的主语

  • 把动词变为被动形式,即”be + 动词过去分词”,并注意其随主语的人称和单复数变化,而动词的时态则保持不变

  • 原主动句中的主语,如需要,则放在by后面,以宾语形式出现,不需要可省略

    (主动) 主语 + 谓语 + 宾语······

    ​ 我 停 车

    (被动) 主语 + be done(+ by + 宾语)

    ​ 车 被停了

  • 无被动

    1. 系动词无被动(主系表)
    2. have,own,possess含义为“拥有” 无被动
    3. 表示“发生”或“爆发”的动词无被动(take place,happen,occur)
    4. 不及物动词无被动(主谓)

时态

时态 一般 进行 完成 完成进行
现在 do/does be doing have/has done have/has been doing
过去 did was/were doing had done had been doing
将来 will do will be doing will have done will have been doing
过去将来 would do would be doing would have done would have been doing

一般现在时(动词原形,第三人称单数)

一般过去时(过去式)

进行时(现在分词)

完成时(过去分词)

长难句分析

没有引导词(that,which,who,where)就是主句的谓语!!!

  1. 找动词(词组)
  2. 词汇解释
  3. 只翻译句子主干

词性的用法

名词

可数个体名词(指可以单独存在的人或物)、集体名词(指某类人或物的总称)

不可数物质名词(指物质和材料)、抽象名词(指看不见摸不着的东西)

可数名词[C]复数
  1. s,x,ch,sh ==> es
  2. **f,fe **==> ves
  3. 辅音结尾 + y ==> y改i + es
不可数名词[U]复数

数词 + 量词 + of + 不可数名词

​ a sense of sadness

​ 一 种 失落感

若无法确定名词是否可数,就在名词前加定冠词 the

冠词
  1. 表示特指
  2. 用于上文中提到的事物
  3. 世上独一无二的事物、方位名词
  4. 用在序数词、形容词最高级以及only前面
代词在句中能指代什么?
  1. 指代人
  2. 指代物
  3. 指代上文整句话的内容

形容词

  1. 形容词的三个级别:原级、比较级、最高级

  2. 与······一样(as + 形容词原级 + as)或(as + 形容词原级 + 名词 + as)

  3. 形容词的比较级前可以用表示程度的词短语来进行修饰(a bit,a little,very much)

  4. 比较级 + and + 比较级 ==> 越来越…

  5. the + 比较级 + 主谓

  6. more than 不仅仅是

  7. more···than···比···多;与其···不如说

  8. better than 比···更好

  9. rather than而不是

  10. less than 少于

副词

分类 举例
时间 just,recently,before,already,finally,today
地点 here,there,nearby
方式 fast,hard,carefully,slowly
程度 much,little,very,enough,particularly
频率 usually,often,sometimes,never,alw ays,hardly,constantly,frequency,seldom
  1. 修饰形容词,放在形容词前
  2. 修饰动词,可以放动词前后,也可以放句末
  3. 时间、地点副词可以放在句 首句末

介词

  • in(地点,指时间,在···的里面,语言;工具,在…方面)
  • on(具体日期,电子类,在…上 )
  • at(小地点,具体时刻[正午、黄昏],在···的里面)
  • for(接时间段[完成时],对于,表去向;目的,由于,作为)
  • by(by + 将来/过去时间:到···为止,被,在···旁边,表程度;数量,通过···的方式,by + 交通工具)
  • with(用,和···一起,随着,伴有···,由于,with + n. + adj./V.-ing/V.ed[独立主格])
  • of(···的)
  • from(从···)
  • about(大约,关于)
  • beyond(超出)
  • against(与···相对/相反)
  • as(作为)
  • before(在···之前[可以指时间;距离])
  • after(在···之后[可以指时间;跟随])
  • since(接过去时间点[用完成时]副词表示“从此以后”)
  • over(在···上面,超过,多于)
  • across(穿过[表面])
  • through(穿过[内部])
  • under(在···下面)
  • above(在···上面)
  • below(在···下面)
  • beside(在···旁边)
  • behind(在···后面)
  • between(在两者之间)
  • among(在三者或三者以上之中)
  • without(没有)
  • concerning(关于)

have,has表拥有,一个口诀全搞定

你有(you),我有(I),大家有(多人以上) → have

他(he),她(she),它(it),个人有(Jack,Lily…) → has


时间大于1天 用in、时间等于1天 用on、时间小于1天 用at

in年in月in四季 (in summer)

上午下午和晚上(in the morning)

on加周末和星期(On Monday)

还有具体某一天(On June 1st)

at要加时间点(at 5o’clock)

中午夜里和黎明(at night)


名词前用what形容词副词前用how形名相连就用what形名不连就用how


一般现在时 I do my homework.
一般过去时 I did my homework.
一般将来时 I will do my homework.
现在进行时 I am doing my homework.
过去进行时 I was doing my homework.
现在完成时 I have done my homework.
过去完成时 I had done my homework.
过去将来时 I would do my homework.

小朋友的可以用:lovely / cute / adorable 可爱的 pretty 漂亮的 sweet 甜美的

女生漂亮可以用:gorgeous 动人的 stunning 惊艳的 elegant 优雅的

男生帅气可以用:handsome 英俊的 good-looking 好看的 charming 有魅力的

风景漂亮可以用:breathtaking 令人惊叹的 ground 雄伟的 spectacular 壮丽的 magnificent 壮观的


0 zero 1 one 2 two 3 three 4 four 5 five 6 six 7 seven 8 eight
9 nine 10 ten 11 eleven 12 twelve 13 thirteen 14 fourteen 15 fifteen 16 sixteen 17 seventeen
18 eighteen 19 nineteen 20 twenty 30 thirty 40 forty 50 fifty 60 sixty 70 seventy 80 eighty
90 ninety 100 one hundred 1000 one thousand 100万 one million 10亿 one billion

有the 表示地点,无the 表示动作;

in hospital 住院, in the hospital 在医院

go to bed 去睡觉, go to the bed 在床边上


去某地 (go + 副词)、(go to + 名词)


一月:January 二月:February 三月:March 四月:April 五月:May 六月:June

七月:July 八月:August 九月:September 十月:October 十一月:November 十二月:December


在公交车上是in the bus 还是on the bus呢? 看腿!!

腿能站起来在上面走路的用on on the bus , on the train , on the plane

腿坐不直只能坐在里面的用**in ** in the taxi , in the car


在墙上是用in the wall 还是 on the wall 呢?

in “i”像钉子 在里面用 in the wall

on “o”像时钟 在墙上用 on the wall


英语中的几个

eat → 需要咀嚼的事物 eat an apple

take → 服用(药物) take some medicine

have → 一日三餐 have breakfast / lunch /dinner have an apple / some medicine


yesterday 昨天 the day before yesterday 前天 three days ago 大前天

tomorrow 明天 the day after tomorrow 后天 in three days 大后天


我要上厕所:I need to go to the bathroom / restroom / washroom

小朋友可以用: I need to pee(尿尿) / poop(便便)

朋友之间可以用:No.1(大号) / No.2(小号)


分开一段时间some time Do you have some time? 你有一些时间

相聚某个时候sometime Let’s do this sometime. 我们某个时候做这件事吧

分开s是几次some times I’ ve been here some times. 我来过这里几次

相聚s是有时sometimes I sometimes go there. 我有时会去那里


Mr. → 先生

Mrs. → 太太(已婚女性)

Miss →未婚女性(最爱美 身材最好 两个S曲线)

Ms. → 女士(未知是否结婚)


open 是把原来关上的东西打开了

turn on 是把原来的电路接通了 turn on the light

turn off 是把原来接通的电路断开了 turn off the light


阅读全文

破解百度网盘限速

2022/3/26

闲话少说,直接开始正题!!!

必备工具:

  1. Google Chrome 浏览器
  2. 游猴插件:(https://greasyfork.org/zh-CN)
  3. IDM:(https://wwx.lanzoux.com/b01nqc7yj)

操作步骤:

  1. 下载好游猴插件并安装到Google Chrome 浏览器

  2. 进入游猴页面,点击GreasyFork,搜索下载百度网盘简易下载助手(作者:软件小妹)

  3. 打开下载好的IDM(同时开启带有游猴插件的Google Chrome 浏览器)

    依次点击并 设置 —->选项 —-> 连接(修改默认最大连接数为: 4) —-> 文件类型(在自动开始下载下列类型的文件下修改下载类型<复制粘贴即可>):3GP 7Z AAC ACE AI AIF ALZ APK APP ARC ARJ ASF AVI BH BIN BR BUNDLE BZ BZ2 CDA CSV DIF DLL DMG DOC DOCX EGG EPS EXE FLV GZ GZIP IMG IPA ISO ISZ JAR KEXT LHA LZ LZH LZMA M4A M4V MDB MID MKV MOV MP3 MP4 MPA MPE MPEG MPG MSI MSU MUI OGG OGV PDF PKG PPT PPTX PSD PST PUB QT R0* R1* RA RAR RM RMVB RTF SEA SIT SITX SLDM SLDX TAR TBZ TBZ2 TGZ TIF TIFF TLZ TXZ UDF VOB VSD VSDM VSDX VSS VSSM VST VSTM VSTX WAR WAV WBK WIM WKS WMA WMD WMS WMV WMZ WP5 WPD WPS XLS XLSX XPS XZ Z ZIP ZIPX ZPAQ ZSTD) —->下载(将下方手动添加用户代理(UA)修改为:netdisk;7.0.3.2;PC;PC-Windows;10.0.17763),点击确定

  4. 打开Google Chrome 浏览器登录百度网盘网页版 登录账号 随后即可看见简易下载助手

  5. 选择想下载的文件点击简易下载助手(仅限单个文件) —->点击获取直链地址(失败就多试几次) —-> 扫公众号获得验证码 —-> 复制直链地址 —-> 返回IDM(新建任务 —-> 复制链接到地址) 即可成功破解百度网盘限速功能!!!!

阅读全文

PTA知识点精讲

2022/3/26

带空格的字符串输入(C++)

cin.get(str, len) 函数可以接收空格,遇回车结束输入,将换行符保留。

str用来存储输入行的数组名称

len是要读取的字符数

#include <iostream>
using namespace std;
int main(){
    char a[50];
    cin.get(a,50);
    cout << a <, endl;
    return 0;
}
输入:I love China回车结束输入,输出结果为I love China。


cin.getline(str, len) 函数可以接收空格,遇回车结束输入,将换行符丢弃。

#include <iostream>
using namespace std;
int main(){
    char a[50];
    cin.getline(a,50);
    cout << a << endl;
    return 0;
}
输入:I love China回车结束输入,输出结果为I love China。


gets() 函数以无限读取,以回车结束读取(在c++中运行会产生bug)

#inlcude <iostream>
#inlcude <cstdio>
using namespace std;
int main(){
    char a[50];
    cin >> a;
    gets(a);
    cout << a << endl;
    return 0;
}
输入:I love China回车结束输入,输出结果为love China。首字符自动丢弃。


④ getline() 函数若定义变量为string类型,则要考虑此函数

#include <iostream>
#include <cstring>
using namespace std;
int main(){
    string a;
    getline(cin,a);
    cout << a << endl;
    return 0;
}
输入:I love China回车并未结束输入,需回车两次才能结束输入,输出结果为:I love China.


cin 是C++中最常用的输入语句,当遇到空格或者回车键即停止

#include <iostream>
#include <cstring>
using namespace std;
int main(){
    char a[50];
    cin >> a;
    cout << a << endl;
    return 0;
}
输入:abcd遇回车输出abcd
缺点:只能输入没有空格的字符串,当输入中含有空格,则只能输出空格之前的字符
输入:I love China输入空格时输入并未停止,遇回车输入停止,输出I,空格后面的均未输出。


⑥ 用得到的函数

#include <isotream>
#include <string>
#include <cstring>
#include <cstdio>
#include <cmath>
#include <map>
#include <set>
#include <algorithm>
#include <sstream>
using namespace std;

⑦ 常用的知识点

  • string

    1. erase函数删掉指定位置的字符

      (1)erase(pos,n); 删除从pos开始的n个字符,比如erase(0,1)就是删除第一个字符
      (2)erase(position);删除position处的一个字符(position是个string类型的迭代器)
      (3)erase(first,last);删除从first到last之间的字符(first和last都是迭代器)

    2. 截取子串

      s.substr(pos, n) 截取s中从pos开始(包括0)的n个字符的子串,并返回

    3. replace(int i, int num, string s)从第i个位置以后的num个字符替换为s

    4. find

      (1)s.find(string s1) 返回s1在s中第一次出现的下一个位置

      (2)s.find(string s1,int num) 从下标num开始查找s1返回在s中的下标位置

    5. char转string
      char str[10];
      string s(str);//或者是s = str;

    6. string转char

      const char *str;
      string s;
      str = s.c_str();

  • 四舍五入

    double a = 11/2.0;
    int b = (int)(a+0.5);

sort 头文件 #include

默认是从小到大
#include <iostream>
#include <algorithm>
using namespace std;
int main(){
    int arr[] = {3,4,7,2,1};
    
    sort(arr,arr+5);
    for(int i = 0; i < 5; i++){
    cout << arr[i];
    }
    return 0;
}

从大到小仅需把第八行改为:
    sort(arr,arr+5,greater<int>());

也可以自造比较函数
    bool cmp(int x, int y){
    return x > y;
}
    sort(arr,arr+5,cmp);
  • 保留两位小数 -> %.2f

  • 比如要将‘8’转换为数字8,在语句中这样写就可以了,“ 8+‘0’ “

  • 字符到数字的转化可以利用s[i] - ‘0’

  • 字符串长度 -> int len = (int)strlen(str); 字符串记得结束符 ‘0’

  • “%”是取尾,“/”是留头 ==> a=1234567 ( a%10 = 7 ,a/10 = 123456)

  •     int a = 123456789;
        for(int i = 0; i < 1; i++)
            printf("%d\n",a/1%10);    =>9
             printf("%d\n",a/10%10);    =>8
            printf("%d\n",a/100%10); =>7
            printf("%d\n",a/1000%10); =>6
        输出每个数字:cout << a %= 10; a /= 10;
    
  • 字符串连接strcat() 字符串复制strcpy()

  • 循环输入终止模板:while(scanf(“%s”, s) != EOF)

PTA空格输出模板

for(int i = 0; i < N; i++){
    if(i != N - 1){
        printf("%lf", num[i]);
    }else{
        printf("%lf\n", num[i]);
    }
}
return 0;
阅读全文

MarkDown语法

2022/3/25

Markdown快速入门(typora)

1、代码块:

//代码块语法:
```java
```c++
①. java代码
package com.pcy.po;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

@Entity
@Table(name = "t_user")
public class User{
    
}
②. c++代码
#include <cstdio>
#include <cstring>
char s[1005];
int main(){
    while(scanf("%s",s) != EOF);
    cout << (int)strlen(s);
    return 0;
}

2、标题:

//标题语法
 # 一级标签
 ## 二级标签
 ### 三级标签
 #### 四级标签
 ##### 五级标签
 ###### 六级标签

一级标签

二级标签

三级标签

四级标签

五级标签
六级标签

3、字体:

//斜体
*第一次写博客*
//加粗
**第一次写博客**
//代码高亮显示
==第一次写博客==
//删除线
~~第一次写博客~~
//下划线
<u>第一次写博客</u>
//脚注
第一次写博客[^Asuna]

第一次写博客
第一次写博客
==第一次写博客==
第一次写博客
第一次写博客

第一次写博客[^Asuna]

4、引用

//引用语法
>作者:Kirito
>>作者:Kirito
>>>作者:Kirito
+
-

作者:Kirito

作者:Kirito

作者:Kirito

5、分割线:

//分割线
---
//分割线2
***


6、图片插入

//在线图片 || 本地图片
![我的照片](/image/XXX.png 或 用图床上传至GitHub 然后右键复制图片链接 再从浏览器中打开 再右键复制图像链接) --图片路径
[敲重点]Markdown可以通过普通的<img>标签来指定图片的 高度 和 宽度
<img src = "https://raw.githubusercontent.com/P-luminary/image/master/data/avatar.png" width = "50%">

我的照片

7、超链接

//超链接语法
[P-luminary GitHub](https://github.com/Luminarness)

8、列表

//无序列表 ("-"是实心圈  "+"是空心圈)
- 目录1
- 目录2
- 目录3
//有序列表
数字键 + "." + 名称
//列表嵌套(需要在子列表中的选项前面添加四个空格)
1. 第一项
    - 第一项嵌套的第一个元素
2. 第二项
    - 第二项嵌套的第二个元素
  • 目录1

  • 目录2

  • 目录3

    1. 首页

    2. 分类

    3. 标签

    4. 第一项

      • 第一项嵌套的第一个元素
    5. 第二项

      • 第二项嵌套的第二个元素

9、表格

在Typora中右键 -> 插入 -> 表格
日期 心情 饱和度
2022-03-25 1:27 非常好
阅读全文

PTA技巧

2022/3/25

时间优化:(写好的代码有测试点运行超时, 可以暂时放下, 不要浪费时间)

  1. 如果出现运行超时考虑是否因为非法输入导致死循环

  2. 尽量创建全局变量(创建一次,后面赋值),不要重复创建临时变量

  3. 尽量使用scanf,printf,而不使用cin,cout (C++的IO存在缓存(可取消))

#include <bits/stdc++.h> //万能头
using namespace std;
→→→    ios::sync_with_stdio(false); ←←←
→→→    cin.tie(0);cout.tie(0);    ←←←
  1. 使用引用访问容器中的元素(遍历,排序==>增强型for循环)
vector<int> vc{0,1,2,3};
for(int& i : vc)
    cout << i;
  1. 使用unordered_map,unordered_set替换map,set可以提升效率(注意改头文件)

  2. 为了实现排序功能,尽量使用sort对数组排序,而不要依赖与map和set的自动排序(map,set结构庞大)

  3. 一般递归算法都比较慢,深搜(DFS)可以通过剪枝优化,并查集可以进行路径压缩提升效率。


时间优化:

  1. 为了方便函数调用,可以都用全局变量(但这是个坏习惯)

  2. 不确定数据多少的情况,不一定非要用动态数组,可以直接根据题目给的数据范围定义一个较大的数组(大小应大于题目所给的范围),一般情况下不会出现内存超限 [数组大小别超过10^8,二维数组不能超过array(10000)(10000)]

  3. 答案错误很有可能是漏了题目的关键信息

  4. 格式错误是因为排版跟题目要求不同,可能多空格或空行

  5. 段错误是因为非法访问才会导致,一般情况下是因为数组访问越界

  6. 涉及到除法的要考虑除数不能为0,一般会有一个测试点

  7. 图论问题用深搜可以得到大部分的分

  8. 选取适当的结构(容器)可以让思路更清晰

  9. 熟悉编译器的调试功能(Debug)可以更快找出bug


必备知识:

​ 数据结构与算法:

  1. 树:二叉树的遍历方式,平衡二叉树的建树过程,根据两种遍历来建树

  2. 图:深搜,广(层)搜,并查集(推荐博客)

  3. 堆:堆结构,建堆过程(堆排序)。

  4. 链表:根据结点连接链表(一般通过结构体进行模拟)。

  5. 排序:熟悉快排和归并排序的排序过程

常用库、函数(黑科技)

容器、字符串处理、lambda表达式、自定义排序、类型转换、数值边界、数学函数、auto关键字、堆、常用功能函数(查找 计数 倒序)
知识点 说明 推荐博客
string容器 封装了一些对字符串的常用操作 C++ String详解
regex正则表达式 使用正则表达式来替换、查找字符串 C++ 正则表达式
stirngstream字符串IO类 使用字符串来进行IO操作 C++ stringstream
常用字符处理函数 isdigit(char ch)
  1. sort函数排序,自定义结构体比较方式

  2. STL标准库list,vector,queue,stack,map,set

  3. 数组(容器)倒序

    1. 倒序函数: void reverse(typename begin, typename end);
    2. 利用反向迭代器倒序(构造new string)
  4. auto关键字

  5. find函数

string str; // string类函数:未找到返回string::npos
if(str.find("substr") != string::npos);
    //......
set<int> st;
if(st.find(0) != st.end()); //set、map成员函数: 未找到返回尾迭代器
    //......
vector<int> vc;
if(find(vc.begin(),vc.end(),0) != vc.end()); //序列式容器可以通过algorithm库中的find
    //......
阅读全文

Redis实战

2022/1/15

黑马点评Redis

实现短信的登录和注册

com/hmdp/controller/UserController.java
package com.hmdp.controller;


import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import com.hmdp.dto.LoginFormDTO;
import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import com.hmdp.entity.UserInfo;
import com.hmdp.service.IUserInfoService;
import com.hmdp.service.IUserService;
import com.hmdp.utils.RegexUtils;
import com.hmdp.utils.SystemConstants;
import com.hmdp.utils.UserHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpSession;

import static com.baomidou.mybatisplus.core.toolkit.Wrappers.lambdaQuery;
import static com.baomidou.mybatisplus.core.toolkit.Wrappers.query;


/**
 * <p>
 * 前端控制器
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    private IUserService userService;

    @Resource
    private IUserInfoService userInfoService;

    /**
     * 发送手机验证码
     */
    @PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
        return userService.sendCode(phone, session);
    }

    /**
     * 登录功能
     * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
     */
    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
        String phone = loginForm.getPhone();
        // 校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号格式错误!");
        }
        // 校验验证码
        Object cacheCode = session.getAttribute("code");
        String code = loginForm.getCode();

        // 不一致 报错
        if (cacheCode == null || !code.equals(cacheCode)) {
            return Result.fail("验证码错误");
        }

        // 一致 根据手机号查询用户 用mybatisplus
        User user = userService.getOne(lambdaQuery(User.class).eq(User::getPhone, phone));
        // 判断用户是否存在
        if (ObjectUtil.isEmpty(user)) {
            // 不存在 创建新用户保存
            user = new User();
            user.setPhone(phone);
            user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
            userService.save(user);
        }
        // 存在 保存用户信息到session当中
        UserDTO userDTO = new UserDTO();
        userDTO.setId(user.getId());
        userDTO.setNickName(user.getNickName());
        userDTO.setPhone(user.getPhone());
        session.setAttribute("user", userDTO); // 确保保存的是 UserDTO 对象
        return Result.ok();
    }

    /**
     * 登出功能
     * @return 无
     */
    @PostMapping("/logout")
    public Result logout(){
        // TODO 实现登出功能
        return Result.fail("功能未完成");
    }

    @GetMapping("/me")
    public Result me(){
        // TODO 获取当前登录的用户并返回
        UserDTO user = UserHolder.getUser();
        return Result.ok(user);
    }

    @GetMapping("/info/{id}")
    public Result info(@PathVariable("id") Long userId){
        // 查询详情
        UserInfo info = userInfoService.getById(userId);
        if (info == null) {
            // 没有详情,应该是第一次查看详情
            return Result.ok();
        }
        info.setCreateTime(null);
        info.setUpdateTime(null);
        // 返回
        return Result.ok(info);
    }
}
com/hmdp/service/impl/UserServiceImpl.java
package com.hmdp.service.impl;

import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.LoginFormDTO;
import com.hmdp.dto.Result;
import com.hmdp.entity.User;
import com.hmdp.mapper.UserMapper;
import com.hmdp.service.IUserService;
import com.hmdp.utils.RegexUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpSession;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Override
    public Result sendCode(String phone, HttpSession session) {
        // 校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 不符合 返回错误信息
            return Result.fail("手机号格式错误!");
        }
        // 符合 生成验证码
        String code = RandomUtil.randomNumbers(6);
        // 保存验证码到session
        session.setAttribute("code", code);
        // 发送验证码
        log.debug("发送短信验证码成功,验证码:{}", code);
        // 返回ok
        return Result.ok();
    }

    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        return null;
    }
}

实现登录校验拦截器

package com.hmdp.config;

import com.hmdp.utils.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/user/code",
                        "/user/login"
                );
    }
}
package com.hmdp.utils;

import com.hmdp.dto.UserDTO;

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();
    }
}
package com.hmdp.utils;

import cn.hutool.core.util.ObjectUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 获取session
        HttpSession session = request.getSession();
        // 获取session中的用户
        Object user = session.getAttribute("user");
        // 判断用户是否存在
        if (ObjectUtil.isEmpty(user)) {
            // 不存在,拦截
            response.setStatus(401);
            return false;
        }
        // 存在,保存用户信息到ThreadLocal
        UserDTO userDTO = (UserDTO) user; // 确保 user 已经是 UserDTO 类型
        UserHolder.saveUser(userDTO);
        // 放行
        return true;
    }

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

集群的Session共享问题

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

session的替代方案应该满足:数据共享、内存存储、key、value结构

发送短信验证码后生成的验证码 保存验证码到Redis → 以手机号作为Key存储验证码 → 发送验证码

com/hmdp/controller/UserController.java
package com.hmdp.controller;


import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import com.hmdp.dto.LoginFormDTO;
import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import com.hmdp.entity.UserInfo;
import com.hmdp.service.IUserInfoService;
import com.hmdp.service.IUserService;
import com.hmdp.utils.RegexUtils;
import com.hmdp.utils.SystemConstants;
import com.hmdp.utils.UserHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpSession;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static com.baomidou.mybatisplus.core.toolkit.Wrappers.lambdaQuery;
import static com.baomidou.mybatisplus.core.toolkit.Wrappers.query;


/**
 * <p>
 * 前端控制器
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    private IUserService userService;

    @Resource
    private IUserInfoService userInfoService;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;


    /**
     * 发送手机验证码
     */
    @PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
        return userService.sendCode(phone, session);
    }

    /**
     * 登录功能
     * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
     */
    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
        String phone = loginForm.getPhone();
        // 校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号格式错误!");
        }
        // 校验验证码
//        Object cacheCode = session.getAttribute("code");
        // 从Redis中获取验证码并校验
        String cacheCode = stringRedisTemplate.opsForValue().get(SystemConstants.LOGIN_CODE_KEY + phone);
        String code = loginForm.getCode();

        // 不一致 报错
        if (cacheCode == null || !code.equals(cacheCode)) {
            return Result.fail("验证码错误");
        }

        // 存储

        // 一致 根据手机号查询用户 用mybatisplus
        User user = userService.getOne(lambdaQuery(User.class).eq(User::getPhone, phone));
        // 判断用户是否存在
        if (ObjectUtil.isEmpty(user)) {
            // 不存在 创建新用户保存
            user = new User();
            user.setPhone(phone);
            user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
            userService.save(user);
        }
        // 保存用户信息到redis中
        // 随机生成token,作为登录令牌
        String token = UUID.randomUUID().toString(true);
        // 将User对象转换为Hash存储
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        // 存储  用了putall 要把userDto转map
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                CopyOptions.create()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
        // 存储   因为存的时候不能设置有效期 要存完以后再去设置有效期
        String tokenKey = SystemConstants.LOGIN_CODE_TOKEN + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
        // 设置token有效期 30min
        stringRedisTemplate.expire(tokenKey, SystemConstants.REDIS_TIMEOUT, TimeUnit.MINUTES);
        // 但是我要如果状态在 就不断更新token的有效期 【更新token有效期】



        // 存在 保存用户信息到session当中
//        UserDTO userDTO = new UserDTO();
//        userDTO.setId(user.getId());
//        userDTO.setNickName(user.getNickName());
//        userDTO.setPhone(user.getPhone());
        // 确保保存的是 UserDTO 对象
        session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
        return Result.ok();
    }

    /**
     * 登出功能
     * @return 无
     */
    @PostMapping("/logout")
    public Result logout(){
        // TODO 实现登出功能
        return Result.fail("功能未完成");
    }

    @GetMapping("/me")
    public Result me(){
        // TODO 获取当前登录的用户并返回
        // userService.login(loginForm, session)
        UserDTO user = UserHolder.getUser();
        return Result.ok(user);
    }

    @GetMapping("/info/{id}")
    public Result info(@PathVariable("id") Long userId){
        // 查询详情
        UserInfo info = userInfoService.getById(userId);
        if (info == null) {
            // 没有详情,应该是第一次查看详情
            return Result.ok();
        }
        info.setCreateTime(null);
        info.setUpdateTime(null);
        // 返回
        return Result.ok(info);
    }
}

解决状态登录刷新的问题【双重拦截器】

请求 → 拦截器①(拦截一切路径)[流程:获取token、查询Redis用户、保存到ThreadLocal、刷新Token有效期、放行] → 拦截器②(拦截需要登录的路径)[查询ThreadLocal的用户, 不存在则拦截、存在则继续]

com/hmdp/utils/LoginInterceptor.java
package com.hmdp.utils;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.TimeUnit;

public class LoginInterceptor implements HandlerInterceptor {
    // 这个类的对象是手动new出来的 没有通过Spring容器 所以需要通过构造方法去引用
    // 拦截器不能给spring容器因为每次都会经过它去访问

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 判断是否需要拦截(ThreadLocal中是否有用户)
        if (UserHolder.getUser() == null) {
            // 没有,需要拦截,设置状态码
            response.setStatus(401);
            // 拦截
            return false;
        }
        // 有用户 放行
        return true;
    }
}
com/hmdp/utils/RefreshTokenInterceptor.java
package com.hmdp.utils;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;

public class RefreshTokenInterceptor implements HandlerInterceptor {
    // 这个类的对象是手动new出来的 没有通过Spring容器 所以需要通过构造方法去引用
    // 拦截器不能给spring容器因为每次都会经过它去访问
    private StringRedisTemplate stringRedisTemplate;

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

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            // 不存在:拦截,返回401状态码
//            response.setStatus(401);
            return true;
        }
        String key = SystemConstants.LOGIN_CODE_TOKEN + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        // 判断用户是否存在
//        if (ObjectUtil.isEmpty(user)) {
        if (userMap.isEmpty()){
            // 不存在,拦截
//            response.setStatus(401);
            return true;
        }
        // 将查询到的Hash数据转为UserDTO对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);

        // 存在,保存用户信息到ThreadLocal
//        UserDTO userDTO = (UserDTO) user; // 确保 user 已经是 UserDTO 类型
        UserHolder.saveUser(userDTO);
        // 刷新token有效期
        stringRedisTemplate.expire(key, SystemConstants.REDIS_TIMEOUT, TimeUnit.MINUTES);
        // 放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserHolder.removeUser();
    }
}
com/hmdp/config/MvcConfig.java
package com.hmdp.config;

import com.hmdp.utils.LoginInterceptor;
import com.hmdp.utils.RefreshTokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 登录拦截器
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                ).order(1);
        // token刷新拦截器
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}

商户查询缓存—练习题

com/hmdp/controller/ShopTypeController.java
/**
 * <p>
 * 前端控制器
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@RestController
@RequestMapping("/shop-type")
public class ShopTypeController {
    @Resource
    private IShopTypeService typeService;

    @GetMapping("list")
    public Result queryTypeList() {
//        List<ShopType> typeList = typeService
//                .query().orderByAsc("sort").list();
//        return Result.ok(typeList);
        return typeService.listShop();
    }
com/hmdp/service/impl/ShopTypeServiceImpl.java
/**
 * <p>
 *  服务实现类
 * </p>
 * @since 2021-12-22
 */
@Service
public class ShopTypeServiceImpl extends ServiceImpl<ShopTypeMapper, ShopType> implements IShopTypeService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public Result listShop() {
        String shopTypeCache = stringRedisTemplate.opsForValue().get(SystemConstants.CACHE_SHOP_TYPE_KEY);

        if (ObjectUtil.isNotEmpty(shopTypeCache)) {
            Long ttl = stringRedisTemplate.getExpire(SystemConstants.CACHE_SHOP_TYPE_KEY, TimeUnit.MINUTES);
            System.out.println("TTL for CACHE_SHOP_TYPE_KEY: " + ttl + " minutes");
            List<ShopType> shopTypeList = JSONUtil.toList(shopTypeCache, ShopType.class);
            return Result.ok(shopTypeList);
        }



        List<ShopType> queryShopTypeList = query().orderByAsc("sort").list();
        if (ObjectUtil.isEmpty(queryShopTypeList)) {
            return Result.fail("查询失败");
        }
        stringRedisTemplate.opsForValue().set(SystemConstants.CACHE_SHOP_TYPE_KEY, JSONUtil.toJsonStr(queryShopTypeList), SystemConstants.SHOP_TYPE_TTL, TimeUnit.MINUTES);
        return Result.ok(queryShopTypeList);
    }
}

添加商铺缓存

com/hmdp/service/impl/ShopServiceImpl.java
package com.hmdp.service.impl;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.SystemConstants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result queryById(Long id) {
        // 从redis中查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(SystemConstants.CACHE_SHOP_KEY + id);
        // 判断是否存在
        if (ObjectUtil.isNotEmpty(shopJson)) {
            // 存在,直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        // 不存在,根据id查询数据库
        Shop shop = getById(id);
        // 不存在,返回错误
        if (ObjectUtil.isEmpty(shop)) {
            return Result.fail("店铺不存在");
        }
        // 存在,写入redis
        stringRedisTemplate.opsForValue().set(SystemConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop));
        // 返回
        return Result.ok(shop);
    }
}

实现商铺缓存与数据库的双写一致性

先更新后删除
com/hmdp/controller/ShopController.java
 /**
     * 更新商铺信息
     * @param shop 商铺数据
     * @return 无
     */
    @PutMapping
    public Result updateShop(@RequestBody Shop shop) {
//        shopService.updateById(shop);
        return shopService.update(shop);

    }
com/hmdp/service/impl/ShopServiceImpl.java
@Override
    public Result update(Shop shop) {
        Long id = shop.getId();
        if (ObjectUtil.isEmpty(id)) {
            return Result.fail("店铺id不能为空");
        }
        // 更新数据库
        updateById(shop);
        // 删除缓存
        stringRedisTemplate.delete(SystemConstants.CACHE_SHOP_KEY + id);
        return Result.ok();
    }

缓存穿透的解决思路

  • 缓存空对象
    • 优点:实现简单,维护方便
    • 缺点
      • 额外的内存消耗
      • 可能造成短期的不一致

    缓存null的时候加一个TTL

  • 布隆过滤器算法[bitMap数组用Hash值去判断]
    • 优点:内存占用较少,没有多余key
    • 缺点
      • 实现复杂
      • 存在误判可能

缓存击穿的解决思路

  • 互斥锁
  • 逻辑过期

缓存雪崩的解决思路

  • 给不同的key的TTL添加随机值
  • 利用Redis集群提高服务的可用性

  • 给缓存业务添加降级限流策略

  • 给业务添加多级缓存

利用互斥锁解决缓存击穿问题setnx设置一把锁

setnx当key不存在的时候才会写入赋值
del lock释放锁
setnx lock 获取锁赋值

在linux中如果用的docker那么就用它
docker连接redis
首先先找到redis的镜像编码
docker ps
找到以后
docker exec -it 5832be55766e redis-cli

[root@localhost ~]# docker exec -it 5832be55766e redis-cli
127.0.0.1:6379> AUTH pass
OK
127.0.0.1:6379> setnx lock 1
(integer) 1
127.0.0.1:6379> get lock
"1"
127.0.0.1:6379> setnx lock 2
(integer) 0
127.0.0.1:6379> setnx lock 3
(integer) 0
127.0.0.1:6379> setnx lock 2
(integer) 0
127.0.0.1:6379> get lock
"1"
127.0.0.1:6379> del lock
(integer) 1
127.0.0.1:6379> setnx lock 3
(integer) 1
    
--------------------------------------------------------------------------
// 加个有效期TTL 避免锁得不到释放 产生死锁
 private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    // 防止自动拆箱产生空值
        return BooleanUtil.isTrue(flag);
    }

// 释放锁
private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }

SETNX 是 Redis 的一个命令,它是 “SET if Not eXists” 的缩写,用于在键不存在时设置键的值。如果键已经存在,SETNX 命令不会执行任何操作,并返回 0;如果键不存在,它会设置键的值,并返回 1

在分布式锁的实现中,SETNX 命令经常被用来尝试获取锁。如果某个键(通常用作锁的标识)不存在,则设置该键并获取锁;如果键已经存在,表示锁已经被其他客户端持有,因此当前客户端无法获取锁。

以下是 SETNX 命令的基本用法:

SETNX key value
  • key:你想要设置的键名。
  • value:你想要设置的值。

从 Redis 2.6.12 版本开始,SETNX 命令被 SET 命令的一个选项所取代,该选项允许你执行类似于 SETNX 的操作,同时还能设置键的超时时间(过期时间),这在实现分布式锁时非常有用。以下是使用 SET 命令的示例:

SET key value [EX seconds] [PX milliseconds] [NX|XX]
  • EX seconds:设置键的过期时间为 seconds 秒。
  • PX milliseconds:设置键的过期时间为 milliseconds 毫秒。
  • NX:只在键不存在时执行设置操作。
  • XX:只在键已经存在时执行设置操作。

因此,现代的分布式锁实现通常会使用如下命令:

SET lock_key unique_value EX 30 NX

这里,lock_key 是锁的键名,unique_value 是一个确保释放锁时安全的唯一值(通常是当前时间戳加上一个随机数或者客户端ID),EX 30 设置了键的超时时间为30秒,NX 确保只有在键不存在时才设置键。

互斥锁的实现

互斥锁通常用于单机应用,确保同一时刻只有一个线程可以执行某个操作。以下是一个简单的示例:

java

复制

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

public class MutexLock {
    private final StringRedisTemplate redisTemplate;
    private final String lockKey = "mutex_lock_key";

    public MutexLock(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public boolean tryLock() {
        ValueOperations<String, String> ops = redisTemplate.opsForValue();
        // 尝试设置锁,如果设置成功返回true,否则返回false
        return ops.setIfAbsent(lockKey, "locked");
    }

    public void unlock() {
        // 删除锁
        redisTemplate.delete(lockKey);
    }
}

分布式锁的实现

分布式锁用于分布式系统,确保在多个不同的机器或服务中只有一个可以执行某个操作。以下是一个简单的示例:

java

复制

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

public class DistributedLock {
    private final StringRedisTemplate redisTemplate;
    private final String lockKey = "distributed_lock_key";
    private final long lockTimeout = 30000; // 锁超时时间,例如30秒

    public DistributedLock(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public boolean tryLock() {
        ValueOperations<String, String> ops = redisTemplate.opsForValue();
        // 尝试设置锁,并设置超时时间
        Boolean success = ops.setIfAbsent(lockKey, "locked", lockTimeout, TimeUnit.MILLISECONDS);
        return Boolean.TRUE.equals(success);
    }

    public void unlock() {
        // 删除锁
        redisTemplate.delete(lockKey);
    }
}

基于逻辑过期方式解决缓存击穿的问题

不要直接在类中添加逻辑过期的字段,这样对代码不好
① 搞一个RedisData.java 设置一个逻辑过期 然后再去实现继承
② RedisData中搞一个private Object data;
热点数据需要提前导入

封装Redis工具类

基于StringRedisTemplate封装一个缓存工具类,满足下列需求:

方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击
穿问题
方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题

Redis总结

优惠券秒杀—全局唯一ID

全局唯一ID生成策略:

  • UUID
  • Redis自增
  • snowflake算法
  • 数据库自增

Redis自增ID策略:

  • 每天一个Key,方便统计订单量
  • ID构造是 时间戳 + 计数器

添加优惠券秒杀针对特价券

每个店铺都可以发布优惠券,分为平价券特价券

平价券可以任意购买,而特价券需要秒杀抢购:

  • tb_voucher:优惠券的基本信息,优惠金额、使用规则等
  • tb_seckill_voucher优惠券的库存开始抢购时间结束抢购时间。特价优惠券才需要填写这些信息
[root@localhost ~]# docker ps
CONTAINER ID   IMAGE                         COMMAND                  CREATED         STATUS             PORTS                                                                                                                                NAMES
03154afad287   seataio/seata-server:1.4.2    "java -Djava.securit…"   11 months ago   Up About an hour   8091/tcp, 0.0.0.0:9200->9200/tcp, :::9200->9200/tcp                                                                                  seata-server
c34a590a5649   xuxueli/xxl-job-admin:2.1.2   "sh -c 'java -jar /a…"   11 months ago   Up About an hour   0.0.0.0:8280->8080/tcp, :::8280->8080/tcp                                                                                            xxl-job-admin
3574b316b5d2   nacos/nacos-server:1.4.0      "bin/docker-startup.…"   11 months ago   Up About an hour   0.0.0.0:8848->8848/tcp, :::8848->8848/tcp                                                                                            nacos
951e83eb8120   rabbitmq:3.8.3-management     "docker-entrypoint.s…"   11 months ago   Up About an hour   4369/tcp, 5671/tcp, 0.0.0.0:5672->5672/tcp, :::5672->5672/tcp, 15671/tcp, 25672/tcp, 0.0.0.0:15672->15672/tcp, :::15672->15672/tcp   rabbitmq
6561e8458fa2   influxdb:1.8.0                "/entrypoint.sh infl…"   11 months ago   Up About an hour   0.0.0.0:8086->8086/tcp, :::8086->8086/tcp, 0.0.0.0:8088->8088/tcp, :::8088->8088/tcp, 0.0.0.0:9083->8083/tcp, :::9083->8083/tcp      influxdb
5832be55766e   redis:5.0.0                   "docker-entrypoint.s…"   11 months ago   Up About an hour   0.0.0.0:6379->6379/tcp, :::6379->6379/tcp                                                                                            redis
07ecd8b04853   mysql:5.7                     "docker-entrypoint.s…"   11 months ago   Up About an hour   0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp                                                                                 mysql
[root@localhost ~]# docker exec -it 5832be55766e redis-cli
127.0.0.1:6379> AUTH pass

库存超卖问题分析每秒上百上千的并发

超卖问题是经典的多线程安全问题,针对这一问题的常见解决方案就是加锁:

秒杀 → 一人一单拒绝黄牛

要求:修改秒杀业务,要求同一个优惠券,一个用户只能下一单

集群下线程的并发安全问题

分布式锁 — 基本原理

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁

MySQL Redis Zookeeper
互斥 利用mysql本身的互斥锁机制 利用setnx这样的互斥命令 利用节点的唯一性和有序性实现互斥
高可用
高性能 一般 一般
安全性 断开连接,自动释放锁 利用锁超时时间,到期释放 临时节点,断开连接自动释放

基于Redis的分布式锁

实现分布式锁时需要实现的两个基本方法:获取锁、释放锁

改进Redis的分布式锁

Redis的事务:可以使用看门狗

Redis的Lua脚本利用Lua去调用Redis(确保原子的一致性)

Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法大家可以参考网站:https://www.runoob.com/lua/lua-tutorial.html

redis.call('命令名称', 'key', '其他参数', ...)

例如我们要执行set name jack,则脚本是这样:
redis.call('set', 'name', 'jack')

例如我们要先执行set name Rose,再执行get name,则脚本如下:

# 先执行 set name jack
redis.call('set', 'name', 'jack')

# 再执行 get name
local name = redis.call('get', 'name')

# 返回
return name
执行Lua脚本

写好脚本后,需要用Redis命令来调用脚本,调用脚本的常见命令如下:

127.0.0.1:6379> help @scripting
EVAL script numkeys key [key ...] arg [arg ...]

要执行redis.call('set', 'name', 'jack')这个脚本,语法如下:

# 调用脚本 [0是脚本需要的key类型的参数个数 => 传参的变量]
EVAL "return redis.call('set', 'name', 'jack')"  0
# 不带变量
127.0.0.1:6379> EVAL "return redis.call('set', 'name', 'Jack')" 0
OK
127.0.0.1:6379> keys *
1) "name"
127.0.0.1:6379> get name
"Jack"

如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:【lua语言数组脚标是从1开始的

# 调用脚本【初始版】
127.0.0.1:6379> EVAL "return redis.call('set', 'name', 'Jack')" 0
# 调用脚本【进阶版】
127.0.0.1:6379> EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 name Rose
OK
127.0.0.1:6379> get name
"Rose"
127.0.0.1:6379> EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 name heihei
OK
127.0.0.1:6379> get name
"heihei"

基于Redis的分布式锁

释放锁的业务流程是这样的:

1.获取锁中的线程标示
2.判断是否与指定的标示 (当前线程标示) 一致
3.如果一致则释放锁 (删除)
4.如果不一致则什么都不做

-- 锁的key
-- local key = "lock:order:5"
-- 不能写死就传参
local threadId = KEYS[1]

-- 当前线程标示
-- local threadId = "fagsidajkldw-33"
-- 不能写死就传参
local threadId = ARGV[1]

-- 获取锁中的线程标识 get key
local id = redis.call('get', key)
-- 比较线程标示与锁中的标示是否一致
if(id == threadId) then
   -- 释放锁 del key
   return redis.call('del', key)
end
return 0

----------------------------- 进阶版 -------------------------------
-- 获取锁中的线程标识 get key
local id = redis.call('get', KEYS[1])
-- 比较线程标示与锁中的标示是否一致
if(id == ARGV[1]) then
   -- 释放锁 del key
   return redis.call('del', KEYS[1])
end
return 0
----------------------------- 进阶简化版 -------------------------------
-- 这里KEYS[1]就是锁的Key, 这里的ARGV[1],就是当前线程标示
-- 获取锁中的标示,判断是否与当前线程标示一致
if(redis.call('GET', KEYS[1]) == ARGV[1]) then
    -- 一致,则删除锁
    return redis.call('DEL', KEYS[1])
end
-- 不一致 直接返回
return 0

再次改进Redis的分布式锁经典白雪

基于Redis的分布式锁实现思路
利用setnx ex获取锁,并设置过期时间,保存线程标示
释放锁时先判断线程标示是否与自己一致,一致则删除锁

特性:
利用set nx满足互斥性
利用set ex保证故障时锁依然能释放,避免死锁,提高安全性
利用Redis集群保证高可用和高并发特性

需求:基于**Lua脚本**实现分布式锁的释放锁逻辑
提示:RedisTemplate调用Lua脚本的API如下:

com/hmdp/utils/SimpleRedisLock.java
package com.hmdp.utils;

import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Collections;
import java.util.concurrent.TimeUnit;

public class SimpleRedisLock implements ILock {

    // 不同业务不同锁
    private String name;
    private StringRedisTemplate stringRedisTemplate;
    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;

    // 是静态的要在静态代码块里面做初始化 这个类一加载 这个代码块就初始化完成了 不用每次释放锁再加载
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    public SimpleRedisLock(String name,
                           StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name,
                        threadId,
                        timeoutSec,
                        TimeUnit.SECONDS);
        // 自动拆箱避免空指针风险
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unLock() {
        // 调用lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                // 制造单集合
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId());
    }

    //    public void unLock() {
//        // 获取线程标识
//        String threadId = ID_PREFIX + Thread.currentThread().getId();
//        // 获取锁
//        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
//        // 判断锁是否ours
//        if (threadId.equals(id)) {
//            // 解锁
//            stringRedisTemplate.delete(KEY_PREFIX + name);
//        }
//    }
}

基于Redis的分布式锁优化

基于setnx实现的分布式锁存在下面的问题:
  • 不可重入:同一个线程无法多次获取同一把锁
  • 不可重试:获取锁只尝试一次就返回false,没有重试机制
  • 超时释放:锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患
  • 主从一致性:如果Redis提供了主从集群 [写操作访问主节点,读操作访问从节点],主从同步存在延迟,当主宕机时,如果从并同步主中的锁数据,则会出现锁实现

Redisson

Redisson是一个在Redis的基础上实现的]ava驻内存数据网格(In-MemoryData Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。

Redisson—官方网站
Redisson—GitHub地址

Redisson可重入锁原理

package com.hmdp;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

@Slf4j
@SpringBootTest
class RedissonTest {

    @Resource
    private RedissonClient redissonClient;

    private RLock lock;

    @BeforeEach
    void setUp() {
        lock = redissonClient.getLock("order");
    }

    @Test
    void method1() throws InterruptedException {
        // 尝试获取锁
        boolean isLock = lock.tryLock(1L, TimeUnit.SECONDS);
        if (!isLock) {
            log.error("获取锁失败 .... 1");
            return;
        }
        try {
            log.info("获取锁成功 .... 1");
            method2();
            log.info("开始执行业务 ... 1");
        } finally {
            log.warn("准备释放锁 .... 1");
            lock.unlock();
        }
    }
    void method2() {
        // 尝试获取锁
        boolean isLock = lock.tryLock();
        if (!isLock) {
            log.error("获取锁失败 .... 2");
            return;
        }
        try {
            log.info("获取锁成功 .... 2");
            log.info("开始执行业务 ... 2");
        } finally {
            log.warn("准备释放锁 .... 2");
            lock.unlock();
        }
    }
}

Redisson的锁重试和WatchDog机制

基于Redis的分布式锁优化
基于setnx实现的分布式锁存在下面的问题:

不可重入:同一个线程无法多次获得同一把锁
不可重试:获取锁只尝试一次就返回false,没有重试机制
超时释放:锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患
主从一致性:如果Redis提供了主从集群主从同步存在延迟,当主宕机时,如果从并同步主中的锁数据,则会出现锁实现

阅读全文
头像
Asuna
You are the one who can always get to me even with screen between us.