开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

基于jsp+Spring+mybatis的SSM企业门户网站

发表于 2021-08-25

这是我参与 8 月更文挑战的第 25天,活动详情查看: 8月更文挑战

前言

人类社会已经迈入了21世纪,信息技术的浪潮也冲击着社会的方方面面。以计算机互联网为核心的科学技术为社会各行各业提供了前所未有的机会和发展潜力。生产业也不例外。互联网走到今天,也有五年的光景了,之间经历了高潮和低谷,让许许多多为之奋斗的人们兴奋过,也沮丧过。一年前,当失去耐心的人开始疾呼互联网是泡沫,大家不要陷进去的时候,中国互联网仍然我行我素,走着自己该走的路。专家们对于互联网企业将有90%关门的预言如今也不攻自破。这些风风雨雨让许多真正了解互联网的人终于看明白了一点:互联网本身并不能创造产值,它是一个服务性行业。以Internet为基础的电子商务就是企业利用计算机技术和网络通讯技术进行商务活动的方式。它为企业与企业之间(BtoB)、企业与消费者(BtoC)之间提供了一种新型的商务活动模式。基于Internet 的企业网站作为企业进行电子商务活动的窗口,是企业为合作伙伴和客户提供访问企业内部各种资源的平台。通过网站,企业的合作伙伴,可以很快获取企业当前及近期的各种生产及经营信息,并根据这些信息对本企业的资源调配和生产调度进行合理优化:通过网站,企业的客户可以查询并了解企业所生产的各种产品的性能、价格等详细资料以及企业能给客户提供的各种服务:通过网站,企业能更好地宣传自己,提高企业知名度,进行有效的网络营销。为了提高产品规模以及知名度,使企业形象走上一个新台阶,利用现有的internet网环境,我们开发、建立良精集团企业网及销售系统。由于是初步的建立,所以只设计了一些基本功能,但功能基本上不会受到影响。

功能模块设计:

管理员角色功能:管理员登录,文章分类管理,文章列表管理,友情链接管理,招聘管理,留言管理,滚动图片管理,联系我们,关于我们,网站管理员管理,日志管理等功能。

学生角色下功能:用户首页,关于我们,服务领域发布,新闻动态,诚聘英才,在线留言,联系我们等功能。

技术框架:

HTML+CSS+JavaScript+jsp+mysql+Spring+SpringMVC+mybatis

数据库: Mysql数据库,任意版本均可,也可使用各种数据库工具,例如Navicat等。

功能截图:

用户首页:

关于我们:

服务领域:

新闻动态:

招聘英才:

在线留言:

联系我们:

后台管理:

用户登陆

后台首页:

内容管理:

文章列表:

招聘管理:

留言管理:

门户图片管理:

联系我们:

关于我们:

系统管理:

资源管理:

日志管理:


​

部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
ini复制代码@RestController
@Controller
public class ConsumerController {

@Autowired
private ConsumerServiceImpl consumerService;

@Configuration
public class MyPicConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
String os = System.getProperty("os.name");
if (os.toLowerCase().startsWith("win")) { // windos系统
registry.addResourceHandler("/img/avatorImages/**")
.addResourceLocations("file:" + Constants.RESOURCE_WIN_PATH + "\img\avatorImages\");
} else { // MAC、Linux系统
registry.addResourceHandler("/img/avatorImages/**")
.addResourceLocations("file:" + Constants.RESOURCE_MAC_PATH + "/img/avatorImages/");
}
}
}

// 添加用户
@ResponseBody
@RequestMapping(value = "/user/add", method = RequestMethod.POST)
public Object addUser(HttpServletRequest req){
JSONObject jsonObject = new JSONObject();
String username = req.getParameter("username").trim();
String password = req.getParameter("password").trim();
String sex = req.getParameter("sex").trim();
String phone_num = req.getParameter("phone_num").trim();
String email = req.getParameter("email").trim();
String birth = req.getParameter("birth").trim();
String introduction = req.getParameter("introduction").trim();
String location = req.getParameter("location").trim();
String avator = req.getParameter("avator").trim();

if (username.equals("") || username == null){
jsonObject.put("code", 0);
jsonObject.put("msg", "用户名或密码错误");
return jsonObject;
}
Consumer consumer = new Consumer();
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
Date myBirth = new Date();
try {
myBirth = dateFormat.parse(birth);
} catch (Exception e){
e.printStackTrace();
}
consumer.setUsername(username);
consumer.setPassword(password);
consumer.setSex(new Byte(sex));
if (phone_num == "") {
consumer.setPhoneNum(null);
} else{
consumer.setPhoneNum(phone_num);
}

if (email == "") {
consumer.setEmail(null);
} else{
consumer.setEmail(email);
}
consumer.setBirth(myBirth);
consumer.setIntroduction(introduction);
consumer.setLocation(location);
consumer.setAvator(avator);
consumer.setCreateTime(new Date());
consumer.setUpdateTime(new Date());

boolean res = consumerService.addUser(consumer);
if (res) {
jsonObject.put("code", 1);
jsonObject.put("msg", "注册成功");
return jsonObject;
} else {
jsonObject.put("code", 0);
jsonObject.put("msg", "注册失败");
return jsonObject;
}
}

// 判断是否登录成功
@ResponseBody
@RequestMapping(value = "/user/login/status", method = RequestMethod.POST)
public Object loginStatus(HttpServletRequest req, HttpSession session){

JSONObject jsonObject = new JSONObject();
String username = req.getParameter("username");
String password = req.getParameter("password");
// System.out.println(username+" "+password);
boolean res = consumerService.veritypasswd(username, password);

if (res){
jsonObject.put("code", 1);
jsonObject.put("msg", "登录成功");
jsonObject.put("userMsg", consumerService.loginStatus(username));
session.setAttribute("username", username);
return jsonObject;
}else {
jsonObject.put("code", 0);
jsonObject.put("msg", "用户名或密码错误");
return jsonObject;
}

}

// 返回所有用户
@RequestMapping(value = "/user", method = RequestMethod.GET)
public Object allUser(){
return consumerService.allUser();
}

// 返回指定ID的用户
@RequestMapping(value = "/user/detail", method = RequestMethod.GET)
public Object userOfId(HttpServletRequest req){
String id = req.getParameter("id");
return consumerService.userOfId(Integer.parseInt(id));
}

// 删除用户
@RequestMapping(value = "/user/delete", method = RequestMethod.GET)
public Object deleteUser(HttpServletRequest req){
String id = req.getParameter("id");
return consumerService.deleteUser(Integer.parseInt(id));
}

// 更新用户信息
@ResponseBody
@RequestMapping(value = "/user/update", method = RequestMethod.POST)
public Object updateUserMsg(HttpServletRequest req){
JSONObject jsonObject = new JSONObject();
String id = req.getParameter("id").trim();
String username = req.getParameter("username").trim();
String password = req.getParameter("password").trim();
String sex = req.getParameter("sex").trim();
String phone_num = req.getParameter("phone_num").trim();
String email = req.getParameter("email").trim();
String birth = req.getParameter("birth").trim();
String introduction = req.getParameter("introduction").trim();
String location = req.getParameter("location").trim();
// String avator = req.getParameter("avator").trim();
// System.out.println(username+" "+password+" "+sex+" "+phone_num+" "+email+" "+birth+" "+introduction+" "+location);

if (username.equals("") || username == null){
jsonObject.put("code", 0);
jsonObject.put("msg", "用户名或密码错误");
return jsonObject;
}
Consumer consumer = new Consumer();
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
Date myBirth = new Date();
try {
myBirth = dateFormat.parse(birth);
}catch (Exception e){
e.printStackTrace();
}
consumer.setId(Integer.parseInt(id));
consumer.setUsername(username);
consumer.setPassword(password);
consumer.setSex(new Byte(sex));
consumer.setPhoneNum(phone_num);
consumer.setEmail(email);
consumer.setBirth(myBirth);
consumer.setIntroduction(introduction);
consumer.setLocation(location);
// consumer.setAvator(avator);
consumer.setUpdateTime(new Date());

boolean res = consumerService.updateUserMsg(consumer);
if (res){
jsonObject.put("code", 1);
jsonObject.put("msg", "修改成功");
return jsonObject;
}else {
jsonObject.put("code", 0);
jsonObject.put("msg", "修改失败");
return jsonObject;
}
}

// 更新用户头像
@ResponseBody
@RequestMapping(value = "/user/avatar/update", method = RequestMethod.POST)
public Object updateUserPic(@RequestParam("file") MultipartFile avatorFile, @RequestParam("id")int id){
JSONObject jsonObject = new JSONObject();

if (avatorFile.isEmpty()) {
jsonObject.put("code", 0);
jsonObject.put("msg", "文件上传失败!");
return jsonObject;
}
String fileName = System.currentTimeMillis()+avatorFile.getOriginalFilename();
String filePath = System.getProperty("user.dir") + System.getProperty("file.separator") + "img" + System.getProperty("file.separator") + "avatorImages" ;
File file1 = new File(filePath);
if (!file1.exists()){
file1.mkdir();
}

File dest = new File(filePath + System.getProperty("file.separator") + fileName);
String storeAvatorPath = "/img/avatorImages/"+fileName;
try {
avatorFile.transferTo(dest);
Consumer consumer = new Consumer();
consumer.setId(id);
consumer.setAvator(storeAvatorPath);
boolean res = consumerService.updateUserAvator(consumer);
if (res){
jsonObject.put("code", 1);
jsonObject.put("avator", storeAvatorPath);
jsonObject.put("msg", "上传成功");
return jsonObject;
}else {
jsonObject.put("code", 0);
jsonObject.put("msg", "上传失败");
return jsonObject;
}
}catch (IOException e){
jsonObject.put("code", 0);
jsonObject.put("msg", "上传失败"+e.getMessage());
return jsonObject;
}finally {
return jsonObject;
}
}

总体来说这个项目功能相对还是比较简单优秀的、适合初学者作为课程设计和毕业设计参考
打卡Java项目更新 20 / 100天

大家可以点赞、收藏、关注、评论我啦 、

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

