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

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


  • 首页

  • 归档

  • 搜索

SpringCloud升级之路20200x版-35 验

发表于 2021-11-16

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

本系列代码地址:github.com/JoJoTec/spr…

上一节我们通过单元测试验证了重试的正确性,这一节我们来验证我们线程隔离的正确性,主要包括:

  1. 验证配置正确加载:即我们在 Spring 配置(例如 application.yml)中的加入的 Resilience4j 的配置被正确加载应用了。
  2. 相同微服务调用不同实例的时候,使用的是不同的线程(池)。

验证配置正确加载

与之前验证重试类似,我们可以定义不同的 FeignClient,之后检查 resilience4j 加载的线程隔离配置来验证线程隔离配置的正确加载。

并且,与重试配置不同的是,通过系列前面的源码分析,我们知道 spring-cloud-openfeign 的 FeignClient 其实是懒加载的。所以我们实现的线程隔离也是懒加载的,需要先调用,之后才会初始化线程池。所以这里我们需要先进行调用之后,再验证线程池配置。

首先定义两个 FeignClient,微服务分别是 testService1 和 testService2,contextId 分别是 testService1Client 和 testService2Client

1
2
3
4
5
6
7
8
9
10
less复制代码@FeignClient(name = "testService1", contextId = "testService1Client")
public interface TestService1Client {
@GetMapping("/anything")
HttpBinAnythingResponse anything();
}
@FeignClient(name = "testService2", contextId = "testService2Client")
public interface TestService2Client {
@GetMapping("/anything")
HttpBinAnythingResponse anything();
}

然后,我们增加 Spring 配置,并且给两个微服务都添加一个实例,使用 SpringExtension 编写单元测试类:

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
less复制代码//SpringExtension也包含了 Mockito 相关的 Extension,所以 @Mock 等注解也生效了
@ExtendWith(SpringExtension.class)
@SpringBootTest(properties = {
//默认请求重试次数为 3
"resilience4j.retry.configs.default.maxAttempts=3",
// testService2Client 里面的所有方法请求重试次数为 2
"resilience4j.retry.configs.testService2Client.maxAttempts=2",
//默认线程池配置
"resilience4j.thread-pool-bulkhead.configs.default.coreThreadPoolSize=10",
"resilience4j.thread-pool-bulkhead.configs.default.maxThreadPoolSize=10",
"resilience4j.thread-pool-bulkhead.configs.default.queueCapacity=1" ,
//testService2Client 的线程池配置
"resilience4j.thread-pool-bulkhead.configs.testService2Client.coreThreadPoolSize=5",
"resilience4j.thread-pool-bulkhead.configs.testService2Client.maxThreadPoolSize=5",
"resilience4j.thread-pool-bulkhead.configs.testService2Client.queueCapacity=1",
})
@Log4j2
public class OpenFeignClientTest {
@SpringBootApplication
@Configuration
public static class App {
@Bean
public DiscoveryClient discoveryClient() {
//模拟两个服务实例
ServiceInstance service1Instance1 = Mockito.spy(ServiceInstance.class);
ServiceInstance service2Instance2 = Mockito.spy(ServiceInstance.class);
Map<String, String> zone1 = Map.ofEntries(
Map.entry("zone", "zone1")
);
when(service1Instance1.getMetadata()).thenReturn(zone1);
when(service1Instance1.getInstanceId()).thenReturn("service1Instance1");
when(service1Instance1.getHost()).thenReturn("www.httpbin.org");
when(service1Instance1.getPort()).thenReturn(80);
when(service2Instance2.getInstanceId()).thenReturn("service1Instance2");
when(service2Instance2.getHost()).thenReturn("httpbin.org");
when(service2Instance2.getPort()).thenReturn(80);
DiscoveryClient spy = Mockito.spy(DiscoveryClient.class);
Mockito.when(spy.getInstances("testService1"))
.thenReturn(List.of(service1Instance1));
Mockito.when(spy.getInstances("testService2"))
.thenReturn(List.of(service2Instance2));
return spy;
}
}
}

编写测试代码,验证配置正确:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
scss复制代码@Test
public void testConfigureThreadPool() {
//防止断路器影响
circuitBreakerRegistry.getAllCircuitBreakers().asJava().forEach(CircuitBreaker::reset);
//调用下这两个 FeignClient 确保对应的 NamedContext 被初始化
testService1Client.anything();
testService2Client.anything();
//验证线程隔离的实际配置,符合我们的填入的配置
ThreadPoolBulkhead threadPoolBulkhead = threadPoolBulkheadRegistry.getAllBulkheads().asJava()
.stream().filter(t -> t.getName().contains("service1Instance1")).findFirst().get();
Assertions.assertEquals(threadPoolBulkhead.getBulkheadConfig().getCoreThreadPoolSize(), 10);
Assertions.assertEquals(threadPoolBulkhead.getBulkheadConfig().getMaxThreadPoolSize(), 10);
threadPoolBulkhead = threadPoolBulkheadRegistry.getAllBulkheads().asJava()
.stream().filter(t -> t.getName().contains("service1Instance2")).findFirst().get();
Assertions.assertEquals(threadPoolBulkhead.getBulkheadConfig().getCoreThreadPoolSize(), 5);
Assertions.assertEquals(threadPoolBulkhead.getBulkheadConfig().getMaxThreadPoolSize(), 5);
}

相同微服务调用不同实例的时候,使用的是不同的线程(池)。

我们需要确保,最后调用(也就是发送 http 请求)的执行的线程池,必须是对应的 ThreadPoolBulkHead 中的线程池。这个需要我们对 ApacheHttpClient 做切面实现,添加注解 @EnableAspectJAutoProxy(proxyTargetClass = true):

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
less复制代码//SpringExtension也包含了 Mockito 相关的 Extension,所以 @Mock 等注解也生效了
@ExtendWith(SpringExtension.class)
@SpringBootTest(properties = {
//默认请求重试次数为 3
"resilience4j.retry.configs.default.maxAttempts=3",
// testService2Client 里面的所有方法请求重试次数为 2
"resilience4j.retry.configs.testService2Client.maxAttempts=2",
//默认线程池配置
"resilience4j.thread-pool-bulkhead.configs.default.coreThreadPoolSize=10",
"resilience4j.thread-pool-bulkhead.configs.default.maxThreadPoolSize=10",
"resilience4j.thread-pool-bulkhead.configs.default.queueCapacity=1" ,
//testService2Client 的线程池配置
"resilience4j.thread-pool-bulkhead.configs.testService2Client.coreThreadPoolSize=5",
"resilience4j.thread-pool-bulkhead.configs.testService2Client.maxThreadPoolSize=5",
"resilience4j.thread-pool-bulkhead.configs.testService2Client.queueCapacity=1",
})
@Log4j2
public class OpenFeignClientTest {
@SpringBootApplication
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public static class App {
@Bean
public DiscoveryClient discoveryClient() {
//模拟两个服务实例
ServiceInstance service1Instance1 = Mockito.spy(ServiceInstance.class);
ServiceInstance service2Instance2 = Mockito.spy(ServiceInstance.class);
Map<String, String> zone1 = Map.ofEntries(
Map.entry("zone", "zone1")
);
when(service1Instance1.getMetadata()).thenReturn(zone1);
when(service1Instance1.getInstanceId()).thenReturn("service1Instance1");
when(service1Instance1.getHost()).thenReturn("www.httpbin.org");
when(service1Instance1.getPort()).thenReturn(80);
when(service2Instance2.getInstanceId()).thenReturn("service1Instance2");
when(service2Instance2.getHost()).thenReturn("httpbin.org");
when(service2Instance2.getPort()).thenReturn(80);
DiscoveryClient spy = Mockito.spy(DiscoveryClient.class);
Mockito.when(spy.getInstances("testService1"))
.thenReturn(List.of(service1Instance1));
Mockito.when(spy.getInstances("testService2"))
.thenReturn(List.of(service2Instance2));
return spy;
}
}
}

拦截 ApacheHttpClient 的 execute 方法,这样可以拿到真正负责 http 调用的线程池,将线程其放入请求的 Header:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typescript复制代码@Aspect
public static class ApacheHttpClientAop {
//在最后一步 ApacheHttpClient 切面
@Pointcut("execution(* com.github.jojotech.spring.cloud.webmvc.feign.ApacheHttpClient.execute(..))")
public void annotationPointcut() {
}

@Around("annotationPointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
//设置 Header,不能通过 Feign 的 RequestInterceptor,因为我们要拿到最后调用 ApacheHttpClient 的线程上下文
Request request = (Request) pjp.getArgs()[0];
Field headers = ReflectionUtils.findField(Request.class, "headers");
ReflectionUtils.makeAccessible(headers);
Map<String, Collection<String>> map = (Map<String, Collection<String>>) ReflectionUtils.getField(headers, request);
HashMap<String, Collection<String>> stringCollectionHashMap = new HashMap<>(map);
stringCollectionHashMap.put(THREAD_ID_HEADER, List.of(String.valueOf(Thread.currentThread().getName())));
ReflectionUtils.setField(headers, request, stringCollectionHashMap);
return pjp.proceed();
}
}

这样,我们就能拿到具体承载请求的线程的名称,从名称中可以看出他所处于的线程池(格式为“bulkhead-线程隔离名称-n”,例如 bulkhead-testService1Client:www.httpbin.org:80-1),接下来我们就来看下不同的实例是否用了不同的线程池进行调用:

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
ini复制代码@Test
public void testDifferentThreadPoolForDifferentInstance() throws InterruptedException {
//防止断路器影响
circuitBreakerRegistry.getAllCircuitBreakers().asJava().forEach(CircuitBreaker::reset);
Set<String> threadIds = Sets.newConcurrentHashSet();
Thread[] threads = new Thread[100];
//循环100次
for (int i = 0; i < 100; i++) {
threads[i] = new Thread(() -> {
Span span = tracer.nextSpan();
try (Tracer.SpanInScope cleared = tracer.withSpanInScope(span)) {
HttpBinAnythingResponse response = testService1Client.anything();
//因为 anything 会返回我们发送的请求实体的所有内容,所以我们能获取到请求的线程名称 header
String threadId = response.getHeaders().get(THREAD_ID_HEADER);
threadIds.add(threadId);
}
});
threads[i].start();
}
for (int i = 0; i < 100; i++) {
threads[i].join();
}
//确认实例 testService1Client:httpbin.org:80 线程池的线程存在
Assertions.assertTrue(threadIds.stream().anyMatch(s -> s.contains("testService1Client:httpbin.org:80")));
//确认实例 testService1Client:httpbin.org:80 线程池的线程存在
Assertions.assertTrue(threadIds.stream().anyMatch(s -> s.contains("testService1Client:www.httpbin.org:80")));
}

这样,我们就成功验证了,实例调用的线程池隔离。

微信搜索“我的编程喵”关注公众号,每日一刷,轻松提升技术,斩获各种offer

本文转载自: 掘金

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

Spring Cloud / Alibaba 微服务架构

发表于 2021-11-16

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

在上一篇文章里我们完成了基于JWT+RSA256的授权中心和鉴权工具类编写并验证了服务的可用性,本篇文章将对比基于Token与基于服务器的身份认证做个总结。

对比基于Token(即JWT)与基于服务器的身份认证

基于服务器的身份认证

1、最为传统的做法,客户端存储Cookie(一般是只存储Session id),服务器存储Session。

2、Session是每次用户认证通过以后,服务器为了记录用户的会话状态信息而创建一条记录保存用户信息,通常是存储在内存中,当然也可以存储在Redis中,随着认证通过的用户越来越多,服务器存储Session的开销就会越来越大,这也是Session的方式存储用户信息的最大问题。

3、由于客户端认证信息保存在Cookie中,在不同域名之前切换时,由于跨域问题的存在,请求可能会被禁止。

基于Token(即JWT)的身份认证

1、JWT与Session的相同点是它们都是存储用户信息的,但它的服务端可以不存储用户的身份信息,用于验证用户身份信息的JWT直接存储在客户端。

2、正由于JWT的方式将用户状态分散到了客户端中,所以可以明显减轻服务端的内存压力。服务器端直接使用算法去解析客户端携带的JWT,即可验证并得到用户的身份信息。

两者优缺点的对比

解析方法(解析规则)

JWT没有依赖,它使用算法和加解密规则直接解析得到用户信息。

而Session需要额外的数据映射,即数据存储来实现匹配,所以JWT更加简单高效。

管理方法

JWT在生成的时候可以填写过期时间,由于服务器端没有记录额外的信息,因此它只有过期时间的限制。

