0%

分布式中间件

技术栈:

https://share.note.youdao.com/s/LKfdB11d

主要使用到的技术包括:

springboot + spring cloud + mybatis + redis + ehcache + rabbit mq + AWS S3 + mysql

ehcache

EhCache直接在JVM中进行缓存,速度快,效率高。与Redis相比,操作简单、易用、高效,虽然EhCache也提供有缓存共享的方案,但对分布式集群的支持不太好,缓存共享实现麻烦。

1
2
3
4
@Cacheable(value = "serviceNameCache", key = "targetClass+methodName+#p0")
@CachePut 用于新增
@CacheEvict 用于删除
@Caching 用于组合条件

redis:

缓存、分布式锁、事务、持久化存储

redis单线程为什么这么快 ?

redis 常见数据结构以及使用场景分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1. String:key-value类型 常规key-value缓存应用; 常规计数:微博数,粉丝数等。 

2. Hash 是一个 string 类型的 field 和 value 的映射表。
hash 特别适合用于存储对象,后续操作的时候,可以直接仅仅修改这个对象中的某个字段的值。
比如存储用户信息,商品信息等等

3. List 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作
微博的关注列表,粉丝列表, 消息列表等功能都可以用Redis的 list 结构来实现。

4. Set 是可以自动排重的。
Redis可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程

5. Sorted Set 增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。
举例: 在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜

删除机制

哨兵机制

缓存穿透

1
2
3
4
5
6
7
简介:
一般是黑客故意去请求缓存中不存在的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量
请求而崩掉。

解决办法:
最常见的是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

缓存雪崩

1
2
3
4
5
6
7
简介:缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。 解决办法:

事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。

事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉

事后:利用 redis 持久化机制保存的数据尽快恢复缓存

extends WebSecurityConfigurerAdapter

implements Filter

1
2
3
4
5
6
7
8
9
10
11
12
13
public class User implements BaseEntity {
private Long id;
private Group group;
private String role;
private Long creatorId;
private String name;
private String email;
private String password;
private Status status;
private Date createTime;
private Date updateTime;

}
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
@EnableWebSecurity
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

System.arraycopy(baseUrls, 0, urls, excludeUrls.size(), baseUrls.length);
http.csrf().disable()
.authorizeRequests()
.antMatchers(urls).permitAll()
.antMatchers("/api/admin/users").hasAnyRole("ADMIN")
.anyRequest()
.authenticated();

// 登录和权限校验失败处理
http.exceptionHandling()
.authenticationEntryPoint((request, response, authException)
-> {
Map<String, String> message = new HashMap<>(4);
message.put("message", I18nContext.getMessage("AUTH_ERROR_403"));
message.put("url", ContextConfig.getConf("WEB_OAUTH_LOGIN_URL"));
response.setStatus(HttpStatus.UNAUTHORIZED.value());
MessageUtils.respMsg(response, message);
})
.accessDeniedHandler((request, response, accessDeniedException)
-> MessageUtils.respStringMsg(HttpStatus.FORBIDDEN));

//add filter
http.addFilterBefore(new OauthLoginFilter(sysUserService),
UsernamePasswordAuthenticationFilter.class);

}
}

rabbit-mq

模式

1
2
3
4
Direct exchange:routingkey完全匹配
Fanout exchange:订阅模式,exchange绑定的所有队列
Topic exchange:routingkey通配
Headers exchange:不通过routingkey,通过请求头信息

死信队列

1
2
3
4
5
6
7
8
9
DLX,全称为Dead-Letter-Exchange , 可以称之为死信交换机。当消息在一个队列中变成死信(dead message)之后,它能被重新发送到另一个交换机中,这个交换机就是DLX ,绑定DLX的队列就称之为死信队列。

消息变成死信,可能是由于以下的原因:

- 消息被拒绝
- 消息过期
- 队列达到最大长度

DLX也是一个正常的交换机,和一般的交换机没有区别,它能在任何的队列上被指定,实际上就是设置某一个队列的属性。当这个队列中存在死信时,Rabbitmq就会自动地将这个消息重新发布到设置的DLX上去,进而被路由到另一个队列,即死信队列。
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
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation {

/**
* 模块信息
*/
String module();

/**
* 操作内容
*/
String operation() default "";

/**
* 哪些字段需要被操作日志锁记录
*/
String[] fields();

/**
* 哪些字段需要联合判断,如时间字段:begin-end
* 当其中一个字段发生变化即认为两者都发生变化。
* 说明:unionFields中字段不要在fields中存在。
*/
String [] unionFields() default {};

/**
* 操作日志中的操作() 中的字段
*/
String operationField();

/**
* 操作者,SpEL表达式,默认从权限工具类获取。
* hoa-log 可以不用依赖hoa-security模块
*/
String operator() default "@userServiceImpl.getCurrentUser().getId()";

/**
* 操作前的值,使用 SPEL 表达式,参考如下:
* #this.getDao().findOne(#p0.id)
*
* @return
*/
String originExpression() default "";

/**
* 操作后的值,使用 SPEL 表达式,参考如下:
* #p0
*
* @return
*/
String currentExpression() default "";

/**
* 使用 SPEL 表达式判断在什么情况下记录本次的操作日志
*
* @return
*/
String condition() default "true";
}

定时任务

1
2
@Scheduled(cron = "${app.monitor.task.day}")
@SchedulerLock(name = "SERVICENAME_SUBSCRIPTION_DAY", lockAtLeastFor = 30000, lockAtMostFor = 60000)

k8s

openfeign

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @FeignClient:
* name/value属性: 这两个的作用是一样的,指定的是调用服务的微服务名称
* url : 指定调用服务的全路径,经常用于本地测试
* 如果同时指定name和url属性: 则以url属性为准,name属性指定的值便当做客户端的名称
*/

@FeignClient(name = "${openfeign.serviceName.accountClient}", url = "${openfeign.serviceName.accountDomain}",
fallbackFactory = FeignHacBackService.class, configuration = FeignConfiguration.class)
@Service
public interface FeignHacService {
@RequestMapping(value = "${openfeign.serviceName.userApi}", method = RequestMethod.GET)
HacMessage<HacUser> syncUsers();
}

spring cloud