归并排序算法(Java实现包含自顶向下的归并排序算法以及自底

发表于 2021-08-24

“这是我参与8月更文挑战的第21天,活动详情查看:8月更文挑战”

一、归并排序的概念

​ 归并:即将两个有序的数组归并成一个更大的有序数组。根据归并这一操作,得出一种简单的递归排序算法:归并排序。要将一个数组排序,可以先(递归地)将它分为两半分别排序,然后将结果归并起来。

​ 归并排序的一个重要性质:它能够保证将任意长度为N的数组排序所需时间和NlogN成正比。它的主要缺点则是:它所需的额外空间和N成正比。

二、原地归并的抽象方法

(一)、原地归并的抽象方法的概念

​ 实现归并的一种直截了当的办法是将两个不同的有序数组归并到第三个数组中,两个数组中的元素应该都实现了Comparable接口。实现的方法很简单,创建一个适当大小的数组然后将两个输入数组的元素一个个从小到大放入这个数组中。

​ 但是,当用归并将要给大数组排序时,需要进行很多次归并,因此在每次归并时都创建一个新数组来存储排序结果会带来问题。

​ 因此,假如有一种原地归并的方法,就可以先将前半部分排序,然后将后半部分排序,然后再数组中移动元素而不需要额外的空间。但实际上已有的实现都非常复杂,尤其是和使用额外空间的方法相比。

​ 但将原地归并并抽象化仍然是有帮助的,与之对应的是我们的方法merge(a,lo,mid,hi),它将子数组a[lo…mid]和a[mid+1…hi]归并成一个有序的数组并将结果存放在a[lo…hi]中。

(二)、原地归并的抽象方法的代码示例

​ 该方法先将所有元素复制到aux[]中,然后再归并回arr[]中。

​ 在第二个for循环(归并)时进行了4个条件判断:左半边用尽(取右半边的元素)、右半边用尽(取左半边的元素)、右半边的当前元素小于左半边的当前元素(取右半边的元素)以及右半边的当前元素大于等于左半边的当前元素(取左半边的元素)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Java复制代码     public static void merge(Comparable[] arr, int lo, int mid, int hi) {
// 将arr[lo...mid]和a[mid+1...hi]归并
int i = lo;
int j = mid + 1;
Comparable[] aux = new Comparable[arr.length];
for (int k = lo; k <= hi; k++) {
aux[k] = arr[k];
}
for (int k = lo; k <= hi; k++) {
if (i > mid) {
arr[k] = aux[j++];
} else if (j > hi) {
arr[k] = aux[i++];
} else if (less(aux[j], aux[i])) {
arr[k] = aux[j++];
} else {
arr[k] = aux[i++];
}
}
}

// 对元素进行比较
private static boolean less(Comparable v, Comparable w) {
// 返回-1/0/1:表示v小于/等于/大于w
return v.compareTo(w) < 0;
}

三、自顶向下的归并排序

(一)、自顶向下的归并排序的概念

​ 自顶向下的归并排序是基于原地归并的抽象实现了另一种递归归并。

​ 这段递归代码是归纳证明算法能够正确地将数组排序的基础:如果它能将两个子数组排序,它就能够通过归并两个子数组来将整个数组排序。

(二)、自顶向下的归并排序的代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
Java复制代码public class Merge {

// 归并所需的辅助数组
private static Comparable[] aux;

public static void sort(Comparable[] arr) {
aux = new Comparable[arr.length];
sort(arr, 0, arr.length - 1);
}

private static void sort(Comparable[] arr, int lo, int hi) {
if (hi <= lo) {
return;
}
int mid = lo + (hi - lo) / 2;
sort(arr, lo, mid);
sort(arr, mid + 1, hi);
merge(arr, lo, mid, hi);
}

public static void merge(Comparable[] arr, int lo, int mid, int hi) {
// 将arr[lo...mid]和a[mid+1...hi]归并
int i = lo;
int j = mid + 1;
for (int k = lo; k <= hi; k++) {
aux[k] = arr[k];
}
for (int k = lo; k <= hi; k++) {
if (i > mid) {
arr[k] = aux[j++];
} else if (j > hi) {
arr[k] = aux[i++];
} else if (less(aux[j], aux[i])) {
arr[k] = aux[j++];
} else {
arr[k] = aux[i++];
}
}
}

// 对元素进行比较
private static boolean less(Comparable v, Comparable w) {
// 返回-1/0/1:表示v小于/等于/大于w
return v.compareTo(w) < 0;
}
}

(三)、自顶向下的归并排序的基本性质

​ 1、对于长度为N的任意数组,自顶向下的归并排序需要1/2NlgN——NlgN次比较。

​ 2、对于长度为N的任意数组,自顶向下的归并排序最多需要访问数组6NlgN次。

​ 以上两个性质说明了归并排序所需的实际和NlgN成正比,它表明我们只需要比遍历整个数组多个对数因子的时间就能将一个庞大的数组排序。可以用归并排序处理数百万甚至更大规模的数组,这是插入排序或选择排序做不到的。

​ 归并排序的主要缺点是辅助数组所使用的额外空间和N的大小成正比。

四、自底向上的归并排序

(一)、自底向上的归并排序的概念

​ 递归实现的归并排序是算法设计中分治思想的典型应用。我们将一个大问题分割成小问题分别解决,然后用所有小问题的答案来解决大问题。尽管我们考虑的问题是归并两个大数组,实际上我们归并的数组大多数都非常小。

​ 实现归并排序的另一种方法是先归并那些微型数组,然后再成对归并得到的子数组,如此这般,直到我们将整个数组归并在一起。这种实现方法比标准递归方法所需要的代码量更少。

(二)、自底向上的归并排序的代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
Java复制代码public class MergeBU {

// 归并所需的辅助数组
private static Comparable[] aux;

public static void sort(Comparable[] arr) {
// 进行lgN次两两归并
int n = arr.length;
aux = new Comparable[n];
// sz:子数组大小
for (int sz = 1; sz < n; sz = sz + sz) {
// lo:子数组索引
for (int lo = 0; lo < n - sz; lo += sz + sz) {
merge(arr, lo, lo + sz - 1, Math.min(lo + sz + sz - 1, n - 1));
}
}
}

private static void merge(Comparable[] arr, int lo, int mid, int hi) {
// 将arr[lo...mid]和a[mid+1...hi]归并
int i = lo;
int j = mid + 1;
for (int k = lo; k <= hi; k++) {
aux[k] = arr[k];
}
for (int k = lo; k <= hi; k++) {
if (i > mid) {
arr[k] = aux[j++];
} else if (j > hi) {
arr[k] = aux[i++];
} else if (less(aux[j], aux[i])) {
arr[k] = aux[j++];
} else {
arr[k] = aux[i++];
}
}
}

// 对元素进行比较
private static boolean less(Comparable v, Comparable w) {
// 返回-1/0/1:表示v小于/等于/大于w
return v.compareTo(w) < 0;
}
}

(三)、自底向上的归并排序的基本性质

​ 1、对于长度为N 的任意数组,自底向上的归并排序需要1/2NlgN至NlgN次比较,最多访问数组6NlgN次。

​ 当数组长度为2的幂时,自顶向下喝自底向上的归并排序所用的比较次数喝数组访问次数正好相同,只是顺序不同。其他时候,两种方法的比较和数组访问的次序会有所不同。

​ 自底向上的归并排序比较使用用链表组织的数组。这种方法只需要重新组织链表链接就能将链表原地排序(不需要创建任何新的链表节点)。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【收藏】MongoDB 常用查询语句汇总

发表于 2021-08-24

这是我参与8月更文挑战的第24天,活动详情查看:8月更文挑

前言

  我们经常使用的MySQL是最流行的关系型数据库管理系统,随着时代的进步,互联网的发展关系型数据库已经不满足于互联网的需求,因此出现了非关系数据库,本文将介绍MongoDB 常用查询语句。

初始MongoDB

  MongoDB 是一个基于分布式文件存储的数据库。由 C++ 语言编写。旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。

  MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。

  • database:数据库,SQL中的database含义。
  • collection:数据库表/集合,SQL中的table含义。
  • document:数据记录行/文档,SQL中的row含义。
  • field:数据字段/域,SQL中的column含义。
  • index:索引,SQL中的index含义。
  • primary key:主键,MongoDB自动将_id字段设置为主键,SQL中的primary key含义。

MongoDB特点

  • MongoDB 是一个面向文档存储的数据库,操作起来比较简单和容易。
  • 你可以在MongoDB记录中设置任何属性的索引 (如:FirstName=”Sameer”,Address=”8 Gandhi Road”)来实现更快的排序。
  • 你可以通过本地或者网络创建数据镜像,这使得MongoDB有更强的扩展性。
  • 如果负载的增加(需要更多的存储空间和更强的处理能力) ,它可以分布在计算机网络中的其他节点上这就是所谓的分片。
  • Mongo支持丰富的查询表达式。查询指令使用JSON形式的标记,可轻易查询文档中内嵌的对象及数组。
  • MongoDb 使用update()命令可以实现替换完成的文档(数据)或者一些指定的数据字段 。
  • Mongodb中的Map/reduce主要是用来对数据进行批量处理和聚合操作。
  • Map和Reduce。Map函数调用emit(key,value)遍历集合中所有的记录,将key与value传给Reduce函数进行处理。
  • Map函数和Reduce函数是使用Javascript编写的,并可以通过db.runCommand或mapreduce命令来执行MapReduce操作。
  • GridFS是MongoDB中的一个内置功能,可以用于存放大量小文件。
  • MongoDB允许在服务端执行脚本,可以用Javascript编写某个函数,直接在服务端执行,也可以把函数的定义存储在服务端,下次直接调用即可。
  • MongoDB支持各种编程语言:RUBY,PYTHON,JAVA,C++,PHP,C#等多种语言。
  • MongoDB安装简单。

快速开始

查询所有

查询数据库表/集合的所有数据

1
js复制代码db.getCollection("test").find();

查询指定字段数字类型

根据userId指定字段查询数据 数字类型

1
js复制代码db.getCollection("test").find({"userId":632});

查询指定字段字符串类型

根据goodsNo指定字段查询数据 字符串类型

1
js复制代码db.getCollection("test").find({"goodsNo":"789789789789"});

多条件查询

多条件查询指定字段数据信息

1
js复制代码db.getCollection("test").find({"userId":632,"supplyGoodsNo":"870000065481"});

查询全数据表中的指定字段数据信息

查询全数据表中的指定字段数据信息,返回主键_id

1
js复制代码db.getCollection("test").find({},{"userId":1,"supplyGoodsNo":1,"url":1});

查询全数据表中的指定字段数据信息,不返回主键_id

1
js复制代码db.getCollection("test").find({},{"userId":1,"supplyGoodsNo":1,"url":1,"_id":0});

查询数据集中指定区间的数据

查询数据集中指定区间的数据 比较大小的数据 其中大于> 【gt】小于<【gt】 小于< 【gt】小于<【lt】 大于等于>=【gte】小于等于<=【gte】 小于等于<= 【gte】小于等于<=【lte】 不等于!=【$ne】

1
js复制代码db.getCollection("test").find({"userId":{"$gte":500,"$lte":800}});

查询不等于的数据信息

查询不等于的数据信息

1
js复制代码db.getCollection("test").find({"userId":{"$ne":500}});

in 包含

in 包含某些数据

1
js复制代码db.getCollection("test").find({"userId":{"$in":[500,600,632]}});

not in 不包含

not in 不包含某些数据

1
js复制代码db.getCollection("test").find({"userId":{"$nin":[123,500,4000]}});

or 或者

or 或者 ,相当于SQL中的select * form test where userId = 632 or supplyGoodsNo = “870000065481”语句

1
js复制代码db.getCollection("test").find({"$or":[{"userId":632},{"supplyGoodsNo":"870000065481"}]});

mod 取模

mod 取模,相当于SQL中 select * from test where (userId mod 5) = 1语句。

1
js复制代码db.getCollection("test").find({"userId":{"$mod":[5,1]}});

not

not语句查询 相当于SQL中 select * from test where not (userId = 600)语句。

1
js复制代码db.getCollection("test").find({"$not":{"userId":600}});

空查询

空查询相当于SQL中 select * from test where userId is null 语句。

1
js复制代码db.getCollection("test").find({"userId":{"$in":[null],"$exists":true}});

正则查询

正则查询

1
js复制代码db.getCollection("test").find({"userId" : /63?/i});

数组查询

对数组的查询,字段url中,既包含”a”,又包含”b”的纪录

1
js复制代码db.getCollection("test").find({url:{$all:["a","e"]}});

对数组的查询, 字段url中,第4个(从0开始)元素是a的纪录

1
js复制代码db.getCollection("test").find({url.3,"a"});

对数组的查询, 查询数组元素个数是3的记录,$size前面无法和其他的操作符复合使用

1
js复制代码db.getCollection("test").find({"url" : {"$size" : 3}});

时间比较

比较时间大小,某个时间段之后的数据(方式1)

1
js复制代码db.getCollection("test").find({"createTime" : {"$gte" : ISODate("2021-08-12 16:03:06.815")}});

比较时间大小,某个时间段之后的数据(方式2)

1
js复制代码db.getCollection("test").find({"createTime":{$gte:new Date(2021,7,12)}});

结语

  作者介绍:【小阿杰】一个爱鼓捣的程序猿,JAVA开发者和爱好者。公众号【Java全栈架构师】维护者,欢迎关注阅读交流。

  好了,感谢您的阅读,希望您喜欢,如对您有帮助,欢迎点赞收藏。如有不足之处,欢迎评论指正。下次见。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

万字总结redis知识点

发表于 2021-08-24

redis数据类型剖析

1.string

sds数据结构,采用空间预分配和惰性空间释放来提升效率,缺点就是耗费内存。

1
2
3
4
5
c复制代码struct sdshdr {
int len; //长度
int free; //剩余空间
char buf[]; //字符串数组
};

空间预分配:当一个sds被修改成更长的buf时,除了会申请本身需要的内存外,还会额外申请一些空间。

惰性空间:当一个sds被修改成更短的buf时,并不会把多余的内存还回去,而是会保存起来。

总结:这种设计的核心思想就是空间换时间,只要 free还剩余足够的空间时,下次string变长的时候,不会像系统申请内存。

2.list

链表被广泛用于实现Redis的各种功能,比如列表键、发布与订阅、慢查询、监视器等。

1
2
3
4
5
c复制代码struct listNode {
struct listNode * prev; //前置节点
struct listNode * next; //后置节点
void * value;//节点的值
};

3.hash

不仅仅是数据类型为hash的才用到hash结构,redis本身所有的k、v就是一个大hash。例如我们经常用的set key value,key就是hash的键,value就是hash的值。

1
2
3
4
5
c复制代码struct dict {
...
dictht ht[2]; //哈希表
rehashidx == -1 //rehash使用,没有rehash的时候为-1
}

rehash:每个字典有两个hash表,一个平时使用,一个rehash的时候使用。rehash是渐进式的,分别是由以下触发:

  • serveCron定时检测迁移。
  • 每次kv变更的时候(新增、更新)的时候顺带rehash。

hash冲突:采用单向链表的方式解决hash冲突,新的冲突元素会被放到链表的表头。

4.zset

有序集合可以被用于一些排序场景,底层采用跳跃表实现。

1
2
3
4
5
6
7
8
9
c复制代码struct zskiplistNode {
struct zskiplistLevel {
struct zskiplistNode *forward;//前进指针
unsigned int span;//跨度
} level[];
struct zskiplistNode *backward;//后退指针
double score;//分值
robj *obj; // 成员对象
};

层高:每个跳跃表节点的层高在1-32之间。

跳跃:通过层来实现跨节点跳跃,达到加速访问的效果。

比如o1到o3只需要通过L4层跨度为2实现跨节点跳跃。

5.set

set的底层为了实现内存的节约,会根据集合的类型和数目而采用不同的数据结构来保存,当集合的元素都是整型且数量不多时会采用整数集合来存储。

1
2
3
4
5
c复制代码struct intset {
uint32_t encoding;//编码方式
uint32_t length;//集合包含的元素数量
int8_t contents[];//保存元素的数组
};

整数集合底层实现为数组,在添加元素的时候,根据需要会修改这个数组的类型(比如int16升级成int32)。

redis为什么那么快

在大型应用架构中,redis作为缓存层已经是非常普遍的现象,其中一个原因就是它非常快。

1.内存型数据库

redis是内存型数据库,大多数操作都是基于内存的。

2.特殊的数据结构

我们知道redis的数据结构是有特殊设计的,比如string类型采用sds数据结构来存储,每次string的空间不够时,总是尝试去申请更多的内存,每次string空间多余的时候,也不是把多余的空间还给系统,通过这种方式来减少内存的申请达到一种快,有序集合采用的跳跃表可以通过不同的层来达到加速访问节点的效果也是快速的体现,渐进式rehash也是高效快速的体现。

3.单线程

redis的主体模式还是单线程的,除了一些持久化相关的fork。单线程相比多线程的好处就是锁的问题,上下文切换的问题。官方也解释到:redis的性能不在cpu,而在内存。

4.IO多路复用

IO多路复用就是多个TCP连接复用一个线程,如果采用多个请求起多个进程或者多个个线程的模式还是比较重的,除了要考虑到进程或者线程的切换之外,还要用户态去遍历检查事件是否到达,效率低下。redis支持select、poll、epoll模式的多路复用,默认情况下,会选择系统支持的最好的模式。通过IO多路复用技术,用户态不用去遍历fds集合,通过内核通知告诉事件的到达,效率比较高。

pipeline的好处是什么

客户端执行一条命令的过程大概是这样:

命令请求->命令排队->命令执行->结果返回。 这个过程我们叫做RTT(Round trip time)往返时间。在实际工作中,我们可能遇到这样一种场景:我们需要不停的incr一个key,但是这个key不能永久存在,得加一个过期时间,于是我们的程序大概长这样:

1
2
3
c复制代码incr key #一次RTT
expire key time #一次RTT
#总共两次RTT

这样我们发现整个过程需要两个RTT。一般我们生产环境都是用的连接池,在连接池有足够的连接的时候,可能我还不需要去创建新的连接,这样就省了TCP三次握手的开销,当需要创建新的连接的时候,这时候还要去建立连接,整个开销又上去了。针对这种批处理命令,为了减少往返的开销,于是管道pipeline诞生了,通过管道我们可以把两条命令合并发送:

1
2
3
4
5
c复制代码# 伪代码
pipeline->send(incr key)
pipeline->send(expire key time)
pipeline->execute() #一次RTT
#总共一次RTT

这样就可以将2次的RTT减少成1次RTT。

节约资源:pipeline可以将多次的请求合并成一次请求,减少网络开销,节约时间。但是通过pipeline合并的请求不能太多,太多的话,可能占了大量的带宽,造成网络拥堵,同时太多的命令会造成客户端等待时间较长,推荐将大批量的命令拆成多个小批的pipeline。

非原子性:pipeline并非是原子性的,假设你通过pipeline发了incr key、expire key两条指令。但是redis执行expire的时候失败了,这样相当于你的key没有设置过期时间,这一点是需要注意的。

redis协议是什么样的

redis自己设计了一套序列化协议RESP,主要有容易实现、解析快、可读性好几个特点。协议的每部分都是\r\n结束的,可以通过特殊符号来区分出数据的类型:

  • 单行回复:以+号开头。
  • 错误回复:以-号开头。
  • 整数回复:以:号开头。
  • 批量回复:以$号开头。
  • 多条批量回复:以*号开头。

因为通过redis客户端看不出来效果,我这里用nc这个tcp工具来模拟下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
c复制代码nc 127.0.0.1 6379 #连接上redis
#发起ping
ping
+PONG

#发送一个不存在的命令
hi
-ERR unknown command 'hi'

#age+1
incr age
:1

#get
$2
go

#mget
mget name1 name2
*2
$2
go
$4
java

上面的每行都是\r\n结束的,Redis协议的实现性能可以和二进制协议的实现性能相媲美, 并且由于 Redis协议的简单性,大部分语言都可以实现这个协议。

redis是如何保证原子性的

什么原子性?程序在执行过程中,要么全部都执行,要么全部都不执行,不可能执行了一半,滞留了一半。我们知道redis是IO多路复用模型,即一个线程来处理多个TCP连接,这样的好处就是,即使客户端并发请求,也得排队处理,一定程度上解决了多线程模型带的并发问题,但是单线程模型并不能解决原子性问题。

还是以incr和expire为例:

1
2
c复制代码命令1:incr key
命令2:expire key time

不管你是pipeline还是非pipeline,这样两条操作你都是无法保持原子性的,命令1的失败不影响命令2的执行,命令2的执行也不影响命令1的结果。

支持原子操作的命令

假设现在有两个客户端希望修改某个值,它们操作的流程是先获取原先的值,再更新新值=老值+1
但是由于时间顺序的问题,可能它们是这样执行的顺序:

1
2
3
4
5
c复制代码# key的value刚开始是10
客户端1:get key #10
客户端2:get key #10
客户端1:set key 11 #11
客户端2:set key 11 #11

会发现客户端2更新的值丢失了,原因在于客户端1在获取到key的值之后,没来的及更新,这时客户端2的get进来了,导致客户端2获取的是老值。

对于这种场景,可以用redis的incr来替代,incr是原子的操作的,它把get和set合并在一起,再利用redis的单线程特性,第一个incr进来的时候,第二个incr一定是等待的,这样就不会存在更新丢失的问题。类型的原子命令还有decr 、setnx。

事务+监控

结合watch监控和事务也可以解决,每个客户端可以通过watch来监控自己即将要更新的key,这样在事务更新的时候,如果发现自己监控的key被修改了,那么拒绝执行,事务执行失败,这样就不会存在更新覆盖的问题。watch的本质还是乐观锁,当客户端执行watch的时候,实际上是watch的key会维护一个客户端的队列,这样就知道这个key被哪些客户端监视了。当其中的一个客户端执行完毕之后,那么它会从这个队列中移除,并且会把这个列表中剩余的所有客户端的CLIENT_DIRTY_CAS标识打开,这样剩余的客户端在执行exec的时候,发现自己的CLIENT_DIRTY_CAS已经被打开,那么就会拒绝执行。

1
2
3
4
5
6
7
8
9
10
c复制代码客户端1 > watch key
OK
客户端1 > MULTI
OK
客户端1 > SET key 100
QUEUED
客户端1 > EXEC
1) OK
客户端1 > GET key
"100"
1
2
3
4
5
6
7
8
9
10
c复制代码客户端2 > watch key
OK
客户端2 > MULTI
OK
客户端2 > SET key 101
QUEUED
客户端2 > EXEC
(nil)
客户端2 > GET key
"100"