而Session数据保存在服务端,所以Session的方式有更强的可控性,毕竟服务端的数据可以随时由服务端去修改和更新。

跨平台

JWT其实就是一段经过加密的字符串,不存在跨平台的问题,可以任意传播。

Session由于需要客户端和服务端来共同控制完成,所以需要有统一的解析平台,比JWT的使用上要繁琐。

时效性

JWT的时效性是在生成的时候就指定好的,一旦生成,独立存在,后期很难做特殊控制。

Session由服务端存储和控制,所以时效性完全由服务端的逻辑说了算。

总结:各自都有各自的优缺点,都是登录、授权的解决方案。

本文转载自: 掘金

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

【Go夯实基础】关于interface的一些自我理解

发表于 2021-11-16

1、Duck Typing 概念

  • 它描述的事物的外部行为,而非内部结构
  • 代码的复用,开发者认为是什么样子它就是什么样子。我只关心这段代码结构能做哪些事情,我复用它,内部结构我不care
  • 可以用于多态的实现

非入侵式的duck typing到底有多好?

  • 侵入式缺点
+ 通过 implements 把实现类与具体接口绑定起来了,因此有了强耦合;
+ 如果我修改了接口,比如改了接口方法,则实现类必须改动,如果我希望实现类再实现一个接口,实现类也必须进行改动;
  • 非入侵式优点
+ 可以根据实际情况把类的功能做好,在具体需要使用的地方,我再定义接口。说的专业点:也就是接口是由使用方根据自己真实需求来定义,并且不用关心是否有其它使用方定义过
  • 优点举例
+ 开发一个商城系统,m端、app端、pc端都有购物车的需求,底层根据不同的需求已经实现了一个Cart类,通过该类可以获取购物车价格、数量等。例如:
1
2
3
4
5
6
7
8
9
10
11
12
go复制代码type Cart struct {
price float32
num int
}

func (c Cart) GetPrice() float32 {
return c.price
}

func (c Cart) GetNum() int {
return c.num
}
+ 不同的高层调用时,他们可以自由定义接口名称用于接受Cart实例,再通过接口调用相应的方法就好了,不同的高层完全可以自己定义一个接口,接口名称、定义的方法顺序都可以不同。
  • 总结:真正做到了:依赖于接口而不是实现,优先使用组合而不是继承

2、接口定义

2.1 接口类型

1
2
3
typescript复制代码type Stringer interface {//接口的定义就是如此的简单。
String() string
}

2.2 接口的实现方式

  • 不需要显示的去实现接口。一个类型如果拥有一个接口需要的所有方法,那么这个类型就自动实现了这个接口,这一特性可以方便的用于多态
  • 一个类型只要实现了接口定义的所有方法(是指有相同名称、参数列表、以及返回值 ),那么这个类型就实现了这个接口,可以直接进行赋值(其实也是隐式转换),比如var t Printer = &User{1, "Tom"}
  • 多继承的概念
    • 一个类型就可以实现多个接口,只要它拥有了这些接口类型的所有方法,那么这个类型就是实现了多个接口
  • 多态
    • 一个接口可以被不同类型实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
go复制代码type Stringer interface {
String() string
}
type Printer interface {
Stringer // 接口嵌⼊。
Print()
}
type User struct {
id int
name string
}
func (self *User) String() string {
return fmt.Sprintf("user %d, %s", self.id, self.name)
}
func (self *User) Print() {
fmt.Println(self.String())
}
func main() {
var t Printer = &User{1, "Tom"} // *User ⽅法集包含 String、 Print。
t.Print()
}

2.3 interface{}空接口的实现

空接⼝ interface{} 没有任何⽅法签名,也就意味着任何类型都实现了空⼝。其作⽤类似⾯向对象语⾔中的根对象object。

2.4 类型断言

  • 一个类型断言检查接口类型实例是否为某一类型 。语法为x.(T) ,x为类型实例,T为目标接口的类型。比如
  • value, ok := x.(T)
+ x :代表要判断的变量
+ T :代表被判断的类型
+ value:代表返回的值
+ ok:代表是否为该类型。
+ **注意:x 必须为inteface类型,不然会报错。**
  • 不过我们一般用switch进行判断,叫做 type switch。注意:不支持fallthrough.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码func main() {
var o interface{} = &User{1, "Tom"}
switch v := o.(type) {
case nil: // o == nil
fmt.Println("nil")
case fmt.Stringer: // interface
fmt.Println(v)
case func() string: // func
fmt.Println(v())
case *User: // *struct
fmt.Printf("%d, %s\n", v.id, v.name)
default:
fmt.Println("unknown")
}
}

2.5 接口转换

  • 可以将拥有超集的接口转换为子集的接口,反之出错。
    • father->son,儿子一定是父类,反之不行(包括多继承)
  • 通过类型判断,如果不同类型转换会发生panic.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码type User struct {
id int
name string
}
func (self *User) String() string {
return fmt.Sprintf("%d, %s", self.id, self.name)
}
func main() {
var o interface{} = &User{1, "Tom"}
if i, ok := o.(fmt.Stringer); ok { //儿子一定是父类,反之不行
fmt.Println(i)
}
u := o.(*User)
// u := o.(User) // panic: interface is *main.User, not main.User
fmt.Println(u)
}

2.6 匿名接口

  • 匿名接口可用作变量类型,或者是结构成员。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码type Tester struct {
s interface {
String() string
}
}
type User struct {
id int
name string
}
func (self *User) String() string {
return fmt.Sprintf("user %d, %s", self.id, self.name)
}
func main() {
t := Tester{&User{1, "Tom"}}
fmt.Println(t.s.String())
}
//输出:
user 1, Tom

3、接口的内部实现

3.1 接口值

  • 接口值可以使用 == 和 !=来进行比较
    • 两个接口值相等仅当它们都是nil值或者它们的动态类型相同,并且动态值也根据这个动态类型的==操作相等
    • 因为接口值是可比较的,所以它们可以用在map的键或者作为switch语句的操作数。
    • 然而,如果两个接口值的动态类型相同,但是这个动态类型是不可比较的(比如切片) ,将它们进行比较就会失败并且panic,除非使用reflect.DeepEqal,深度比较

3.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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
go复制代码// 没有方法的interface
type eface struct {
_type *_type //类型信息
data unsafe.Pointer //数据指针
}

type _type struct {
size uintptr //类型的大小
ptrdata uintptr //存储所有指针的内存前缀的大小
hash uint32 //类型的hash
tflag tflag //类型的tags
align uint8 //结构体内对齐
fieldalign uint8 //结构体作为field时的对齐
kind uint8 //类型编号 定义于 runtime/typekind.go
alg *typeAlg // 类型元方法 存储hash 和equal两个操作。
gcdata *byte //GC 相关信息
str nameOff //类型名字的偏移
ptrToThis typeOff
}

// 有方法的interface
type iface struct {
tab *itab
data unsafe.Pointer
}

type itab struct {
inter *interfacetype //接口定义的类型信息
_type *_type //接口实际指向值的类型信息
link *itab
hash uint32
bad bool
inhash bool
unused [2]byte
fun [1]uintptr //接口方法实现列表,即函数地址列表,按字典序排序
}

// interface数据类型对应的type
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}
  • _type记录着Go语言中某个数据类型的基本特征,_type是go所有类型的公共描述
  • 可以简单的认为,接口可以通过一个 _type *_type 直接或间接表述go所有的类型就可以了
  • 存在两种interface,一种是带有方法的interface,一种是不带方法的interface
+ 对于不带方法的接口类型,Go语言中的所有变量都可以赋值给interface{}变量,interface可以表述go所有的类型,\_type存储类型信息,data存储类型的值的指针,指向实际值或者实际值的拷贝。
+ 对于带方法的接口类型,`tab *itab` 存储指向了iTable的指针,ITable存储了类型相关(\_type)的信息以及相关方法集,而data 同样存储了实例值的指针,指向实际值或者是实际值的一个拷贝。
  • go语言interface的源码表示,接口其实是一个两个字段长度的数据结构。所以任何一个interface变量都是占用16个byte的内存空间。从大的方面来说,如图:

1.png

  • 注意
    • var n notifier n=user("Bill") 将一个实现了notifier接口实例user赋给变量n。接口n 内部两个字段 tab *itab 和 data unsafe.Pointer, 第一个字段存储的是指向ITable(接口表)的指针,这个内部表包括已经存储值的类型和与这个值相关联的一组方法。第二个字段存储的是,指向所存储值的指针。注意:这里是将一个值赋值给接口,并非指针,那么就会先将值拷贝一份,开辟内存空间存储,然后将此内存地址赋给接口的data字段。也就是说,值传递时,接口存储的值的指针其实是指向一个副本。
    • 如果是将指针赋值给接口类型,那么第二个字段data存储的就是指针的拷贝,指向的是原来的内存,如下

2.jpg

  • 每种数据类型都存在一个与之对应的_type结构体(Go语言原生的各种数据类型,用户自定义的结构体,用户自定义的interface等等)。

3.png

  • 小结:总的来说接口是一个类型,它是一个struct,是一个或多个方法的集合。任何类型都可以实现接口,并且是隐式实现,可以同时实现多个接口。接口内部只有方法声明没有实现。接口内部存储的其实就是接口值的类型和值,一部分存储类型等各种信息,另一部分存储指向值的指针。如果是将值传给接口,那么这里第二个字段存储的就是原值的副本的指针。接口可以调用实现了接口的方法。

4、方法集

4.1 方法集定义

  • 方法集:方法集定义了一组关联到给定类型的值或者指针的方法。定义方法时使用的接受者的类型决定了这个方法是关联到值,还是关联到指针,还是两个都关联。
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
go复制代码// 这个示例程序展示 Go 语言里如何使用接口
package main
import (
"fmt"
)

// notifier 是一个定义了
// 通知类行为的接口
type notifier interface {
notify()
}

// user 在程序里定义一个用户类型
type user struct {
name string
email string
}

// notify 是使用指针接收者实现的方法
func (u *user) notify() {
fmt.Printf("Sending user email to %s<%s>\n",
u.name,
u.email)
}

// main 是应用程序的入口
func main() {
// 创建一个 user 类型的值,并发送通知30
u := user{"Bill", "bill@email.com"}


sendNotification(u)
// panic:不能将 u(类型是 user)作为
// sendNotification 的参数类型 notifier:
// user 类型并没有实现 notifier
// (notify 方法使用指针接收者声明)


}

// sendNotification 接受一个实现了 notifier 接口的值
// 并发送通知
func sendNotification(n notifier) {
n.notify()
}

4.2 方法集规则

  • 方法集规则详解
  • 举个例子
1
2
3
kotlin复制代码fun (t T)MyMethod(s string) {
// ...
}
+ 可以理解成是 `func(T, string)` 类型的方法。方法接收器**像其他参数一样**通过值传递给函数。
+ 因为所有的参数都是通过值传递的,任何一个 `Cat` 类型的值可能会有很多 `*Cat` 类型的指针指向它,如果我们尝试通过 `Cat` 类型的值来调用 `*Cat` 的方法,根本就不知道对应的是哪个指针
+ 相反,如果 `Dog` 类型上有一个方法,通过 `*Dog` 来调用这个方法可以确切的找到该指针对应的 `Gog` 类型的值,从而调用上面的方法。运行时,Go 会自动帮我们做这些,所以我们不需要像 C语言中那样使用类似如下的语句 `d->Speak()`
  • 简单讲就是,接受者是(t T),那么T 和 *T 都可以实现接口,如果接受者是(t *T)那么只有 *T才算实现接口
  • 原因:编译器并不是总能自动获得一个值的地址,即一个指针类型可以通过其相关的值类型来访问值类型的方法,但是反过来不

5、嵌入类型时接口实现

  • 嵌入类型:是将已有的类型直接声明在新的结构类型里。被嵌入的类型被称为新的外部类型的内部类型。
  • 实现方法重写:外部类型也可以通过声明与内部类型标识符同名的标识符来覆盖内部标识符的字段或者方法。
  • 注意声明字段和嵌入类型在语法上的不同 ,嵌入类型直接是写个类型名就行
  • 内部类型的标识符提升到了外部类型,可以直接通过外部类型的值来访问内部类型的标识符。 也可以通过内部类型的名间接访问内部类型方法和标识符。
  • 内部类型实现接口外部类型默认也实现了该接口。注意方法集的规则。
  • 如果内部类型和外部类型同时实现一个接口,就近原则,外部类型不会直接调用内部类型实现的同名方法,而是自己的。当然可以通过内部类型间接显示的去调用内部类型的方法。

