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

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


  • 首页

  • 归档

  • 搜索

网络协议——RSTP快速生成树边缘端口

发表于 2021-11-24

本文正在参与 “网络协议必知必会”征文活动

📢CSDN主页:新网工李白

📢路由交换专栏 : HCIE Routing&Switching

📢RSTP中的边缘端口特性

  • 💬边缘端口的作用
  • 💬使用边缘端口时遇到的问题
  • 💬总结

在这里插入图片描述

\

💬边缘端口的作用

  • 边缘端口在端口UP之后立即进入转发状态;不需等待2倍的转发延时就可转发流量
  • P/A机制中,边缘端口不阻塞,可持续进行流量转发
  • 边缘端口UP时,不出发TC机制

💬使用边缘端口时遇到的问题

在这里插入图片描述

\

环境描述

SW1的g0/0/1和g0/0/2配置了edge port,连接着一台非网管式的交换机

环路形成描述

当链路刚接上去时,配置了edge port的端口立即进入forwarding状态,在没有收到bpdu时,hub的e0/0/0、e0/0/1以及SW1的g0/0/1、g0/0/2全都处于forwarding状态,于是环路出现。

当边缘端口收到bpdu时,就会散失边缘端口的特性,成为普通的stp端口,并参与stp计算,环路消失。由于需要计算stp(确定端口角色,端口状态,等待转发延时),所以会带来网络震荡。

边缘端口使用场景

接DHCP客户端,使其快速获取IP地址

windows DHCP客户端在一定时间中没有获得DHCP服务器分配的地址,会自动使用169.254.X.X/16地址(使用这个地址的目的是在未能分配到地址的情况下,可与其他设备互相通信),此时DHCP客户端不再请求IP,如果这个等待时间较短,小于30s,会使得DHCP客户端无法较快有效的IP地址

交换机连接重要服务器时,如图,当SW1和SW2之间链路恢复后,会进行P/A机制,此时如果接PC的接口未配置成边缘端口,则此端口会阻塞,30s后才进入转发状态,PC1和PC2之间的通信将中断30s,配置边缘端口后,P/A机制中该端口不被阻塞,PC1和PC2之间互访不中断。

在这里插入图片描述

\

连接信息插座的接口

当网络中有大量终端时,终端的频繁上线会产生TC,使得交换机频繁删除自己的MAC表项及ARP表项,会产生大量未知单播报文,在网络中进行泛洪,影响网络性能(结RSTP中TC机制讲)。当网络发生TC时,未配置边缘端口的接口对应的MAC及ARP表项也会删除,同样也会产生未知单播报文进行泛洪。

当网络中有用户私接交换机或有意构造非法bpdu对网络进行攻击的场景;网络中有用户私接交换机或进行bpdu攻击时,可使用边缘端口配合bpdu防护,此时边缘端口收到bpdu后将会把端口shutdown;

shutdown后的接口需要管理员手工开启或配置 “error-down auto-recovery causes bpdu-protection interval XX”命令,超时时间后自动开启。

💬总结

1️⃣本篇文章分享了RSTP中的边缘端口特性

2️⃣华为认证资料和视频都在微信订阅号上感兴趣的小伙伴们可以去订阅一波不迷路哦~当然一键三连+关注更是妙不可言!

本文转载自: 掘金

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

关于Java的Velocity模板使用 1 Velocity

发表于 2021-11-24

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

Velocity是一个基于java的模板引擎,它允许任何人仅仅使用简单的模板语言来引用由java代码定义的对象。

1 Velocity的简介

Velocity模板引擎, 作为一款成熟的基于java的模板引擎,能够帮我们实现页面静态化,同时它将Java代码与网页分开,将模板和填入数据整合,生成我们需要的页面.

1 基本语法

1 关键字

Velocity模板中的关键字, 都是以#开头表示的

  • #set 设置一个变量
  • #if 条件分支判断
  • #else 另一个条件分支
  • #end 语句结束
  • #foreach 循环语句

2 变量

Velocity模板中的变量, 都是以$开头表示的

如: $user用户 $password 用户密码

{}变量

对于明确的Velocity变量, 可以使用{}包括起来, 可以在页面上展示如下效果:

${user}Name, 此时页面上可以表示为$someoneName的效果.

!变量

如上述内容,Velocity模板中如果变量不存在, 在页面会显示$user, 这种形式影响展示的效果. 可以使用$!user表示.

$!user表示, 存在则展示,不存在则为空白

1
2
3
4
5
6
7
8
9
10
11
12
vm复制代码## 定义一个user变量为李白, password变量为123456 
#set{$user = "李白"}
#set{$password = "123456"}

## 变量引用
#set{$student.name = "李白"}
## 数字
#set{$student.age = 22}
## 字符串
#set{$student.class = "大班"}
## 属性引用
#set($student.address = $address.info)

3 转义字符和逻辑操作符

Velocity模板中转义字符是 \

1
2
3
4
5
6
vm复制代码#set{$user = "李白"}
## 输入 结果
$user 李白
\$user $user
\\$user \李白
\\\$user \$user

&& 且

|| 或

! 取反

4 循环

Velocity模板中list集合循环语法

循环遍历,可以得到每个元素,每个元素的序号,以及总的集合长度

1
2
3
4
5
6
7
8
vm复制代码#foreach ( $element in $list)
## 集合中每个元素
$element
## 集合的序号 从1开始
${velocityCount}
## 集合的长度
${list.size()}
#end

map集合循环语法

1
2
3
4
vm复制代码#foreach ($entry in $map.entrySet())
## map的key map的value值
$entry.key => $entry.value
#end

5 条件

Velocity模板中条件语法if-ifelse-else结构

1
2
3
4
5
6
7
vm复制代码#if (condition1)
// 执行业务
#elseif (condition2)
// 执行业务
#else
// 执行业务
#end

常用的条件语句是if-else结构

1
2
3
4
5
vm复制代码#if (condition1)
// 执行业务
#else
// 执行业务
#end

#break

表示跳出循环

1
2
3
4
5
6
7
8
vm复制代码#if (condition1)
## 条件符合跳过
#if($user == "李白")
#break;
#end
#else
// 执行业务
#end

#stop

表示终止指令,终止模板解析

1
2
3
4
5
6
7
8
vm复制代码#if (condition1)
## 条件符合直接终止
#if($user == "李白")
#stop
#end
#else
// 执行业务
#end

6 注释

单行注释 ##

1
2
vm复制代码## 定义一个user变量为李白
#set{$user = "李白"}

多行注释 #* *#

1
2
3
4
5
vm复制代码#*  
定义一个user变量
将user变量赋值为 李白
*#
#set{$user = "李白"}

文档注释 #** *#

1
2
3
4
5
vm复制代码 #** 
@version 1.1
@author 李白
*#
#set{$user = "李白"}

7 引入资源

#include

表示引入外部资源,引入的资源不被引擎所解析

1
vm复制代码#include( "one.gif","two.txt","three.htm" )

#parse

用于导入脚本, 引入的资源会被引擎所解析

1
2
3
4
5
6
7
8
vm复制代码##  a.vm文件
#set($user = "李白")


## b.vm文件
#parse("a.vm")
## 变量 值
$user 李白

2 Velocity的使用