客户端1和客户端2都尝试来修改key的值,然而因为客户端1先更新了。那么客户端2在更新的时候通过watch发现值已经被修改了,就会拒绝执行,返回个nil

lua脚本

redis在2.6之后开始支持开发者编写lua脚本传到redis中,使用lua脚本的好处是:

  1. 减少网络开销,通过lua脚本可以一次性的将多个请求合并成一个请求。
  2. 原子操作,redis将lua脚本作为一个整体,执行过程中,不会被其他命令打断,不会出现竞态问题。
  3. 复用,客户端发送的lua脚本会永远存在redis服务中。

我们来看看lua是如何保证原子性的,假设现在有个逻辑,我们要先判断key不存在,再设置key,存在的话,就不设置了。

不用lua:

1
2
3
4
5
c复制代码#伪代码
客户端1:if key not exist
客户端2:if key not exist
客户端1:set key 10086
客户端2:set key 10086 #多余的

由于上述不具备原子性,导致客户端2多执行了一次。

使用lua:

1
2
3
4
c复制代码127.0.0.1:6379> eval "if redis.call('get', KEYS[1]) == false then redis.call('set', KEYS[1], ARGV[1]) return 0 else return 1 end" 1 key "10086"
(integer) 0 #执行成功
127.0.0.1:6379> get key1
"10086"

使用了lua之后,首先redis本身会把整个lua脚本当成一个整体,运行期间不会收到其他命令的干扰。使用lua脚本之后,我们可以编写自己的复杂业务来保证原子性。当lua脚本很长时,在命令行里执行不太优雅,redis提供load lua的命令,导入lua脚本文件,导入成功后会返回一个sha1编码的id,后期通过这个id可以反复执行。

生产环境大key如何删除

当生产环境去删除一个大key的时候,可能会造成线上阻塞,这是一个非常危险的操作,可以根据实际情况选择以下方法:

  1. 根据业务场景判断,低峰期去删除可以有效降低损失。
  2. 对于hset可以通过scan分批获取删除,对于set和zset可以每次取一批数据删除,对于list直接pop删除。
  3. redis4.0支持unlink异步删除,不阻塞主线程。

缓存穿透、击穿、雪崩如何解决

高性能架构中,我们一般会在db层之上加个cache层,因为cache的数据是在内存中的,这样当大量数据访问的时候,如果cache的命中率高,那么就可以阻挡大量的请求打到我们的db中去,起到保护db和加速访问的作用。如果cache miss了,那么当数据库中读到数据的时候,我们也会写入一份到缓存中去,这样下次请求的时候也会从缓存获得数据。如果某个cache一直miss,或者某个cache miss之后突然并发进来大量请求以及缓存在某一瞬间大面积失效咋办?

1.缓存穿透

当我们访问一个非法的数据的时候(缓存和数据库都不存在的数据),每次先去缓存获取,获取不到,然后去数据库获取,依然获取不到,比如user_id=-1这种(一个用户的id是不可能为负数的)。出现这种情况,每次必然是要去数据库请求一次不存在的数据,这时候因为没有数据,所以也不会写入缓存,下一次同样的请求还是会重蹈覆辙。


