Coding & Life

求知若饥,虚心若愚

pic

浏览器缓存是前端开发中不可避免的问题,对于web应用来说,它是提升页面性能同时减少服务器压力的利器。本文将简单地描述总结下浏览器缓存的知识和应用,希望对自己和大家都有所帮助

浏览器缓存类型

  • 强缓存
    不会向服务器发送请求,直接从缓存中读取资源,在chrome控制台的network选项中可以看到该请求返回200的状态码,并且size显示from disk cache或from memory cache

  • 协商缓存
    向服务器发送请求,服务器会根据这个请求的request header的一些参数来判断是否命中协商缓存,如果命中,则返回304状态码并带上新的response header通知浏览器从缓存中读取资源

两者的共同点是,都是从客户端缓存中读取资源;区别是强缓存不会发请求,协商缓存会发请求

缓存有关的header

强缓存

Expires:response header里的过期时间,浏览器再次加载资源时,如果在这个过期时间内,则命中强缓存

Cache-Control:当值设为max-age=300时,则代表在这个请求正确返回时间(浏览器也会记录下来)的5分钟内再次加载资源,就会命中强缓存

cache-control-expire.png

Expires和Cache-Control:max-age=*** 的作用是差不多的,区别就在于 Expires 是http1.0的产物,Cache-Control是http1.1的产物,两者同时存在的话,Cache-Control优先级高于Expires;在某些不支持HTTP1.1的环境下,Expires就会发挥用处。所以Expires其实是过时的产物,现阶段它的存在只是一种兼容性的写法

Expires和Cache-Control的区别还有一个:Expires是一个具体的服务器时间,这就导致一个问题,如果客户端时间和服务器时间相差较大,缓存命中与否就不是开发者所期望的。Cache-Control是一个时间段,控制就比较容易

协商缓存

ETag和If-None-Match:这两个要一起说。Etag是上一次加载资源时,服务器返回的response header,是对该资源的一种唯一标识,只要资源有变化,Etag就会重新生成。浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的Etag值放到request header里的If-None-Match里,服务器接受到If-None-Match的值后,会拿来跟该资源文件的Etag值做比较,如果相同,则表示资源文件没有发生改变,命中协商缓存

Last-Modified和If-Modified-Since:这两个也要一起说。Last-Modified是该资源文件最后一次更改时间,服务器会在response header里返回,同时浏览器会将这个值保存起来,在下一次发送请求时,放到request header里的If-Modified-Since里,服务器在接收到后也会做比对,如果相同则命中协商缓存

response header

etag-lastmodified.png

request header

if-none-match.png

ETag和Last-Modified的作用和用法也是差不多,说一说他们的区别。
首先在精确度上,Etag要优于Last-Modified。Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的Last-Modified也有可能不一致。

第二在性能上,Etag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值。
第三在优先级上,服务器校验优先考虑Etag

浏览器缓存过程

  1. 浏览器第一次加载资源,服务器返回200,浏览器将资源文件从服务器上请求下载下来,并把response header及该请求的返回时间一并缓存;

  2. 下一次加载资源时,先比较当前时间和上一次返回200时的时间差,如果没有超过cache-control设置的max-age,则没有过期,命中强缓存,不发请求直接从本地缓存读取该文件(如果浏览器不支持HTTP1.1,则用expires判断是否过期);如果时间过期,则向服务器发送header带有If-None-Match和If-Modified-Since的请求;

  3. 服务器收到请求后,优先根据Etag的值判断被请求的文件有没有做修改,Etag值一致则没有修改,命中协商缓存,返回304;如果不一致则有改动,直接返回新的资源文件带上新的Etag值并返回200;

  4. 如果服务器收到的请求没有Etag值,则将If-Modified-Since和被请求文件的最后修改时间做比对,一致则命中协商缓存,返回304;不一致则返回新的last-modified和文件并返回200;

用户行为对浏览器缓存的控制

  1. 地址栏访问,链接跳转是正常用户行为,将会触发浏览器缓存机制;
  2. F5刷新,浏览器会设置max-age=0,跳过强缓存判断,会进行协商缓存判断;
  3. ctrl+F5刷新,跳过强缓存和协商缓存,直接从服务器拉取资源。

原文链接

OAuth2有四种授权模式 授权码模式(authorization code) 简化模式(implicit) 密码模式(resouce owner password credentials) 客户端模式,具体理解OAuth2可以参考阮一峰文章

今天我们来实现OAuth2的密码模式

使用场景

我们在日常生活中经常会出现微信,微博等第三方登录的场景。我们在使用这些第三方登录时,不需要注册,直接授权登录即可,非常快捷方便。对于开发者来说,也不需要存储用户的用户名密码,只需要存储第三方平台的唯一标识即可

如果我们自己来实现这种第三方的服务作为认证中心,其他服务就可共用该认证中心,实现登录的功能。并且只要一次登录,便可在多个服务中自由通行

密码模式流程

密码模式的实现流程图如下:

  1. 用户向客户端提供用户名和密码
  2. 客户端将用户名和密码发送给认证服务器,向后者请求令牌
  3. 认证服务器确认无误后,向客户端提供访问令牌

bg2014051206-password.png