5.1 嵌入类型实现接口,同样应用到外部类型

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
go复制代码// 这个示例程序展示如何将一个类型嵌入另一个类型,以及
// 内部类型和外部类型之间的关系
package main
import (
"fmt"
)
// notifier 是一个定义了
// 通知类行为的接口
type notifier interface {
notify()
}

// user 在程序里定义一个用户类型
type user struct {
name string
email string
}
// 通过 user 类型值的指针
// 调用的方法
func (u *user) notify() {
fmt.Printf("Sending user email to %s<%s>\n",
u.name,
u.email)
}

// admin 代表一个拥有权限的管理员用户
type admin struct {
user // 嵌入类型
level string
}
// main 是应用程序的入口
func main() {
// 创建一个 admin 用户
ad := admin{
user: user{
name: "john smith",
email: "john@yahoo.com",
},
level: "super",
}
// 给 admin 用户发送一个通知
// 用于实现接口的内部类型的方法,被提升到
// 外部类型
sendNotification(&ad)
}
// sendNotification 接受一个实现了 notifier 接口的值
// 并发送通知
func sendNotification(n notifier) {
n.notify()
}

5.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
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
go复制代码// 这个示例程序展示如何将一个类型嵌入另一个类型,以及
// 内部类型和外部类型之间的关系
package main

import (
"fmt"
)

// notifier 是一个定义了
// 通知类行为的接口
type notifier interface {
notify()
}

// user 在程序里定义一个用户类型
type user struct {
name string
email string
}

// 通过 user 类型值的指针
// 调用的方法
func (u *user) notify() {
fmt.Printf("Sending user email to %s<%s>\n",
u.name,
u.email)
}

// admin 代表一个拥有权限的管理员用户
type admin struct {
user // 嵌入类型
level string
}

// 通过 admin 类型值的指针
// 调用的方法
func (a *admin) notify() {
fmt.Printf("Sending admin email to %s<%s>\n",
a.name,
a.email)
}

// main 是应用程序的入口
func main() {
// 创建一个 admin 用户
ad := admin{
user: user{
name: "john smith",
email: "john@yahoo.com",
},
level: "super",
}

// 给 admin 用户发送一个通知,就近原则
sendNotification(&ad)
// 我们可以直接访问内部类型的方法
ad.user.notify()

// 内部类型的方法没有被提升
ad.notify()
}

// sendNotification 接受一个实现了 notifier 接口的值
// 并发送通知
func sendNotification(n notifier) {
n.notify()
}


//输出
Sending admin email to john smith<john@yahoo.com>
Sending user email to john smith<john@yahoo.com>
Sending admin email to john smith<john@yahoo.com>

本文转载自: 掘金

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

【算法攻坚】最长公共前缀

发表于 2021-11-16

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

今日题目

编写一个函数来查找字符串数组中的最长公共前缀。

如果不存在公共前缀,返回空字符串 “”。

示例 1:

输入:strs = [“flower”,”flow”,”flight”]
输出:”fl”

示例 2:

输入:strs = [“dog”,”racecar”,”car”]
输出:””
解释:输入不存在公共前缀。

提示:

1 <= strs.length <= 200
0 <= strs[i].length <= 200
strs[i] 仅由小写英文字母组成

思路

公共最长前缀的特点: 前缀必然存在每一个字符串。

使用第一个字符串与其他字符串进行逐一对比得出前缀长度;并且所有长度取最小值就是题解

可优化的点:

  1. 公共前缀不会超过任意一个字符串的长度,不用每次都比较整个字符串,只要比较当前的前缀长度即可
  2. 如果没有公共前缀,那么不需要遍历剩余的字符串

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public String longestCommonPrefix(String[] strs) {
if (strs.length == 0) {
return "";
}
// 最长前缀末尾位置
int end = strs[0].length() - 1;
for (int i = 1; i < strs.length; i++) {
// 优化1: 不用比较整个字符串, 只要比较前缀长度即可
int j = 0;
for (; j <= end && j < strs[i].length(); j++) {
if (strs[0].charAt(j) != strs[i].charAt(j)) {
//如果第一个字符都不相等,就不用比较了
if(j == 0){
return "";
}
break;
}
}
end = Math.min(end, j - 1);
}
return strs[0].substring(0, end + 1);
}

执行用时:1 ms, 在所有 Java 提交中击败了70.59%的用户

内存消耗:36.3 MB, 在所有 Java 提交中击败了89.59%的用户

解法二

第二种解法其实和第一种是一样的思想,只不过可以利用String的api方法

startWith()方法,有了这个方法可以使代码实现更加优雅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码 public String longestCommonPrefix(String[] strs) {
if (strs.length == 0) {
return "";
}
// 最长前缀
String commonPrefix = strs[0];
for (int i = 1; i < strs.length; i++) {
while (!strs[i].startsWith(commonPrefix)) {
if (commonPrefix.length() == 0) {
return "";
}
commonPrefix = commonPrefix.substring(0, commonPrefix.length() - 1);
}
}
return commonPrefix;
}

执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户

内存消耗:36.5 MB, 在所有 Java 提交中击败了44.26%的用户

小结

这两天的题目相对来说比较简单,但是能够一次完全写对还是有难度的,需要多注意常用的api写法

比如:

  • map的循环方式,entry的写法
  • String的常用api,比如substring(start, end)注意不是驼峰, 区间是左闭右开**[start, end)**
  • 字符串和链表获取串长度是方法ss.length(),而数组的是变量array.length
  • 数字与字符串之间的互转,String.valueOf(int), Integer.parseIn(string)这个方法不会频繁拆箱装箱
  • 而且做题有注意审题,这道题我刚开始还以为是找最长公共子串

今天多学一点知识,明天就少说一句求人的话

本文转载自: 掘金

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

Redis在docker下的的安装和基本使用 Redis的安

发表于 2021-11-16

Redis的安装和使用

1redis的安装

(1)下载镜像文件

在linux虚拟机中启动docker,从docker中将redis从网络上pull下来

1
复制代码docker pull redis

(2)创建redis实例并启动

创建redis配置文件目录

1
bash复制代码mkdir -p /usr/local/docker/redis/conf

在配置文件录下创建redis.conf配置文件(因为redis镜像中这个redis.conf是一个目录所以要先创建一个这个配置文件,否在我们本地挂载点也会变成一个目录)

1
bash复制代码touch /usr/local/docker/redis/conf/redis.conf

在linux上挂载redis并启动

1
2
3
4
bash复制代码sudo docker run -p 6379:6379 --name redis \
-v /usr/local/docker/redis/data:/data \
-v /usr/local/docker/redis/conf/redis.conf:/etc/redis/redis.conf \
-d redis redis-server /etc/redis/redis.conf

(3)查看正在运行的进程

1
复制代码docker ps

(4)控制台直接连接redis测试

1
bash复制代码docker exec -it redis bash

(5)检测redisb版本

1
vbscript复制代码Redis-server –v

或者

1
复制代码redis-cli -v

或者直接将上面的两个步骤合为一个步骤也可以,指令如下

1
bash复制代码docker exec -it redis redis-cli

(6)停止redis服务

1
arduino复制代码docker stop redis

(7)启动redis服务

在docker中

1
sql复制代码docker start redis

传统方式启动(非docker环境)

1
2
3
bash复制代码redis-server 					#默认找redis.conf配置文件
redis-server & #上面ctrl+c中断reis会退出,这个不会
redis-server redis6380.conf #指定配置文件,这样可以启动多个实例

(8)重启redis服务

在docker中

1
复制代码docker restart redis

(9)查看redis服务版本

1
bash复制代码docker exec -it redis redis-server -v

(10)关闭防火墙

1
arduino复制代码systemctl stop firewalld

(11)设置访问密码

默认没有密码,可以随意访问。redis速度相当快,在一个较好的服务器下,外部用户每秒可以进行15w次的密码尝试,这意味着必须指定非常强大的密码来防止暴力破解。如果要使用密码,打开redis.conf配置文件,

1
bash复制代码requirepass 123456    #480行,设置请求密码,这样访问时都需要先登录

修改完配置文件以后,要重启redis服务?(docker start redis)

1
2
ruby复制代码127.0.0.1:6379> auth 123456		#客户端访问方式
jedis.auth(“123456”); #jedis访问方式(学了以后用)

(12)Redis.conf配置文件

Redis 支持很多的参数,但都有默认值。

daemonize

默认情况下, redis 不是在后台运行的,如果需要在后台运行,把该项的值更改为 yes。

pidfile

当 Redis 在后台运行的时候, Redis 默认会把 pid 文件放在/var/run/redis.pid,你可以配置到其他地址。当运行多个 redis 服务时,需要指定不同的 pid 文件和端口

bind

指定 Redis 只接收来自于该 IP 地址的请求,如果不进行设置,那么将处理所有请求,在生产环境中最好设置该项

port

监听端口,默认为 6379

timeout

设置客户端连接时的超时时间,单位为秒。当客户端在这段时间内没有发出任何指令,那么关闭该连接

loglevel

log 等级分为 4 级, debug, verbose, notice, 和 warning。生产环境下一般开启 notice

logfile

配置 log 文件地址,默认使用标准输出,即打印在命令行终端的窗口上

databases

设置数据库的个数,可以使用 SELECT 命令来切换数据库。默认使用的数据库是 0

save

设置 Redis 进行数据库镜像的频率。

if(在 60 秒之内有 10000 个 keys 发生变化时){

进行镜像备份 (redis/data/)

}else if(在 300 秒之内有 10 个 keys 发生了变化){

进行镜像备份

}else if(在 900 秒之内有 1 个 keys 发生了变化){

进行镜像备份

}

rdbcompression

在进行镜像备份时,是否进行压缩

dbfilename

镜像备份文件的文件名

dir

数据库镜像备份的文件放置的路径。

这里的路径跟文件名要分开配置是因为 Redis 在进行备份时,先会将当前数据库的状态写入到一个临时文件中

等备份完成时,再把该临时文件替换为上面所指定的文件,而这里的临时文件和上面所配置的备份文件都会放在这个指定的路径当中

slaveof

设置该数据库为其他数据库的从数据库

masterauth

当主数据库连接需要密码验证时,在这里指定

requirepass

设置客户端连接后进行任何其他指定前需要使用的密码。

警告:因为 redis 速度相当快,所以在一台比较好的服务器下,一个外部的用户可以在一秒钟进行 150K 次的密码尝试,这意味着你需要指定非常非常强大的密码来防止暴力破解。

maxclients

限制同时连接的客户数量。当连接数超过这个值时, redis 将不再接收其他连接请求,

客户端尝试连接时将收到 error 信息。

maxmemory

设置 redis 能够使用的最大内存。

appendonly

默认情况下, redis 会在后台异步的把数据库镜像备份到磁盘,但是该备份是非常耗时的,而且备份也不能很频繁,如果发生诸如拉闸限电、拔插头等状况,那么将造成比较大范围的数据丢失。

所以 redis 提供了另外一种更加高效的数据库备份及灾难恢复方式。开启 append only 模式之后, redis 会把所接收到的每一次写操作请求都追加到appendonly.aof 文件中,当 redis 重新启动时,会从该文件恢复出之前的状态。

但是这样会造成 appendonly.aof 文件过大,所以 redis 还支持了 BGREWRITEAOF 指令,对appendonly.aof 进行重新整理。

所以我认为推荐生产环境下的做法为关闭镜像,开启appendonly.aof,同时可以选择在访问较少的时间每天对 appendonly.aof 进行重写一次。

appendfsync

设置对 appendonly.aof 文件进行同步的频率。 always 表示每次有写操作都进行同步,

everysec 表示对写操作进行累积,每秒同步一次。这个需要根据实际业务场景进行配置

vm-enabled

是否开启虚拟内存支持。因为 redis 是一个内存数据库,而且当内存满的时候,无法接收新的写请求,所以在 redis 2.0 中,提供了虚拟内存的支持。

但是需要注意的是, redis中,所有的 key 都会放在内存中,在内存不够时,只会把 value 值放入交换区。这样保证了虽然使用虚拟内存,但性能基本不受影响

同时,你需要注意的是你要把vm-max-memory 设置到足够来放下你的所有的 key

vm-swap-file

设置虚拟内存的交换文件路径

vm-max-memory

这里设置开启虚拟内存之后, redis 将使用的最大物理内存的大小。默认为 0, redis 将把他所有的能放到交换文件的都放到交换文件中,以尽量少的使用物理内存。

在生产环境下,需要根据实际情况设置该值,最好不要使用默认的 0