解决:

  • 前端校验:某些情况,比如用户在自己的个人中心页面通过商品订单ID来搜索,前端可以判断下对于非法ID(如负数)的订单直接拦截。
  • 后端校验:在接口的开始处,校验一些常规的正负数,比如负数的user_id直接返回报错。
  • 空值缓存:有时候我们也对于数据库查不到的数据,也做个缓存,这个缓存的时间可以短一些。
  • hash拦截:hash校验使用一些数据量不多的场景,比如店铺的商品信息,上架一个商品的时候,我们商品做下hash标记(map[“商品ID”]=1),这样如果请求的商品id都不在hash表里,直接返回了。
  • 位图标记:类似hash,但是使用比特位来标记。
  • 布隆过滤器:当我们关心的数据量非常大的时候 hash和位图那得多大,不现实,这时可以用布隆过滤器,布隆过滤器不像hash和位图那样可以做到百分百的拦截,但是可以做到绝大部分的非法的拦截。布隆过滤器的思想就是在有限的空间里,通过多个hash函数来定位一条数据,当只要有一个hash没中,那么一定是不存在的,但是当多个hash全中的话,也不一定是存在的,这一点是需要注意的。

2.缓存击穿

热点数据在某一时刻缓存过期,然后突然大量请求打到db中,这时如果db扛不住,可能就挂了,引起线上连锁反应。


解决:

  • 分布式锁:分布式系统中,并发请求的问题,第一时间想到的就是分布式锁,只放一个请求进去(可以用redis setnx、zookeeper等等)
  • 单机锁:也并不一定非得需要分布式锁,单机锁在集群节点不多的情况下也是ok的(golang可以用synx.mutex、 java 可以用JVM 锁),保证一台机器上的所有请求中只有一个能进去。假设你有10台机器,那么最多也就同时10个并发打到db,对数据库来说影响也不大。相比分布式锁来说开销要小点,但是如果你的机器多达上千,还是慎重考虑。
  • 二级缓存:当我们的第一级缓存失效后,也可以设置一个二级缓存,二级缓存也可以拦截下,二级缓存可以是内存缓存也可以是其他缓存数据库。
  • 热点数据不过期:某些时候,热点数据就不要过期。

3.缓存雪崩

当某一些时刻,突然大量缓存失效,所有的请求都打到了db,与缓存击穿不同的是,雪崩是大量的key,击穿是一个key,这时db的压力也不言而喻。

解决:

  • 缓存时间随机些:对于所有的缓存,尽量让每个key的过期时间随机些,降低同时失效的概率
  • 上锁:根据场景上锁,保护db
  • 二级缓存:同缓存击穿
  • 热点数据不过期:同缓存击穿

redis的持久化方案有哪些

1.rdb

save:SAVE是手动保存方式,它会使redis进程阻塞,直至RDB文件创建完毕,创建期间所有的命令都不能处理。

1
2
3
c复制代码127.0.0.1:6379> save
OK
27004:M 31 Jul 15:06:11.761 * DB saved on disk

bgsave:与SAVE命令不同的是BGSAVE,BGSAVE可以不阻塞redis进程,通过BGSAVE redis会fork一个子进程去执行rdb的保存工作,主进程继续执行命令。

1
2
3
c复制代码127.0.0.1:6379> BGSAVE
Background saving started
27004:M 31 Jul 15:07:08.665 * Background saving terminated with success

BGSAVE执行期间与其他一些IO命令会存在一些互斥:

  • BGSAVE期间,所有的SAVE命令会被拒绝执行,避免父子进程同时执行,造成一些竞争问题。
  • BGSAVE期间,如果有新的BGSAVE那么也就被拒绝,也是竞争问题。
  • BGSAVE期间,如果来了个BGREWRITEAOF,那么BGREWRITEAOF会被延迟到BGSAVE之后再执行。
  • 如果BGREWRITEAOF在执行,那么BGSAVE命令会被拒绝。

BGSAVE与BGREWRITEAOF都是由两个子进程处理,目标也是不同的文件,本身没什么冲突,主要是两个都可能要大量的IO,这对服务本身来说不是很友好。

用户可以通过配置,让每隔一段时间来执行bgsave:

1
2
3
c复制代码save 900 1 #900s内至少修改了1次
save 300 10 #300s内至少修改了10次
save 60 10000 #60s内至少修改了10000次

以上条件只要满足了一个就可以执行bgsave。

这里涉及到两个参数来记录次数和时间,分别是dirty计数器和lastsave。

  • dirty计数器记录距离上一次成功执行SAVE命令或者BGSAVE命令之后,服务器对数据库状态(服务器中的所有数据库)进行了多少次修改(包括写入、删除、更新等操作)。
  • lastsave属性是一个UNIX时间戳,记录了服务器上一次成功执行SAVE命令或者BGSAVE命令的时间。

以上两个指标是基于redis的serverCron来完成的,serverCron是一个定期执行的程序,默认每隔100ms执行一次。每次serverCron执行的时候会遍历所有的条件,然后检查计数是否ok,时间是否ok,都ok的话就执行一次bgsave,并且记录最新的lastsave时间,重置dirty为0。

导入:redis没有专门的用户导入的命令,redis在启动的时候会检测是否有RDB文件,有的话,就自动导入。

1
2
3
c复制代码27004:M 31 Jul 14:46:51.793 # Server started, Redis version 3.2.12
27004:M 31 Jul 14:46:51.793 * DB loaded from disk: 0.000 seconds
27004:M 31 Jul 14:46:51.793 * The server is now ready to accept connections on port 6379

DB loaded from disk就是载入rdb的描述,服务在载入RDB期间是阻塞的。当然如果也开启了AOF,那么就会优先使用AOF来恢复,只有在服务器未开启AOF的时候,才会选择RDB来恢复数据,导入的时候也会自动过滤过期的key。

2.aof

AOF就是一个命令追加的模式,假设执行了:

1
2
3
4
c复制代码RPUSH list 1 2 3 4
RPOP list
LPOP list
LPUSH list 1

最终以redis协议方式存储:

1
c复制代码*2$6SELECT$10*6$5RPUSH$4list$11$12$13$14*2$4RPOP$4list*2$4LPOP$4list*3$5LPUSH$4list$11

aof先是写到aof_buf的缓冲区中,redis提供三种方案将buf的缓冲区的数据刷到磁盘,当然也是serverCron来根据策略处理的。

1
2
3
c复制代码appendfsync always
appendfsync everysec
appendfsync no
  1. always:将aof_buf缓冲区所有的内容写入并同步到AOF文件。
  2. everysec:将aof_buf缓冲区所有的内容写入AOF文件,如果上次同步的时间和这次的超过1s,那么再次执行同步,并且这个同步是由一个线程完成的。
  3. no:将aof_buf写入AOF文件,但是不执行同步,何时同步由操作系统决定。

在现代操作系统中,为了提高文件写入的效率,当我们调用write写入一个数据的时候,操作系统并不会立刻写入磁盘,而是放在一个缓冲区里,当缓冲区满了或者到了一定时间后,才会真正的刷入到磁盘中。这样存在一定风险,就是内存的数据没等到刷入磁盘的时候,机器宕机了,那么数据就丢失了,于是操作系统也提供了同步函数fsync,让用户可以自己决定什么时候同步。

AOF重写:随着命令越来越多,aof的体积会越来越大,例如:

1
2
3
4
c复制代码incr num
incr num
incr num
incr num

执行4条incr num,num的最终的值是4,然后可以直接用一条set num 4 代替,这样存储就节省了很多。重写也不是分析现有aof,重写就是从数据库读取现有的key,然后尽量用一条命令代替。并不是所有的都可以用一条命令代替,例如sadd 每次最多只能add 64个,如果超过64个就要分批了。

创建新的aof -> 遍历数据库 ->遍历所有的key->忽略过期的->写入aof。

fork:aof的重写涉及大量的IO,在当前进程里去做肯定不合适,理所当然也是fork一个子进程来做,不使子线程的原因是避免一些锁的问题。
使用子进程需要考虑的问题就是在子进程写入的时候,主进程还在源源不断的接收新的请求,那么针对这种情况redis设置了一个aof重写缓冲区,缓冲区在子进程创建的时候开始使用,那么在新的请求来的时候,除了写入aof缓冲区外,还要写入aof重写缓冲区,此过程不阻塞。
那么在子进程重写完了之后,会发信号给主进程,主进程收到信号后,会把重写缓冲区的数据再次同步给新的aof文件,然后rename新的aof,原子的覆盖老的aof,完成重写,这个过程是阻塞的。

何时执行重写:

1
2
c复制代码auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
  1. 100代表当前AOF文件是上次重写的两倍时候才重写
  2. 文件最小重写大小 默认64mb

导入:
redis启动的时候,会创建一个伪客户端,然后执行aof文件里面的命令。

redis过期键是如何删除的

  • 惰性删除:每当我们获取一个key的时候,先去检查下它的过期时间是否已到,如果已经过期,那么执行删除。
  • 定期删除:redis对于带过期时间的和不带过期时间的key分了两个字典,serverCron每次执行的时候会从带过期时间的字典里随机取一部分key检查,如果过期则删除。

rdb和aof对过期键是如何处理的

rdb:对于主从模式来说,主服务器载入rdb文件的时候会自动过滤过期的key,从服务器载入rdb文件的时候不会过滤过期的key,因为主从在进行同步数据的时候,从会清空自己的数据。

aof:当服务器以AOF持久化模式运行时,如果数据库中的某个键已经过期,但它还没有被惰性删除或者定期删除,那么AOF文件不会因为这个过期键而产生任何影响。当过期键被惰性删除或者定期删除之后,程序会向AOF文件追加(append)一条DEL命令,来显式地记录该键已被删除。aof重写的时候会自动过滤过期的key。

主从模式下过期键是怎样的

一般主从模式下,主负责提供写,从负责提供读。从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,而是继续像处理未过期的键一样来处理过期键。主服务器在删除一个过期键之后,会显式地向所有从服务器发送一个DEL命令,告知从服务器删除这个过期键。

redis的淘汰策略是怎样的

淘汰策略是一个灵活的配置选项,一般根据业务来选择合适的淘汰策略,当然是我们进行add key或者update一个更大的key,这时候如果内存不足会触发我们设置的淘汰策略。

  • noeviction:当内存使用超过配置的时候会返回错误,不会驱逐任何键
  • allkeys-lru:通过LRU算法驱逐最久没有使用的键
  • volatile-lru:通过LRU算法从设置了过期时间的键集合中驱逐最久没有使用的键
  • allkeys-random:从所有key中随机删除
  • volatile-random:从过期键的集合中随机驱逐
  • volatile-ttl:从配置了过期时间的键中驱逐马上就要过期的键
  • volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键
  • allkeys-lfu:从所有键中驱逐使用频率最少的键

serverCron是干嘛的

redis内部有定期执行的函数servercron,它默认每隔100ms执行一次,它的作用主要是以下:

  • 更新服务器时间缓存:redis有不少功能需要获取当前时间,每次获取时间的话要进行一次系统调用,对于一些对时间实时性要求不是很高的场景,redis通过缓存来减少系统调用的次数。实时性要求不高的主要有:打印日志、更新服务器的LRU时钟、决定是否执行持久化任务、计算服务器上线时间。对于设置设置过期时间、添加慢查询日志还是会系统调用获取实时时间的。
  • 更新LRU时钟:redis中有个lruclock属性用于保存服务器的lru时钟,每个对象也有一个lru时钟,通过服务器的lru减去对象的lru,可以得出对象的空转时间,serverCron默认会以每10s一次更新一次lruclock,所以这也是一个模糊值。
  • 更新每秒执行的次数:这是一个估算值,每次执行的时候,会根据上一次抽样时间和服务器的当前时间,以及上一次抽样的已执行命令数量和服务器当前的已执行命令数量,计算出两次调用之间,服务器平均每毫秒处理了多少个命令请求,然后将这个平均值乘以1000,这就得到了服务器在一秒钟内能处理多少个命令请求的估计值。