在微服务流行的今天,一个电商平台的背后可能是由多个服务构成,比如订单服务、用户服务等。要想用户登录之后可以访问任意微服务,一定需要携带一个凭证,来标识自己的身份。此处的凭证就是OAuth2中的access_token

实现

一 认证服务端

1.引入maven

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

2.配置application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
spring:
application:
name: oauth2server
datasource:
url: jdbc:mysql://localhost:3306/oauth2server?characterEncoding=UTF-8&useSSL=false
username: root
password: abc123
hikari:
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
maximum-pool-size: 9

server:
port: 8001

jwt:
signKey: wangweiye

3.spring security基础配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}

// 允许匿名访问 oatuh 接口
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().and().authorizeRequests().antMatchers(HttpMethod.POST, "/oauth/token").permitAll();
}
}

这个类的重点是声明PasswordEncoderAuthenticationManager两个Bean. PasswordEncoder是一个密码加密工具,可以实现不可逆的加密,AuthenticationManager是为了实现OAuth2的password模式必须指定的授权管理Bean

4.实现UserDetailsService

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
@Component
public class CustomUserDetailsService implements UserDetailsService {
private static Logger log = LoggerFactory.getLogger(CustomUserDetailsService.class);

@Autowired
private PasswordEncoder passwordEncoder;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info(String.format("username is: %s", username));

// 查询数据库操作
if (!username.equals("admin")) {
throw new UsernameNotFoundException("the user is not found");
} else {
// 用户角色也应在数据库中获取
String role = "ROLE_ADMIN";
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(role));

// 线上环境应该通过用户名查询数据库获取加密后的密码
String password = passwordEncoder.encode("123456");

// 返回自定义的CustomUserDetailService
User user = new User(username, password, authorities);
return user;
}
}
}

核心是loadUserByUsername方法,接收一个字符串用户名,然后返回一个UserDetails对象。在生产环境中,此处的用户名和密码都需要在数据库中查出,此处为了便于举例,写死为用户名admin,密码123456

5.OAuth2配置文件

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
@Configuration
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
@Autowired
private TokenEnhancer jwtTokenEnhancer;

@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;

@Autowired
private TokenStore jwtTokenStore;

@Autowired
private UserDetailsService customUserDetailsService;

@Autowired
private AuthenticationManager authenticationManager;

@Autowired
private DataSource dataSource;

@Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) {
// jwt增强模式
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> enhancerList = new ArrayList<>();
enhancerList.add(jwtTokenEnhancer);
enhancerList.add(jwtAccessTokenConverter);
enhancerChain.setTokenEnhancers(enhancerList);
endpoints.tokenStore(jwtTokenStore)
.userDetailsService(customUserDetailsService)
.authenticationManager(authenticationManager)
.tokenEnhancer(enhancerChain)
.accessTokenConverter(jwtAccessTokenConverter);
}

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
}

@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();
security.checkTokenAccess("isAuthenticated()");
security.tokenKeyAccess("isAuthenticated()");
}
}

此处重写了三个configure方法

AuthorizationServerEndpointsConfigurer参数的重写:

authenticationManage() 调用此方法才能支持 password 模式

userDetailsService() 设置用户验证服务

tokenStore() 指定token的存储方式

ClientDetailsServiceConfigurer参数的重写:

clients.jdbc(dataSource) 是指客户端的管理有dataSource数据库来管理。数据库脚本已放入文末地址中。其中authorized_grant_types列可以填写的内容有authorization_code: 授权码模式, implicit: 隐式授权类型, password: 资源所有者密码类型, client_credentials: 客户端凭据(客户端ID以及Key)类型, refresh_token: 通过以上授权获得的刷新令牌来获取新的令牌

6.增强JWT

如果需求需要在jwt中添加额外字段,可以使用TokenEnhancer增强器

1
2
3
4
5
6
7
8
9
public class JWTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
Map<String, Object> info = new HashMap<>();
info.put("jwt-ext", "JWT 扩展信息");
((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(info);
return oAuth2AccessToken;
}
}

二 客户端

pexels-disha-sheta-3489514.jpg

定义

Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能成为java语言的反射机制

反射涉及的类

Class类

代表类的实体,在运行的Java应用程序中表示类和接口

相关方法

方法 用途
forName(String className) 根据类名返回类的对象
getName() 获得类的完整路径名字
newInstance() 创建类的实例
getPackage() 获得类的名字
getSimpleName() 获得类的名字
getField(String name) 获得某个公有的属性对象
getFields() 获得所有公有的属性对象
getDeclaredField(String name) 获得某个属性对象
getDeclaredFields() 获得所有属性对象
getConstructor(Class…<?> parameterTypes) 获得该类中与参数类型匹配的公有构造方法
getConstructors() 获得该类的所有公有构造方法
getDeclaredConstructors() 获得该类所有构造方法
getMethod(String name, Class…<?> parameterTypes) 获得该类某个公有的方法
getMethods() 获得该类所有公有的方法
getDeclaredMethod(String name, Class…<?> parameterTypes) 获得该类某个方法
getDeclaredMethods() 获得该类所有方法
isInstance(Object obj) 如果obj是该类的实例则返回true

Field类

代表类的成员变量(属性)