Velocity常用的案例和工具类

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
java复制代码public class VelocityUtils {

public static void main(String[] args) {

// 模板路径
String templatePath = "D:\\work";
// 模板名称
String templateName = "index.html.vm";
// 生成文件路径
String outFilePath = "D:\\index.html";
// 模板中所需参数
Map<String, Object> params = new HashMap<>();
params.put("name", "world");
List<String> list = new ArrayList<>();
list.add("李白");
list.add("杜甫");
list.add("陆游");
params.put("list", list);

getFile(templatePath,templateName,outFilePath,params);
}

/**
* 读取本地模板,生成文件
* @param templatePath 模板路径
* @param templateName 模板名称
* @param outFilePath 生成文件路径
* @param params 模板中填充参数
*/
public static void getFile(String templatePath, String templateName, String outFilePath,
Map<String, Object> params) {
try {
// 创建属性
loadTemplateFileByTwo(templatePath);

// 封装填充参数
VelocityContext context = new VelocityContext(params);
// 获取模板
Template tpl = Velocity.getTemplate(templateName, "UTF-8");
// 创建输出流
Writer writer = new PrintWriter(new FileOutputStream(new File(outFilePath)));
// 模板与数据填充
tpl.merge(context, writer);
// 刷新数据
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}

/**
* 送请求, 将生成的模板文件使用zip压缩包返回
* @param response 响应对象
* @param params 模板封装参数
* @throws IOException
*/
public static void createFile(HttpServletResponse response, Map<String, Object> params)
throws IOException {

ByteArrayOutputStream output = new ByteArrayOutputStream();
ZipOutputStream outZip = new ZipOutputStream(output);

// 设置velocity资源加载器
loadTemplateFileByOne();

// 封装模板数据
VelocityContext context = new VelocityContext(params);

//获取模板列表
List<String> templates = getTemplates();
for (String template : templates) {
// 渲染模板
StringWriter sw = new StringWriter();
Template tpl = Velocity.getTemplate(template, "UTF-8");
tpl.merge(context, sw);

// 添加数据
outZip.putNextEntry(new ZipEntry(getFileName(template)));
IOUtils.write(sw.toString(), outZip, "UTF-8");
IOUtils.closeQuietly(sw);
}

IOUtils.closeQuietly(outZip);
byte[] data = output.toByteArray();

// 生成zip压缩包响应
response.setHeader("Content-Disposition", "attachment; filename=\"template-file.zip\"");
response.addHeader("Content-Length", String.valueOf(data.length));
response.setContentType("application/octet-stream; charset=UTF-8");
IOUtils.write(data, response.getOutputStream());

}

/**
* 获取文件名
*/
/**
* @param template 模板名 如index.html.vm
*/
private static String getFileName(String template) {
return template.replace(".vm", "");
}

/**
* 获取模板
*/
private static List<String> getTemplates() {
List<String> templates = Lists.newArrayList();
// 后端相关模板
templates.add("index.html.vm");
return templates;
}


/**
* 加载配置文件方法一 加载classpath目录下的vm文件
*/
public static void loadTemplateFileByOne() {
Properties p = new Properties();
p.put("file.resource.loader.class",
"org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
Velocity.init(p);
}

/**
* 加载配置文件方法二 加载绝对路径目录下的加载vm文件
*
* @param templatePath 模板路径
*/
public static void loadTemplateFileByTwo(String templatePath) {
Properties p = new Properties();
// 设置模板加载路径 为D盘 work文件夹
p.setProperty(VelocityEngine.FILE_RESOURCE_LOADER_PATH, templatePath);
Velocity.init(p);
}

/**
* 加载配置文件方法三 使用配置文件
*
* @param propertiesPath ,如:/velocity.properties
*/
public static void loadTemplateFileByThree(String propertiesPath) throws IOException {
Properties p = new Properties();
p.load(VelocityUtils.class.getClass().getResourceAsStream(propertiesPath));
Velocity.init(p);
}

}

其中index.html.vm模板文件在D:盘work文件夹中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
html复制代码<table cellspacing="0" cellpadding="5" width="20%" >
<tr>
<td bgcolor="#eeeeee" align="center">
Names:${name}
</td>
</tr>
#foreach($name in $list)
<tr>
<td>
第${velocityCount}个, 名字为 $name , 总共 ${list.size()} 个
</td>
</tr>
#end
</table>

生成的index.html文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
html复制代码<table cellspacing="0" cellpadding="5" width="20%" >
<tr>
<td bgcolor="#eeeeee" align="center">
Names:world
</td>
</tr>
<tr>
<td>
第1个, 名字为 李白 , 总共 3 个
</td>
</tr>
<tr>
<td>
第2个, 名字为 杜甫 , 总共 3 个
</td>
</tr>
<tr>
<td>
第3个, 名字为 陆游 , 总共 3 个
</td>
</tr>
</table>

从上面测试的案例,可知,name参数有了, list集合参数有. 对于一些日常常规的循环条件判断等, Velocity模板引擎非常好用.

参考资料:

www.51gjie.com/javaweb/896…

yanglinwei.blog.csdn.net/article/det…

本文转载自: 掘金

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

Java中的代理模式

发表于 2021-11-24

1、什么是代理模式

代理模式:就是为其他对象提供一种代理以控制对这个对象的访问。

代理可以在不改动目标对象的基础上,增加其他额外的功能(扩展功能)。

image.png

举个例子来说明代理的作用: 一般我们想邀请明星来当我们的代言人,我们并不能直接联系到明星,而是通过其经纪人,来告诉经纪人我们需要和明星进行合作,然后通过经纪人来转达给明星。,明星只需要做好代言工作就好,其他繁琐的事情就交于经纪人就可以。这里的经经纪人就是一个代理对象,明星就是一个目标对象。

用图表示如下:

image.png

2、三种代理模式

2.1 静态代理

静态代理在使用时,需要定义接口或者父类,被代理对象(目标对象)与代理对象(Proxy)一起实现相同的接口或者是继承相同父类。

下面通过代码演示下:

接口IUserDao:

1
2
3
4
5
6
7
8
csharp复制代码/**
* 接口
*/
public interface IUserDao {

void save();

}

目标对象:UserDao:

1
2
3
4
5
6
7
8
9
10
11
csharp复制代码/**
* 实现接口
* 目标对象
*/
public class UserDao implements IUserDao {

public void save() {
System.out.println("----保存数据成功!----");
}

}

代理对象:UserDaoProxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
csharp复制代码/**
* 代理对象(静态代理)
*/
public class UserDaoProxy implements IUserDao{
//接收保存目标对象
private IUserDao target;
public UserDaoProxy(IUserDao target){
this.target=target;
}

public void save() {
System.out.println("开始事务...");
target.save();//执行目标对象的方法
System.out.println("提交事务...");
}
}

测试类:AppTest:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
arduino复制代码/**
* 测试类
*/
public class AppTest {
public static void main(String[] args) {
//目标对象
UserDao target = new UserDao();

//代理对象,把目标对象传给代理对象,建立代理关系
UserDaoProxy proxy = new UserDaoProxy(target);

proxy.save();//执行的是代理的方法
}
}

静态代理总结:

可以实现在不修改目标对象的基础上,对目标对象的功能进行扩展。

但是由于代理对象需要与目标对象实现一样的接口,所以会有很多代理类,类太多.同时,一旦接口增加方法,目标对象与代理对象都要维护.

可以使用动态代理方式来解决。

2.2 动态代理(JDK代理)

*动态代理有以下特点:

1.代理对象,不需要实现接口

2.代理对象的生成,是利用JDK的API,动态的在内存中创建代理对象(需要我们指定创建代理对象/目标对象实现的接口的类型)

3.动态代理也叫做:JDK代理,接口代理

JDK中生成代理对象的API

代理类所在包:java.lang.reflect.Proxy

JDK实现代理只需要使用newProxyInstance方法,但是该方法需要接收三个参数,完整的写法是:

1
scss复制代码static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h )

方法是在Proxy类中是静态方法,且接收的三个参数依次为:

1
arduino复制代码ClassLoader loader  //指定当前目标对象使用类加载器
1
less复制代码Class<?>[] interfaces  //目标对象实现的接口的类型,使用泛型方式确认类型
1
arduino复制代码InvocationHandler h  //事件处理器

下面进行代码演示:

接口类IUserDao

1
2
3
4
5
6
7
8
csharp复制代码/**
* 接口
*/
public interface IUserDao {

void save();

}

目标对象UserDao

1
2
3
4
5
6
7
8
9
10
11
12
csharp复制代码/**
* 接口实现
* 目标对象
*/
public class UserDao implements IUserDao {

public void save() {

System.out.println("----保存数据成功!----");
}

}

代理工厂类:ProxyFactory

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
typescript复制代码/**
* 创建动态代理对象
* 动态代理不需要实现接口,但是需要指定接口类型
*/
public class ProxyFactory{

//维护一个目标对象
private Object target;
public ProxyFactory(Object target){
this.target=target;
}

//给目标对象生成代理对象
public Object getProxyInstance(){
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("开始事务111");
//执行目标对象方法
Object returnValue = method.invoke(target, args);
System.out.println("提交事务111");
return returnValue;
}
}
);
}

}

测试类:App:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
scss复制代码/**
* 测试类
*/
public class App {
public static void main(String[] args) {
// 目标对象
IUserDao target = new UserDao();
// 【原始的类型 class com.zhong.UserDao】
System.out.println(target.getClass());

// 给目标对象,创建代理对象
IUserDao proxy = (IUserDao) new ProxyFactory(target).getProxyInstance();
// class $Proxy0 内存中动态生成的代理对象
System.out.println(proxy.getClass());

// 执行方法 【代理对象】
proxy.save();
}
}

在这里我们会想:代理对象是谁,是如何生成这个代理对象的呢?接下来我们主要看这个方法 getProxyInstance()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typescript复制代码//给目标对象生成代理对象
public Object getProxyInstance(){
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("开始事务111");
//执行目标对象方法
Object returnValue = method.invoke(target, args);
System.out.println("提交事务111");
return returnValue;
}
}
);