1
2
3
makefile复制代码INFO stats
...
instantaneous_ops_per_sec:1
  • 更新内存峰值:每次执行的时候会查看当前内存使用量,然后和上一次的峰值对比,判断是否需要更新峰值。
1
2
3
4
c复制代码INFO stats
...
used_memory_peak:2026832
used_memory_peak_human:1.93M
  • 处理SIGTERM信号:收到SIGTERM信号的时候,redis会打个标识,然后等待serverCron到来的时候,根据标识状态处理一些在shutdown之前的工作,比如持久化。
  • 处理客户端资源:如果客户端和服务之间很长时间没通信了,那么就会释放这个客户端。如果输入缓冲区的大小超过了一定长度,那么就会释放当前客户端的输入缓冲区,然后重建一个默认大小的输入缓冲区,防止输入缓冲区占用较大的内存。同时也会关闭输出缓冲区超过限制的客户端。
  • 延迟执行aof重写:在服务器执行BGSAVE的时候,如果BGREWRITEAOF也到来了,那么BGREWRITEAOF会被延迟到BGSAVE执行完毕之后,这时会记个标记aof_rewrite_scheduled,每次serverCron执行的时候会检查当前是否有BGSAVE或BGREWRITEAOF在执行,如果没有且aof_rewrite_scheduled已标记,那么就会执行BGREWRITEAOF。
  • 持久化:serverCron每次执行的时候,会判断当前是否在进行持久化,如果没有的话,会判断aof重写是否被延迟,延迟的话,执行aof重写,没有的话,会检查rdb条件是否满足,满足的话执行rdb持久化,否则判断aof重写条件是否满足,满足的话执行aof重写。在开启aof的时候,会根据设置看需不需要把aof缓冲区的数据写入到aof文件中。
  • 记录执行次数:serverCron每次执行的时候,都会记录下执行的次数。

slave断线重连后会发生什么

当slave执行slave of之后,会发个sync命令给master,master在收到sync之后,开始在后台执行bgsave,同时这期间的写记录在缓冲区中,当bgsave完成之后,master会把rdb发给从,slave根据rdb加载数据,同时master把缓冲区里的变更也发给slave,后续master的变更记录都会通过命令传播的形式传给slave。然而如果在某个时刻因为网络原因slave和manster断线了,这时候如果slave连接上了,会发生什么?断线期间的变更怎么办?

  • 在redis2.8以前,哪怕slave断线1s,只要连上之后发送sync,那么master就会无脑执行bgsave,然后发送给slave,slave再重新load rdb,可以看出整个过程还是非常低效的,本身bgsave就非常耗费IO,发送数据还耗费带宽。
  • 从redis2.8开始,支持增量复制,如果断线了且后来重连上了,在一些情况下,支持把断线期间丢失的变更单独同步,不用同步整个rdb文件。实现部分同步的关键主要是主从的复制偏移量,主的复制积压缓冲区,以及服务器的运行id(run_id) 三个指标。slave在第一次slave of master之后会保存master的run_id。每当master把命令同步给从的时候,自己会记录同步的偏移量,slave在接收到master同步的数据之后,也会记录自己的偏移量,同时master还会把同步的命令放在自己的复制积压缓冲区中,这个缓冲区默认大小是1M,遵循FIFO的原则。这样当slave断线重连后会做:
    • 把自己保存的master的run_id发给当前的主。
    • 把自己的偏移量发给当前的主。
      master收到信息后,首先确认slave发的run_id是自己,然后再确认slave的偏移量是否还在缓冲区中,如果这两点都满足的话,那么master就会把积压缓冲区中从slave发的偏移量之后的所有数据发给slave,实现部分同步。但是只要有一点不满足,那么就会执行全部同步。

如何解决redis脑裂问题

什么是脑裂问题:在redis集群中,如果存在两个master节点就是脑裂问题,这时候客户端连着哪个master,就往哪个master上写数据,导致数据不一致。

脑裂问题是如何产生的:一般可能是由于master所处的网络发生了问题,导致master和其余的slave无法正常通信,但是master和客户端的通信是ok的,这时哨兵会从剩下的slave中选举一个master,当原master网络恢复后,就会被降级成slave。

脑裂产生的影响:在原master失联的期间,和它通信的client会把变更记录在原master中,当原master网络恢复并成为新master的slave的时候,它会清空自己的数据,同步新master的数据。那么在网络失联的期间,往原master写入数据都是丢失的。

如何解决:主要通过两个配置参数来解决:

1
2
c复制代码min-slaves-to-write 1
min-slaves-max-lag 10
  • min-slaves-to-write:这个配置项设置了主库能进行数据同步的最少从库数量;
  • min-slaves-max-lag:这个配置项设置了主从库间进行数据复制时,从库给主库发送 ACK 消息的最大延迟(以秒为单位)。

如果两个配置都不满足的话,那么master就拒绝客户端的请求,通过以上配置可以将丢失的数据控制在10s内。

redis集群是怎样的

  • 首先集群是由多个节点组成的,节点通过握手来将其他节点加入到自己的集群中。
  • 集群中一共有16384个槽。
  • 每个节点都会记录自己负责的槽,和剩下的槽是由哪个节点处理的。
  • 节点收到命令时,会先检查这个键否是自己负责的,不是的话,会返回一个MOVED错误,通过MOVED错误携带的信息引导客户端转向负责此槽的节点。
  • 如果想要重新分配槽,那得用redis-trib工具。
  • 重新分槽的过程中,如果节点A正在迁移槽i到B节点,那么请求到槽i的时候,节点A会返回一个ASK错误,引导客户端到槽B去查找。
  • 每个节点增加从节点,来实现高可用。

redis分布式锁一定是安全的吗

我们知道redis是单线程的,命令是一个一个处理的,所以用redis做分布式锁是ok的?最常用的就是:

1
c复制代码set key value PX seconds NX

锁时间到了:一般生产环境我们为了安全会给锁加个自动过期时间,这样就算出现意外没有解锁,锁也会自动过期,降低损失风险。然而如果锁的时间到了,我们的业务还没处理完怎么办?一般解决方法如下:

  • 提前给足时间,尽量保证锁在自动失效之前,完成业务。
  • 开个线程来监控,例如线程每1/3锁失效时间来检查一次,如果业务还没处理完,则延长锁的时间。
  • 如果我们解锁时,发现锁已经被其他用户获取了,那么就认为此时是不安全的,选择回滚是个不错的选择。

主从模式下:在主从模式下,一个主至少有一个从,主负责写,从负责读,当我们设置锁的时候,是写在master上,但是在将数据同步到slave前master出现问题,导致触发选举新的slave为master,而此时锁的信息在新master上是丢失的,这样就会导致并发不安全。

  • 关于这个问题,redis的作者提出了一个叫RedLock的方式。首先如果采用RedLock那么就得放弃主从模式,只有多个主节点,官方建议5个。当我们想要获取一把锁的时候,先记录下当前时间,然后依次尝试从5个节点获取锁,获取锁的时候客户端得设置一个过期时间,这个过期时间应该小于锁的失效时间,防止客户端与一个宕机的服务通信而阻塞。当获取失败后,应立刻去下一个节点获取。当从大多数节点(3个)获取到锁后,锁的真正有效时间其实等于一开始设定的时间减去获取锁成功的时间。如果因为其他原因导致没能从大多数节点获得锁,那么就应该依次解锁节点的锁,此次上锁就是失败的。这种方案实际应用不多,主要是因为:
    • 成本高,多个主节点。
    • 依赖系统时钟,如果此时通过3个节点已经获取到锁,但是某个实例的系统时间走的快,那么导致这个实例的锁提前失效,下一个请求过来发现又有3个节点可以获取成功,那么就出现了问题。

总结:redis的分布式锁在要求强一致性的情况下可能并不适合,但是在某些场景下还是适合的,比如:就算在某个时候锁失效了,存在多个请求进入安全区,多个请求可能也就是多执行几次db查询,对整体业务并无大碍。

如何解决热key问题

什么是热key:经常被访问的key就是热key,比如双11的商品信息,秒杀的商品信息。当热key的并发量非常大,使得QPS达到几十万级别,这时候如果没有做好防护,可能出现问题就是灾难级别的。

如何解决:

  • 首先热key肯定是要缓存的,提前把热数据加载到缓存中,一上线就直接读取缓存。
  • 缓存至少集群架构,保证多个从,这样就算一个从挂了,还有备份。
  • 二级缓存,使用机器的内存再做一道拦截。比如像秒杀的商品基本信息可以直接使用机器的内存。
  • 限流,预估支持的qps,拦截多余的请求。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Flink 从0-1实现 电商实时数仓 - 分层介绍 & 新

发表于 2021-08-24

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

分层介绍

需求分析及实现思路

  在之前介绍实时数仓概念时讨论过,建设实时数仓的目的,主要是增加数据计算的复用性。每次新增加统计需求时,不至于从原始数据进行计算,而是从半成品继续加工而成。

  采集到 kafka 直接作为 ODS 层。

  从 kafka 的 ODS 层读取用户行为日志以及业务数据,并进行简单处理,写回到 kafka 作为 DWD 层。

  从 DWD 的 DWS 可能有重复计算,所以抽取出来 DWM 层。

  DWS 轻度聚合,应对很多实时查询。

  ADS 简单集合,提供对外接口。

image.png

每层具体职责

分层 数据描述 生成计算工具 存储媒介
ODS 原始数据,日志和业务数据。 日志服务器,maxwell kafka
DWD 根据数据对象为单位进行分流,比如订单、页面访问等等。 FLINK kafka
DIM 维度数据 FLINK HBase
DWM 对于部分数据对象进行进一步加工,比如独立访问、跳出行为。依旧是明细数据。 FLINK kafka
DWS 根据某个维度主题将多个事实数据轻度聚合,形成主题宽表。 FLINK Clickhouse
ADS 把Clickhouse中的数据根据可视化需要进行筛选聚合。 Clickhouse SQL 可视化展示

计算项目准备

  1. 新建项目 tmall-realtime
  2. 新建包
包名 用途
app flink 所有计算程序
app.dwd flink dwd层 计算程序
app.dwm flink dwm层 计算程序
app.dws flink dws层 计算程序
app.func 计算函数
bean 实体类
common 公共常量
enums 枚举类
utils 工具类
3. pom 添加依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
xml复制代码 <!-- 配置版本 -->
<properties>
<java.version>1.8</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<flink.version>1.12.0</flink.version>
<scala.version>2.12</scala.version>
<hadoop.version>3.1.3</hadoop.version>
</properties>

<!-- 依赖 -->
<dependencies>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-java</artifactId>
<version>${flink.version}</version>
</dependency>

<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-java_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>

<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kafka_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>

<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-elasticsearch6_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>

<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-clients_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>

<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-cep_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>

<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-json</artifactId>
<version>${flink.version}</version>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.68</version>
</dependency>

<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>${hadoop.version}</version>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>

<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
<version>2.14.0</version>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>com.alibaba.ververica</groupId>
<artifactId>flink-connector-mysql-cdc</artifactId>
<version>1.2.0</version>
</dependency>

<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.3</version>
</dependency>

<dependency>
<groupId>org.apache.phoenix</groupId>
<artifactId>phoenix-spark</artifactId>
<version>5.0.0-HBase-2.0</version>
<exclusions>
<exclusion>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
</exclusion>
</exclusions>
</dependency>

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

<dependency>
<groupId>ru.yandex.clickhouse</groupId>
<artifactId>clickhouse-jdbc</artifactId>
<version>0.3.0</version>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-jdbc_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>

</dependencies>
<!-- 打包插件 -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
  1. 配置文件
1
2
3
4
5
ini复制代码log4j.rootLogger=info,stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n

下期预告:ODS层 和 DWD层 的具体实现

关注专栏持续更新 👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringSecurity-Shiro-初见 简介 实战环