vm-page-size

设置虚拟内存的页大小,如果你的 value 值比较大,比如说你要在 value 中放置博客、新闻之类的所有文章内容,就设大一点,如果要放置的都是很小的内容,那就设小一点。

vm-pages

设置交换文件的总的 page 数量, 需要注意的是, page table 信息会放在物理内存中,每8 个 page 就会占据 RAM 中的 1 个 byte。总的虚拟内存大小 = vm-page-size * vm-pages

vm-max-threads

设置 VM IO 同时使用的线程数量。因为在进行内存交换时,对数据有编码和解码的过程,所以尽管 IO 设备在硬件上本上不能支持很多的并发读写,但是还是如果你所保存的 vlaue 值比较大,将该值设大一些,还是能够提升性能的

glueoutputbuf

把小的输出缓存放在一起,以便能够在一个 TCP packet 中为客户端发送多个响应,具体原理和真实效果我不是很清楚。所以根据注释,你不是很确定的时候就设置成 yes

hash-max-zipmap-entries

在 redis 2.0 中引入了 hash 数据结构。当 hash 中包含超过指定元素个数并且最大的元素没有超过临界时, hash 将以一种特殊的编码方式(大大减少内存使用)来存储,这里可以设置这两个临界值

activerehashing

开启之后, redis 将在每 100 毫秒时使用 1 毫秒的 CPU 时间来对 redis 的 hash 表进行重新 hash,可以降低内存的使用。

当你的使用场景中,有非常严格的实时性需要,不能够接受 Redis 时不时的对请求有 2 毫秒的延迟的话,把这项配置为 no。

如果没有这么严格的实时性要求,可以设置为 yes,以便能够尽可能快的释放内存

2Redis应用基础

2.1基础命令

(1)redis-cli

默认连接:IP 127.0.0.1 端口 6379

1
复制代码redis-cli

指定IP端口:

1
css复制代码redis-cli –h 127.0.0.1 –p 6379

Redis提供了PING-PONG机制,测试与客户端和服务器链接是否正常

1
复制代码redis-cli ping

或

1
2
3
复制代码redis-cli
redis 127.0.0.1:6379>ping
PONG

正常回复

1
2
bash复制代码127.0.0.1:6379>SET test 123
OK

错误回复(以error开头,后面跟着错误信息)

1
2
bash复制代码127.0.0.1:6379>TEST
(error) ERR unknown command 'TEST'

整数回复

1
2
bash复制代码127.0.0.1:6379>INCR test_incr
(integer) 1

字符串回复(最长久的一种回复,双引号包裹)

1
2
arduino复制代码127.0.0.1:6379>get test
“123”

多行字符串回复

1
2
3
makefile复制代码127.0.0.1:6379>KEYS *
1) "test_incr"
2) "test"

退出

1
ruby复制代码127.0.0.1:6379> exit

关闭

1
ruby复制代码127.0.0.1:6379> shutdown

(2)keys

字符串类型是redis中最基本的数据类型,它能存储任何形式的字符串,包括二进制数据。可以存储JSON化的对象、字节数组等。一个字符串类型键允许存储的数据最大容量是512MB。

赋值与取值:

SET key value

GET key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ruby复制代码127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> set test1 123
OK
127.0.0.1:6379> set test2 ab
OK
127.0.0.1:6379> keys *
1) "test1"
2) "test2"
127.0.0.1:6379> get test1
"123"
127.0.0.1:6379> get test2
"ab"
127.0.0.1:6379> get test3
(nil)
127.0.0.1:6379>

(3)keys通配符

获取符合规则的建名列表。

1
2
3
css复制代码KEYS *
keys test[_]*
keys t[a-d]

说明:

? 匹配一个字符,例如 keys ?est1

* 匹配任意个(包括0个)字符

[] 匹配括号间的任一字符,例如 keys test[12]。还可以使用“-“表示范围。

例如test[1-3]匹配test1/test2/test3

\x 匹配字符x,用于转义符合,如果要匹配“?“就需要使用?

(4)select

redis默认支持16个数据库,对外都是以一个从0开始的递增数字命名,可以通过参数database来修改默认数据库个数。客户端连接redis服务后会自动选择0号数据库,可以通过select命令更换数据库,例如选择1号数据库:

1
2
3
4
makefile复制代码127.0.0.1:6379>SELECT 1
OK
127.0.0.1:6379>GET test
(nil)

说明:

Redis不支持自定义数据库名称。

Redis不支持为每个数据库设置访问密码。

Redis的多个数据库之间不是安全隔离的,FLUSHALL命令会清空所有数据库的数据。

清除屏幕内容

1
arduino复制代码clear

(5)exists

判断一个键是否存在。

如果键存在则返回整数类型1,否则返回0。

1
2
3
4
5
6
7
8
ruby复制代码127.0.0.1:6379> keys *
1) "test_incr"
2) "test1"
127.0.0.1:6379> exists test1
(integer) 1
127.0.0.1:6379> exists test3
(integer) 0
127.0.0.1:6379>

(6)del

删除键,可以删除一个或者多个键,多个键用空格隔开,返回值是删除的键的个数。

1
2
3
4
5
6
7
ruby复制代码127.0.0.1:6379> del test1
(integer) 1
127.0.0.1:6379> del test1
(integer) 0
127.0.0.1:6379> del test1 test_incr
(integer) 1
127.0.0.1:6379>

(7)type

获得键值的数据类型,返回值可能是string(字符串)、hash(散列类型)、list(列表类型)、set(集合类型)、zset(有序集合类型)。

1
2
3
4
5
6
7
ruby复制代码127.0.0.1:6379> keys *
1) "test1"
2) "test2"
127.0.0.1:6379> type test1
string
127.0.0.1:6379> type test2
string

(8)help

1
2
3
4
5
6
7
8
9
10
11
12
vbnet复制代码127.0.0.1:6379> help
redis-cli 2.8.19
Type: "help @<group>" to get a list of commands in <group>
"help <command>" for help on <command>
"help <tab>" to get a list of possible help topics
"quit" to exit
127.0.0.1:6379> help type

TYPE key
summary: Determine the type stored at key
since: 1.0.0
group: generic

官网:www.redis.io帮助

(9)flushall

清空所有数据库。

1
2
ruby复制代码127.0.0.1:6379> FLUSHALL
OK

(10)flushdb

1
2
ruby复制代码127.0.0.1:6379> FLUSHDB
OK

2.2Redis数据类型之字符串(重点)

存放的字符串为二进制是安全的。字符串长度支持到512M。

(1)incry/incyby

递增数字INCR key当存储的字符串是整数时,redis提供了一个实用的命令INCR,其作用是让当前键值递增,并返回递增后的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ruby复制代码127.0.0.1:6379> keys *
1) "test1"
2) "test2"
127.0.0.1:6379> get test1
"123"
127.0.0.1:6379> get test1
"abc"
127.0.0.1:6379> get test2
(nil)
127.0.0.1:6379> incr num
(integer) 1
127.0.0.1:6379> keys *
1) "num"
2) "test1"
3) "test"
127.0.0.1:6379> incr num
(integer) 2
127.0.0.1:6379> incr num
(integer) 3
127.0.0.1:6379>

从上面例子可以看出,如果num不存在,则自动会创建,如果存在自动+1。

指定增长系数

语法:INCRBY key increment

1
2
3
4
5
6
7
8
9
10
11
12
13
ruby复制代码127.0.0.1:6379> incr num
(integer) 2
127.0.0.1:6379> incr num
(integer) 3
127.0.0.1:6379> incrby num 2
(integer) 5
127.0.0.1:6379> incrby num 2
(integer) 7
127.0.0.1:6379> incrby num 2
(integer) 9
127.0.0.1:6379> incr num
(integer) 10
127.0.0.1:6379>

(2)decr/decrby

减少指定的整数

DECR key 按照默认步长(默认为1)进行递减

DECRBY key decrement 按照指定步长进行递减

1
2
3
4
5
ruby复制代码127.0.0.1:6379> incr num
(integer) 10
127.0.0.1:6379> decr num
(integer) 9
127.0.0.1:6379> decrby num 3

(3)incrbyfloat

整数时,第一次加可以得到正确结果,浮点数后再加浮点就会出现精度问题。

原来下面的例子2.8.7注意在新版本中已经修正了这个浮点精度问题。3.0.7

INCRBYFLOAT key decrement

1
2
3
4
5
6
ruby复制代码127.0.0.1:6379> set num 131
(integer) 131
127.0.0.1:6379> incrbyfloat num 0.7
“131.7”
127.0.0.1:6379> incrbyfloat num 0.7
“132.3999999999999999”

(4)append

向尾部追加值。如果键不存在则创建该键,其值为写的value,即相当于SET key value。返回值是追加后字符串的总长度。

语法:APPEND key value

1
2
3
4
5
6
7
8
9
10
11
ruby复制代码127.0.0.1:6379> keys *
1) "num"
2) "test1"
3) "test"
127.0.0.1:6379> get test
"123"
127.0.0.1:6379> append test "abc"
(integer) 6
127.0.0.1:6379> get test
"123abc"
127.0.0.1:6379>

(5)strlen

字符串长度,返回数据的长度,如果键不存在则返回0。注意,如果键值为空串,返回也是0。

语法:STRLEN key

1
2
3
4
5
6
7
8
9
10
11
12
13
ruby复制代码127.0.0.1:6379> get test
"123abc"
127.0.0.1:6379> strlen test
(integer) 6
127.0.0.1:6379> strlen tnt
(integer) 0
127.0.0.1:6379> set tnt ""
OK
127.0.0.1:6379> strlen tnt
(integer) 0
127.0.0.1:6379> exists tnt
(integer) 1
127.0.0.1:6379>

(6)mset/mget

同时设置/获取多个键值

语法:MSET key value [key value …]

MGET key [key …]

1
2
3
4
5
6
7
8
9
10
11
ruby复制代码127.0.0.1:6379> flushall
OK
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> mset a 1 b 2 c 3
OK
127.0.0.1:6379> mget a b c
1) "1"
2) "2"
3) "3"
127.0.0.1:6379>

2.3Redis有效时间(重点)

(1)Expire (设置生效时长-单位秒)

Redis在实际使用过程中更多的用作缓存,然而缓存的数据一般都是需要设置有效时间的(缓存内存是有限的,不可能无限制增加),即到期后数据自动销毁。

语法:EXPIRE key seconds

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ruby复制代码127.0.0.1:6379> flushall
OK
127.0.0.1:6379> set bomb tnt
OK
127.0.0.1:6379> expire bomb 10
(integer) 1
127.0.0.1:6379> ttl bomb
(integer) 5
127.0.0.1:6379> ttl bomb
(integer) 3
127.0.0.1:6379> ttl bomb
(integer) 3
127.0.0.1:6379> ttl bomb
(integer) 2
127.0.0.1:6379> ttl bomb
(integer) 1
127.0.0.1:6379> ttl bomb
(integer) -2
127.0.0.1:6379> ttl bomb
(integer) -2
127.0.0.1:6379>

TTL查看key的剩余时间,当返回值为-2时,表示键被删除。

当 key 不存在时,返回 -2 。 当 key 存在但没有设置剩余生存时间时,返回 -1 。 否则,以毫秒为单位,返回 key 的剩余生存时间。

注意:在 Redis 2.8 以前,当 key 不存在,或者 key 没有设置剩余生存时间时,命令都返回 -1 。

(2)Persist(取消时长设置)

通过persist让对特定key设置的生效时长失效。

语法:PERSIST key

1
2
3
4
5
6
7
8
9
10
11
ruby复制代码127.0.0.1:6379> set bomb tnt
OK
127.0.0.1:6379> expire bomb 60
(integer) 1
127.0.0.1:6379> ttl bomb
(integer) 49
127.0.0.1:6379> persist bomb
(integer) 1
127.0.0.1:6379> ttl bomb
(integer) -1
127.0.0.1:6379>

设置新的数据时需要重新设置该key的生存时间,重新设置值也会清除生存时间。

(3)pexpire(单位毫秒)

pexpire 让key的生效时长以毫秒作为计量单位,可应用于秒杀场景。

语法:PEXPIRE key milliseconds

1
2
3
4
5
6
7
8
9
10
11
ruby复制代码127.0.0.1:6379> set bomb tnt
OK
127.0.0.1:6379> pexpire bomb 10000
(integer) 1
127.0.0.1:6379> ttl bomb
(integer) 6
127.0.0.1:6379> ttl bomb
(integer) 3
127.0.0.1:6379> ttl bomb
(integer) -2
127.0.0.1:6379>