相关方法

方法 用途
equals(Object obj) 属性与obj相等则返回true
get(Object obj) 获得ojb中对应的属性值
set(Object obj, Object value) 设置obj中对应属性值

Method类

代表类的方法

相关方法

方法 用途
invoke(Object obj, Object… args) 传递object对象及参数调用该对象对应的方法

Constructor类

代表类的构造方法

相关方法

方法 用途
newInstance(Object… initargs) 根据传递的参数创建类的对象

示例

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
package com.wang;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Person {
private final static String classString = "com.wang.Person";

private String name;

private Integer age;

public Person() {
}

private Person(String name, Integer age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

// 创建实例
public static void reflectNewInstance() {
try {
Class<?> aClass = Class.forName(classString);

Object object = aClass.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}

// 反射私有的构造方法
public static void reflectPrivateConstructor() {
try {
Class<?> aClass = Class.forName(classString);
Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(String.class, Integer.class);
declaredConstructor.setAccessible(true);
Object object = declaredConstructor.newInstance("wangweiye", 23);
System.out.println(object.toString());
} catch (Exception e) {
e.printStackTrace();
}
}

// 反射私有属性
public static void reflectPrivateField() {
try {
Class<?> aClass = Class.forName(classString);
Object object = aClass.newInstance();

Field field = aClass.getDeclaredField("classString");
field.setAccessible(true);
System.out.println(field.get(object));
} catch (Exception e) {
e.printStackTrace();
}
}

// 反射私有方法
public static void reflectPrivateMethod() {
try {
Class<?> aClass = Class.forName(classString);
Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(String.class, Integer.class);
Object lbj = declaredConstructor.newInstance("lbj", 35);

Method method = aClass.getDeclaredMethod("getName");
method.setAccessible(true);

System.out.println(method.invoke(lbj).toString());
} catch (Exception e) {
e.printStackTrace();
}
}

public static void main(String[] args) {
reflectNewInstance();

reflectPrivateConstructor();

reflectPrivateField();

reflectPrivateMethod();
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

在开发高并发系统时,有三把利器来保护系统:缓存 降级 限流

限流目的

限流的目的是通过对并发请求进行限速,一旦达到限制速率则可以拒绝服务或者排队等待等处理

限流算法

常用的限流算法有令牌桶算法漏桶算法

漏桶算法要求处理请求以一个恒定的速率,不能允许突发请求的快速处理,而令牌桶算法就比较适合

令牌桶算法的原理就是系统以恒定的速率产生令牌放入令牌桶中。令牌桶有容量,当满时,再放入的令牌就会被丢弃。当想处理一个请求的时候,需要从令牌桶中取出一个令牌,如果没有,则拒绝(非阻塞式)或者等待(阻塞式)

4179645397-5b6e4903ec371_articlex.png

Google开源项目Guava中RateLimiter使用的就是令牌桶算法,下面实例是使用自定义注解实现限制接口流量

定义一个注解

1
2
3
4
5
6
7
@Inherited
@Documented
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimitAspect {

}

定义切面

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
@Component
@Scope
@Aspect
public class RateLimitAop {
@Autowired(required = false)
private HttpServletResponse response;

private RateLimiter rateLimiter = RateLimiter.create(5); //比如说,我这里设置"并发数"为5

@Pointcut("@annotation(cc.wangweiye.ratelimit.RateLimitAspect)")
public void serviceLimit() {

}

@Around("serviceLimit()")
public Object around(ProceedingJoinPoint joinPoint) {
Boolean flag = rateLimiter.tryAcquire();
Object obj = "无返回";
try {
if (flag) {
obj = joinPoint.proceed();
} else {
String result = "failure";

output(response, result);
}
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("flag=" + flag + ",obj=" + obj);
return obj;
}

public void output(HttpServletResponse response, String msg) throws IOException {
response.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = null;
try {
outputStream = response.getOutputStream();
outputStream.write(msg.getBytes("UTF-8"));
} catch (IOException e) {
e.printStackTrace();
} finally {
outputStream.flush();
outputStream.close();
}
}
}

源码地址

apples-5543778_1280.jpg

floyd算法

弗洛伊德算法又称插点法是解决任意两点间的最短路径的一种算法

适用范围

边权可正可负,运行一次算法即可求得任意两点间的最短路径(无负权回路即可)

可以解决“多源最短路径”

什么是负权回路

图中1号点到3号点的最短路径是什么?

6.10.png

由于每次经过1->2->3这样的环,最短路径就会-1,所有永远找不到最短路径

实例

6.2-floyd.png

计算图中各个顶点到各个顶点的最短距离

首先我们定义一个二维数组来存储图中最易可见的点对点(不允许经过第三点)之间的距离,如果不能到达使用∞表示。另外规定自己到自己的距离是0,具体表示如下

6.3-.png

如果要使两点之间距离变短,唯一方法就是通过第三个点来解决。例如a点到b点的距离通过顶点k来中转那么a->k->b才有可能缩短a->b的距离。那么这个k点是哪个点呢?是否不只通过1个k点最短,而是经过两个或者更多点最短,比如a->k1->k2>b

比如图中4->3的距离是12,如果经过1点,那么4->1->3的距离就变为11。现在只允许经过1号点,求i点到j点的最短距离,只需判断e[i][1]+e[1][j]是否比e[i][j]要小。代码实现如下

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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import copy

# 创建一个4行4列的二维数组
m = [[0]*4 for i in range(4)]

# 为数组赋值
m[0][0] = 0
m[0][1] = 2
m[0][2] = 6
m[0][3] = 4
m[1][0] = 9999
m[1][1] = 0
m[1][2] = 3
m[1][3] = 9999
m[2][0] = 7
m[2][1] = 9999
m[2][2] = 0
m[2][3] = 1
m[3][0] = 5
m[3][1] = 9999
m[3][2] = 12
m[3][3] = 0

print('原路径数据:', m)

# 允许经过1号点的最短路径
n = copy.deepcopy(m)
i = 0
j = 0

for i in range(4):
for j in range(4):
if (n[i][j] > n[i][0] + n[0][j]):
n[i][j] = n[i][0] + n[0][j]

print('在允许经过1号点后的最短路径:', n)

执行结果如下:

原路径数据: [[0, 2, 6, 4], [9999, 0, 3, 9999], [7, 9999, 0, 1], [5, 9999, 12, 0]]

在允许经过1号点后的最短路径: [[0, 2, 6, 4], [9999, 0, 3, 9999], [7, 9, 0, 1], [5, 7, 11, 0]]

6.5-1.png

通过图中白色背景处我们可以看到通过1号点中转的情况下,3->2,4->2,4->3的路径都变短了

接下来求只允许1号和2号点的情况下任意两点间的最短距离,如何做呢?我们只需要在只经过1号点时任意两点最短距离的结果下,再判断经过2号顶点如何使i号点到j号点的路径边的更短

1
2
3
4
5
6
7
8
9
10
11
# 再允许经过2号点后的路径
o = copy.deepcopy(n)
i = 0
j = 0

for i in range(4):
for j in range(4):
if(o[i][j] > o[i][1] + o[1][j]):
o[i][j] = o[i][1] + o[1][j]

print('再允许经过2号点后的最短路径:', o)

执行结果如下:

再允许经过2号点后的最短路径: [[0, 2, 5, 4], [9999, 0, 3, 9999], [7, 9, 0, 1], [5, 7, 10, 0]]

6.6-1-2.png

同理继续在只允许1,2,3进行中转的情况下求最短距离。

6.7-1-2-3.png

最后在允许通过所有顶点作为中转,任意两点之间最终的最短路径为:

6.8-1-2-3-4.png

整个算法最终代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
k = 0
i = 0
j = 0

for k in range(4):
for i in range(4):
for j in range(4):
if (m[i][j] > m[i][k] + m[k][j]):
m[i][j] = m[i][k] + m[k][j]

print('最终路径:', m)

最终路径: [[0, 2, 5, 4], [9, 0, 3, 4], [6, 8, 0, 1], [5, 7, 10, 0]]

通过这种方法我们可以求出任意两个点之间最短路径。它的时间复杂度是 O(n3)

tree-5491570_1280.jpg

什么是Elasticsearch

Elasticsearch是一个开源的,分布式,高可用的数据库,是目前全文搜索的首选。它可以快速的存储、搜索、分析海量数据。

Elastic是基于开源库Lucene开发的,提供了REST API的操作接口,简单易用

优秀案例

GitHub使用ElasticSearch做PB级搜索;包括但不限于维基百科、携程等成功案例;

为什么ES全文搜索查询速度快

ES检索速度极快的很重要原理就是使用了倒排索引。什么是倒排索引?通常我们理解的索引就是通过key找到对应的value,所以通俗来讲倒排索引就是通过value找到对应的key

termindex.png

理解上图可以了解倒排索引的基本原理。首先ES索引时会先将文本进行分词,然后记录分词和文档的对应关系。当查询时,对查询条件同样进行分词,然后根据分词匹配对应的文档。

基本概念

Node和Cluster

Elastic 本质上是一个分布式数据库,允许多台服务器协同工作,每台服务器可以运行多个Elastic实例

单个 Elastic 实例称为一个节点(node)。一组节点构成一个集群(cluster)

Index

索引是具有某种相似特征文档的集合。类似于MySql中的Database

Type

es6.x建议在一个index中保持一个type

Document

文档是可以被索引的基本信息单元。文档用JSON表示。类似于MySql中的一条数据(Row)

Field

属性,类似于MySql中的某个字段(Column)

安装Elasticsearch(6.3.2)

自己安装,此处选择6.3.2版本

Kibana安装

Kibana 是为 Elasticsearch设计的开源分析和可视化平台。你可以使用 Kibana 来搜索,查看存储在 Elasticsearch 索引中的数据并与之交互。你可以很容易实现高级的数据分析和可视化,以图标的形式展现出来。

注意与安装的Elasticsearch版本号(6.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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# 新增数据1
PUT /person/_doc/1
{
"first_name": "John",
"last_name": "Smith",
"age": 25,
"about": "I love to go rock climbing",
"interests": [
"sports",
"music"
]
}

# 新增数据2
PUT /person/_doc/2
{
"first_name": "Eric",
"last_name": "Smith",
"age": 23,
"about": "I love basketball",
"interests": [
"sports",
"reading"
]
}

# 获得1
GET /person/_doc/1
# 获得2
GET /person/_doc/2

# bool搜索
POST /person/_search
{
"query": {
"bool": {
"should": [
{
"match": {
"first_name": "Eric"
}
}
]
}
}
}

# bool搜索
POST /person/_search
{
"query": {
"bool": {
"should": [
{
"match": {
"last_name": "Smith"
}
},
{
"match": {
"about": "basketball"
}
}
]
}
}
}

ik分词器

ik分词器是最流行的中文分词器,点击可查看ik和ES的版本对照关系以及安装方式

ik提供了两个分词算法ik_smartik_max_word

通过以下两个图,可以看出两个算法的区别。ik_smart为智能切分,ik_max_word为最细粒度划分

ik_smart

smart.png

ik_max_word

WX20200508-145012-max-word.png

根据两个算法的区别,一般遵循以下原则:

索引时,为了提供索引的覆盖范围,通常会采用ik_max_word分析器,会以最细粒度分词索引,搜索时为了提高搜索准确度,会采用ik_smart分析器,会以粗粒度分词

快照创建于恢复

使用无论哪个存储数据的软件,定期备份你的数据都是很重要的,es提供了快照机制。你可以使用snapshotAPI。这个会拿到你集群里当前的状态和数据然后保存到一个共享仓库里。这个备份过程是”智能”的。你的第一个快照会是一个数据的完整拷贝,但是所有后续的快照会保留的是已存快照和新数据之间的差异。随着你不时的对数据进行快照,备份也在增量的添加和删除。这意味着后续备份会相当快速,因为它们只传输很小的数据量。

创建仓库

1
2
3
4
5
6
7
 PUT _snapshot/my_backup ①
{
"type": "fs", ②
"settings": {
"location": "/mount/backups/my_backup" ③
}
}

① 给我们的仓库取一个名字,在本例它叫 my_backup。
② 我们指定仓库的类型应该是一个共享文件系统。
③ 最后,我们提供一个已挂载的设备作为目的地址。注意:共享文件系统路径必须确保集群所有节点都可以访问到。

创建快照

一个仓库可以包含多个快照。每个快照跟一系列索引相关(比如所有索引,一部分索引,或者单个索引)。当创建快照的时候,你指定你感兴趣的索引然后给快照取一个唯一的名字。

快照所有打开的索引

让我们从最基础的快照命令开始:

PUT _snapshot/my_backup/snapshot_1

这个会备份所有打开的索引到 my_backup 仓库下一个命名为 snapshot_1 的快照里。这个调用会立刻返回,然后快照会在后台运行。如果你想阻塞调用直到快照完成可以添加wait_for_completion标记实现

PUT _snapshot/my_backup/snapshot_1?wait_for_completion=true

快照指定索引

1
2
3
4
PUT _snapshot/my_backup/snapshot_2
{
"indices": "index_1,index_2"
}

这个快照命令现在只会备份index1index2了。

获取快照信息

获取某个仓库下所有快照信息

GET _snapshot/my_backup/_all

获取某个仓库下单个快照信息

GET _snapshot/my_backup/snapshot_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
{
"snapshots": [
{
"snapshot": "snapshot_202005080945",
"version_id": 2030599,
"version": "2.3.5",
"indices": [
"pipe"
],
"state": "SUCCESS",
"start_time": "2020-05-08T01:45:57.540Z",
"start_time_in_millis": 1588902357540,
"end_time": "2020-05-08T01:45:57.586Z",
"end_time_in_millis": 1588902357586,
"duration_in_millis": 46,
"failures": [],
"shards": {
"total": 1,
"failed": 0,
"successful": 1
}
}
]
}

删除快照

DELETE _snapshot/my_backup/snapshot_2

使用快照恢复数据

快照的目的就是为了备份,为了恢复。一旦你备份过了数据,恢复它就简单了:只要在你希望恢复回集群的快照 ID后面加上_restore即可

POST _snapshot/my_backup/snapshot_1/_restore

默认行为是把这个快照里存有的所有索引都恢复。如果snapshot_1包括五个索引,这五个都会被恢复到我们集群里。和snapshot`API 一样,我们也可以选择希望恢复具体哪个索引。

还有附加的选项用来重命名索引。这个选项允许你通过模式匹配索引名称,然后通过恢复进程提供一个新名称。如果你想在不替换现有数据的前提下,恢复老数据来验证内容,或者做其他处理,这个选项很有用。让我们从快照里恢复单个索引并提供一个替换的名称:

1
2
3
4
5
6
POST /_snapshot/my_backup/snapshot_1/_restore
{
"indices": "index_1", ①
"rename_pattern": "index_(.+)", ②
"rename_replacement": "restored_index_$1" ③
}

① 只恢复index_1索引,忽略快照中存在的其余索引。
② 查找所提供的模式能匹配上的正在恢复的索引。
③ 然后把它们重命名成替代的模式。

这个会恢复index_1到你及群里,但是重命名成了restored_index_1

grapes-5603367_1280.jpg

什么是websocket

Websocket是一种在单个TCP连接上进行全双工通信的协议

为什么需要websocket

HTTP协议有一个缺陷:通信只能由客户端发起

这种单向请求的特点导致如果服务端出现连续状态的变化,客户端要想获知就比较麻烦,我们只能使用”轮询”模式

轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此,工程师们一直在思考,有没有更好的方法。WebSocket 就是这样发明的。

bg2017051502.png

特点

  1. 建立在TCP协议之上,服务端的实现比较容易
  2. 与HTTP协议有良好的兼容。默认端口也是80和443,并且握手阶段采用HTTP协议,因此握手时不容易屏蔽,能通过各种HTTP代理服务器
  3. 数据格式比较轻,新能开销小,通信高效
  4. 可以发送文本,也可以发送二进制数据
  5. 没有同源限制,客户端可以与任意服务器通信
  6. 协议标识符是ws(如果加密,则为wss),服务器网址就是URL

实战

STOMP协议

STOMP即Simple Text Orientated Messaging Protocol,简单文本定向消息协议,它提供了一个可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互

后端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 表示客户端订阅地址的前缀信息,也就是客户端接收服务端消息的地址的前缀信息
config.enableSimpleBroker("/topic", "/user");

//指服务端接收地址的前缀,意思就是说客户端给服务端发消息的地址的前缀
config.setApplicationDestinationPrefixes("/app");
config.setUserDestinationPrefix("/user/");
}

// 注册STOMP端点
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/my-websocket").setAllowedOrigins("*").withSockJS();
}
}
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
@Controller
@EnableScheduling
@SpringBootApplication
public class App {

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

@Autowired
private SimpMessagingTemplate messagingTemplate;

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

@Scheduled(fixedRate = 1000)
public Object time() throws Exception {
// 发现消息
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
messagingTemplate.convertAndSend("/topic/time", df.format(new Date()));
return "time";
}

@Scheduled(fixedRate = 2000)
@SendToUser("/greetings")
public String greeting2() {
messagingTemplate.convertAndSendToUser("2", "/greetings", "欢迎您,用户: 2");
return "OK";
}

@Scheduled(fixedRate = 2000)
@SendToUser("/greetings")
public String greeting1() {
messagingTemplate.convertAndSendToUser("1", "/greetings", "欢迎您,用户: 1");
return "OK";
}

@Scheduled(fixedRate = 9000)
public Object notification() {
// 发送消息
messagingTemplate.convertAndSend("/topic/notification", "hello world!");
return "ok";
}
}

前端

demo1和demo2都是订阅了两个topic。1个是获得服务器推送的时间,另一个是获得定向推送的消息(demo1向userId=1的用户推送,demo2向userId=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
<template>
<div id="app">
<div>
<label>WebSocket连接状态:</label>
<button type="button" :disabled="connected" @click="connect()">连接</button>
<button type="button" @click="disconnect()" :disabled="!connected">断开</button>
</div>

<div v-if="connected">
<label>当前服务器时间:{{ time }}</label>
<br />消息列表:
<br />
<hr />
<table>
<thead>
<tr>
<th>内容</th>
</tr>
</thead>
<tbody>
<tr v-for="row in lala">
<td>{{row}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>

<script>
// @ is an alias to /src
export default {
name: "Demo1",
data() {
return {
stompClient: "",
// 连接状态
connected: false,
lala: [],
time: ""
};
},
methods: {
connect() {
const socket = new SockJS("http://localhost:8080/my-websocket");
this.stompClient = Stomp.over(socket);
const that = this
this.stompClient.connect({}, function(frame) {
// 注册发送消息(demo1和demo2区别在于此)
that.stompClient.subscribe("/user/1/greetings", function(msg) {
that.lala.push(msg);
});
// 注册推送时间回调
that.stompClient.subscribe("/topic/time", function(response) {
that.time = response.body;
});

that.connected = true;
});
},
disconnect() {
if (this.stompClient != null) {
this.stompClient.disconnect();
}

this.connected = false;
this.lala = [];
}
}
};
</script>

demo3是订阅后端topic,每隔一段时间推送消息给浏览器,随后浏览器显示Notification

并且该实例有自动重连的实现,心跳机制使用SockJS内部实现支持

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
<template>
<div id="app">
<div>服务器推送,客户端显示Notification</div>
<div>WebSocket连接状态:{{ connected }}</div>
<div>
<button type="button" :disabled="connected" @click="connect()">
连接
</button>
<button type="button" @click="disconnect()" :disabled="!connected">
断开
</button>
</div>
</div>
</template>

<script>
// @ is an alias to /src

export default {
name: "Demo3",
data() {
return {
stompClient: "",
// 连接状态
connected: false,
socketUrl: "http://localhost:8080/my-websocket",
// socketUrl: "http://back.mac.com:8888/finance-back-websocket",
lockReconnect: false,
}
},
methods: {
showNotification(info) {
// 弹窗
if (window.Notification) {
var popNotice = function () {
if (Notification.permission == "granted") {
var notification = new Notification("Hi,你好", {
body: info,
icon: "https://pcoss.guan18.com/%E6%A9%98%E8%92%9C.jpeg",
})

notification.onclick = function () {
notification.close()
}
}
}

if (Notification.permission == "granted") {
// 已经授权接受通知
popNotice()
} else if (Notification.permission != "denied") {
// 未拒绝接受通知,提示用户授权
Notification.requestPermission(function (permission) {
popNotice()
})
}
} else {
alert("浏览器不支持Notification")
}
},
successCallback() {
console.log('连接成功!')
this.connected = true
this.stompClient.subscribe("/topic/notification", (frame) => {
this.showNotification(frame.body)
})
},
reconnect() {
if(this.lockReconnect) {
return
}
this.lockReconnect = true
const reconTimeout = setTimeout(() => {
console.log('重连中...')
this.lockReconnect = false
this.socket = new SockJS(this.socketUrl)
this.stompClient = Stomp.over(this.socket)
this.stompClient.connect({}, (frame) => {
// 连接成功,清除定时器
clearTimeout(reconTimeout)
this.successCallback()
}, () => {
// 进行连接
this.connected = false
this.connect()
})
}, 5000)
},
connect() {
var socket = new SockJS(this.socketUrl)
this.stompClient = Stomp.over(socket)
this.stompClient.connect({}, this.successCallback, this.reconnect)
},
disconnect() {
if (this.stompClient != null) {
this.stompClient.disconnect()
}

this.connected = false
},
},
}
</script>

demo3演示
Kapture-2020-02-20-at-14.13.37.gif

前端源码

后端源码

问题

最近遇到个需求:前端登录后,后端返回tokenrefreshToken,当token过期时需要使用refreshToken去获取新的token和新的refreshToken,前端需要做到无感知刷新token

方法

利用 axios 的拦截器,拦截返回后的数据。当接口返回token过期后,先刷新token然后重试

难点

由于发起网络请求是异步的,当同时发起多个请求,而刷新 token 的接口还没有返回时,如何让这些请求等待刷新接口的返回,然后使用返回的新 token 重新发起请求呢?

实现

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
import axios from "axios";

// 创建一个axios实例
const instance = axios.create({
baseURL: "/api",
timeout: 300000,
headers: {
"Content-Type": "application/json",
"X-Token": getLocalToken() // headers塞token
}
});

// 从localStorage中获取token
function getLocalToken() {
const token = window.localStorage.getItem("token");
return token;
}

// 给实例添加一个setToken方法,用于登录后将最新token动态添加到header,同时将token保存在localStorage中
instance.setToken = token => {
instance.defaults.headers["X-Token"] = token;
window.localStorage.setItem("token", token);
};

function refreshToken() {
// instance是当前request.js中已创建的axios实例
return instance.post("/refreshtoken").then(res => res.data);
}

// 是否正在刷新的标记
let isRefreshing = false;
// 重试队列,每一项将是一个待执行的函数形式
let retryRequests = [];
// 请求后拦截 axios.interceptors.request.use()
instance.interceptors.response.use(
response => {
const { code } = response.data;
// 约定当code === 4001时,为token过期
if (code === 4001) {
// config是为请求提供的配置信息
const config = response.config;
if (!isRefreshing) {
isRefreshing = true;
return refreshToken().then(res => {
const { token } = res.data;
instance.setToken(token);
config.headers["X-Token"] = token;
// 注意: 原请求已经将baseURL进行拼接,此处不要重复拼接
config.baseURL = "";
// 将队列中的请求进行重试
retryRequests.forEach(cb => cb(token));
retryRequests = [];
return instance(config);
}).catch(res => {
console.error("refreshtoken error =>", res);
window.location.href = "/";
}).finally(() => {
// 保证下次刷新能够正常进入
isRefreshing = false;
});
} else {
// 正在刷新token,将返回一个未执行resolve的promise
return new Promise(resolve => {
// 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
retryRequests.push(token => {
config.baseURL = "";
config.headers["X-Token"] = token;
resolve(instance(config));
});
});
}
}
return response;
},
error => {
return Promise.reject(error);
}
);

export default instance;

函数防抖(debounce)

应用场景

在浏览器 DOM 事件里面,有一些事件会随着用户的操作不间断触发。比如:重新调整浏览器窗口大小(resize),浏览器页面滚动(scroll),鼠标移动(mousemove)。也就是说用户在触发这些浏览器操作的时候,如果脚本里面绑定了对应的事件处理方法,这个方法就不停的触发。

这并不是我们想要的,因为有的时候如果事件处理方法比较庞大,DOM 操作比如复杂,还不断的触发此类事件就会造成性能上的损失,导致用户体验下降(UI 反映慢、浏览器卡死等)。所以通常来讲我们会给相应事件添加延迟执行的逻辑。

一段时间内多次触发同一事件,只执行最后一次或者只执行第一次,中间的不执行

代码

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
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>函数防抖</title>
</head>

<body>
<div id="demo" style="height: 5000px"></div>
<script>
var COUNT = 0;
var demo = document.getElementById('demo');

function testFn() {
demo.innerHTML += 'testFn 被调用了 ' + ++COUNT + '次<br>';
}

// version0: 《JavaScript高级程序设计》中的方法,把定时器ID存为函数的一个属性
/*
function throttle(method, context) {
clearTimeout(method.tid);
method.tid = setTimeout(function () {
method.call(context);
}, 100);
}

window.onscroll = function () {
throttle(testFn);
}
*/

// version1: -> 错误 timer不是相对全局的变量每次scroll会生成一个timer
/*
window.onscroll = function () {
var timer = null;
clearTimeout(timer);

timer = setTimeout(function () {
testFn();
}, 100);
};
*/

// version2: -> 正确, 但是会多添加一个相对全局的变量,有可能影响业务逻辑
/*
var timer = null;
window.onscroll = function () {
clearTimeout(timer);
timer = setTimeout(function() {
testFn();
}, 100);
};
*/

// version3: -> 正确,使用闭包
/**
* 函数节流方法
* @param Function fn 延时调用函数
* @param Number delay 延迟多长时间
* @return Function 延迟执行的方法
*/

/*
var throttle = function (fn, delay) {
var timer = null;

return function () {
clearTimeout(timer);
timer = setTimeout(function () {
fn();
}, delay);
}
};
*/

// 第一种调用方式
/*
var f = throttle(testFn, 200);
window.onscroll = function () {
f();
};
*/

// 第二种调用方式
/* window.onscroll = throttle(testFn, 200);*/
</script>
</body>

</html>

函数节流(throttle)

指连续触发事件但是在n秒中只执行一次函数。即2n秒内执行 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
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>函数节流</title>
</head>

<body>
<div id="demo" style="height: 5000px"></div>
<script>
var COUNT = 0;
var demo = document.getElementById('demo');

function testFn() {
demo.innerHTML += 'testFn 被调用了 ' + ++COUNT + '次<br>';
}

// versin4:最终模式
var throttle = function (fn, delay, atleast) {
var timer = null;
var previous = null;

return function () {
var now = +new Date();

if (!previous) previous = now;

if (atleast && now - previous > atleast) {
fn();
// 重置上一次开始时间为本次结束时间
previous = now;
clearTimeout(timer);
} else {
clearTimeout(timer);
timer = setTimeout(function () {
fn();
previous = null;
}, delay);
}
}
};

// atleast参数选填
window.onscroll = throttle(testFn, 200, 1000);
// window.onscroll = throttle(testFn, 200);
</script>
</body>

</html>

完整代码

基本概念

目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。

在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务分布式锁等。有的时候,我们需要保证一个方法在同一时间内只能被同一个线程执行。在单机环境中,Java中其实提供了很多并发处理相关的API,但是这些API在分布式场景中就无能为力了。也就是说单纯的Java Api并不能提供分布式锁的能力。

分布式锁应该具备的条件

  1. 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
  2. 高可用的获取锁与释放锁;
  3. 高性能的获取锁与释放锁;
  4. 具备可重入特性;
  5. 具备锁失效机制,防止死锁;
  6. 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。

基于Redis的实现方式

Redis命令介绍

  • SETNX key val 当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0
  • expire key timeout 为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁
  • delete key 删除key

实现思想

  1. 获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID(用来标识一次网络请求)
  2. 获取锁的时候还设置一个超时时间,若超过这个时间则放弃锁
  3. 释放锁的时候,通过UUID判断是不是该锁,若是,则执行delete进行释放

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
51
import redis.clients.jedis.Jedis;

import java.util.Collections;

public class RedisTool {

private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";

/**
* 尝试获取分布式锁
*
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}

private static final Long RELEASE_SUCCESS = 1L;

/**
* 释放分布式锁
*
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {

String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;

}
}

需要加锁的业务逻辑实现

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
import cc.wangweiye.distributelock.DistributedLock;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

import java.util.UUID;

public class Service2 {

private static JedisPool pool = null;
private RedisTool lock = new RedisTool();

int n = 500;

static {
JedisPoolConfig config = new JedisPoolConfig();
// 设置最大连接数
config.setMaxTotal(500);
// 设置最大空闲数
config.setMaxIdle(8);
// 设置最大等待时间
config.setMaxWaitMillis(1000 * 100);
// 在borrow一个jedis实例时,是否需要验证,若为true,则所有jedis实例均是可用的
config.setTestOnBorrow(true);
pool = new JedisPool(config, "127.0.0.1", 6379, 3000);
}

public void seckill() {
Jedis jedis = pool.getResource();
// 返回锁的value值,供释放锁时候进行判断
String uuid = UUID.randomUUID().toString();

while (true) {
boolean locked = lock.tryGetDistributedLock(jedis, "resource", uuid, 500);

if (locked) {
System.out.println(Thread.currentThread().getName() + "获得了锁");
System.out.println(--n);
break;
}
}

lock.releaseDistributedLock(jedis, "resource", uuid);
}
}

线程执行逻辑

1
2
3
4
5
6
7
8
9
10
11
12
public class ThreadB extends Thread {
private Service2 service2;

public ThreadB(Service2 service2) {
this.service2 = service2;
}

@Override
public void run() {
service2.seckill();
}
}

测试代码

1
2
3
4
5
6
7
8
9
public class Test2 {
public static void main(String[] args) {
Service2 service2 = new Service2();
for (int i = 0; i < 499; i++) {
ThreadB threadB = new ThreadB(service2);
threadB.start();
}
}
}
0%