发表于 2021-08-24

简介

在 Web 开发中,安全一直是非常重要的一个方面。

安全虽然属于应用的非功能性需求,但是应该在应用开发的初期就考虑进来。

市面上存在比较有名的:Shiro,Spring Security !

首先我们看下它的官网介绍:Spring Security官网地址

1
2
3
4
5
6
7
vbnet复制代码Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.

Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements

Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它实际上是保护基于spring的应用程序的标准。

Spring Security是一个框架,侧重于为Java应用程序提供身份验证和授权。与所有Spring项目一样,Spring安全性的真正强大之处在于它可以轻松地扩展以满足定制需求

从官网的介绍中可以知道这是一个权限框架。

Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。

一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分,Spring Security 框架都有很好的支持。

1
2
3
复制代码在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。在用户授权方面,

Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。

实战环境搭建

项目代码地址:gitee.com/zwtgit/spri…

1、新建一个初始的springboot项目web模块,thymeleaf模块

2、导入静态资源,资源上面有

3、controller跳转!

1
2
3
4
5
6
7
8
java复制代码@Controller
public class RouterController {

@RequestMapping({"/","/index"})
public String index(){
return "index";
}
}

4、测试实验环境是否OK!

SpringSecurity

Spring Security 是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,

他可以实现强大的Web安全控制,对于安全控制,我们仅需要引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理!

记住几个类:

  • WebSecurityConfigurerAdapter:自定义Security策略
  • AuthenticationManagerBuilder:自定义认证策略
  • @EnableWebSecurity:开启WebSecurity模式

Spring Security的两个主要目标是 “认证” 和 “授权”(访问控制)。

“认证”(Authentication)

身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份。

身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。

“授权” (Authorization)

授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。

这个概念是通用的,而不是只在Spring Security 中存在。

认证和授权

1、引入 Spring Security 模块

1
2
3
4
java复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

2、编写 Spring Security 配置类

参考官网:spring.io/projects/sp…

查看我们自己项目中的版本,找到对应的帮助文档:

docs.spring.io/spring-secu… #servlet-applications 8.16.4

3、编写基础配置类

1
2
3
4
5
6
7
8
java复制代码@EnableWebSecurity // 开启WebSecurity模式
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {

}
}

4、定制请求的授权规则

1
2
3
4
5
6
7
8
9
java复制代码@Override
protected void configure(HttpSecurity http) throws Exception {
// 定制请求的授权规则
// 首页所有人可以访问
http.authorizeRequests().antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");
}

5、测试一下:发现除了首页都进不去了!因为我们目前没有登录的角色,因为请求需要登录的角色拥有对应的权限才可以!

6、在configure()方法中加入以下配置,开启自动配置的登录功能!

1
2
3
4
java复制代码// 开启自动配置的登录功能
// /login 请求来到登录页
// /login?error 重定向到这里表示登录失败
http.formLogin();

7、测试一下:发现,没有权限的时候,会跳转到登录的页面!

8、查看刚才登录页的注释信息;

我们可以定义认证规则,重写configure(AuthenticationManagerBuilder auth)方法

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码//定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {

//在内存中定义,也可以在jdbc中去拿....
auth.inMemoryAuthentication()
.withUser("kuangshen").password("123456").roles("vip2","vip3")
.and()
.withUser("root").password("123456").roles("vip1","vip2","vip3")
.and()
.withUser("guest").password("123456").roles("vip1","vip2");
}

9、测试,我们可以使用这些账号登录进行测试!发现会报错!

There is no PasswordEncoder mapped for the id “null”

10、原因,我们要将前端传过来的密码进行某种方式加密,否则就无法登录,修改代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码//定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//在内存中定义,也可以在jdbc中去拿....
//Spring security 5.0中新增了多种加密方式,也改变了密码的格式。
//要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密
//spring security 官方推荐的是使用bcrypt加密方式。

auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("zwt").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
.and()
.withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
.and()
.withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2");
}

11、测试,发现,登录成功,并且每个角色只能访问自己认证下的规则!

权限控制和注销

1、开启自动配置的注销的功能

1
2
3
4
5
6
7
8
java复制代码//定制请求的授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
//....
//开启自动配置的注销的功能
// /logout 注销请求
http.logout();
}

2、我们在前端,增加一个注销的按钮,index.html 导航栏中

3、我们可以去测试一下,登录成功后点击注销,发现注销完毕会跳转到登录页面!

4、但是,我们想让他注销成功后,依旧可以跳转到首页,该怎么处理呢?

1
2
java复制代码// .logoutSuccessUrl("/"); 注销成功来到首页
http.logout().logoutSuccessUrl("/");

5、测试,注销完毕后,发现跳转到首页OK

6、我们现在又来一个需求:根据权限访问页面

我们需要结合thymeleaf中的一些功能

sec:authorize=”isAuthenticated()”:是否认证登录!来显示不同的页面

Maven依赖:

1
2
3
4
5
6
java复制代码<!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity4 -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>

7、修改我们的 前端页面

  1. 导入命名空间

8、重启测试,我们可以登录试试看,登录成功后确实,显示了我们想要的页面;

9、如果注销404了,就是因为它默认防止csrf跨站请求伪造,因为会产生安全问题,我们可以将请求改为post表单提交,或者在spring security中关闭csrf功能;

1
2
java复制代码http.csrf().disable();//关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
http.logout().logoutSuccessUrl("/");

权限控制和注销搞定!

记住我

1、开启记住我功能

1
2
3
4
5
6
7
java复制代码//定制请求的授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
//。。。。。。。。。。。
//记住我
http.rememberMe();
}

原理: spring security 登录成功后,将cookie发送给浏览器保存,

以后登录带上这个cookie,只要通过检查就可以免登录了。

如果点击注销,则会删除这个cookie,具体的原理在JavaWeb阶段都讲过了,这里就不在多说了!

Shiro

Apache Shiro是一个Java的安全(权限)框架。

Shiro可以完成,认证,授权,加密,会话管理,Web集成,缓存等。

三大核心组件

Shiro有三大核心组件,即Subject、SecurityManager和Realm

组件
Subject 用户,认证主体 应用代码直接交互的对象是Subject,Subject。包含Principals和Credentials两个信息
SecurityManager 管理所有用户,为安全管理员。 是Shiro架构的核心。与Subject的所有交互都会委托给SecurityManager, Subject相当于是一个门面,而SecurityManager才是真正的执行者。它负责与Shiro 的其他组件进行交互。
Realm 连接数据,是一个域。 可以把Realm看成DataSource,即安全数据源。

Pricipals:代表身份。可以是用户名、邮件、手机号码等等,用来标识一个登陆主题的身份。

Credentials:代表凭证。常见的有密码、数字证书等等。

在官网的例子中了解Shiro

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
java复制代码public class Quickstart {

private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);


public static void main(String[] args) {


Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);


// 获取当前用户
Subject currentUser = SecurityUtils.getSubject();

// 通过当前用户拿到session
Session session = currentUser.getSession();
//在session中存值
session.setAttribute("someKey", "aValue");
String value = (String) session.getAttribute("someKey");
if (value.equals("aValue")) {
log.info("Retrieved the correct value! [" + value + "]");
}

// 判断当前的用户是否被认证
if (!currentUser.isAuthenticated()) {
//token 令牌 随意设置
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
//设置记住我
token.setRememberMe(true);
try {
currentUser.login(token); //执行了登录操作
} catch (UnknownAccountException uae) { //用户名不存在
log.info("There is no user with username of " + token.getPrincipal());
} catch (IncorrectCredentialsException ice) { //密码错误
log.info("Password for account " + token.getPrincipal() + " was incorrect!");
} catch (LockedAccountException lae) { //用户被锁定
log.info("The account for username " + token.getPrincipal() + " is locked. " +
"Please contact your administrator to unlock it.");
}
// ... catch more exceptions here (maybe custom ones specific to your application?
catch (AuthenticationException ae) { //认证异常
//unexpected condition? error?
}
}


//获取当前用户的认证
log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");

//获得当前用户的角色
if (currentUser.hasRole("schwartz")) {
log.info("May the Schwartz be with you!");
} else {
log.info("Hello, mere mortal.");
}

//是否拥有粗粒度(简单)权限
if (currentUser.isPermitted("lightsaber:wield")) {
log.info("You may use a lightsaber ring. Use it wisely.");
} else {
log.info("Sorry, lightsaber rings are for schwartz masters only.");
}

//是否拥有更高权限
if (currentUser.isPermitted("winnebago:drive:eagle5")) {
log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " +
"Here are the keys - have fun!");
} else {
log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
}

//注销
currentUser.logout();
//结束
System.exit(0);
}
}

快速上手

导入依赖

1
2
3
4
5
6
xml复制代码        <!--shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>

编写shiro配置类

1
2
3
4
5
6
7
8
9
10
java复制代码@Configuration
public class ShiroConfig {


//shiroFilterBean 第三步

//DefaultWebSecurityManager 第二步

//创建Realm对象 需要自定义类 第一步
}

自定义Realm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码//自定义Realm 需要继承AuthorizingRealm
public class UserRealm extends AuthorizingRealm {
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("执行了授权=>");
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行了认证");
return null;
}
}

继续编写shiro配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码@Configuration
public class ShiroConfig {


//ShiroFilterFactoryBean 第三步
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("manager") DefaultWebSecurityManager manager){
ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean();
//设置安全管理器
filter.setSecurityManager(manager);
return filter;
}

//DefaultWebSecurityManager 第二步
@Bean(name="manager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
//关联UserRealm
manager.setRealm(userRealm);
return manager;
}

//创建Realm对象 需要自定义类 第一步
@Bean
public UserRealm userRealm(){
return new UserRealm();
}

}

实现登录拦截

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码//ShiroFilterFactoryBean 第三步
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("manager") DefaultWebSecurityManager manager){
ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean();
//设置安全管理器
filter.setSecurityManager(manager);
//增加shiro内置过滤器
/**
* anon: 无需认证就可以访问
* authc: 必须认证才能访问
* user: 必须拥有记住我功能才能访问
* perms: 拥有对某个资源的权限才能访问
* role:拥有某个角色权限才能访问
*/
Map<String, String> map = new LinkedHashMap<>();
map.put("/user/add","authc");
map.put("/user/update","authc");
filter.setFilterChainDefinitionMap(map);
//设置登录页面
filter.setLoginUrl("/login");
return filter;
}

用户认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
java复制代码   //用户登录功能
@PostMapping("/tologin")
public String tologin(String username, String password, Model model){
//获取用户
Subject subject = SecurityUtils.getSubject();
//封装用户登录数据并且生成一个token令牌
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try{
subject.login(token);//登录 如果没有异常就说明OK了
return "index";//返回首页
}catch (UnknownAccountException e){
model.addAttribute("msg","用户名错误");
return "login";
}catch (IncorrectCredentialsException e){
model.addAttribute("msg","密码错误");
return "login";
}




//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行了认证");
//用户名密码 数据库中取
String name="admin";
String password="123";
UsernamePasswordToken userName=(UsernamePasswordToken)token;
if (!userName.getUsername().equals(name)){
return null;//抛出异常
}
//密码认证 shiro做
return new SimpleAuthenticationInfo("",password,"");
}

shiro整合mybais

导入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码      <!--spring boot整合mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<!--Mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--SpringbootJDBC-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

编写配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yml复制代码spring:
thymeleaf:
cache: false #关闭模板引擎的缓存
# 配置数据源 serverTimezone=UTC 设置时区
datasource:
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useSSL=true&useUnicode=true&characterEncoding=utf8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
type-aliases-package: com.zwt.pojo
mapper-locations: classpath:mapper/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

编写实体类,编写接口,编写映射文件

修改自定义Realm代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码 @Autowired
private UserMapper userMapper;
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行了认证");
UsernamePasswordToken userName=(UsernamePasswordToken)token;
//连接真实数据库
User user = userMapper.queryUserbyName(userName.getUsername());
if(user==null){ //没查出用户
return null;
}
//密码认证 shiro做 密码加密
return new SimpleAuthenticationInfo("",user.getPwd(),"");
}