设置生存时间为毫秒,可以做到更精确的控制。

2.4Redis高级中的hash结构(重点)

在redis中用的最多的就是hash和string类型。

(1)问题

假设有User对象以JSON序列化的形式存储到redis中,User对象有id、username、password、age、name等属性,存储的过程如下:

保存、更新:

User对象->json(string)->redis

如果在业务上只是更新age属性,其他的属性并不做更新应该怎么做呢?

Redis数据类型之散列类型hash

散列类型存储了字段(field)和字段值的映射,但字段值只能是字符串,不支持其他类型,也就是说,散列类型不能嵌套其他的数据类型。一个散列类型可以包含最多232-1个字段。

(2)hset/hget

相关命令

1
2
3
4
5
vbnet复制代码HSET key field value
HGET key field
HMSET key field value [field value…]
HMGET key field [field]
HGETALL key

HSET和HGET赋值和取值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ruby复制代码127.0.0.1:6379> hset user username chenchen
(integer) 1
127.0.0.1:6379> hget user username
"chenchen"
127.0.0.1:6379> hset user username chen
(integer) 0
127.0.0.1:6379> keys user
1) "user"
127.0.0.1:6379> hgetall user
1) "username"
2) "chen"
127.0.0.1:6379>
127.0.0.1:6379> hset user age 18
(integer) 1
127.0.0.1:6379> hset user address "xi'an"
(integer) 1
127.0.0.1:6379> hgetall user
1) "username"
2) "chen"
3) "age"
4) "18"
3) "address"
4) "xi'an"
127.0.0.1:6379>

HSET命令不区分插入和更新操作,当执行插入操作时HSET命令返回1,当执行更新操作时返回0。

(3)hincrby

1
2
3
4
ruby复制代码127.0.0.1:6379> hdecrby article total 1		#执行会出错
127.0.0.1:6379> hincrby article total -1 #没有hdecrby自减命令
(integer) 1
127.0.0.1:6379> hget article total #获取值

(4)hmset/hmget

HMSET和HMGET设置和获取对象属性

1
2
3
4
5
6
7
8
9
10
11
ruby复制代码127.0.0.1:6379> hmset person username tony age 18
OK
127.0.0.1:6379> hmget person age username
1) "18"
2) "tony"
127.0.0.1:6379> hgetall person
1) "username"
2) "tony"
3) "age"
4) "18"
127.0.0.1:6379>

注意:上面HMGET字段顺序可以自行定义

(5)hexists

属性是否存在

1
2
3
4
5
6
7
8
9
ruby复制代码127.0.0.1:6379> hexists killer
(error) ERR wrong number of arguments for 'hexists' command
127.0.0.1:6379> hexists killer a
(integer) 0
127.0.0.1:6379> hexists user username
(integer) 1
127.0.0.1:6379> hexists person age
(integer) 1
127.0.0.1:6379>

(6) hdel

删除属性

1
2
3
4
5
6
7
8
9
10
11
ruby复制代码127.0.0.1:6379> hdel user age
(integer) 1
127.0.0.1:6379> hgetall user
1) "username"
2) "chen"
127.0.0.1:6379> hgetall person
1) "username"
2) "tony"
3) "age"
4) "18"
127.0.0.1:6379>

(7)hkeys/hvals

只获取字段名HKEYS或字段值HVALS

1
2
3
4
5
6
ruby复制代码127.0.0.1:6379> hkeys person
1) "username"
2) "age"
127.0.0.1:6379> hvals person
1) "tony"
2) "18"

(8)hlen

元素个数

1
2
3
4
5
ruby复制代码127.0.0.1:6379> hlen user
(integer) 1
127.0.0.1:6379> hlen person
(integer) 2
127.0.0.1:6379>

2.5Redis高级中的list结构

(1)问题

Redis高级中的list结构

Redis的list类型其实就是一个每个子元素都是string类型的双向链表。可以通过push,pop操作从链表的头部或者尾部添加删除元素。这使得list既可以用作栈,也可以用作队列。

有意思的是list的pop操作还有阻塞版本的,当我们[lr]pop一个list对象时,如果list是空,或者不存在,会立即返回nil。但是阻塞版本的b[lr]pop可以则可以阻塞,当然可以加超时时间,超时后也会返回nil。为什么要阻塞版本的pop呢,主要是为了避免轮询。举个简单的例子如果我们用list来实现一个工作队列。执行任务的thread可以调用阻塞版本的pop去获取任务这样就可以避免轮询去检查是否有任务存在。当任务来时候工作线程可以立即返回,也可以避免轮询带来的延迟。

(2)lpush

在key对应list的头部添加字符串元素

1
2
3
4
5
6
7
8
bash复制代码redis 127.0.0.1:6379> lpush mylist "world"
(integer) 1
redis 127.0.0.1:6379> lpush mylist "hello"
(integer) 2
redis 127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "world"
redis 127.0.0.1:6379>

其中,Redis Lrange 返回列表中指定区间内的元素,区间以偏移量 START 和 END 指定。 其中 0 表示列表的第一个元素, 1 表示列表的第二个元素,以此类推。 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。

(3)rpush

在key对应list的尾部添加字符串元素

1
2
3
4
5
6
7
8
bash复制代码redis 127.0.0.1:6379> rpush mylist2 "hello"
(integer) 1
redis 127.0.0.1:6379> rpush mylist2 "world"
(integer) 2
redis 127.0.0.1:6379> lrange mylist2 0 -1
1) "hello"
2) "world"
redis 127.0.0.1:6379>

(4)查看list

1
复制代码redis 127.0.0.1:6379> lrange mylist3 0 -1

(5)del

1
less复制代码redis 127.0.0.1:6379> del mylist

(6)linsert

在key对应list的特定位置之前或之后添加字符串元素

1
2
3
4
5
6
7
8
9
10
11
bash复制代码redis 127.0.0.1:6379> rpush mylist3 "hello"
(integer) 1
redis 127.0.0.1:6379> rpush mylist3 "world"
(integer) 2
redis 127.0.0.1:6379> linsert mylist3 before "world" "there"
(integer) 3
redis 127.0.0.1:6379> lrange mylist3 0 -1
1) "hello"
2) "there"
3) "world"
redis 127.0.0.1:6379>

(7)lset

设置list中指定下标的元素值(一般用于修改操作)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bash复制代码redis 127.0.0.1:6379> rpush mylist4 "one"
(integer) 1
redis 127.0.0.1:6379> rpush mylist4 "two"
(integer) 2
redis 127.0.0.1:6379> rpush mylist4 "three"
(integer) 3
redis 127.0.0.1:6379> lset mylist4 0 "four"
OK
redis 127.0.0.1:6379> lset mylist4 -2 "five"
OK
redis 127.0.0.1:6379> lrange mylist4 0 -1
1) "four"
2) "five"
3) "three"
redis 127.0.0.1:6379>

(8)lrem

从key对应list中删除count个和value相同的元素,count>0时,按从头到尾的顺序删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bash复制代码redis 127.0.0.1:6379> rpush mylist5 "hello"
(integer) 1
redis 127.0.0.1:6379> rpush mylist5 "hello"
(integer) 2
redis 127.0.0.1:6379> rpush mylist5 "foo"
(integer) 3
redis 127.0.0.1:6379> rpush mylist5 "hello"
(integer) 4
redis 127.0.0.1:6379> lrem mylist5 2 "hello"
(integer) 2
redis 127.0.0.1:6379> lrange mylist5 0 -1
1) "foo"
2) "hello"
redis 127.0.0.1:6379>

count<0时,按从尾到头的顺序删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bash复制代码redis 127.0.0.1:6379> rpush mylist6 "hello"
(integer) 1
redis 127.0.0.1:6379> rpush mylist6 "hello"
(integer) 2
redis 127.0.0.1:6379> rpush mylist6 "foo"
(integer) 3
redis 127.0.0.1:6379> rpush mylist6 "hello"
(integer) 4
redis 127.0.0.1:6379> lrem mylist6 -2 "hello"
(integer) 2
redis 127.0.0.1:6379> lrange mylist6 0 -1
1) "hello"
2) "foo"
redis 127.0.0.1:6379>

count=0时,删除全部

1
2
3
4
5
6
7
8
9
10
11
12
13
bash复制代码redis 127.0.0.1:6379> rpush mylist7 "hello"
(integer) 1
redis 127.0.0.1:6379> rpush mylist7 "hello"
(integer) 2
redis 127.0.0.1:6379> rpush mylist7 "foo"
(integer) 3
redis 127.0.0.1:6379> rpush mylist7 "hello"
(integer) 4
redis 127.0.0.1:6379> lrem mylist7 0 "hello"
(integer) 3
redis 127.0.0.1:6379> lrange mylist7 0 -1
1) "foo"
redis 127.0.0.1:6379>

(9)ltrim

保留指定key 的值范围内的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bash复制代码redis 127.0.0.1:6379> rpush mylist8 "one"
(integer) 1
redis 127.0.0.1:6379> rpush mylist8 "two"
(integer) 2
redis 127.0.0.1:6379> rpush mylist8 "three"
(integer) 3
redis 127.0.0.1:6379> rpush mylist8 "four"
(integer) 4
redis 127.0.0.1:6379> ltrim mylist8 1 -1
OK
redis 127.0.0.1:6379> lrange mylist8 0 -1
1) "two"
2) "three"
3) "four"
redis 127.0.0.1:6379>

(10)lpop

从list的头部删除元素,并返回删除元素

1
2
3
4
5
6
7
8
arduino复制代码redis 127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "world"
redis 127.0.0.1:6379> lpop mylist
"hello"
redis 127.0.0.1:6379> lrange mylist 0 -1
1) "world"
redis 127.0.0.1:6379>

(11)rpop

从list的尾部删除元素,并返回删除元素:

1
2
3
4
5
6
7
8
arduino复制代码redis 127.0.0.1:6379> lrange mylist2 0 -1
1) "hello"
2) "world"
redis 127.0.0.1:6379> rpop mylist2
"world"
redis 127.0.0.1:6379> lrange mylist2 0 -1
1) "hello"
redis 127.0.0.1:6379>

(12)llen

返回key对应list的长度:

1
2
3
bash复制代码redis 127.0.0.1:6379> llen mylist5
(integer) 2
redis 127.0.0.1:6379>

(13)index

返回名称为key的list中index位置的元素:

1
2
3
4
5
6
7
8
arduino复制代码redis 127.0.0.1:6379> lrange mylist5 0 -1
1) "three"
2) "foo"
redis 127.0.0.1:6379> lindex mylist5 0
"three"
redis 127.0.0.1:6379> lindex mylist5 1
"foo"
redis 127.0.0.1:6379>

(14)rpoplpush

从第一个list的尾部移除元素并添加到第二个list的头部,最后返回被移除的元素值,整个操作是原子的.如果第一个list是空或者不存在返回nil:

1
2
复制代码rpoplpush lst1 lst1
rpoplpush lst1 lst2

2.6Redis高机中的set结构

Redis的Set是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。Redis中Set集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。集合中最大的成员数为232 - 1 (4294967295每个集合可存储40多亿个成员)。

(1)sadd

添加元素,重复元素添加失败,返回0

1
2
3
4
5
6
7
8
ruby复制代码127.0.0.1:6379> sadd name tony
(integer) 1
127.0.0.1:6379> sadd name hellen
(integer) 1
127.0.0.1:6379> sadd name rose
(integer) 1
127.0.0.1:6379> sadd name rose
(integer) 0

(2)smembers

获取内容

1
2
3
4
ruby复制代码127.0.0.1:6379> smembers name
1) "hellen"
2) "rose"
3) "tony"

(3)spop

移除并返回集合中的一个随机元素

1
2
3
4
5
6
7
8
9
10
11
12
ruby复制代码127.0.0.1:6379> smembers internet
1) "amoeba"
2) "redis"
3) "rabbitmq"
4) "nginx"
127.0.0.1:6379> spop internet
"rabbitmq"
127.0.0.1:6379> spop internet
"nginx"
127.0.0.1:6379> smembers internet
1) "amoeba"
2) "redis"

(4)scard

获取成员个数

1
2
ruby复制代码127.0.0.1:6379> scard name
(integer) 3

(5)smove