我们看到其返回了一个Proxy类的对象,即JDK的动态代理,是通过一个叫Proxy的类的静态方法newProxyInstance来实现的,其那么我们就去它的源码里看一下它到底都做了些什么?

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
java复制代码public static Object newProxyInstance(ClassLoader loader,
Class>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
//检查h 不为空,否则抛异常
Objects.requireNonNull(h);

final Class>[] intfs = interfaces.clone();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}

/*
* 获得与指定类装载器和一组接口相关的代理类类型对象
*/
Class> cl = getProxyClass0(loader, intfs);

/*
* 通过反射获取构造函数对象并生成代理类实例
*/
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}
//获取代理对象的构造方法(也就是$Proxy0(InvocationHandler h))
final Constructor> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
//生成代理类的实例并把InvocationHandlerImpl的实例传给它的构造方法
return cons.newInstance(new Object[]{h});
} catch (IllegalAccessException|InstantiationException e) {
throw new InternalError(e.toString(), e);
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new InternalError(t.toString(), t);
}
} catch (NoSuchMethodException e) {
throw new InternalError(e.toString(), e);
}
}

上面的代码表明,首先通过getProxyClass0获得这个代理类,然后通过c1.getConstructor()拿到构造函数,最后一步,通过cons.newInstance返回这个新的代理类的一个实例,注意:调用newInstance的时候,传入的参数为h,即我们自己定义好的InvocationHandler类 。我们再进去getProxyClass0方法看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
csharp复制代码 /**
* Generate a proxy class. Must call the checkProxyAccess method
* to perform permission checks before calling this.
*/
private static Class> getProxyClass0(ClassLoader loader,
Class>... interfaces) {
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}

// If the proxy class defined by the given loader implementing
// the given interfaces exists, this will simply return the cached copy;
// otherwise, it will create the proxy class via the ProxyClassFactory
return proxyClassCache.get(loader, interfaces);
}

这里用到了缓存,先从缓存里查一下,如果存在,直接返回,不存在就新创建。

真相还是没有来到,继续,看一下proxyClassCache

1
2
3
4
5
arduino复制代码/**
* a cache of proxy classes
*/
private static final WeakCache[], Class>>
proxyClassCache = new WeakCache(new KeyFactory(), new ProxyClassFactory());

再看下proxyClassCache.get方法,

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
java复制代码 public synchronized V get() { // serialize access
// re-check
Supplier supplier = valuesMap.get(subKey);
if (supplier != this) {
// something changed while we were waiting:
// might be that we were replaced by a CacheValue
// or were removed because of failure ->
// return null to signal WeakCache.get() to retry
// the loop
return null;
}
// else still us (supplier == this)

// create new value
V value = null;
try {
value = Objects.requireNonNull(valueFactory.apply(key, parameter));
} finally {
if (value == null) { // remove us on failure
valuesMap.remove(subKey, this);
}
}
// the only path to reach here is with non-null value
assert value != null;

// wrap value with CacheValue (WeakReference)
CacheValue cacheValue = new CacheValue(value);

// try replacing us with CacheValue (this should always succeed)
if (valuesMap.replace(subKey, this, cacheValue)) {
// put also in reverseMap
reverseMap.put(cacheValue, Boolean.TRUE);
} else {
throw new AssertionError("Should not reach here");
}

// successfully replaced us with new CacheValue -> return the value
// wrapped by it
return value;
}
}

其中,value = Objects.requireNonNull(valueFactory.apply(key, parameter));

提到了apply(),是Proxy类的内部类ProxyClassFactory实现其接口的一个方法,具体实现如下:

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
java复制代码/**
* A factory function that generates, defines and returns the proxy class given
* the ClassLoader and array of interfaces.
*/
private static final class ProxyClassFactory
implements BiFunction[], Class>>
{
// prefix for all proxy class names
private static final String proxyClassNamePrefix = "$Proxy";

// next number to use for generation of unique proxy class names
private static final AtomicLong nextUniqueNumber = new AtomicLong();

@Override
public Class> apply(ClassLoader loader, Class>[] interfaces) {

Map, Boolean> interfaceSet = new IdentityHashMap(interfaces.length);
for (Class> intf : interfaces) {
/*
* Verify that the class loader resolves the name of this
* interface to the same Class object.
*/
Class> interfaceClass = null;
try {
interfaceClass = Class.forName(intf.getName(), false, loader);
} catch (ClassNotFoundException e) {
}
if (interfaceClass != intf) {
throw new IllegalArgumentException(
intf + " is not visible from class loader");
}
/*
* Verify that the Class object actually represents an
* interface.
*/
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException(
interfaceClass.getName() + " is not an interface");
}
/*
* Verify that this interface is not a duplicate.
*/
if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
throw new IllegalArgumentException(
"repeated interface: " + interfaceClass.getName());
}
}

String proxyPkg = null; // package to define proxy class in
int accessFlags = Modifier.PUBLIC | Modifier.FINAL;

/*
* Record the package of a non-public proxy interface so that the
* proxy class will be defined in the same package. Verify that
* all non-public proxy interfaces are in the same package.
*/
for (Class> intf : interfaces) {
int flags = intf.getModifiers();
if (!Modifier.isPublic(flags)) {
accessFlags = Modifier.FINAL;
String name = intf.getName();
int n = name.lastIndexOf('.');
String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
if (proxyPkg == null) {
proxyPkg = pkg;
} else if (!pkg.equals(proxyPkg)) {
throw new IllegalArgumentException(
"non-public interfaces from different packages");
}
}
}

if (proxyPkg == null) {
// if no non-public proxy interfaces, use com.sun.proxy package
proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
}

/*
* Choose a name for the proxy class to generate.
*/
long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;

/*
* Generate the specified proxy class.
*/
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
try {
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
/*
* A ClassFormatError here means that (barring bugs in the
* proxy class generation code) there was some other
* invalid aspect of the arguments supplied to the proxy
* class creation (such as virtual machine limitations
* exceeded).
*/
throw new IllegalArgumentException(e.toString());
}
}
}

这里我们看到了熟悉的方法Class.forName();要加载指定的接口,即是生成类,那就有对应的class字节码

1
2
ini复制代码/生成字节码
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);

接下来我们也使用测试一下,使用这个方法生成的字节码是个什么样子:

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
ini复制代码package com.adam.java.basic;

import java.io.FileOutputStream;
import java.io.IOException;
import sun.misc.ProxyGenerator;