请求授权

编写Shiro过滤器

1.设置访问/user/add路径时,需要[user:add权限

2.没有此权限访问时,会跳转指定路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码 //ShiroFilterFactoryBean 第三步
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("manager") DefaultWebSecurityManager manager){
ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean();
//设置安全管理器
filter.setSecurityManager(manager);
//增加shiro内置过滤器
/**
* anon: 无需认证就可以访问
* authc: 必须认证才能访问
* user: 必须拥有记住我功能才能访问
* perms: 拥有对某个资源的权限才能访问
* role:拥有某个角色权限才能访问
*/
Map<String, String> map = new LinkedHashMap<>();
// map.put("/user/add","authc");
map.put("/user/update","authc");
map.put("/user/add","perms[user:add]"); //访问此路径需要user:add权限
filter.setFilterChainDefinitionMap(map);
//设置登录页面
filter.setLoginUrl("/login");
filter.setUnauthorizedUrl("/noperms");//无权限时执行这个请求
return filter;
}

授权:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码   //授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("执行了授权=>");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermission("user:add");//对每个用户进行授权

//取出用户信息
Subject subject = SecurityUtils.getSubject();
User principal = (User)subject.getPrincipal();

//将用户信息的权限信息 设置进去
//动态设置权限 需要新增一些数据库表 如:权限表
//info.addStringPermission(principal.getParms());
return info;
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

基于系统稳定性建设,你做了哪些事情?(下)

发表于 2021-08-24

上期聊完了如何提升系统可靠性,我们继续聊,如何提升系统可用性及稳定性。

2、提升系统可用性,缩短故障时间,快速止损

故障时长 = 发现问题时长 + 定位问题时长 + 解决问题时长

图片


上线规范:

上一章在“可靠性”的章节,也提到了上线规范,但出发点不同,基于“可用性”的上线规范,主要是从“快速止损”的角度,即:发现、定位、解决闭环来思考的。

  • 观察巡检。 在灰度上线以后,没有发现问题,我们继续推全量后,这个时候,我们需要从三个维度进行持续地观察巡检。

① 基础维度,我们需要观察CPU、内存、IO、网络、JVM等,上线前后的各项指标是否有明显异常。

② 应用维度,我们需要观察服务接口的QPS和响应时间(包括平均响应时间、95Line、尤其是99Line),上线前后是否存在明显异常。

③ 业务维度,这个相对简单一些,系统里面的核心流程走一遍,观察一下业务日志有没有报异常,就可以了。

为什么我们灰度上线完,在全量上线后还需要观察巡检得如此详细,这是因为灰度上线,如果是用户维度灰度的话,样本率过低,机器维度灰度的话,缓存和DB等共用资源层面压力给得不够,所以并不能完全排除问题。

  • 回滚方案。 如果在观察巡检中发现了问题,那么我们下一步要做的就是,如何去解决问题。但是,如果我们没有针对于本次上线事先做回滚方案,而是临时见招拆招的话,会极大限度地延长了故障时间。一般来讲,常见的回滚方案有:代码回滚、表结构回滚、数据回滚,以及程序开关切换等。

监控告警:

解决的是三部曲中,发现问题时长方面。

监控也是分为上面说的三个维度,即基础维度、应用维度和业务维度,需要日常进行巡检来保证范围的足够覆盖,当发生问题的时候,一定是先收到我们系统自己的监控报警,而不是让用户和业务人员先反馈过来。

报警也是有一定策略的, 强调的是exactly once(精确一次),这本身也是一种做减法的过程。因为过多的报警(误报),不仅对工程师是一种打扰,且长期处于“狼来了”的情况下,反而会让工程师对报警变得忽视,出现处理不及时或者不处理的情况。


应急预案:

故障三部曲中,定位问题和解决问题方面。

在发生故障的时候,大多数人的脑子都是一片空白,很难迅速做出最合理的反应。衡量一个应急预案的好坏,关键是看它是否足够“无脑化”。

举个例子:如果发现了故障A,那么我们需要排查B和C两个方面来精准定位问题,定位后,我们再通过D—>E—>F三个操作来解决问题。

即:整个过程中,只需要照做,不需要思考,因为思考就会产生选择,而选择取舍是最耗费时间的事情,如果做不到这点,那么证明应急预案还有优化的空间。

另外,预案也不是一蹴而就的事情,需要跟随架构和业务的演进,不断进行更新迭代,同时,也需要不断做减法,该摒弃的摒弃,该合并的合并,如果只增不减,那么一年以后,预案就会变成一本书。


故障演练:

故障三部曲中,定位问题和解决问题方面。

故障演练需要从已知、半已知和未知三个维度去做。

  • 已知:已知故障类型,按照应急预案SOP,一步一步去做,从生疏到熟练,从20分钟到5分钟,从有限的人能做变成所有人能做。
  • 半已知:在已知故障类型的演练中,发现了未知因素,如:当缓存集群故障的时候,切换到备用缓存集群,但发现备用缓存集群里面的缓存数据有问题,需要重新清空及重新进行缓存预热。
  • 未知:未知故障类型,临时决策安排人员有序散开排查,临时根据现象定位问题,采用回滚版本或者重启等方式尝试性解决问题,或定位到问题后,采用有损的方式试图临时解决问题等。

自动防御:

这是件很能体现出工程师能力和素养的事情,需要工程师在coding过程中,在任何关键环节都具备安全意识。如:

  • 当下游依赖的存储集群,由于不可用而触发代码中的失败次数或失败时间阈值时,自动切换到备用的存储集群。
  • 当下游依赖的核心服务,由于不可用而触发代码中的失败次数或失败时间阈值时,自动降级到备用方案,如:将请求切换到存储非实时数据的ES中,接受有损。
  • 当系统由于响应时间激增而导致服务不可用时,自动对下游依赖的非核心服务进行降级熔断,减少服务接口的整体响应时间,保证可用。

3、提升系统稳定性,在可用、可靠的前提下,性能稳定

如果说,提升系统可靠性和可用性是重要紧急的事情,那么保持性能稳定就属于重要,但又不那么紧急,却需要长期坚持的事情。属于那种“身是菩提树,心是明镜台。时时勤拂拭,以免惹尘埃。”

可用性和可靠性更加关注服务接口的长尾耗时(响应时间的99Line),因为有可能少量慢请求会拖挂整个服务。那么性能稳定则更加关注服务接口的平均响应时间,且这个需要以周环比和日环比的方式来进行量化关注的,如果系统的平均响应时间在逐渐慢慢变长,那么就需要引起高度重视了。

  • 如果服务是因为接口的数量逐渐增加,从而导致系统整体负载慢慢变高,那么就需要考虑以业务模型维度进行服务拆分了,因为系统架构没有一蹴而就的,都是根据业务情况来逐渐演化,同时,最好连DB拆分一起做了,这样也可以降低DB的负载。
  • 如果服务的接口数量并没怎么增加,而是由于数据库主表的数据量越来越多从而变慢了,那么就可以考虑进行水平分库分表、或者冷数据归档来降低系统压力了,因为这个时候,所有的瓶颈可能都在这张主表上,服务拆分所带来的收益并不大。
  • 如果服务的接口数量没有增加,但是几个主要业务接口的业务复杂度增加了,比如:以前这个接口只需要2次DB查询和3次下游服务调用,现在发展成10次DB查询,12次下游调用了,且并行调用、缓存、SQL优化等各种优化方式已经用到极致了,这个时候,我们就需要进行接口拆分+服务拆分了。先按照业务维度和重要等级维度,把一个大接口拆分成几个小接口,同时按照业务模型和重要等级的角度,把服务进行拆分,给以后的业务继续扩展留下空间。

全文完。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

微服务的痛:你的微服务还好吗? 一、没人知道系统整体整体架构

发表于 2021-08-24

在前面我们说了微服务的两个痛点:微服务的职责划分和微服务的粒度拆分痛点,这里接着聊剩下的痛点:

一、没人知道系统整体整体架构的全貌

不知道大家有没有碰到过这种情况:每隔几个月或半年,大领导就会发话让我们汇报下每个部门的微服务数量、公司微服务总数量、每个微服务都用来做什么等情况。因为企业微服务数较多,所以每次给大领导汇报时,都是长长的一条清单。

然后大领导开始抱怨:“几百个微服务?系统这么复杂了吗?谁能知道所有系统的全貌啊,如果出现问题,我们如何快速定位问题点呢?”此时负责人们只好乖乖地低着头,其中一个同事偷偷嘀咕:“我连自己部门的微服务列表都没搞清楚呢。”

在以前,我们首先会把公司的整个架构系统全貌搞清楚,之后一旦出现问题,也就容易定位故障点了。可是自从来到这家使用微服务的公司后,便再也没有这样的冲动了,只要求搞懂自己的一亩三分地就行,如果出现问题临时学习一下相关系统就好了。

因此,在实际工作中,很难找到这么一个人,他能知道系统整体架构的全貌,这就是微服务的一个痛点。

二、重复代码多

在以前的公司,我们把所有的代码放在了同一个工程中,如果发现某些代码可以重复使用,把这些代码抽取出来存放在 Common 包中就行。但是这种代码设计在微服务中经常会出现问题,这里我还是举个例子说明下。

比如某个团队做了一个日志自动埋点的功能,它能自动记录一些特定方法的调用。其他团队知道这个功能后,感觉很不错,想直接拿来用,于是埋点团队开心地给出了 Maven 的声明。但是第一个吃螃蟹的团队使用后,立马报出了一个 JAR 版本冲突问题,这时如果他们将冲突的 JAR 进行升级,原始代码就不能使用了。为节省人力成本,他们只好询问埋点团队如何实现版本兼容。

为了兼容这个螃蟹团队的 JAR 版本,自动埋点团队又重新设计了一版埋点的 JAR,并去掉了一些特定 API 的使用,最终 2 个团队终于可以正常使用了。

不过呢,第三个使用埋点的 JAR 的团队又汇报了一个 JAR 版本冲突问题,此时自动埋点团队从投入产出比角度考虑,不得不放弃维护这个公用的 JAR 了,并直接告知其他团队:代码就在 Git 上,你们自己直接 fork 修改吧。因此,这个代码在不同团队的微服务中最终存在了多个不同版本。

后来我们复盘了下,得出结论:重用 JAR 本身没有错,错就错在我们使用的 JAR 版本太多了,必须改变这个局面。

于是我们将所有 JAR 版本进行统一的项目正式立项了。第二天,因紧急业务需求下来了,大家都忘掉了这回事。又过了一段时间,有人提起了这个中心级项目,结果又被紧急的业务需求 PK 下去了。后来大家逐渐明白,这个项目没法做,因为投入产出比不高。

其实微服务之间存在重复的代码也没事,因为部门之间的重复代码比比皆是,而且技术中心每个部门都有自己的 framework/Common/shared/arc 的 GitLab subgroups,它们可以实现对部门内部的通用代码进行重用。

不过,维护这些小小的重复代码总比统一排期做重构、统一评审 JAR 版本的成本低得多。

三、耗费更多服务器资源

曾经了解到一个小公司。他们原来使用的是单体式架构,一共部署了 5 台服务器,后面他们一直抱怨系统耦合性太强,代码之间经常互相影响,并且强烈要求将架构进行迁移。

于是,根据业务模块,把原来的单体式架构拆分为了 6 个微服务。考虑到高可用,每个服务至少需要部署在 2 个节点上,再加上网关层需要 2 台服务器,最终,一共部署了 15 台服务器。(因为其中一个服务比较耗资源,为了保险起见,多加了一个节点。)

在这个拆分过程中,业务没有变,流量没有变,代码逻辑改动也不大,却无缘无故多出了 9 台服务器,为此事还发生过争执,当时的争议点是如果是这种情形,就不应该一台服务器只部署一种服务,比如我们可以把服务 AB 部署在 1 个节点,BC 服务部署在 1 个节点,AC 再部署在一个节点,如下图所示:
file