移动一个元素到另外一个集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ruby复制代码127.0.0.1:6379> sadd internet amoeba nginx redis
(integer) 3
127.0.0.1:6379> sadd bigdata hadopp spark rabbitmq
(integer) 3
127.0.0.1:6379> smembers internet
1) "amoeba"
2) "redis"
3) "nginx"
127.0.0.1:6379> smembers bigdata
1) "hadopp"
2) "spark"
3) "rabbitmq"
127.0.0.1:6379> smove bigdata internet rabbitmq
(integer) 1
127.0.0.1:6379> smembers internet
1) "amoeba"
2) "redis"
3) "rabbitmq"
4) "nginx"
127.0.0.1:6379> smembers bigdata
1) "hadopp"
2) "spark"
127.0.0.1:6379>

(6)sunion

并集

1
2
3
4
5
6
7
ruby复制代码127.0.0.1:6379> sunion internet bigdata
1) "redis"
2) "nginx"
3) "rabbitmq"
4) "amoeba"
5) "hadopp"
6) "spark"

2.7Redis数据持久化的两种模式(重点)

(1)简介

Redis中为了保证在系统宕机(类似进程被杀死)情况下,能更快的进行故障恢复,设计了两种数据持久化方案,分别为rdb和aof。

Rdb方式是通过手动(save-阻塞式或bgsave-异步)或周期性方式保存redis中key/value的一种机制,Rdb方式一般为redis的默认数据持久化方式.

Aof方式是通过记录写操作日志的方式,记录redis数据的一种持久化机制,这个机制默认是没有开启的.

(2)rdb和aof比较

rdb aof
fork一个进程,遍历hash table,利用copy on write,把整个db dump保存下来。 save,bgsave,shutdown, slave 命令会触发这个操作。粒度比较大,如果save, shutdown, slave 之前crash了,则中间的操作没办法恢复。 把写操作指令,持续的写到一个类似日志文件里。(类似于从postgresql等数据库导出sql一样,只记录写操作) 粒度较小,crash(宕机)之后,只有crash之前没有来得及做日志的操作,这些数据是没办法恢复。

两种区别就是,一个是持续的用日志记录写操作,crash(崩溃)后利用日志恢复;一个是平时写操作的时候不触发写,只有手动提交save命令,或者是shutdown关闭命令时,才触发备份操作。

选择的标准,就是看系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能,待手动运行save的时候,再做备份(rdb)。rdb这个就更有些 最终一致性(eventually consistent)的意思了。

2.8Redis事务管理(重点)

(1)背景

大多数数据库的事务控制,假如是乐观锁的方式,一般都是基于数据版本(version)的记录机制实现的。即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个”version”字段来实现读取出数据时,将此版本号一同读出,之后更新时,对此版本号加1。此时,将提交数据的版本号与数据库表对应记录的当前版本号进行比对,如果提交的数据版本号大于数据库当前版本号,则予以更新,否则认为是过期数据。

Redis也采用类似的机制,使用watch命令会监视给定的key,当exec时候如果监视的key从调用watch后发生过变化,则整个事务会失败。也可以调用watch多次监视多个key。这样就可以对指定的key加乐观锁了。注意watch的key是对整个连接有效的,事务也一样。如果连接断开,监视和事务都会被自动清除。当然exec,discard,unwatch命令都会清除连接中的所有监视。

(2)基本概念

redis是单线程(但是在6.0中真正引用多线程的应用),提交命令时,其它命令无法插入其中,轻松利用单线程实现了事务的原子性。那如果执行多个redis命令呢?自然就没有事务保证,于是redis有下列相关的redis命令来实现事务管理。

multi 开启事务

exec 提交事务

discard 取消事务

watch 监控,如果监控的值发生变化,则提交事务时会失败

unwatch 去掉监控

Redis保证一个事务中的所有命令要么都执行,要么都不执行(原子性)。如果在发送EXEC命令前客户端断线了,则Redis会清空事务队列,事务中的所有命令都不会执行。而一旦客户端发送了EXEC命令,所有的命令就都会被执行,即使此后客户端断线也没关系,因为Redis中已经记录了所有要执行的命令。

(3)exec提交事务

例如:模拟转账,王有200,张有700,张给王转100。过程如下:

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
ruby复制代码127.0.0.1:6379> set w 200
OK
127.0.0.1:6379> set z 700
OK
127.0.0.1:6379> mget w z
1) "200"
2) "700"
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby z 100
QUEUED #注意此命令根本没有执行,而是把其放在一个队列中
127.0.0.1:6379> incrby w 100
QUEUED
127.0.0.1:6379> mget w z
QUEUED
127.0.0.1:6379> get w #同时,这些相关的变量也不能再读取
QUEUED
127.0.0.1:6379> get z
QUEUED
127.0.0.1:6379> exec
1) (integer) 600
2) (integer) 300
3) 1) "300"
2) "600"
4) "300"
5) "600"
127.0.0.1:6379> mget w z
1) "300"
2) "600"
127.0.0.1:6379>

(4)如果有错误指令,自动取消

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ruby复制代码127.0.0.1:6379> mget w z
1) "300"
2) "600"
127.0.0.1:6379> multi
OK
127.0.0.1:6379> get w
QUEUED
127.0.0.1:6379> set w 100
QUEUED
127.0.0.1:6379> abc
(error) ERR unknown command 'abc'
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> mget w z #可以看出数据并未变化
1) "300"
2) "600"
127.0.0.1:6379>

(5)discard取消事务

注意redis事务太简单,没有回滚,而只有取消。

1
2
3
4
5
6
7
8
9
10
11
12
13
ruby复制代码127.0.0.1:6379> mget z w
1) "600"
2) "300"
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incrby z 100
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> get z
"600"
127.0.0.1:6379> exec
(error) ERR EXEC without MULTI

(6)秒杀抢票事务处理

客户端1:

1
2
3
4
5
6
7
8
9
10
11
12
ruby复制代码127.0.0.1:6379> set ticket 1
OK
127.0.0.1:6379> set money 0
OK
127.0.0.1:6379> watch ticket #乐观锁,对值进行观察,改变则事务失败
OK
127.0.0.1:6379> multi #开启事务
OK
127.0.0.1:6379> decr ticket
QUEUED
127.0.0.1:6379> incrby money 100
QUEUED

客户端2:还没等客户端1提交事务,此时客户端2把票买到了。

1
2
3
4
ruby复制代码127.0.0.1:6379> get ticket
"1"
127.0.0.1:6379> decr ticket
(integer) 0

客户端1:

1
2
3
4
5
ruby复制代码127.0.0.1:6379> exec
(nil) #执行事务,失败
127.0.0.1:6379> get ticket
"0"
127.0.0.1:6379> unwatch #取消监控

3java中操作redis数据库

(1)jedis的应用

添加依赖

1
2
3
4
5
xml复制代码<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.5.2</version>
</dependency>

快速入门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Test
void testRedisStringOper(){
Jedis jedis=new Jedis("192.168.174.130", 6379);
//jedis.auth("123456");假如你的redis设置了密码
jedis.set("id", "101");
jedis.set("name", "tony");
System.out.println("set ok");
String id=jedis.get("id");
String name=jedis.get("name");
System.out.println("id="+id+";name="+name);
jedis.incr("id");
jedis.incrBy("id", 2);
System.out.println(jedis.strlen("name"));
//......
}

说明:当在测试之前,我们需要将redis.conf配置文件中的bind 127.0.0.1元素注释掉,

并且将其保护模式(protected-mode)设置为no(redis3.0之后默认开启了这个策略) ,当修改了配置以后,一定要记得重启redis,然后再进行访问.

(2)连接池JedisPool应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Test
void testJedisPool(){
// 构建连接池配置信息
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
// 设置最大连接数
jedisPoolConfig.setMaxTotal(200);
// 构建连接池
JedisPool jedisPool = new JedisPool(jedisPoolConfig, "192.168.174.130", 6379);
// 从连接池中获取连接
Jedis jedis = jedisPool.getResource();
// 读取数据
System.out.println(jedis.get("name"));
// 释放连接池
jedisPool.close();
}

(3)RedisTemplate应用

第一步:创建spring boot项目

第二步:添加redis依赖 (spring-boot-starter-data-redis)

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

(4)快速入门

编写一段代码,通过StringRedisTemplate对象操作远端redis中的字符串数据。

第一步:配置连接redis的url和端口(application.yml)

1
2
3
4
yaml复制代码spring:
redis:
host: 192.168.64.129
port: 6379

第二步:基于StringRedisTemplate实现对象远端redis中字符串操作

1
2
3
4
5
6
7
8
9
10
11
java复制代码@SpringBootTest
public class StringRedisTemplateTests {
/**此对象为spring提供的一个用于操作redis数据库中的字符串的一个对象*/
@Autowired
private StringRedisTemplate stringRedisTemplate;

@Test
void testOpsForValueSet(){
stringRedisTemplate.opsForValue().set("age", "18");
}
}

第三步:基于RedisTemplate实现对远端复杂redis中数据的操作。

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
java复制代码@SpringBootTest
public class RedisTemplateTests {
/**
* 通过此对象操作redis中复杂数据类型的数据,例如hash结构
*/
@Autowired
private RedisTemplate redisTemplate;

@Test
void testSetData(){
SetOperations setOperations=redisTemplate.opsForSet();
setOperations.add("setKey1", "A","B","C","C");
Object members=setOperations.members("setKey1");
System.out.println("setKeys="+members);
//........
}

@Test
void testListData(){
//向list集合放数据
ListOperations listOperations = redisTemplate.opsForList();
listOperations.leftPush("lstKey1", "100"); //lpush
listOperations.leftPushAll("lstKey1", "200","300");
listOperations.leftPush("lstKey1", "100", "105");
listOperations.rightPush("lstKey1", "700");
Object value= listOperations.range("lstKey1", 0, -1);
System.out.println(value);
//从list集合取数据
Object v1=listOperations.leftPop("lstKey1");//lpop
System.out.println("left.pop.0="+v1);
value= listOperations.range("lstKey1", 0, -1);
System.out.println(value);
}

/**通过此方法操作redis中的hash数据*/
@Test
void testHashData(){
HashOperations hashOperations = redisTemplate.opsForHash();//hash
Map<String,String> blog=new HashMap<>();
blog.put("id", "1");
blog.put("title", "hello redis");
hashOperations.putAll("blog", blog);
hashOperations.put("blog", "content", "redis is very good");
Object hv=hashOperations.get("blog","id");
System.out.println(hv);
Object entries=hashOperations.entries("blog");
System.out.println("entries="+entries);
}

}

4 Redis高级特性应用

1redis分片

何为分片

Redis中的分片思想就是把鸡蛋放到不同的篮子中进行存储。因为一个redis服务的存储能力是有限。分片就是实现redis扩容的一种有效方案。

2启动多个服务

传统方式(了解)

参数:port端口,daemonize后台运行,protected-mode保护模式

1
2
3
css复制代码redis-server --port 6379 --daemonize yes --protected-mode no

redis-server --port 6380 --daemonize yes --protected-mode no

查看进程

1
csharp复制代码[root@localhost redis-5.0.0]# ps -ef|grep redis

root 6405 1 0 13:11 ? 00:00:44 redis-server *:6379

root 8858 1 2 15:20 ? 00:00:00 redis-server *:6380

root 8863 8715 0 15:21 pts/1 00:00:00 grep –color=auto redis

docker方式:

1
2
3
4
bash复制代码sudo docker run -p 6380:6379 --name redis01 \
-v /usr/local/docker/redis/data:/data \
-v /usr/local/docker/redis/conf/redis.conf:/etc/redis/redis.conf \
-d redis redis-server /etc/redis/redis.conf
1
2
3
4
bash复制代码sudo docker run -p 6381:6379 --name redis02 \
-v /usr/local/docker/redis/data:/data \
-v /usr/local/docker/redis/conf/redis.conf:/etc/redis/redis.conf \
-d redis redis-server /etc/redis/redis.conf
1
2
sql复制代码docker start redis01
docker start redis02

3Redis客户端分片存储实现

实现分布式缓存,Redis多个节点的透明访问

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复制代码@Test	//分片
public void shard(){


//构造各个节点链接信息,host和port
List<JedisShardInfo> infoList =
new ArrayList<JedisShardInfo>();
JedisShardInfo info1 =
new JedisShardInfo("192.168.163.200",6379);
//info1.setPassword("123456");
infoList.add(info1);
JedisShardInfo info2 = new JedisShardInfo("192.168.163.200",6380);
infoList.add(info2);
JedisShardInfo info3 = new JedisShardInfo("192.168.163.200",6381);
infoList.add(info3);

//分片jedis

JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(500); //最大链接数

ShardedJedisPool pool = new ShardedJedisPool(config, infoList);
//ShardedJedis jedis = new ShardedJedis(infoList);
ShardedJedis jedis = pool.getResource(); //从pool中获取
for(int i=0;i<10;i++){
jedis.set("n"+i, "t"+i);
}
System.out.println(jedis.get("n9"));
jedis.close();
}