public class DynamicProxyTest {

public static void main(String[] args) {

IUserDao userdao = new UserDao();

ProxyFactory handler = new ProxyFactory (
userdao);

IUserDao proxy = (IUserDao ) handler.getProxyInstance();

proxy.save();

String path = "C:/$Proxy0.class";
byte[] classFile = ProxyGenerator.generateProxyClass("$Proxy0",
UserDao.class.getInterfaces());
FileOutputStream out = null;

try {
out = new FileOutputStream(path);
out.write(classFile);
out.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

不是原始的IUserDao里的save()方法了,而是新生成的代理类的save()方法,我们将生成的$Proxy0.class文件用jd-gui打开

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public final void save()
throws
{
try
{
this.h.invoke(this, m3, null);
return;
}
catch (Error|RuntimeException localError)
{
throw localError;
}
catch (Throwable localThrowable)
{
throw new UndeclaredThrowableException(localThrowable);
}
}

核心就在于this.h.invoke(this. m3, null);此处的h是啥呢?我们看看这个类的类名:

public final class $Proxy0 extends Proxy implements IUserDao

不难发现,新生成的这个类,继承了Proxy类实现了IUserDao这个接口,而这个UserService就是我们指定的接口,所以,这里我们基本可以断定,JDK的动态代理,生成的新代理类就是继承了Proxy基类,实现了传入的接口的类。那这个h到底是啥呢?我们再看看这个新代理类,看看构造函数:

1
2
3
4
5
6
swift复制代码public $Proxy0(InvocationHandler paramInvocationHandler)  
throws
{
super(paramInvocationHandler);

}

这里传入了InvocationHandler类型的参数,而之前有一句代码:

return cons.newInstance(new Object[]{h});

这是newInstance方法的最后一句,传入的h,就是这里用到的h,也就是我们最初自己定义的MyInvocationHandler类的实例。所以,我们发现,其实最后调用的save()方法,其实调用的是ProxyFactory的invoke()方法.继续看:

1
2
3
4
5
6
7
8
9
10
vbnet复制代码static
{
try
{
m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
m3 = Class.forName("com.zhong.IUserDao").getMethod("save", new Class[0]);
m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
return;
}

m3就是原接口的save()方法.

通过跟踪提示代码可以看出:当代理对象调用真实对象的方法时,其会自动的跳转到代理对象关联的handler对象的invoke方法来进行调用。

总结:

动态代理实现过程:

  1. 通过getProxyClass0()生成代理类。JDK生成的最终真正的代理类,它继承自Proxy并实现了我们定义的接口.
  2. 通过Proxy.newProxyInstance()生成代理类的实例对象,创建对象时传入InvocationHandler类型的实例。
  3. 调用新实例的方法,即此例中的save(),即原InvocationHandler类中的invoke()方法。

代理对象不需要实现接口,但是目标对象一定要实现接口,否则不能用动态代理

2.3.Cglib代理

JDK的动态代理机制只能代理实现了接口的类,而不能实现接口的类就不能实现JDK的动态代理,cglib是针对类来实现代理的,他的原理是对指定的目标类生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对final修饰的类进行代理。

Cglib代理,也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能的扩展

Cglib子类代理实现方法:

1.需要引入cglib的jar文件,但是Spring的核心包中已经包括了Cglib功能,所以直接引入Spring-core.jar即可.

2.引入功能包后,就可以在内存中动态构建子类

3.代理的类不能为final,否则报错

4.目标对象的方法如果为final/static,那么就不会被拦截,即不会执行目标对象额外的业务方法.

代码演示如下:

1
2
3
4
5
6
7
8
9
csharp复制代码/**
* 目标对象,没有实现任何接口
*/
public class UserDao {

public void save() {
System.out.println("----保存数据成功!----");
}
}

Cglib代理工厂:ProxyFactory

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
typescript复制代码/**
* Cglib子类代理工厂
* 对UserDao在内存中动态构建一个子类对象
*/
public class ProxyFactory implements MethodInterceptor{
//维护目标对象
private Object target;

public ProxyFactory(Object target) {
this.target = target;
}

//给目标对象创建一个代理对象
public Object getProxyInstance(){
//1.工具类
Enhancer en = new Enhancer();
//2.设置父类
en.setSuperclass(target.getClass());
//3.设置回调函数
en.setCallback(this);
//4.创建子类(代理对象)
return en.create();

}

@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("开始事务...");

//执行目标对象的方法
Object returnValue = method.invoke(target, args);

System.out.println("提交事务...");

return returnValue;
}
}

测试类APPTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码/**
* 测试类
*/
public class AppTest {

@Test
public void test(){
//目标对象
UserDao target = new UserDao();

//代理对象
UserDao proxy = (UserDao)new ProxyFactory(target).getProxyInstance();

//执行代理对象的方法
proxy.save();
}
}

本文转载自: 掘金

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

kafka数据可靠性与一致性 ISR 复制机制 数据一致性保

发表于 2021-11-24

Kafka是一款高性能、高可用的分布式发布订阅消息系统。 从 0.10 版本开始,Kafka Streams java库提供了流处理数据的基本操作,从此Kafka 的标语已经改为一个分布式流平台。保证Kafka高可用模型依靠的是副本机制,副本机制保障机器宕机不会发生数据丢失问题。那么如何解决写多份带来一致性问题呢?如何解决一致性问题又会带来性能问题?

微信图片_20211124195045.png

ISR 复制机制

ISR (In-Sync Replicas)是Leader在Zookeeper(/brokers/topics/[topic]/partitions/[partition]/state)目录中动态维护基本保持同步的Replica列表,该列表中保存的是与Leader副本保持消息同步的所有副本对应的节点id。如果一个Follower宕机或者其落后情况超过任意参数replica.lag.time.max.ms(延迟时间)、replica.lag.max.messages(延迟条数,Kafka 0.10.x版本后移除)设置阈值,则该Follower副本节点将从ISR列表中剔除并存入OSR(Outof-Sync Replicas)列表。

ISR冗余备份机制核心逻辑围绕HW值、LEO值展开。

LEO(log end offset)日志末端偏移量,记录了该副本对象底层日志文件中下一条消息的位移值。

HW(highwatermark),高水印值,任何一个副本对象的HW值一定不大于其LEO值,而小于或等于HW值的所有消息被认为是“已提交的”或“已备份的”。consumer只能消费已提交的消息,HW之后的数据对consumer不可见。

u=1743374214,2048573772&fm=173&app=25&f=JPEG.jpg

数据同步过程如下:

Follower向Leader发送fetch请求(此过程类似于普通Customer,区别在于内部broker的读取请求,没有HW的限制)。

Leader接收到Follwer fetch操作后根据fetch请求中Postion从自身log中获取相应数据,并根据fetch请求中Postion更新leader中存储的follower LEO。通过follower LEO读取存在于ISR列表中副本的LEO(包括leader自己的LEO)值,并选择最小的LEO值作为HW值。

Follower接收到leader的数据响应后,开始向底层log写数据,每当新写入一条消息,其LEO值就会加1,写完数据后,通过比较当前LEO值与FETCH响应中leader的HW值,取两者的小者作为新的HW值。

由此可见,Kafka复制机制既不是完全的同步复制,也不是单纯的异步复制,Kafka通过 ISR复制机制在保障数据一致性情况下又可提供高吞吐量。

数据一致性保证

  • request.required.acks:该参数在producer向leader发送数据时设置。

0:producer无需等待来自broker的确认而继续发送下一批消息。这种情况下数据传输效率最高,但是数据可靠性确是最低的。

1(默认):producer在ISR中的leader已成功收到数据并得到确认。如果leader宕机了,则会丢失数据。

-1:producer需要等待ISR中的所有follower都确认接收到数据后才算一次发送完成,可靠性最高。但是这样也不能保证数据不丢失,比如当ISR中只剩下一个leader时,这样就变成了acks=1的情况。

min.insync.replicas:该参数在broker或者topic层面进行设置,设定ISR中的最小副本数是多少,默认值为1,当且仅当request.required.acks参数设置为-1时,此参数才生效。如果ISR中的副本数少于min.insync.replicas配置的数量时,客户端会返回异常:org.apache.kafka.common.errors.NotEnoughReplicasExceptoin: Messages are rejected since there are fewer in-sync replicas than required。

  • unclean.leader.election.enable:

true:默认值,所有replica都有成为leader的可能。

false:只有在ISR中存在的replica才有成为leader的可能。

要保证数据写入到Kafka是安全的,高可靠的,需要如下的配置:

topic的配置:replication.factor>=3,即副本数至少是3个;

2<=min.insync.replicas<=replication.factor

broker的配置:leader的选举条件unclean.leader.election.enable=false

producer的配置:request.required.acks=-1(all),producer.type=sync(同步)

本文转载自: 掘金

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

网络编程相关概念 网络编程相关概念

发表于 2021-11-24

网络编程相关概念

最近在看Tomcat源码,涉及到网络编程模型,所以将这块的知识点进行一个归纳整理。

网络

网络是由若干节点(计算机)和连接这些节点的链路构成,表示诸多对象及其相互联系。

网络特性

  • 1、资源共享

网络的主要功能就是资源共享。共享的资源包括软件资源、硬件资源以及存储在公共数据库中的各类数据资源。网上用户能部分或全部地共享这些资源,使网络中的资源能够互通有无、分工协作,从而大大提高系统资源的利用率。

  • 2、快速传输信息

分布在不同地区的计算机系统,可以通过网络及时、高速地传递各种信息,交换数据,发送电子邮件,使人们之间的联系更加紧密。

  • 3、提高系统可靠性

在网络中,由于计算机之间是互相协作、互相备份的关系,以及在网络中采用一些备份的设备和一些负载调度、数据容错等技术,使得当网络中的某一部分出现故障时,网络中其他部分可以自动接替其任务。因此,与单机系统相比,计算机网络具有较高的可靠性。

  • 4、易于进行分布式处理

在网络中,还可以将一个比较大的问题或任务分解为若干个子问题或任务,分散到网络中不同的计算机上进行处理计算。这种分布处理能力在进行一些重大课题的研究开发时是卓有成效的。

  • 5、综合信息服务

在当今的信息化社会里,个人、办公室、图书馆、企业和学校等,每时每刻都在产生并处理大量的信息。这些信息可能是文字、数字、图像、声音甚至是视频,通过网络就能够收集、处理这些信息,并进行信息的传送。因此,综合信息服务将成为网络的基本服务功能。

网络四要素

  • 通信线路和通信设备
  • 有独立功能的计算机
  • 软件支持
  • 实现数据通信与资源共

网络分类

1、按拓扑结构分类:总线型、环型、星型、网状 2、按信息交换方式分类:电路交换、报文交换、报文分组交换 3、按覆盖范围分类:局域网LAN、广域网WAN

局域网:局部区域网络(local area network)通常简称为”局域网”,缩写为LAN。局域网是结构复杂程度最低的计算机网络。局域网仅是在同一地点上经网络连在一起的一组计算机。局域网通常挨得很近,它是如今应用最广泛的一类网络。

广域网:广域网网络(wide area network)又称外网、公网。是连接不同地区局域网或城域网计算机通信的远程网。

TCP与IP

定义

TCP/IP协议(Transfer Control Protocol/Internet Protocol)叫做传输控制/网际协议,又叫网络通讯协议,它包括上百个各种功能的协议,而TCP协议和IP协议是保证数据完整传输的两个基本的重要协议。 通常说TCP/IP是Internet协议簇,而不单单是TCP和IP TCP/IP协议的基本传输单位是数据包(Datagram)。TCP协议负责把数据分成若干个数据包,并给每个数据包加上包头;IP协议在每个包头上再加上接收端主机地址, 这样数据找到自己要去的地方。如果传输过程中出现数据丢失、数据失真等情况,TCP协议会自动要求数据重新传输,并重新组包。总之,IP协议保证数据的传输,TCP协议保证数据传输的质量

TCP与IP协议结构

TCP/IP协议数据的传输基于TCP/IP协议的四层结构:应用层(应用层、表示层、会话层)、传输层、网络层、网络接口层(数据链路层、物理层),数据在传输时每通过一层就要在数据上加个包头, 其中的数据供接收端同一层协议使用,而在接收端,每经过一层要把用过的包头去掉,这样来保证传输数据的格式完全一致。如下为4层(包含OSI七层)示意图:

各层作用 - 物理层:原始的比特流的传输,电子信号传输。 - 数据链路层:将数据分帧,并且处理流控制,指定拓扑结构并且提供硬件寻址。 - 网络层:通过寻址建立俩个节点之间的连接。 - 传输层:数据递送 - 会话层:在两个节点之间建立连接 - 表示层:格式化数据,方便为应用程序提供通用的接口 - 应用层:直接为应用程序提供服务

IP地址

Internet依靠TCP/IP协议,在全球范围内实现不同硬件结构、不同操作系统、不同网络系统的互联。在Internet上,每一个节点都依靠的IP地址互相区分和相互联系。 IP地址是一个32位二进制数的地址,由4个8位字段组成,每个字段之间用点号隔开,用于标识TCP/IP宿主机。

IO

在linux系统中有一句话:一切皆文件 ,文件就是流的概念,在进行信息的交流过程中,对这些流进行数据的收发 操作就是IO操作。

不管socket、管道、终端,对Linux来说,一切都是文件,一切都是流。在信息 交换的过程中,我们都是对这些流进行数据的收发操作, 简称为I/O操作(input and output),往流中读出数据,系统调用read,写入数据,系统调用write。值得一提的是计算机里有这么多的流, 怎么知道要操作哪个流呢?这里就有可文件描述符的概念,即通常所说的fd,一个fd就是一个整数,所以,对这个整数的操作,就是对这个文件(流)的操作。 我们创建一个socket,通过系统调用会返回一个文件描述符,那么剩下对socket的操作就会转化为对这个描述符的操作。

同步

所谓同步就是每件事情必须有回应才能进行下一件事情,也就是说必须一件一件的事情来做。如下图:

异步

所谓异步就是客户端发送完事件后可以干其他的事情,可以继续发送其他的请求,等到服务端处理完成后,通过 回调将结果告知客户端就可以。如下图:

阻塞

所谓阻塞就是在请求没有处理完成之前,线程一直处于等待的状态,直到线程处理完成后才给返回结果。

非阻塞

所谓非阻塞就是在请求没有处理完成之前,直接返回结果。不用等到线程处理完成之后才有响应。

IO模型

同步阻塞IO模型

所有的套接字默认都是阻塞的,它要等到有 数据包到达并且被复制到应用程序的进程缓冲区中或者出现了异常 才被返回 否则一直阻塞。模型如下图:

原理:用户线程通过系统调用read发起IO读操作,由用户空间转到内核空间。 内核等到数据包到达后,然后将接收的数据拷贝到用户空间,完成read操作。

同步非阻塞IO模型

非阻塞IO通过进程反复调用IO函数(多次系统调用,并马上返回),知道等待待成功的通知。如下图:

原理:由于socket是非阻塞的方式,因此用户线程发起IO请求时立即返回。 但并未读取到任何数据, 用户线程需要不断地发起IO请求,直到数据到达后,才真正读取到数据,继续执行。

同步IO多路复用模型

可以通过系统调用select、poll、epoll实现IO复用模型。此时进程就会组赛在这些系统调用上,而不是阻塞在真正的IO操作上, 直到有就绪事件了,这些系统调用就会返回哪些套接字可读写,然后就可以进行把数据包复制到应用进程缓冲区了 IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。如下图:

原理:将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。 当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。

异步IO模型

由用户进程告知内核启动一个操作,并且由内核去操作,操作完后给用户进程发一个通知,通知用户进程操作完了。如下图

异步IO模型中,当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户线程指定的缓冲区内,内核在IO完成后通知用户线程直接使用即可

小结

以上的相关概念是作者认为学习网络编程中,我们应该知道的一些概念。我们在学习以后的内容的时候,都是以这些为基础的。

本文转载自: 掘金

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

java jasperreports PDF导出,中文不显示

发表于 2021-11-24
  1. 入门参考连接

  1. 解决中文不显示

2.1 jasperreports studio 需要的操作

1
2
makefile复制代码现在C:\Windows\Fonts 中搜索 STSONG.TTF 复制到其他目录中(直接在这个目录找不到字体文件)
在 jasperreports studio中添加字体 windows -> preferences

image.png

image.png

image.png

image.png

2.2 项目中需要修改的地方

image.png

2.2.1 fonts.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<fontFamilies>
<fontFamily name="华文宋体">
<normal>jasperreport/fonts/STSONG.TTF</normal>
<bold>jasperreport/fonts/STSONG.TTF</bold>
<italic>jasperreport/fonts/STSONG.TTF</italic>
<boldItalic>jasperreport/fonts/STSONG.TTF</boldItalic>
<pdfEncoding>Identity-H</pdfEncoding>
<pdfEmbedded>true</pdfEmbedded>
<exportFonts>
<export key="net.sf.jasperreports.html">'华文宋体', Arial, Helvetica, sans-serif</export>
<export key="net.sf.jasperreports.xhtml">'华文宋体', Arial, Helvetica, sans-serif</export>
</exportFonts>
</fontFamily>
</fontFamilies>

2.2.3 STSONG.TTF

1
复制代码2.1 中在 windows fonts 中粘贴出来的字体

2.2.4 jasperreports_extension.properties

1
2
ini复制代码net.sf.jasperreports.extension.registry.factory.simple.font.families=net.sf.jasperreports.engine.fonts.SimpleFontExtensionsRegistryFactory
net.sf.jasperreports.extension.simple.font.families.lobstertwo=jasperreport/fonts/fonts.xml

3.导出代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ini复制代码Map<String, Object> paramMap = getMapData();
JRBeanCollectionDataSource dataSource = new JRBeanCollectionDataSource(getEntities());
ClassPathResource resource = new ClassPathResource(TEMPLATE_PATH + "StudentDetails.jasper");
try {
InputStream inputStream = resource.getInputStream();
// 详情 + 表头
JasperPrint jasperPrint = JasperFillManager.fillReport(inputStream, paramMap, dataSource);
byte[] bytes = JasperExportManager.exportReportToPdf(jasperPrint);
FileOutputStream outFile = new FileOutputStream(FILE_PATH);
outFile.write(bytes);
outFile.close();
} catch (JRException | IOException e) {
e.printStackTrace();
}

3.1 导出效果图

image.png

4.实际导出中遇到问题

image.png

4.1 列表头重复输出

image.png

4.2 行间距问题

image.png

5.源码地址

github.com/mingyujiao/…

gitee.com/breaksb/exp…

5.1 测试方法

image.png

注意:需要修改pdf生成目录
image.png

本文转载自: 掘金

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

【阅读 GinSkeleton】参数验证器 validato

发表于 2021-11-24

validator

推荐学习:validator库参数校验若干实用技巧

官方文档:validator

这里主要介绍validator以注册功能为例从注册到使用的全过程。

注册验证器

首先你需要在http\validator\common\register_validator 这里注册你的验证器,也就是将它以键值对的形式进行存储到容器里面。当然了,这里需要进行初始化。

1
2
go复制代码	key = consts.ValidatorPrefix + "UsersRegister"
containers.Set(key, users.Register{})

编写验证器

这里注释写的很清楚了。

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
go复制代码// 给出一些最常用的验证规则:
//required 必填;
//len=11 长度=11;
//min=3 如果是数字,验证的是数据范围,最小值为3,如果是文本,验证的是最小长度为3,
//max=6 如果是数字,验证的是数字最大值为6,如果是文本,验证的是最大长度为6
// mail 验证邮箱
//gt=3 对于文本就是长度>=3
//lt=6 对于文本就是长度<=6

type Register struct {
BaseField
// 表单参数验证结构体支持匿名结构体嵌套、以及匿名结构体与普通字段组合
Phone string `form:"phone" json:"phone"` // 手机号, 非必填
CardNo string `form:"card_no" json:"card_no"` //身份证号码,非必填
}

// 特别注意: 表单参数验证器结构体的函数,绝对不能绑定在指针上
// 我们这部分代码项目启动后会加载到容器,如果绑定在指针,一次请求之后,会造成容器中的代码段被污染

func (r Register) CheckParams(context *gin.Context) {
//1.先按照验证器提供的基本语法,基本可以校验90%以上的不合格参数
if err := context.ShouldBind(&r); err != nil {
errs := gin.H{
"tips": "UserRegister参数校验失败,参数不符合规定,user_name 长度(>=1)、pass长度[6,20]、不允许注册",
"err": err.Error(),
}
response.ErrorParam(context, errs)
return
}
//2.继续验证具有中国特色的参数,例如 身份证号码等,基本语法校验了长度18位,然后可以自行编写正则表达式等更进一步验证每一部分组成
// r.CardNo 获取值继续校验,这里省略.....

// 该函数主要是将本结构体的字段(成员)按照 consts.ValidatorPrefix+ json标签对应的 键 => 值 形式绑定在上下文,便于下一步(控制器)可以直接通过 context.Get(键) 获取相关值
extraAddBindDataContext := data_transfer.DataAddContext(r, consts.ValidatorPrefix, context)
if extraAddBindDataContext == nil {
response.ErrorSystem(context, "UserRegister表单验证器json化失败", "")
} else {
// 验证完成,调用控制器,并将验证器成员(字段)递给控制器,保持上下文数据一致性
(&web.Users{}).Register(extraAddBindDataContext)
}

}

复杂参数的解决

如果这样一个一个进行获取在大量参数上会很复杂的。

其实很简单就是你单独设置一个就可了。

从工厂中取出验证器

1
2
3
4
5
6
7
8
9
10
11
go复制代码func Create(key string) func(context *gin.Context) {
//通过键key取出验证器对象。
if value := container.CreateContainersFactory().Get(key); value != nil {
//通过断言拿到对象
if val, isOk := value.(interf.ValidatorInterface); isOk {
return val.CheckParams
}
}
variable.ZapLog.Error(my_errors.ErrorsValidatorNotExists + ", 验证器模块:" + key)
return nil
}

然后就会执行对象的CheckParams接口(验证器)。

控制器

1
go复制代码(&web.Users{}).Register(extraAddBindDataContext)

接下来就是你编写控制器日常的操作了。

获取参数。

1
2
3
go复制代码	userName := context.GetString(consts.ValidatorPrefix + "user_name")
pass := context.GetString(consts.ValidatorPrefix + "pass")
userIp := context.ClientIP()

剩余就常规处理了。

以上整体的流程如下:

  • 创建一个对象,实现CheckParams接口
  • 项目初始化,注册到容器里面
  • 当你请求来的时候,从工厂中取出验证器(切入验证器)将参数添加到上下文中,供全局使用。
  • 然后进入控制器,之后就是service和model

我只能说优雅!!!!

本文转载自: 掘金

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

【第三方库】Casbin casbin

发表于 2021-11-24

casbin

推荐学习:casbin开发文档

Go 每日一库之 casbin

casbin视频

在 Go 语言中使用 casbin 实现基于角色的 HTTP 权限控制

什么是casbin

Casbin是一个强大的、高效的开源访问控制框架,其权限管理机制支持多种访问控制模型。

说起访问控制,之前所用的只是 jwt 方式,一个简单的鉴权,但是遇到了复杂场景,那么这个方式就不太合适了。

说到鉴权,先问三个问题:

  • 如何鉴别一个人身份?
  • 如何允许一个人的行为?
  • 如何将身份与行为进行关联?

ACL模型

ACL 模型至少会包含以下四个部分:

[request_definition]:请求定义

1
2
ini复制代码[request_definition]
r = sub, obj, act

sub, obj, act 表示经典三元组: 访问实体 (Subject),访问资源 (Object) 和访问方法 (Action)。

通俗话讲:sub 指是谁来访问,obj 指你要访问什么,act 指你打算怎么访问

这里规定:你需要按照这个定义去发起一个访问。

[policy_definition]:策略定义

1
2
ini复制代码[policy_definition]
p = sub, obj, act,(eft)

这里也是经典三元组sub, obj, act 。当然啦,你也可以定义其他名称。

eft 是影响,你可以去制定影响,但是这里的值只能是 allow 和 deny 。

这里规定:可以通过哪些部分进行限制

[matchers]:规则

1
2
ini复制代码[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act

通过两部分(request_definition,policy_definition)去制定访问的规则。

您可以使用算术(如 +、-、*、/)和逻辑运算符(如 &&、||、!)。

[policy_effect]:策略的影响

这里的策略影响只能有以下这五个,不允许有其他的。

Policy effect 意义 示例
some(where (p.eft == allow)) 至少一个allow ACL, RBAC, etc.
!some(where (p.eft == deny)) 不允许有一个deny Deny-override
some(where (p.eft == allow)) && !some(where (p.eft == deny)) 至少一个allow但是不能有deny Allow-and-deny
priority(p.eft) deny
subjectPriority(p.eft) 基于角色的优先级 主题优先级

以上是一个模型的部分。

我们要根据这个模型去指定一个访问控制的规则。

你可以打开 Casbin 的编译器,进行模拟使用。

image-20211118163554237

简述一下流程:

  • 以alice, data1, read去发起请求,那么在模型中就会进行对标 sub=alice,obj=data1,act=read
  • 策略这边会根据你所写的进行对标sub=alice,obj=data1,act=read
  • 利用请求和策略中拿到的值,进行规则的匹配,就会有一个eft ,也就是结果(allow 或者 deny)。
  • eft,就会进入 policy_effect,再进行一次验证。
  • 最后得出结果

注意点

①如果你希望去控制一个策略的eft,你需要在policy_definition ,否则会默认,你没有这个值。即使你的策略后面加了,也是没有用的。

image-20211118165314217

因为他的 eft 是通过匹配生成的,而不是你匹配上就给 eft 赋值。

②如果你想使用!some(where (p.eft == deny))这样的匹配规则,那么你需要给policy_definition加上 eft 字段,否则在这里相当于没有规则。

image-20211118165832331

你发起的一个请求,会与所有的策略进行匹配,如果没有匹配成功,不会将 eft 去赋值一个默认的 allow 或者一个定义 deny。最后结果这次的请求的 eft 没有一个deny (可能是一个空值)。这也对应了他的要求:匹配完成后不存在一个 deny。所以返回结果就是 true。

你可以利用 Casbin 去验证自己的所有的猜想。

这种模型,就好像是一对一的比较,对于一些复杂场景,例如:只要是学生就可以吃饭,我总不能把所有学生的名字都这样一个一个写上去吧,好像就有点玩不来了。我们来看下一个模型。

RBAC

这个模型又比 ACL 高级在哪里呢?

[role_definition]:身份定义

1
2
ini复制代码[role_definition]
g = _, _

第一个下划线代表名字(具体的请求实体),第二个下划线代表处于什么身份(角色集)

我们回到刚才提到的问题:只要是学生就可以吃饭。

加了一个 role_definition 就能解决了吗?

根据这个问题进行拆分:角色集:学生,资源:饭,行为:吃。

只要当前请求实体身份是学生,想要的资源是饭,做出的行为是吃,就可以通过这次请求。

image-20211118171726304

在这个模型里面,你可以通过身份达到对权限控制,同时也可以通过精准匹配达到权限控制。

g(r.sub, p.sub)他干了什么?进行身份的匹配,如果匹配的上,那么后续的策略匹配,我会带着这个身份去进行匹配,如果匹配不上,那么我只能去进行精准匹配了。

在某些特定的场景,好像这个也玩不来。例如:我希望学生中启明星工作室的能够拿学习资料。这里就有两个身份了,并且这两个身份启明星工作室是学生的子集。

扩展RBAC

他比 RBAC 多了一个域。在我看来就是又再次在身份下面又划分出一个域。就类似于有的学生还有启明星工作室这个身份。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ini复制代码[request_definition]
r = sub, dom, obj, act

[policy_definition]
p = sub, dom, obj, act

[role_definition]
g = _, _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act

之前是基于一个全局的概念去进行的权限控制,现在可以在全局下再分多个域出来,基于这个域进行权限控制。

注意点

image-20211118182637014

g(r.sub, p.sub, r.dom) 感觉好像只是做了一个身份验证,还需要其他的限制才能更好的控制域。

没有看源码!只是自己的猜测。后面看源码后再做进一步的解释。

ABAC

RBAC模型对于实现比较规则的、相对静态的权限管理非常有用。但是对于特殊的、动态的需求,RBAC就显得有点力不从心了。例如,我们在不同的时间段对数据data实现不同的权限控制。正常工作时间9:00-18:00所有人都可以读写data,其他时间只有数据所有者能读写。这种需求我们可以很方便地使用ABAC(attribute base access list)模型完成

image-20211118183224493

其他控制模型

控制模型远不止点,还有很多。

访问控制模型 Model 文件 Policy 文件
ACL basic_model.conf basic_policy.csv
具有超级用户的ACL basic_with_root_model.conf basic_policy.csv
没有用户的ACL basic_without_users_model.conf basic_without_users_policy.csv
没有资源的ACL basic_without_resources_model.conf basic_without_resources_policy.csv
RBAC rbac_model.conf rbac_policy.csv
支持资源角色的RBAC rbac_with_resource_roles_model.conf rbac_with_resource_roles_policy.csv
支持域/租户的RBAC rbac_with_domains_model.conf rbac_with_domains_policy.csv
ABAC abac_model.conf 无
RESTful keymatch_model.conf keymatch_policy.csv
拒绝优先 rbac_with_not_deny_model.conf rbac_with_deny_policy.csv
同意与拒绝 rbac_with_deny_model.conf rbac_with_deny_policy.csv
优先级 priority_model.conf priority_policy.csv
明确优先级 priority_model_explicit priority_policy_explicit.csv
主体优先级 subject_priority_model.conf subject_priority_policyl.csv

这一块学的不太好!!!有点点迷糊!

本文转载自: 掘金

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

Netty编程(五)—— ByteBuf

发表于 2021-11-24

这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战

创建

ByteBuf 可以通过ByteBufAllocator选择allocator并调用对应的 buffer( )方法来创建的,默认使用直接内存作为ByteBuf,容量为256个字节,可以指定初始容量的大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class ByteBufStudy {
public static void main(String[] args) {
// 创建ByteBuf
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(16);
ByteBufUtil.log(buffer);

// 向buffer中写入数据
StringBuilder sb = new StringBuilder();
for(int i = 0; i < 20; i++) {
sb.append("a");
}
buffer.writeBytes(sb.toString().getBytes(StandardCharsets.UTF_8));

// 查看写入结果
ByteBufUtil.log(buffer);
}
}

结果可视化:

在这里插入图片描述

  • 当ByteBuf的容量无法容纳所有数据时,ByteBuf会进行扩容操作
  • 如果在handler中创建ByteBuf,建议使用 ChannelHandlerContext ctx.alloc().buffer() 来创建

直接内存与堆内存

通过下面这种方法创建的ByteBuf,使用的是基于直接内存的ByteBuf

1
java复制代码ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(16);

此外,还可以使用下面的代码来创建池化基于堆的 ByteBuf

1
java复制代码ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(16);

也可以使用下面的代码来创建池化基于直接内存的 ByteBuf

1
java复制代码ByteBuf buffer = ByteBufAllocator.DEFAULT.directBuffer(16);
  • 直接内存创建和销毁的代价昂贵,但读写性能高(少一次内存复制),适合配合池化功能一起用
  • 直接内存对 GC 压力小,因为这部分内存不受 JVM 垃圾回收的管理,但也要注意及时主动释放

池化与非池化

类似于线程池的思想,预先把昂贵的资源创建好,省去创建的时间和步骤,用完后需要归还。池化的最大意义在于可以重用 ByteBuf,他的优点有:

  • 没有池化,则每次都得创建新的 ByteBuf 实例,这个操作对直接内存代价昂贵,就算是堆内存,也会增加 GC 压力
  • 有了池化,则可以重用池中 ByteBuf 实例,并且采用了与 jemalloc 类似的内存分配算法提升分配效率
  • 高并发时,池化功能更节约内存,减少内存溢出的可能
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class ByteBufStudy {
public static void main(String[] args) {
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(16);
System.out.println(buffer.getClass());

buffer = ByteBufAllocator.DEFAULT.heapBuffer(16);
System.out.println(buffer.getClass());

buffer = ByteBufAllocator.DEFAULT.directBuffer(16);
System.out.println(buffer.getClass());
}
}
1
2
3
4
5
6
7
8
arduino复制代码// 使用池化的直接内存
class io.netty.buffer.PooledUnsafeDirectByteBuf

// 使用池化的堆内存
class io.netty.buffer.PooledUnsafeHeapByteBuf

// 使用池化的直接内存
class io.netty.buffer.PooledUnsafeDirectByteBuf

组成

ByteBuf主要有以下几个组成部分

  • 最大容量与当前容量
+ 在构造ByteBuf时,可传入两个参数,分别代表初始容量和最大容量,若未传入第二个参数(最大容量),最大容量默认为Integer.MAX\_VALUE
+ 当ByteBuf容量无法容纳所有数据时,会进行扩容操作,若**超出最大容量**,会抛出`java.lang.IndexOutOfBoundsException`异常
  • 对于读写操作与ByteBuffer只用position进行控制不同的是, ByteBuf分别由读指针和写指针两个指针控制
+ 进行读写操作时,无需进行模式的切换


    - 读指针前的部分被称为废弃部分,是已经读过的内容
    - 读指针与写指针之间的空间称为可读部分
    - 写指针与当前容量之间的空间称为可写部分

在这里插入图片描述

写入

常用方法如下

方法签名 含义 备注
writeBoolean(boolean value) 写入 boolean 值 **用一字节 01
writeByte(int value) 写入 byte 值
writeShort(int value) 写入 short 值
writeInt(int value) 写入 int 值 Big Endian(大端写入),即 0x250,写入后 00 00 02 50
writeIntLE(int value) 写入 int 值 Little Endian(小端写入),即 0x250,写入后 50 02 00 00
writeLong(long value) 写入 long 值
writeChar(int value) 写入 char 值
writeFloat(float value) 写入 float 值
writeDouble(double value) 写入 double 值
writeBytes(ByteBuf src) 写入 netty 的 ByteBuf
writeBytes(byte[] src) 写入 byte[]
writeBytes(ByteBuffer src) 写入 nio 的 ByteBuffer
int writeCharSequence(CharSequence sequence, Charset charset) 写入字符串 CharSequence为字符串类的父类,第二个参数为对应的字符集

注意

  • 这些方法的未指明返回值的,其返回值都是 ByteBuf,意味着可以链式调用来写入不同的数据
  • 网络传输中,默认习惯是 Big Endian,使用 writeInt(int value)
  • CharSequence 是String、StringBuffer、StringBuilder的父类

下面这个例子分别写入了字节类型,int类型以及long类型,值得注意的是,写long类型会进行扩容:

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
java复制代码public class test {

public static void main(String[] args) {
// 创建ByteBuf
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(16, 20);

// 向buffer中写入byte类型,1字节大小
buffer.writeBytes(new byte[]{1, 2, 3, 4});
// 向buffer中写入int类型,4字节大小
buffer.writeInt(5);
// 向buffer中小端写入int类型,4字节大小
buffer.writeIntLE(6);
// 向buffer中写入long类型,8字节大小
buffer.writeLong(7);
log(buffer);
}

private static void log(ByteBuf buffer) {
//ByteBuf 可视化
int length = buffer.readableBytes();
int rows = length / 16 + (length % 15 == 0 ? 0 : 1) + 4;
StringBuilder buf = new StringBuilder(rows * 80 * 2)
.append("read index:").append(buffer.readerIndex())
.append(" write index:").append(buffer.writerIndex())
.append(" capacity:").append(buffer.capacity())
.append(NEWLINE);
appendPrettyHexDump(buf, buffer);
System.out.println(buf.toString());
}
}

在这里插入图片描述

扩容

当ByteBuf中的容量无法容纳写入的数据时,会进行扩容操作,借用上面说的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
perl复制代码buffer.writeLong(7);
log(buffer);

// 扩容前
read index:0 write index:12 capacity:16
...

// 扩容后
read index:0 write index:20 capacity:20
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 00 00 00 05 06 00 00 00 00 00 00 00 |................|
|00000010| 00 00 00 07 |.... |
+--------+-------------------------------------------------+----------------+

扩容规则

  • 如何写入后数据大小未超过 512 字节,则选择下一个 16 的整数倍进行扩容,例如写入后大小为 12 字节,则扩容后 capacity 是 16 字节
  • 如果写入后数据大小超过 512 字节,则选择下一个2n2^n2n ,例如写入后大小为 513 字节,则扩容后 capacity 是 210=1024 字节(292^929=512 已经不够了)
  • 扩容不能超过 maxCapacity,否则会抛出java.lang.IndexOutOfBoundsException异常

读取

读取主要是通过一系列read方法进行读取,读取时会根据读取数据的字节数移动读指针,此外Netty支持重复读取,如果需要重复读取,需要调用buffer.markReaderIndex()对读指针进行标记,并通过buffer.resetReaderIndex()将读指针恢复到mark标记的位置。具体可以看下面的代码:

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
java复制代码public static void main(String[] args) {
// 创建ByteBuf
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(16, 20);

// 向buffer中写入数据
buffer.writeBytes(new byte[]{1, 2, 3, 4});
buffer.writeInt(5);
ByteBufferUtil.log(buffer);

// 读取4个字节
System.out.println(buffer.readByte());
System.out.println(buffer.readByte());
System.out.println(buffer.readByte());
System.out.println(buffer.readByte());
ByteBufferUtil.log(buffer);

// 通过mark与reset实现重复读取
buffer.markReaderIndex();
System.out.println(buffer.readInt());
ByteBufferUtil.log(buffer);

// 恢复到mark标记处
buffer.resetReaderIndex();
ByteBufferUtil.log(buffer);
}

在这里插入图片描述

释放

Netty 采用了引用计数法来控制回收内存,每个 ByteBuf 都实现了 ReferenceCounted 接口

  • 每个 ByteBuf 对象的初始计数为 1
  • 调用 release 方法计数减 1,如果计数为 0,ByteBuf 内存被回收
  • 调用 retain 方法计数加 1,表示调用者没用完之前,其它 handler 即使调用了 release 也不会造成回收
  • 当计数为 0 时,底层内存会被回收,这时即使 ByteBuf 对象还在,其各个方法均无法正常使用

释放规则

因为 pipeline 的存在,一般需要将 ByteBuf 传递给下一个 ChannelHandler,如果在每个 ChannelHandler 中都去调用 release ,就失去了传递性(如果在这个 ChannelHandler 内这个 ByteBuf 已完成了它的使命,那么便无须再传递),所以基本规则是,谁是最后使用者,谁负责 release

  • 入站 ByteBuf 处理原则
+ 对原始 ByteBuf 不做处理,调用 ctx.fireChannelRead(msg) 向后传递,这时无须 release
+ **将原始 ByteBuf 转换为其它类型的 Java 对象,这时 ByteBuf 就没用了,必须 release**
+ **如果不调用 ctx.fireChannelRead(msg) 向后传递,那么也必须 release**
+ **注意各种异常,如果 ByteBuf 没有成功传递到下一个 ChannelHandler,必须 release**
+ 假设消息**一直向后传**,那么 TailContext 会负责释放未处理消息(原始的 ByteBuf)
  • 出站 ByteBuf 处理原则
+ **出站消息最终都会转为 ByteBuf 输出,一直向前传,由 HeadContext flush 后 release**
  • 异常处理原则
+ 有时候不清楚 ByteBuf 被引用了多少次,但又必须彻底释放,可以**循环调用 release 直到返回 true :**



1
java复制代码while (!buffer.release()) {}

当ByteBuf被传到了pipeline的head与tail时,ByteBuf会被其中的方法彻底释放。

切片Slice

之前在NIO网络编程(十)—— 零拷贝技术 - 掘金 (juejin.cn)中介绍了一下NIO中的零拷贝问题,这里介绍一下Netty中使用Slice方法实现零拷贝的。

ByteBuf切片是零拷贝的体现之一,对原始 ByteBuf 进行切片成多个 ByteBuf,切片后的 ByteBuf 并没有发生内存复制,还是使用原始 ByteBuf 的内存,切片后的 ByteBuf 维护独立的 read,write 指针。

  • 得到分片后的buffer后,要调用其retain方法,使其内部的引用计数加一。避免原ByteBuf释放,导致切片buffer无法使用
  • 修改原ByteBuf中的值,也会影响切片后得到的ByteBuf
  • 切片不能扩容,即不能向切片写入内容
  • 一般使用slice,就需要retain,使用完后使用 release

img

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 main(String[] args) {
// 创建ByteBuf
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(16, 20);

// 向buffer中写入数据
buffer.writeBytes(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10});

// 将buffer分成两部分,参数:start,length,在切片过程中没有发生数据复制,同一块内存
ByteBuf slice1 = buffer.slice(0, 5);
ByteBuf slice2 = buffer.slice(5, 5);

// 需要让分片的buffer引用计数加一
// 避免原Buffer释放导致分片buffer无法使用
slice1.retain();
slice2.retain();

ByteBufferUtil.log(slice1);
ByteBufferUtil.log(slice2);

// 更改原始buffer中的值
System.out.println("===========修改原buffer中的值===========");
buffer.setByte(0,5);

System.out.println("===========打印slice1===========");
ByteBufferUtil.log(slice1);
}

上方的代码的执行结果:

在这里插入图片描述

本文转载自: 掘金

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

36 Redis支撑秒杀场景的关键技术和实践都有哪些?

发表于 2021-11-24

基本概念

  • 秒杀场景的业务特点是限时限量,读→读写(原子操作) 可以通过缓存兜住大量读\
  • 秒杀场景
+ 秒杀前
+ 秒杀中
+ 秒杀后

秒杀场景的负载特征对支撑系统的要求

  • 特点
+ 瞬时并发访问量非常高
+ 读多写少,都是简单的查询工作(读库存数远远大于下单数)

Redis 可以在秒杀场景的哪些环节发挥作用?

  • 秒杀活动的三个阶段:秒杀活动前+秒杀活动开始+秒杀活动结束后
  • 秒杀活动前
+ 场景特点:用户会不断刷新商品详情页
+ 解决办法:把商品详情页的页面元素静态化,然后使用 CDN 或是浏览器把这些静态化的元素缓存起来
+ 不会把请求真正打到redis
  • 秒杀活动开始
+ 场景特点:大量请求打到服务器,查询和扣减库存
+ 流程


    - 查询库存
    - 扣减库存
    - 生成订单,执行后续流程  从生成订单开始后需要通过数据库事务进行支持
+ 此时库存数全部由缓存记录
  • 秒杀活动结束
+ 场景特点:可能还会有部分用户刷新商品详情页,尝试等待有其他用户退单
+ 服务端可以轻松支撑

\

Redis 的哪些方法可以支撑秒杀场景?

  • 根本需求
+ 支持高并发


    - redis本身支持高并发
    - 将不同的实例保存在不同slot中
+ 保证库存查验和库存扣减原子性执行


    - 基于lua脚本实现原子性
    - 基于分布式锁实现原子性
  • 基于原子操作支撑秒杀场景
+ 使用lua脚本
+ 使用eval执行脚本
  • 基于分布式锁支撑秒杀场景
+ 大量秒杀请求会因为没有获取分布式锁而被过滤掉(取决业务代码怎么实现)
+ 优化:我们可以使用切片集群中的不同实例来分别保存分布式锁和商品库存信息
1
2
3
4
5
6
7
8
ini复制代码#获取商品库存信息             
local counts = redis.call("HMGET", KEYS[1], "total", "ordered"); #将总库存转换为数值
local total = tonumber(counts[1]) #将已被秒杀的库存转换为数值
local ordered = tonumber(counts[2]) #如果当前请求的库存量加上已被秒杀的库存量仍然小于总库存量,就可以更新库存
if ordered + k <= total then #更新已秒杀的库存量 redis.call("HINCRBY",KEYS[1],"ordered",k)
return k;
end
return 0

总结

  • 秒杀场景有 2 个负载特征,分别是瞬时高并发请求和读多写少(为什么不利用消息队列消峰谷 是因为想要带给用户更好的体验嘛)\
  • 秒杀前:前端 CDN 和浏览器缓存拦截大量秒杀前的请求
  • 秒杀中:利用lua脚本或分布式锁完成原子操作
  • 库存信息持久存储,并且保证淘汰策略不淘汰allkey\

\

本文转载自: 掘金

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

1…209210211…956

开发者博客

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