可是这个方案很快就被大家否定了,因为如果每个服务器只部署一种服务,服务器的名字直接以服务的名字命名就行,之后运维排查问题时也比较方便。可是如果我们把不同的微服务混合部署,服务器又该怎么命名呢?

于是,有人提议:“要不这样吧,反正服务器比较便宜,多几台也无所谓。”大家纷纷附和赞同。公司的钱就被这帮程序员浪费了,不过你别以为只有小公司这样做,大公司同样如此。

过了几天后,CTO 召集所有研发人员正式开会:“这个季度,我们的服务器预算太多了,财务部门审核不通过,你们需要想办法缩减一下服务器数量,把不用的服务器都下掉。”

会议结束后,大家各自回到工位,开始对每个服务进行检查,于是就有了下面这段对话。

A:“这个服务怎么使用了这么多台服务器?很耗资源吗?”
B:“不是,主要是公司强制要求我们实现多数据中心部署。”
A:“这个服务很重要吗?内部使用的吗?”
B:“是,这个目前只是开发人员在使用。”
A:“那干吗做负载均衡,下了下了,只留一台。”
B:“好吧。”
A:“现在我们缩减了多少台服务器了?”
B:“……”

在大部分公司中,这种情形很常见,因此不得不说微服务真的很耗服务器。

四、分布式事务

分布式事务这个痛点对于微服务来说,简直就是地狱。为了深刻理解这个痛点,我们先以曾经经历的下单流程为例。

原本的下单流程是这样的:插入订单——>修改库存——>插入交易单——>插入财务应收款单——>返回结果给用户,让用户跳转。

在单体式架构中,我们只需要把上面的下单流程包在一个事务里就行了,如果某个流程出错了,直接回滚数据,并通过业务代码告知用户出错了就行,让用户重试就好了。

可是迁移到微服务后,因为这几个流程分别存放在不同的服务中,所以我们需要更新不同的数据库,也就需要纠结以下逻辑。

某个流程出错是否需要将数据全部回滚?如果需要的话,那么我们需要在每个流程中写上回滚代码。那万一回滚失败了呢?我们是不是还需要写回滚的回滚代码,回滚的回滚代码算回滚吗?

要不就某些流程回滚,某些流程不回滚?那哪些流程回滚哪些流程不回滚呢?

要不就统一不回滚,失败就重试?这样岂不是需要做成异步?如果做成异步,会不会出现时间超时?如果超时了,用户怎么办?需要回滚吗?(怎么又要回滚了?)

如果我们只是纠结某些特定的流程也就罢了,问题是这种分布式服务更新数据的场景实在是太多了,如果每个场景都要纠结这些逻辑,我们得疯了。本来业务部门就嫌我们交付太慢,我们还要花时间扯这些逻辑,干脆整个部门解散得了。

因此,针对这种情况,在大部分的场景下我们不考虑回滚和重试,只考虑写 Happy Path,如果报错就记个异常日志,再线下手工处理,So easy!

结果你们也知道,机房网络抖动是常有的事情(运维经常拿这个当理由),以至于三天两头数据更新出现异常,比如上游数据更新了,下游数据没有更新,这时数据就对不上了,特别尴尬。

以至于业务部门经常抱怨:“咦,这个订单怎么找不到收款单了?咦,这个交易单怎么没有交易流水?”然后我们只能回怼过去:“不是跟你解释过了吗?提个工单就好了,嘀嘀咕咕啥,生怕老板不知道吗?”

使用微服务时,分布式事务一直是痛点也是难点,因此我们痛定思痛,决定好好解决这个问题,关于此问题的解决方案我们将在后面的内容中进行说明。

感兴趣的朋友欢迎关注微信公众号:服务端技术精选

个人博客:jiangyi.cool

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SQL 注入和 XSS 攻击

发表于 2021-08-24

1.SQL 注入

SQL 注入,一般是通过把 SQL 命令插入到 Web 表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的 SQL 命令。
在登录界面,后端会根据用户输入的用户(username)和密码(password),到 MySQL 数据库中去验证用户的身份。

用户输入用户名【cedric】 , 密码【123456】,在后端处理时,会进行如下 sql 语句拼接,当验证用户名和密码成功后,即成功登录。

1
2
csharp复制代码用户名为 cedric , 密码为 123456 
select username from users where username='cedric' and password='123456';

但是,如果用户在输入框恶意输入用户名【cedric’ – 】(注意最后面有个空格)和随意一个错误的密码【111】,在后端处理时,会进行如下 sql 语句拼接,也会成功登录。

1
2
csharp复制代码// 符号 ‘--’ 后面的语句相当于被注释了
select username from users where username='cedric -- ' and password='111';

或者,如果用户在输入框恶意输入用户名【cedric’;delete from users; – 】和随意一个错误的密码【111】,在后端处理时,会进行如下 sql 语句拼接,则结果会导致数据库中所有用户被删除。

1
2
sql复制代码// 符号 ‘--’ 后面的语句相当于被注释了
select username from users where username='cedric';delete from users; -- ' and password='111';

SQL 注入预防

Node 环境下,使用 mysql 的 escape 函数处理输入内容,就能将参数中的特殊字符进行转义。

在所有输入 sql 语句的地方,用 escape 函数处理一下即可, 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
ini复制代码const login = (username, password) => {

// 预防 sql 注入
username = escape(username)
password = escape(password)

const sql = `
select username from users where username=${username} and password=${password};
`

// 然后按上面语句执行 sql 查询
···
}
  1. XSS 攻击

XSS 是一种在web应用中的计算机安全漏洞,它允许恶意web用户将代码(代码包括HTML代码和客户端脚本)植入到提供给其它用户使用的页面中。

XSS 攻击示例

xss攻击主要是针对表单的 input/textarea 文本框发起的,比如在文本框中输入:

1
xml复制代码<script> alert(1) </script>

如果前端不进行过滤直接提交到后端(比如Node ),而服务端也没有进行过滤直接写入数据库库,那么在下一次(或其他用户)进入页面时,就会执行alert(1), 页面弹出 1 。

窃取网页中的cookie值

或者,文本框中恶意输入:

1
xml复制代码<script> alert(document.cookie) </script>

就可以获取用户 cookie 了。

劫持流量实现恶意跳转

文本框中恶意输入:

1
xml复制代码<script>window.location.href="www.abc.com";</script>

导致,所访问的网站就会自动跳转到 www.abc.com 了。

XSS 攻击预防

对用户输入的数据进行HTML Entity编码, 也就是对<script>、<a>等标签的< >进行转换,然后再保存到后台数据库。

Node环境下,安装:

1
ruby复制代码$ npm install xss

然后修改:

1
2
3
4
ini复制代码const xss = require('xss')

const inputValue = content // 未进行 xss 防御
const inputValue = xss(content) // 已进行 xss 防御

然后如果在 input 输入框 恶意输入 <script> alert(1) </script>, 就会被转换为下面的语句并存入数据库:

&lt;script&gt; alert(1) &lt;/script&gt;,已达到无法执行 <script> 的目的。

本文转自 www.cnblogs.com/cckui/p/109…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MyBatis_Plus联表分页查询

发表于 2021-08-24

MyBatis_Plus联表分页查询

当我们需要关联表格分页查询时,MyBatis_plus封装的单表方法已经满足不了我们的需求了,那么我们需要进行联表分页查询

假设我们需要的 SQL 语句如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
sql复制代码        SELECT
su.id,
su.username,
su.sex,
su.user_identity,
su.user_company,
su.status,
su.third_type,
su.telephone,
su.avatar,
su.email,
su.realname,
su.post,
su.del_flag,
su.create_time,
sr.role_name
FROM
sys_user AS su
LEFT JOIN sys_user_role AS sur ON su.id = sur.user_id
LEFT JOIN sys_role AS sr ON sur.role_id = sr.id
order by su.create_time desc

那么我们需要进行如下操作:

  • 1、新建 UserInfoVO.java

UserInfoVo实际上是一个页面数据对象,由于页面上需要显示用户表的数据还需要根据用户去查询另一张表中的角色名称,所以UserInfoVO类似构造了一个MyBatis中的result,在MP中我们可以使用IPage<xxxEntity/DTO/Vo>来返回自定义多表联合查询列表数据并分页的展示需求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
text复制代码import lombok.Data;

@Data
public class UserInfoVO extends UserInfo {
/**
* id
*/
@TableId(type = IdType.ASSIGN_ID)
private String id;

/**
* 登录账号
*/
@Excel(name = "登录账号", width = 15)
private String username;

/**
* 真实姓名
*/
@Excel(name = "真实姓名", width = 15)
private String realname;

/**
* 密码
*/
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String password;

/**
* md5密码盐
*/
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String salt;

/**
* 头像
*/
@Excel(name = "头像", width = 15, type = 2)
private String avatar;


/**
* 性别(1:男 2:女)
*/
@Excel(name = "性别", width = 15, dicCode = "sex")
@Dict(dicCode = "sex")
/**
* 电子邮件
*/
@Excel(name = "电子邮件", width = 15)
private String email;

/**
* 电话
*/
@Excel(name = "电话", width = 15)
private String phone;

/**
* 删除状态(0,正常,1已删除)
*/
@TableLogic
@Excel(name = "删除状态", width = 15, dicCode = "del_flag")
private Integer delFlag;

//其他表的数据
@TableField(exist = false)
private String roleName;

}
123456789
  • 2、UserInfoMapper.java 中

Constants.WRAPPER:在MP官网的解释是:根据entity条件查询记录,所以

1
2
Java复制代码IPage<UserInfoVO> getUserList(@Param("username") String username, @Param("realname") String realname, @Param("status") Integer status, @Param("page") Page<UserInfoVO> page, @Param(Constants.WRAPPER) Wrapper<UserInfoVO> wrapper);
}
  • 3、UserInfoMapper.xml 中

${ew.customSqlSegment}:表示将自定义的SQL代码包裹,使用QueryWrapper(LambdaQueryWrapper)进行输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
sql复制代码    <select id="getUserList" resultType="org.jeecg.modules.system.entity.SysUser">
SELECT
su.id,
su.username,
su.sex,
su.user_company,
su.status,
su.telephone,
su.avatar,
su.email,
su.realname,s
su.del_flag,
su.create_time,
sr.role_name
FROM
sys_user AS su
LEFT JOIN sys_user_role AS sur ON su.id = sur.user_id
LEFT JOIN sys_role AS sr ON sur.role_id = sr.id
<where>
su.open_id is null and del_flag = 0
<if test="username !=null and username!=''">
and su.username like concat('%',#{username},'%')
</if>
<if test="realname !=null and realname!=''">
and su.realname like concat('%',#{realname},'%')
</if>

<if test="status != null and status!=''">
and su.status like concat('%',#{status}'%')
</if>
</where>
order by su.create_time desc
${ew.customSqlSegment}
</select>
  • 4、UserInfoServiceImpl.java 中
1
2
3
4
java复制代码    @Override
public IPage<UserInfoVO> getUserList(String username, String realname, Integer status, Page<UserInfoVO> page, QueryWrapper<UserInfoVO> queryWrapper) {
return userMapper.getUserList(username, realname, status, page, queryWrapper);
}

由此可见,serviceImpl返回的是一个IPage<XXXEntity/Dto/Vo>对象,IPage是一个分页对象,在Controller中可以在IPage中传入Page分页数据。

  • 5、UserController.java中
1
2
3
4
java复制代码QueryWrapper<UserInfoVO> queryWrapper = new QueryWrapper<>();
Page<UserInfoVO> page = new Page<UserInfoVO>(pageNo, pageSize);
IPage<UserInfoVO> userPageList = sysUserService.getUserList(sysUser.getUsername(), sysUser.getRealname(), sysUser.getStatus(), page, queryWrapper);
return Result.OK(userPageList);

以上就是分页查询(联表)时的操作,使用拼写SQL的方式实现多表联合分页查询,是效率最高的一种。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…551552553…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%