本文转载自: 掘金

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

JMM:Java内存屏障,不是Java内存模型!!!

发表于 2021-11-16

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

背景

随着cpu由单核变成多核,又有了超线程。所以就会出现这样的问题,多核cpu在各自的缓存处理数据后,当同步数据到同一块主内存时,无法确定以谁的缓存数据为准。

所以为了解决cpu缓存一致性的问题,特地制定了一些操作协议,例如MSI、MOSI、Firefly等。而在这些操作协议下,对特定的内存或高速缓存进行读写访问的过程,就是内存模型。不同架构(ARM/X86等)的物理机有不同的内存模型。
在这里插入图片描述

Java为了一次编译、到处运行的跨平台性,使用class字节码作为中间代码,让不同架构的系统通过JVM来转换为对应的机器码。同样,为了屏蔽不同架构的机器上的内存访问差异,让Java程序在各种平台下都能达到一致的内存访问效果,所以Java制定了一套内存操作协议,即Java内存模型。

JMM(Java Memory Model)可以理解成一种协议,通过各种操作来定义对变量的读写、监视器的加锁和释放、线程启动和合并,保证多线程下变量的缓存一致性。
在这里插入图片描述

工作内存和主内存

JMM主要目标是定义程序中各个变量的访问规则,即在JVM中将变量存储到内存和从内存中取出变量这样的底层细节,来实现缓存一致性。JMM在定义上将内存分为主内存和工作内存,主内存对应Heap,属于线程公有,工作内存对应虚拟机栈,属于每个线程私有。

同时JMM对内存中的变量做了以下规定:

  1. 规定所有的变量都存储在主内存中,线程不能直接读写主内存中的变量
  2. 每个线程有自己的工作内存,对变量的读取、赋值等必须在工作内存中进行
  3. 线程之间值的传递都需要通过主内存来完成。工作内存无法互相访问
  4. 工作内存保存了线程用到的变量的主内存副本拷贝

原子操作

JMM规定了变量在主内存和工作内存中如何传输,同时也提供了八个原子指令来具体实现细节。
在这里插入图片描述
对于上面八个原子操作的使用,JMM制定了一些规则:

  1. read/load、store/write必须成对出现
  2. 不允许线程丢弃最近的assign操作,即工作内存中变量修改之后必须同步到主内存
  3. 不允许线程无原因地(没有assign操作)将变量工作内存同步到主内存
  4. 变量只能在主内存中诞生,并且必须在工作内存中初始化才能使用。即use、store之前必须经过load和assign
  5. 一个变量在同一时刻只能被一个线程lock。但一个线程可以lock多次。几次lock,只有对应次数的unlock变量才能解锁
  6. 一个变量被lock后,会清空所有工作内存中此变量的值。再次使用需要重新load、assign来初始化
  7. 一个变量未被lock,则不允许对它执行unlock,也不允许unlock其他线程lock的变量
  8. 一个变量unlock前,必须先把此变量同步到主内存中(store、write)

如何理解这八条规则和八条指令?

lock/unlock指令让一个变量被一个线程独享,其他六个指令都是变量在工作内存中的赋值和传输操作。JMM的出现是为了让一个线程对变量修改时,其他线程停止修改,并能获取最新值,从而保证数据的一致性。

而lock可以锁定变量,让变量只能被一个线程修改,且其他线程工作内存中的此变量的值会失效,必须通过指令来获取最新的值。

JMM特性

  1. 原子性:lock和unlock提供了大范围的原子性,其他操作都具有原子性
  2. 可见性:当一个线程修改了变量的值,其他线程能立即得知修改
  3. 有序性:在本线程内操作都是有序的,相对于多个线程是无序的

场景分析

synchronize可以看做是lock/unlock的实现,底层是由monitorenter/monitorexit来实现的。

测试代码如下:

1
2
3
4
5
java复制代码public void test() {
synchronized (this.getClass()) {
System.out.println("Hello World!");
}
}

字节码指令如下:
在这里插入图片描述
在synchronize的作用范围内,在同一时间内只能一个线程获取锁进行操作,其他线程工作内存中的变量会失效,当此线程修改完unlock的时候,其他线程会重新加载变量最新值来进行操作,从而保证数据的一致性。

volatile关键字也通过内存屏障实现了lock/unlock的功能,下一篇内存屏障会写一下。

Happen-Before先行原则

为了不让所有的操作有序都借助synchronized、volatile来完成,所有就有了以下无需借助同步器的天然先行发生关系,即不用锁就能保证执行顺序。

  1. 程序顺序规则:程序中操作A在B前,线程中A操作也必须在B之前执行
  2. 监视器加锁规则:在监视器锁上的解锁操作必须在加锁之前执行
  3. volatile变量规则:对volatile变量的写入必须在读取之前执行
  4. 线程启动规则:Thread.start()调用必须在该线程执行任何操作之前
  5. 线程结束规则:线程中任何操作都先行发生于对此线程的终止检测
  6. 中断规则:对线程interrupt()的调用先于被中断线程检测到中断事件的发生
  7. 终结器规则:对象构造方法执行先于它的finalize()方法
  8. 传递性:如果操作A先于B,操作B先于C,那操作A必先于C

本文转载自: 掘金

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

SpringCloudAlibaba配置中心之Nacos应用

发表于 2021-11-16

配置中心之Nacos应用实践

Nacos配置快速入门

创建项目

(1)创建maven项目

新建module,名字为sca-nacos-config,选中父项目01-sca,右键new->module其中的 pom.xml文件内容如下:

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
pom复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>01-sca</artifactId>
<groupId>com.cy</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>sca-nacos-config</artifactId>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
</dependencies>

</project>

(2)创建配置文件

在resource目录下创建bootstrap.yml配置文件(启动优先级最高)代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
yml复制代码#bootstrap.yml加载的优先级要高于application.yml文件
server:
tomcat:
threads:
max: 248
port: 8080

spring:
application:
name: nacos-config
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
group: DEFAULT_GROUP # Group, default is DEFAULT_GROUP
file-extension: yml # Configure the data format of the content, default to properties

(3)启动测试

创建启动类,对环境启动测试,代码如下

1
2
3
4
5
6
7
8
9
10
11
java复制代码package com.cy;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class NacosConfigApplication {
public static void main(String[] args) {
SpringApplication.run(NacosConfigApplication.class,args);
}
}

(4)Nacos基本配置

打开nacos配置中心,新建配置,如图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lfhfZmLg-1622904728365)(C:\Users\沉思\AppData\Roaming\Typora\typora-user-images\1622899269607.png)]
其中Data IDs的值要与bootstrap.yml中定义的spring.application.name的值相同(服务名-假如有多个服务一般会创建多个配置实例,不同服务对应不同的配置实例)。

(5)创建Controller处理器

创建配置中心Controller,也可以将Controller添加到启动类内部,如图所示:

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复制代码package com.cy;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
public class NacosConfigApplication {

public static void main(String[] args) {
SpringApplication.run(NacosConfigApplication.class,args);
}

@RefreshScope//支持配置动态刷新
@RestController
@RequestMapping("/config/")
public class NacosConfigController{
@Value("${logging.level.com.cy:info}")
private String logLevel;

@RequestMapping("/doGetLogLevel")
public String doGetLogLevel(){
return "Log level is "+logLevel;
}
}
}

其中,@RefreshScope的作用是,在配置中心的相关配置发生变化以后,能够及时看到更新

Controller编写好以后,启动配置中心服务,然后进行访问测试。,打开浏览器直接在地址栏输入http://localhost:8080/config/doGetLogLevel,检测输出结果是否为我们配置中配置的信息,如图所示。

地址

1
bash复制代码http://localhost:8080/config/doGetLogLevel

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h5wlLHeK-1622904728369)(C:\Users\沉思\AppData\Roaming\Typora\typora-user-images\1622900042352.png)]

因为内部使用了@RefreshScope注解支持配置动态刷新,所以在nacos中进行更改可以动态的改变配置
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-05Q5gccC-1622904728371)(C:\Users\沉思\AppData\Roaming\Typora\typora-user-images\1622900087472.png)]
更改后配置,再次访问发现配置已经改变
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zRWQ8RcM-1622904728373)(C:\Users\沉思\AppData\Roaming\Typora\typora-user-images\1622900222224.png)]

Nacos配置管理模型

Nacos 配置管理模型由三部分构成,如图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mLQXpiH2-1622904728375)(C:\Users\沉思\AppData\Roaming\Typora\typora-user-images\1622900575360.png)]

其中:

  • Namespace:命名空间,对不同的环境进⾏隔离,⽐如隔离开发环境和⽣产环境。
  • Group:分组,将若⼲个服务或者若⼲个配置集归为⼀组。
  • Service/DataId:某⼀个服务或配置集,一般对应一个配置文件。

Nacos中的命名空间一般用于配置隔离,这种命名空间的定义一般会按照环境(开发,生产等环境)进行设计和实现.我们默认创建的配置都存储到了public命名空间,如图所示:
在这里插入图片描述
创建新的开发环境并定义其配置,然后从开发环境的配置中读取配置信息,该如何实现呢?

第一步:创建新命名空间,如图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nyg3fj5D-1622904728377)(C:\Users\沉思\AppData\Roaming\Typora\typora-user-images\1622900937555.png)]命名空间成功创建以后,会在如下列表进行呈现。
命名空间成功创建以后,会在如下列表进行呈现。
在这里插入图片描述
在指定命名空间下添加配置,也可以直接取配置列表中克隆,例如:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X7d2eHBL-1622904728378)(C:\Users\沉思\AppData\Roaming\Typora\typora-user-images\1622901253266.png)]克隆成功以后,我们会发现在指定的命名空间中有了我们克隆的配置,如图所示:
克隆成功以后,我们会发现在指定的命名空间中有了我们克隆的配置,如图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LBkyO2nv-1622904728379)(C:\Users\沉思\AppData\Roaming\Typora\typora-user-images\1622901723441.png)]

此时我们修改dev1命名空间中Data Id的nacos-config配置,如图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EpJ9lsjh-1622904728380)(C:\Users\沉思\AppData\Roaming\Typora\typora-user-images\1622901813672.png)]
修改项目module中的配置文件bootstrap.yml,添加如下配置,关键代码如下:

去namespace中找NamespaceID,添加到配置文件中
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JGHeYRXP-1622904728380)(C:\Users\沉思\AppData\Roaming\Typora\typora-user-images\1622901994265.png)]

1
2
3
4
5
yaml复制代码spring:
cloud:
nacos:
config:
namespace: 5c27fe4a-1141-4836-a14e-cbac77fb2130

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xoi9BrSG-1622904728381)(C:\Users\沉思\AppData\Roaming\Typora\typora-user-images\1622902051409.png)]
其中,namespace后面的字符串为命名空间的id,可直接从命名空间列表中进行拷贝,如图所示:

重启服务,继续刷新http://localhost:8080/config/doGetLogLevel地址。检测输出,看看输出的内容是什么,是否为dev命名空间下配置的内容,如图所示:

1
bash复制代码http://localhost:8080/config/doGetLogLevel

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ijwKMNrY-1622904728381)(C:\Users\沉思\AppData\Roaming\Typora\typora-user-images\1622902139790.png)]
我们还可以创建生产环境,依次类推进行设计和实现即可。

分组设计及实现

当我们在指定命名空间下,按环境或服务做好了配置以后,有时还需要基于服务做分组配置,例如,一个服务在不同时间节点(节假日,活动等)切换不同的配置,可以在新建配置时指定分组名称,如图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P0u5nZAH-1622904728382)(C:\Users\沉思\AppData\Roaming\Typora\typora-user-images\1622903671504.png)]
配置发布以后,修改boostrap.yml配置类,在其内部指定我们刚刚创建的分组,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
yml复制代码server:
port: 8070
spring:
application:
name: nacos-config
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
group: DEV_GROUP_51 # Group, default is DEFAULT_GROUP
file-extension: yml # Configure the data format of the content, default to properties
namespace: 5c27fe4a-1141-4836-a14e-cbac77fb2130

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8pXXOxSO-1622904728382)(C:\Users\沉思\AppData\Roaming\Typora\typora-user-images\1622903764351.png)]
在NacosConfigController类中添加属性和方法用于获取和输出DEV_GROUP_51配置中设置的线程数,代码如下:

1
2
3
4
5
6
7
yml复制代码 		@Value("${server.tomcat.threads.max:200}")
private Integer serverThreadMax;

@RequestMapping("/doGetServerThreadMax")
public String doGetserverThreadMax(){
return "server.threads.max is "+serverThreadMax;
}

然后重启服务,进行测试,检测内容输出,如图所示:

1
bash复制代码http://localhost:8080/config/doGetServerThreadMax

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SYgB6Vlf-1622904728383)(C:\Users\沉思\AppData\Roaming\Typora\typora-user-images\1622903861845.png)]

共享配置设计及读取

当同一个namespace的多个配置文件中都有相同配置时,可以对这些配置进行提取,然后存储到nacos配置中心的一个或多个指定配置文件,哪个微服务需要,就在服务的配置中设置读取即可。例如:

第一步:在nacos中创建一个共享配置文件,例如:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0jVB4P4n-1622904728383)(C:\Users\沉思\AppData\Roaming\Typora\typora-user-images\1622904151351.png)]
第二步:在指定的微服务配置文件(bootstrap.yml)中设置对共享配置文件的读取,例如:

见红色区域内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
yml复制代码spring:
application:
name: nacos-config
cloud:
nacos:
config:
server-addr: localhost:8848
# 命名空间
namespace: 83ed55a5-1dd9-4b84-a5fe-a734e4a6ec6d
# 分组名
# group: DEFAULT_GROUP
# 配置中心文件扩展名
file-extension: yml
# 共享配置
shared-configs[0]:
data-id: application-dev.yml
group: DEFAULT_GROUP
refresh: true #默认false

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-537ma25p-1622904728383)(C:\Users\沉思\AppData\Roaming\Typora\typora-user-images\1622904343791.png)]
第三步:在指定的业务类中读取和应用共享配置即可,例如:

1
2
3
4
5
6
7
8
9
java复制代码		@Value("${page.pageSize:50}")
private Integer pageSize;

@GetMapping("/config/doGetPageSize")
public String doGetPageSize(){

System.out.println("page size is "+pageSize);
return "page size is "+pageSize;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S9iad5E8-1622904728384)(C:\Users\沉思\AppData\Roaming\Typora\typora-user-images\1622904498951.png)]

访问链接

1
bash复制代码http://localhost:8080/config/config/doGetPageSize

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wm8tvo3V-1622904728385)(C:\Users\沉思\AppData\Roaming\Typora\typora-user-images\1622904586964.png)]

本文转载自: 掘金

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

理解JVM之内存模型JMM

发表于 2021-11-16

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

JMM内存模型图

java内存模型就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制和规范。目的是保证并发编程场景中的原子性、可见性和有序性。

根据java虚拟机规范,java虚拟机管理的内存将分为下面五大区域。

五大内存区域

图中黄色块为线程的共享区域,紫色块为线程的私有区域。

堆

堆是java虚拟机管理内存最大的一块内存区域,因为堆中存放的对象是线程共享的,故多线程环境中通常需要同步机制。

所有对象实例及数组都要在堆上分配内存,它是线程共享的,同时它也是GC所管理的主要区域,因此常被称为GC堆。

关于在Java堆中经常出现的“新生代、老年代、永久代”等概念,只是一部分垃圾收集器的公有特性或者说是设计风格而划分的区域,Java虚拟机中并未有这样的内存布局。

Java堆既可以被实现成固定大小,也可以动态扩展,通过参数-Xmx和-Xms设定,当对象无法在堆中分配实例,并且堆无法再扩展时,Java虚拟机将会抛出OutOfMemoryErrno异常

方法区

方法区同堆一样,是所有线程共享的内存区域,为了区分堆,又被称为非堆。

用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。

关于永久代,在Jdk8以前有着这样的设计,但由于永久代有-XX:MaxPermSize的上限,会使Java应用更容易遇到内存溢出。而在jdk8以后,就废弃了永久代的概念,改为用本地内存实现的元空间代替,该部分的内存有本机内存大小相关。

在方法区区域的回收目标主要是针对常量池的回收和对类型的卸载。如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。

关于运行时常量池,是方法区的一部分,class文件除了有类的字段、接口、方法等描述信息之外,还有常量池用于存放编译期间生成的各种字面量和符号引用。该部分的内容将在类加载后放到方法去的运行时常量池中,在运行期间,可以利用String的intern()方法将常量加入常量池。

虚拟机栈

虚拟机栈为线程私有区域,为方法执行的内存区。每个方法在执行时会在虚拟机栈中创建一个栈帧。栈帧用于存储
数据和部分过程结果的数据结构,同时也被用来处理动态链接、返回返回值和异常分派。一个完整的栈帧包含
局部变量表、操作数栈、动态链接信息、方法正常完成和异常完成信息。每个方法被调用的过程对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

1
2
3
4
5
makefile复制代码栈帧: 是用来存储数据和部分过程结果的数据结构。

栈帧大小确定时间: 编译期确定,不受运行期数据影响。

局部变量表是一组变量值的存储空间,它用于存储方法,参数,以及方法内部定义的局部变量。

局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型)和retyrnAddress类型(指向了一条字节码指令的地址)


Java虚拟机栈可能出现两种类型的异常:

  • 线程请求的栈深度大于虚拟机允许的栈深度,将抛出StackOverflowError。
  • 虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时,抛出OutOfMemory异常。

本地方法栈

本地方法栈与虚拟栈类似,区别在于为本地方法服务,即使用native方法调用底层c或c++代码。

程序计数器

程序计数器是一块很小的内存空间,它是线程私有的,可以认作为当前线程的行号指示器。

由于一个线程有着多个指令,为了线程切换可以恢复到正确执行位置,每个线程都有着私有的程序计数器,不同线程之间的程序计数器互不影响、独立存储。

需要注意的是,这块内存区域在java虚拟机规范中是唯一不会发生OutOfMemoryError的区域。当执行本地方法时,程序计数器为空(undefined)

直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但该部分内存也被频繁使用,也有可能导致OutOfMemoryError异常出现。

NIO(New Input/Output)引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用直接操作。

一个直接内存导致的溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的遗产情况。如果发生溢出后产生的Dump文件很小,而程序中间接使用了DirectMemory(NIO),就有可能是这方面的原因了。

参考资料

java内存区域

本文转载自: 掘金

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

简单理解正向代理和反向代理

发表于 2021-11-16

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

上一篇文章说到反向代理是用来做负载均衡的,同时我就想到了那么正向代理是不是也可以说一说,可能还是有很多人是弄不清他俩的区别是什么的吧?

那么本次文章就用借钱的例子来阐述一下什么是正向代理,什么是反向代理

正向代理

正向代理就是一个位于客户端和目标服务器之间的服务器,这是一个代理服务器

客户端为了从目标服务器获取内容,但是客户端由于限制无法直接访问到目标服务器,那么客户端就可以向一个代理服务器发送一个请求并指定目标服务器

代理服务器收到请求后,就会向目标服务器转交请求并将获得的内容返回给客户端

咱们用借钱来比喻一下,就会很容易明白

小明,想找一个老板借钱,但是小明由于自身太菜,没有办法和老板谈借钱的事情

但是小明很聪明,他认识老板身边的秘书,然后他就通过和这个秘书沟通,将借钱的事情想秘书说清楚,秘书进而去向老板借钱

整条链路,小明达到了借钱的目的,老板的钱也被借出去了,可是,老板不知道到底是谁在借钱,只知道钱给了秘书

这就是正向代理,一般是用在客户端侧,是属于客户端的代理,能够帮助客户端访问自身无法访问的服务器资源

正向代理的使用场景

  • 可以突破客户端自身的访问限制
  • 可以提高访问服务器的速度
  • 可以隐藏客户端的真实 IP

第一点和第三点,通过上面借钱的例子,大家比较好理解,正向代理服务器是帮助客户端去访问服务器,服务器并不知道具体的客户端是谁

提高访问服务器的速度如何理解?

一般情况下,正向代理服务器上面都会设置一个硬件缓冲区,并且会将客户端的部分请求放到缓冲区中

当有其他客户端进来访问的时候,正向带来服务器就可以将缓冲区中的数据给到客户端,进而提高访问速度

反向代理

反向代理也是一个位于客户端和目标服务器之间的服务器

反向代理就是指以代理服务器来接收互联网上的连接请求,然后将这些请求转发给内部的多个服务器

并将从服务器上得到的结果返回给互联网上请求的对应客户端,这个时候的代理服务器就是一个反向代理服务器

还是一个借钱的例子

老板想把钱借出去,但是老板自己懒得去找借钱的人,于是他就将钱给到某机构,让这个机构把自己的钱借出去

这个时候,小明仍然缺钱,于是找到了某机构借钱,小明借到的这个钱,其实是老板的,但是小明不会知道这个钱具体是谁的,他只知道是机构借给他的

这就是反向代理,一般是用在服务端侧,是属于服务端的代理,一般是用来做服务端的负载均衡

反向代理的应用场景也就不言而喻了吧,与上面正向代理相对的也有如下几点:

  • 可以做负载均衡
  • 可以提高访问服务器的速度
  • 可以隐藏服务端的真实 IP
  • 可以做服务器的安全保障

前三点都比较好理解,第一点上一篇文章说过,那么第四点如何理解呢?

外部的请求都是先过代理服务器,再到内部服务器上的,那么在代理服务器上面就可以做一些安全的能力,例如 防 DDOS , IP 白名单,加密的能力等等

正向代理和反向代理的区别

看了上述的例子对于正向代理和反向代理的区别,我们再来简单的对比一下

正向代理,属于客户端代理,服务端不知道到底是谁访问自己

反向代理,用于服务端,属于服务端代理,客户端不知道自己具体是访问的哪个服务器

当看到正向代理和反向代理的时候,咱们想想借钱的案例就懂了

今天就到这里,学习所得,若有偏差,还请斧正

欢迎点赞,关注,收藏

朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力

好了,本次就到这里

技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

我是小魔童哪吒,欢迎点赞关注收藏,下次见~

本文转载自: 掘金

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

在linux系统的安装

发表于 2021-11-16

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

1.什么是虚拟机

虚拟机指通过软件系统功能的、运行在一个完全隔离环境中的完整计算机系统。实体计算机中能够完成的工作在虚拟机中都能够实现。在计算机中创建虚拟机时,需要将实体机的部分硬盘和内存容量作为虚拟机的硬盘和内存容量。每个虚拟机都有独立的CMOS、硬盘和操作系统,可以像使用实体机一样对虚拟机进行操作。(百度百科)

那我们今天就需要从虚拟机上安装linux操作系统

2.linux操作系统(GNU/Linux)

linux操作系统的类别有许多,如:

红帽企业 Linux(RHEL) ;社区企业操作系统(CentOS);ubuntu;Fedora(费多拉);Debian…..

本文选择的linux系统为centos

下载地址:阿里巴巴开源镜像站-OPSX镜像站-阿里云开发者社区

步骤:

)​

)“)​)​

)“)​)​

)​

3.在虚拟机上安装linux系统

(1)点击文件—–新建虚拟机—-选择自定义安装

)“)​)​

(2)下一步

)​

(3)稍后安装操作系统 然后选择linux

)“)​)​

(4)虚拟机的位置可以自己定义

)​

(5)处理器的配置,这里都设置为1 后面可以修改

)​

(6)指定虚拟机占用的内存大小

)​

(7)选择网络类型 根据情况来选择

)​

自我理解:

桥接模式:即主机和虚拟机ip在同一局域网下,那么虚拟机和本机可以相互ping通

NAT模式:虚拟机可以与本机互通,但是他们虚拟机的访问需要联网,因为他是借助的外部网络

)​

VMnet0:用于虚拟桥接网络下的虚拟交换机

VMnet1:用于虚拟 Host-only 网络下的虚拟交换机

VMnet8:用于虚拟 NAT 网络下的虚拟交换机

(8)继续下一步

)“)​)​

)“)​)​

)“)​)​

安装成功后出现如下界面

)​

点击可以做相应的修改

)​

修改一下图:

x)​

后续 :注意点击开启虚拟机 出现内部错误 应退出然后以管理员身份打开

1.点击centos7安装,进入如下界面

)​

2.Localization和software部分不需要进行任何设置,system部分需要规划配置,点击红色部分进入如下界面:

)​

3.)​

可以修改主机名

)​

)​

然后可以点击安装 进入如下界面:

)​

修改用户密码 点击进入修改 修改成功后 报红就消失了 后面等待安装成功!

内容来自自己的博客

本文转载自: 掘金

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

1…318319320…956

开发者博客

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