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

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


  • 首页

  • 归档

  • 搜索

Redis 分布式锁 概述 Redis 分布式锁 总结 参考

发表于 2021-10-19

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文已参与「掘力星计划>」,赢取创作大礼包,挑战创作激励金。

概述

本文主要是讲述分布式锁的实现和代码解析。

Redis 分布式锁

大家项目中都会使用到分布式锁把,通常用来做数据的有序操作场景,比如一笔订单退款(如果可以退多次的情况)。或者用户多端下单。

Maven 依赖

我主要是基于 Spring-Boot 2.1.2 + Jedis 进行实现

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
xml复制代码<?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">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.2.RELEASE</version>
</parent>

<groupId>cn.edu.cqvie</groupId>
<artifactId>redis-lock</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<redis.version>2.9.0</redis.version>
<spring-test.version>5.0.7</spring-test.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>${redis.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

配置文件

application.properties 配置文件内容如下:

1
2
3
4
5
6
7
8
9
10
shell复制代码spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
spring.redis.timeout=30000
spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.min-idle=2
spring.redis.jedis.pool.max-idle=4


logging.level.root=INFO

接口定义

接口定义,对于锁我们核心其实就连个方法 lock 和 unlock.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public interface RedisLock {

long TIMEOUT_MILLIS = 30000;

int RETRY_MILLIS = 30000;

long SLEEP_MILLIS = 10;

boolean tryLock(String key);

boolean lock(String key);

boolean lock(String key, long expire);

boolean lock(String key, long expire, long retryTimes);

boolean unlock(String key);
}

分布式锁实现

我的实现方式是通过 setnx 方式实现了,如果存在 tryLock 逻辑的话,会通过 自旋 的方式重试

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
java复制代码// AbstractRedisLock.java 抽象类
public abstract class AbstractRedisLock implements RedisLock {

@Override
public boolean lock(String key) {
return lock(key, TIMEOUT_MILLIS);
}

@Override
public boolean lock(String key, long expire) {
return lock(key, TIMEOUT_MILLIS, RETRY_MILLIS);
}
}

// 具体实现
@Component
public class RedisLockImpl extends AbstractRedisLock {

private Logger logger = LoggerFactory.getLogger(getClass());

@Autowired
private RedisTemplate<String, String> redisTemplate;
private ThreadLocal<String> threadLocal = new ThreadLocal<String>();
private static final String UNLOCK_LUA;

static {
StringBuilder sb = new StringBuilder();
sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
sb.append("then ");
sb.append(" return redis.call(\"del\",KEYS[1]) ");
sb.append("else ");
sb.append(" return 0 ");
sb.append("end ");
UNLOCK_LUA = sb.toString();

}

@Override
public boolean tryLock(String key) {
return tryLock(key, TIMEOUT_MILLIS);
}

public boolean tryLock(String key, long expire) {
try {
return !StringUtils.isEmpty(redisTemplate.execute((RedisCallback<String>) connection -> {
JedisCommands commands = (JedisCommands) connection.getNativeConnection();
String uuid = UUID.randomUUID().toString();
threadLocal.set(uuid);
return commands.set(key, uuid, "NX", "PX", expire);
}));
} catch (Throwable e) {
logger.error("set redis occurred an exception", e);
}
return false;
}

@Override
public boolean lock(String key, long expire, long retryTimes) {
boolean result = tryLock(key, expire);

while (!result && retryTimes-- > 0) {
try {
logger.debug("lock failed, retrying...{}", retryTimes);
Thread.sleep(SLEEP_MILLIS);
} catch (InterruptedException e) {
return false;
}
result = tryLock(key, expire);
}
return result;
}

@Override
public boolean unlock(String key) {
try {
List<String> keys = Collections.singletonList(key);
List<String> args = Collections.singletonList(threadLocal.get());
Long result = redisTemplate.execute((RedisCallback<Long>) connection -> {
Object nativeConnection = connection.getNativeConnection();

if (nativeConnection instanceof JedisCluster) {
return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args);
}
if (nativeConnection instanceof Jedis) {
return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args);
}
return 0L;
});
return result != null && result > 0;
} catch (Throwable e) {
logger.error("unlock occurred an exception", e);
}
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
46
47
48
49
50
51
52
53
54
55
56
57
java复制代码@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisLockImplTest {

private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private RedisLock redisLock;
@Autowired
private StringRedisTemplate redisTemplate;
private ExecutorService executors = Executors.newScheduledThreadPool(8);

@Test
public void lock() {
// 初始化库存
redisTemplate.opsForValue().set("goods-seckill", "10");
List<Future> futureList = new ArrayList<>();

for (int i = 0; i < 100; i++) {
futureList.add(executors.submit(this::seckill));
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

// 等待结果,防止主线程退出
futureList.forEach(action -> {
try {
action.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});

}

public int seckill() {
String key = "goods";
try {
redisLock.lock(key);
int num = Integer.valueOf(Objects.requireNonNull(redisTemplate.opsForValue().get("goods-seckill")));
if (num > 0) {
redisTemplate.opsForValue().set("goods-seckill", String.valueOf(--num));
logger.info("秒杀成功,剩余库存:{}", num);
} else {
logger.error("秒杀失败,剩余库存:{}", num);
}
return num;
} catch (Throwable e) {
logger.error("seckill exception", e);
} finally {
redisLock.unlock(key);
}
return 0;
}
}

总结

本文是 Redis 锁的一种简单的实现方式,基于 jedis 实现了锁的重试操作。
但是缺点还是有的,不支持锁的自动续期,锁的重入,以及公平性(目前通过自旋的方式实现,相当于是非公平的方式)。

参考文档

  • github.com/zhengsh/red…

本文转载自: 掘金

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

更好的 java 重试框架 sisyphus 背后的故事 情

发表于 2021-10-19

sisyphus 综合了 spring-retry 和 gauva-retrying 的优势,使用起来也非常灵活。

今天,让我们一起看一下西西弗斯背后的故事。

情景导入

简单的需求

产品经理:实现一个按条件,查询用户信息的服务。

小明:好的。没问题。

代码

  • UserService.java
1
2
3
4
5
6
7
8
9
10
java复制代码public interface UserService {

/**
* 根据条件查询用户信息
* @param condition 条件
* @return User 信息
*/
User queryUser(QueryUserCondition condition);

}
  • UserServiceImpl.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public class UserServiceImpl implements UserService {

private OutService outService;

public UserServiceImpl(OutService outService) {
this.outService = outService;
}

@Override
public User queryUser(QueryUserCondition condition) {
outService.remoteCall();
return new User();
}

}

谈话

项目经理:这个服务有时候会失败,你看下。

小明:OutService 在是一个 RPC 的外部服务,但是有时候不稳定。

项目经理:如果调用失败了,你可以调用的时候重试几次。你去看下重试相关的东西

重试

重试作用

对于重试是有场景限制的,不是什么场景都适合重试,比如参数校验不合法、写操作等(要考虑写是否幂等)都不适合重试。

远程调用超时、网络突然中断可以重试。在微服务治理框架中,通常都有自己的重试与超时配置,比如dubbo可以设置retries=1,timeout=500调用失败只重试1次,超过500ms调用仍未返回则调用失败。

比如外部 RPC 调用,或者数据入库等操作,如果一次操作失败,可以进行多次重试,提高调用成功的可能性。

V1.0 支持重试版本

思考

小明:我手头还有其他任务,这个也挺简单的。5 分钟时间搞定他。

实现

  • UserServiceRetryImpl.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码public class UserServiceRetryImpl implements UserService {

@Override
public User queryUser(QueryUserCondition condition) {
int times = 0;
OutService outService = new AlwaysFailOutServiceImpl();

while (times < RetryConstant.MAX_TIMES) {
try {
outService.remoteCall();
return new User();
} catch (Exception e) {
times++;

if(times >= RetryConstant.MAX_TIMES) {
throw new RuntimeException(e);
}
}
}

return null;
}

}

V1.1 代理模式版本

易于维护

项目经理:你的代码我看了,功能虽然实现了,但是尽量写的易于维护一点。

小明:好的。(心想,是说要写点注释什么的?)

代理模式

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

在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介作用。

其特征是代理与委托类有同样的接口。

实现

小明想到以前看过的代理模式,心想用这种方式,原来的代码改动量较少,以后想改起来也方便些。

  • UserServiceProxyImpl.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public class UserServiceProxyImpl implements UserService {

private UserService userService = new UserServiceImpl();

@Override
public User queryUser(QueryUserCondition condition) {
int times = 0;

while (times < RetryConstant.MAX_TIMES) {
try {
return userService.queryUser(condition);
} catch (Exception e) {
times++;

if(times >= RetryConstant.MAX_TIMES) {
throw new RuntimeException(e);
}
}
}
return null;
}

}

V1.2 动态代理模式

方便拓展

项目经理:小明啊,这里还有个方法也是同样的问题。你也给加上重试吧。

小明:好的。

小明心想,我在写一个代理,但是转念冷静了下来,如果还有个服务也要重试怎么办呢?

  • RoleService.java
1
2
3
4
5
6
7
8
9
10
java复制代码public interface RoleService {

/**
* 查询
* @param user 用户信息
* @return 是否拥有权限
*/
boolean hasPrivilege(User user);

}

代码实现

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

private final Object subject;

public DynamicProxy(Object subject) {
this.subject = subject;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
int times = 0;

while (times < RetryConstant.MAX_TIMES) {
try {
// 当代理对象调用真实对象的方法时,其会自动的跳转到代理对象关联的handler对象的invoke方法来进行调用
return method.invoke(subject, args);
} catch (Exception e) {
times++;

if (times >= RetryConstant.MAX_TIMES) {
throw new RuntimeException(e);
}
}
}

return null;
}

/**
* 获取动态代理
*
* @param realSubject 代理对象
*/
public static Object getProxy(Object realSubject) {
// 我们要代理哪个真实对象,就将该对象传进去,最后是通过该真实对象来调用其方法的
InvocationHandler handler = new DynamicProxy(realSubject);
return Proxy.newProxyInstance(handler.getClass().getClassLoader(),
realSubject.getClass().getInterfaces(), handler);
}

}
  • 测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@Test
public void failUserServiceTest() {
UserService realService = new UserServiceImpl();
UserService proxyService = (UserService) DynamicProxy.getProxy(realService);

User user = proxyService.queryUser(new QueryUserCondition());
LOGGER.info("failUserServiceTest: " + user);
}


@Test
public void roleServiceTest() {
RoleService realService = new RoleServiceImpl();
RoleService proxyService = (RoleService) DynamicProxy.getProxy(realService);

boolean hasPrivilege = proxyService.hasPrivilege(new User());
LOGGER.info("roleServiceTest: " + hasPrivilege);
}

V1.3 动态代理模式增强

对话

项目经理:小明,你动态代理的方式是挺会偷懒的,可是我们有的类没有接口。这个问题你要解决一下。

小明:好的。(谁?写服务竟然不定义接口)

  • ResourceServiceImpl.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class ResourceServiceImpl {

/**
* 校验资源信息
* @param user 入参
* @return 是否校验通过
*/
public boolean checkResource(User user) {
OutService outService = new AlwaysFailOutServiceImpl();
outService.remoteCall();
return true;
}

}

字节码技术

小明看了下网上的资料,解决的办法还是有的。

  • CGLIB

CGLIB 是一个功能强大、高性能和高质量的代码生成库,用于扩展JAVA类并在运行时实现接口。

  • javassist

javassist (Java编程助手)使Java字节码操作变得简单。

它是Java中编辑字节码的类库;它允许Java程序在运行时定义新类,并在JVM加载类文件时修改类文件。

与其他类似的字节码编辑器不同,Javassist提供了两个级别的API:源级和字节码级。

如果用户使用源代码级API,他们可以编辑类文件,而不需要了解Java字节码的规范。

整个API只使用Java语言的词汇表进行设计。您甚至可以以源文本的形式指定插入的字节码;Javassist动态编译它。

另一方面,字节码级API允许用户直接编辑类文件作为其他编辑器。

  • ASM

ASM 是一个通用的Java字节码操作和分析框架。

它可以用来修改现有的类或动态地生成类,直接以二进制形式。

ASM提供了一些通用的字节码转换和分析算法,可以从这些算法中构建自定义复杂的转换和代码分析工具。

ASM提供与其他Java字节码框架类似的功能,但主要关注性能。

因为它的设计和实现都尽可能地小和快,所以非常适合在动态系统中使用(当然也可以以静态的方式使用,例如在编译器中)。

实现

小明看了下,就选择使用 CGLIB。

  • CglibProxy.java
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
java复制代码public class CglibProxy implements MethodInterceptor {

@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
int times = 0;

while (times < RetryConstant.MAX_TIMES) {
try {
//通过代理子类调用父类的方法
return methodProxy.invokeSuper(o, objects);
} catch (Exception e) {
times++;

if (times >= RetryConstant.MAX_TIMES) {
throw new RuntimeException(e);
}
}
}

return null;
}

/**
* 获取代理类
* @param clazz 类信息
* @return 代理类结果
*/
public Object getProxy(Class clazz){
Enhancer enhancer = new Enhancer();
//目标对象类
enhancer.setSuperclass(clazz);
enhancer.setCallback(this);
//通过字节码技术创建目标对象类的子类实例作为代理
return enhancer.create();
}

}
  • 测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Test
public void failUserServiceTest() {
UserService proxyService = (UserService) new CglibProxy().getProxy(UserServiceImpl.class);

User user = proxyService.queryUser(new QueryUserCondition());
LOGGER.info("failUserServiceTest: " + user);
}

@Test
public void resourceServiceTest() {
ResourceServiceImpl proxyService = (ResourceServiceImpl) new CglibProxy().getProxy(ResourceServiceImpl.class);
boolean result = proxyService.checkResource(new User());
LOGGER.info("resourceServiceTest: " + result);
}

V2.0 AOP 实现

对话

项目经理:小明啊,最近我在想一个问题。不同的服务,重试的时候次数应该是不同的。因为服务对稳定性的要求各不相同啊。

小明:好的。(心想,重试都搞了一周了,今天都周五了。)

下班之前,小明一直在想这个问题。刚好周末,花点时间写个重试小工具吧。

设计思路

  • 技术支持

spring

java 注解

  • 注解定义

注解可在方法上使用,定义需要重试的次数

  • 注解解析

拦截指定需要重试的方法,解析对应的重试次数,然后进行对应次数的重试。

实现

  • Retryable.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Retryable {

/**
* Exception type that are retryable.
* @return exception type to retry
*/
Class<? extends Throwable> value() default RuntimeException.class;

/**
* 包含第一次失败
* @return the maximum number of attempts (including the first failure), defaults to 3
*/
int maxAttempts() default 3;

}
  • RetryAspect.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
java复制代码@Aspect
@Component
public class RetryAspect {

@Pointcut("execution(public * com.github.houbb.retry.aop..*.*(..)) &&" +
"@annotation(com.github.houbb.retry.aop.annotation.Retryable)")
public void myPointcut() {
}

@Around("myPointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
Method method = getCurrentMethod(point);
Retryable retryable = method.getAnnotation(Retryable.class);

//1. 最大次数判断
int maxAttempts = retryable.maxAttempts();
if (maxAttempts <= 1) {
return point.proceed();
}

//2. 异常处理
int times = 0;
final Class<? extends Throwable> exceptionClass = retryable.value();
while (times < maxAttempts) {
try {
return point.proceed();
} catch (Throwable e) {
times++;

// 超过最大重试次数 or 不属于当前处理异常
if (times >= maxAttempts ||
!e.getClass().isAssignableFrom(exceptionClass)) {
throw new Throwable(e);
}
}
}

return null;
}

private Method getCurrentMethod(ProceedingJoinPoint point) {
try {
Signature sig = point.getSignature();
MethodSignature msig = (MethodSignature) sig;
Object target = point.getTarget();
return target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}

}

方法的使用

  • fiveTimes()

当前方法一共重试 5 次。
重试条件:服务抛出 AopRuntimeExption

1
2
3
4
5
6
less复制代码@Override
@Retryable(maxAttempts = 5, value = AopRuntimeExption.class)
public void fiveTimes() {
LOGGER.info("fiveTimes called!");
throw new AopRuntimeExption();
}
  • 测试日志
1
2
3
4
5
6
7
8
ini复制代码2018-08-08 15:49:33.814  INFO  [main] com.github.houbb.retry.aop.service.impl.UserServiceImpl:66 - fiveTimes called!
2018-08-08 15:49:33.815 INFO [main] com.github.houbb.retry.aop.service.impl.UserServiceImpl:66 - fiveTimes called!
2018-08-08 15:49:33.815 INFO [main] com.github.houbb.retry.aop.service.impl.UserServiceImpl:66 - fiveTimes called!
2018-08-08 15:49:33.815 INFO [main] com.github.houbb.retry.aop.service.impl.UserServiceImpl:66 - fiveTimes called!
2018-08-08 15:49:33.815 INFO [main] com.github.houbb.retry.aop.service.impl.UserServiceImpl:66 - fiveTimes called!

java.lang.reflect.UndeclaredThrowableException
...

V3.0 spring-retry 版本

对话

周一来到公司,项目经理又和小明谈了起来。

项目经理:重试次数是满足了,但是重试其实应该讲究策略。比如调用外部,第一次失败,可以等待 5S 在次调用,如果又失败了,可以等待 10S 再调用。。。

小明:了解。

思考

可是今天周一,还有其他很多事情要做。

小明在想,没时间写这个呀。看看网上有没有现成的。

spring-retry

Spring Retry 为 Spring 应用程序提供了声明性重试支持。 它用于Spring批处理、Spring集成、Apache Hadoop(等等)的Spring。

在分布式系统中,为了保证数据分布式事务的强一致性,大家在调用RPC接口或者发送MQ时,针对可能会出现网络抖动请求超时情况采取一下重试操作。 大家用的最多的重试方式就是MQ了,但是如果你的项目中没有引入MQ,那就不方便了。

还有一种方式,是开发者自己编写重试机制,但是大多不够优雅。

注解式使用

  • RemoteService.java

重试条件:遇到 RuntimeException

重试次数:3

重试策略:重试的时候等待 5S, 后面时间依次变为原来的 2 倍数。

熔断机制:全部重试失败,则调用 recover() 方法。

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

private static final Logger LOGGER = LoggerFactory.getLogger(RemoteService.class);

/**
* 调用方法
*/
@Retryable(value = RuntimeException.class,
maxAttempts = 3,
backoff = @Backoff(delay = 5000L, multiplier = 2))
public void call() {
LOGGER.info("Call something...");
throw new RuntimeException("RPC调用异常");
}

/**
* recover 机制
* @param e 异常
*/
@Recover
public void recover(RuntimeException e) {
LOGGER.info("Start do recover things....");
LOGGER.warn("We meet ex: ", e);
}

}
  • 测试
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class RemoteServiceTest {

@Autowired
private RemoteService remoteService;

@Test
public void test() {
remoteService.call();
}

}
  • 日志
1
2
3
4
5
6
7
8
9
yaml复制代码2018-08-08 16:03:26.409  INFO 1433 --- [           main] c.g.h.r.spring.service.RemoteService     : Call something...
2018-08-08 16:03:31.414 INFO 1433 --- [ main] c.g.h.r.spring.service.RemoteService : Call something...
2018-08-08 16:03:41.416 INFO 1433 --- [ main] c.g.h.r.spring.service.RemoteService : Call something...
2018-08-08 16:03:41.418 INFO 1433 --- [ main] c.g.h.r.spring.service.RemoteService : Start do recover things....
2018-08-08 16:03:41.425 WARN 1433 --- [ main] c.g.h.r.spring.service.RemoteService : We meet ex:

java.lang.RuntimeException: RPC调用异常
at com.github.houbb.retry.spring.service.RemoteService.call(RemoteService.java:38) ~[classes/:na]
...

三次调用的时间点:

1
2
3
yaml复制代码2018-08-08 16:03:26.409 
2018-08-08 16:03:31.414
2018-08-08 16:03:41.416

缺陷

spring-retry 工具虽能优雅实现重试,但是存在两个不友好设计:

一个是重试实体限定为 Throwable 子类,说明重试针对的是可捕捉的功能异常为设计前提的,但是我们希望依赖某个数据对象实体作为重试实体,
但 sping-retry框架必须强制转换为Throwable子类。

另一个就是重试根源的断言对象使用的是 doWithRetry 的 Exception 异常实例,不符合正常内部断言的返回设计。

Spring Retry 提倡以注解的方式对方法进行重试,重试逻辑是同步执行的,重试的“失败”针对的是Throwable,
如果你要以返回值的某个状态来判定是否需要重试,可能只能通过自己判断返回值然后显式抛出异常了。

@Recover 注解在使用时无法指定方法,如果一个类中多个重试方法,就会很麻烦。

guava-retrying

谈话

小华:我们系统也要用到重试

项目经理:小明前段时间用了 spring-retry,分享下应该还不错

小明:spring-retry 基本功能都有,但是必须是基于异常来进行控制。如果你要以返回值的某个状态来判定是否需要重试,可能只能通过自己判断返回值然后显式抛出异常了。

小华:我们项目中想根据对象的属性来进行重试。你可以看下 guava-retry,我很久以前用过,感觉还不错。

小明:好的。

guava-retrying

guava-retrying 模块提供了一种通用方法, 可以使用Guava谓词匹配增强的特定停止、重试和异常处理功能来重试任意Java代码。

  • 优势

guava retryer工具与spring-retry类似,都是通过定义重试者角色来包装正常逻辑重试,但是Guava retryer有更优的策略定义,在支持重试次数和重试频度控制基础上,能够兼容支持多个异常或者自定义实体对象的重试源定义,让重试功能有更多的灵活性。

Guava Retryer也是线程安全的,入口调用逻辑采用的是 java.util.concurrent.Callable 的 call() 方法

代码例子

入门案例

遇到异常之后,重试 3 次停止

  • HelloDemo.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public static void main(String[] args) {
Callable<Boolean> callable = new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
// do something useful here
LOGGER.info("call...");
throw new RuntimeException();
}
};

Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
.retryIfResult(Predicates.isNull())
.retryIfExceptionOfType(IOException.class)
.retryIfRuntimeException()
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
.build();
try {
retryer.call(callable);
} catch (RetryException | ExecutionException e) {
e.printStackTrace();
}

}
  • 日志
1
2
3
4
5
6
7
8
9
10
11
12
ruby复制代码2018-08-08 17:21:12.442  INFO  [main] com.github.houbb.retry.guava.HelloDemo:41 - call...
com.github.rholder.retry.RetryException: Retrying failed to complete successfully after 3 attempts.
2018-08-08 17:21:12.443 INFO [main] com.github.houbb.retry.guava.HelloDemo:41 - call...
2018-08-08 17:21:12.444 INFO [main] com.github.houbb.retry.guava.HelloDemo:41 - call...
at com.github.rholder.retry.Retryer.call(Retryer.java:174)
at com.github.houbb.retry.guava.HelloDemo.main(HelloDemo.java:53)
Caused by: java.lang.RuntimeException
at com.github.houbb.retry.guava.HelloDemo$1.call(HelloDemo.java:42)
at com.github.houbb.retry.guava.HelloDemo$1.call(HelloDemo.java:37)
at com.github.rholder.retry.AttemptTimeLimiters$NoAttemptTimeLimit.call(AttemptTimeLimiters.java:78)
at com.github.rholder.retry.Retryer.call(Retryer.java:160)
... 1 more

总结

优雅重试共性和原理

正常和重试优雅解耦,重试断言条件实例或逻辑异常实例是两者沟通的媒介。

约定重试间隔,差异性重试策略,设置重试超时时间,进一步保证重试有效性以及重试流程稳定性。

都使用了命令设计模式,通过委托重试对象完成相应的逻辑操作,同时内部封装实现重试逻辑。

spring-retry 和 guava-retry 工具都是线程安全的重试,能够支持并发业务场景的重试逻辑正确性。

优雅重试适用场景

功能逻辑中存在不稳定依赖场景,需要使用重试获取预期结果或者尝试重新执行逻辑不立即结束。比如远程接口访问,数据加载访问,数据上传校验等等。

对于异常场景存在需要重试场景,同时希望把正常逻辑和重试逻辑解耦。

对于需要基于数据媒介交互,希望通过重试轮询检测执行逻辑场景也可以考虑重试方案。

谈话

项目经理:我觉得 guava-retry 挺好的,就是不够方便。小明啊,你给封装个基于注解的吧。

小明:……

更好的实现

于是小明含泪写下了 sisyphus.

java 重试框架——sisyphus

希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。

我是老马,期待与你的下次重逢。

本文转载自: 掘金

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

Hands-on Rust 学习之旅(3)—— 游戏基本流程

发表于 2021-10-19

第三章开始我们就进入真正的 Game 学习阶段,在整个第三章我们能学到的知识主要有:

  1. game loop,
  2. basic program flow with a state machine,
  3. add a player,
  4. simulate gravity,
  5. and make the player’s dragon flap its wings.
  6. Finally, add obstaches and scorekeeping to the game.

这也是基本的游戏框架了。

Game Loop

关于这个 game loop,我们参考作者给的流程图,再结合自己的理解。

截图来自文中,版权作者所有

游戏主要流程在于:

  1. 初始化(Configure App, Window & Graphics),这个好理解,就是渲染游戏的最初状态和界面。
  2. Poll OS for Input State. 我理解的就是游戏都需要有一个触发机制去改变状态 (state)的,比如,一个事件、一个动作等等;
  3. Call Tick Function. 这个我的理解就好比每个游戏的游戏引擎,帮助我们把所有的事件、动作转化为游戏理解的逻辑,去修改状态,本文主要使用 bracket-lib 引擎。
  4. Update Screen. 状态的改变,就需要去实时更新界面了。

周而复始,就能把游戏运行起来了。

文中介绍了 bracket-lib 引擎。下面我们就来看看如何使用这个。

Bracket-Lib 使用

引入插件:

1
2
ini复制代码[dependencies]
bracket-lib="~0.8.1"

关于插件的版本号说明,可以看第一章介绍的:

=0.8.1 只能使用该版本;

^0.8.1 可以使用大于等于该版本,但是只能局限在0.x 版本,比如:1.x 就不能使用;

~0.8.1 则可以使用任何大于等于该版本,即使未来的一些版本有 bug。

具体代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
rust复制代码use bracket_lib::prelude::*;

struct State {}

impl GameState for State {
fn tick(&mut self, ctx: &mut BTerm) {
ctx.cls();
ctx.print(1, 1, "Hello, Bracket Terminal!");
}
}

fn main() ->BError {
println!("Hello, world!");

let context = BTermBuilder::simple80x50()
.with_title("Flappy Dragon")
.build()?;

main_loop(context, State{})
}

先看运行效果:

简简单单的就可以在终端屏幕打印出文字:Hello, Bracket Terminal!。

Creating Different Game Modes

我觉得这个更像是游戏的状态描述,在这个游戏 demo 中,需要有三个 modes:

  1. Menu: The player is waiting at the main menu.
  2. Playing: Game play is in progress.
  3. End: The game is over.

显然这个 modes 可以用一个 enum 表示:

1
2
3
4
5
vbnet复制代码enum GameMode {
Menu,
Playing,
End,
}

这就需要在我们定义的 State 加上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
rust复制代码struct State {
mode: GameMode,
}

// 并 new 时初始化
impl State {
fn new() -> Self {
State {
mode: GameMode::Menu,
}
}
}

// main 函数同时修改
main_loop(context, State::new())

有了状态描述了,接下来就是需要通过不同的状态下,执行不同的动作,以达到游戏的效果:

1
2
3
4
5
6
7
8
9
10
11
rust复制代码impl GameState for State {
fn tick(&mut self, ctx: &mut BTerm) {
// ctx.cls();
// ctx.print(1, 1, "Hello, Bracket Terminal!");
match self.mode {
GameMode::Menu => self.main_menu(ctx),
GameMode::End => self.dead(ctx),
GameMode::Playing => self.play(ctx),
}
}
}

在这个 tick 函数里,“实时”监听所有的状态变化,执行不同的动作,如:在 Playing 状态下,执行 self.play(ctx) 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
rust复制代码fn play(&mut self, ctx: &mut BTerm) {
// TODO: Fill in this stub later
ctx.cls();
ctx.print_centered(5, "Playing Flappy Dragon");
ctx.print_centered(8, "(E) Play End");
ctx.print_centered(9, "(Q) Quit Game");

if let Some(key) = ctx.key {
match key {
VirtualKeyCode::E => self.mode = GameMode::End,
VirtualKeyCode::Q => ctx.quitting = true,
_ => {}
}
}

// 直接状态改变
// self.mode = GameMode::End;
}

在 play 函数里,根据监听不同的按键,决定状态的改变,这就把游戏运转起来了。

执行效果如下:

完整的代码:

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
rust复制代码use bracket_lib::prelude::*;

enum GameMode {
Menu,
Playing,
End,
}

struct State {
mode: GameMode,
}

impl State {
fn new() -> Self {
State {
mode: GameMode::Menu,
}
}

fn restart(&mut self) {
self.mode = GameMode::Playing;
}

fn main_menu(&mut self, ctx: &mut BTerm) {
ctx.cls();
ctx.print_centered(5, "Welcome to Flappy Dragon");
ctx.print_centered(8, "(P) Play Game");
ctx.print_centered(9, "(Q) Quit Game");

if let Some(key) = ctx.key {
match key {
VirtualKeyCode::P => self.restart(),
VirtualKeyCode::Q => ctx.quitting = true,
_ => {}
}
}
}

fn dead(&mut self, ctx: &mut BTerm) {
ctx.cls();
ctx.print_centered(5, "You are dead!");
ctx.print_centered(8, "(P) Play Again");
ctx.print_centered(9, "(Q) Quit Game");

if let Some(key) = ctx.key {
match key {
VirtualKeyCode::P => self.restart(),
VirtualKeyCode::Q => ctx.quitting = true,
_ => {}
}
}
}

fn play(&mut self, ctx: &mut BTerm) {
// TODO: Fill in this stub later
ctx.cls();
ctx.print_centered(5, "Playing Flappy Dragon");
ctx.print_centered(8, "(E) Play End");
ctx.print_centered(9, "(Q) Quit Game");

if let Some(key) = ctx.key {
match key {
VirtualKeyCode::E => self.mode = GameMode::End,
VirtualKeyCode::Q => ctx.quitting = true,
_ => {}
}
}

// 直接状态改变
// self.mode = GameMode::End;
}
}

impl GameState for State {
fn tick(&mut self, ctx: &mut BTerm) {
// ctx.cls();
// ctx.print(1, 1, "Hello, Bracket Terminal!");
match self.mode {
GameMode::Menu => self.main_menu(ctx),
GameMode::End => self.dead(ctx),
GameMode::Playing => self.play(ctx),
}
}
}

fn main() ->BError {
println!("Hello, world!");

let context = BTermBuilder::simple80x50()
.with_title("Flappy Dragon")
.build()?;

main_loop(context, State::new())
}

今天临时安排其他工作,跟着书中流程,顺出代码,通过 bracket-lib 引擎实时监听 State 的变化,执行不同的游戏动作,虽然简单的利用虚拟按键,响应状态的变化来模拟游戏的推进,但基本也就构成了游戏的逻辑。

明天继续第三章的学习!

本文转载自: 掘金

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

什么是可重入锁?什么是不可重入锁?【代码示例】

发表于 2021-10-19

不可重入锁示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class Lock{
private boolean isLocked = false;
public synchronized void lock() throws InterruptedException{
while(isLocked){
wait();
}
isLocked = true;
}
public synchronized void unlock(){
isLocked = false;
notify();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class Count{
Lock lock = new Lock();
// print()方法中调用doAdd();会死锁
public void print(){
lock.lock();
doAdd();
lock.unlock();
}
public void doAdd(){
lock.lock();
//do something
lock.unlock();
}
}

可重入锁示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码public class Lock{

boolean isLocked = false;
Thread lockedBy = null;
int lockedCount = 0;
public synchronized void lock()
throws InterruptedException{
Thread thread = Thread.currentThread();
while(isLocked && lockedBy != thread){
wait();
}
isLocked = true;
lockedCount++;
lockedBy = thread;
}
public synchronized void unlock(){
if(Thread.currentThread() == this.lockedBy){
lockedCount--;
if(lockedCount == 0){
isLocked = false;
notify();
}
}
}

这就是一个可重入锁:

相对来说,可重入就意味着:线程可以进入任何一个它已经拥有的锁所同步着的代码块。

第一个线程执行print()方法,得到了锁,使lockedBy等于当前线程,也就是说,执行的这个方法的线程获得了这个锁,执行add()方法时,同样要先获得锁,因不满足while循环的条件,也就是不等待,继续进行,将此时的lockedCount变量,也就是当前获得锁的数量加一。

当释放了所有的锁,才执行notify()。如果在执行这个方法时,有第二个线程想要执行这个方法,因为lockedBy不等于第二个线程,导致这个线程进入了循环,也就是等待,不断执行wait()方法。只有当第一个线程释放了所有的锁,执行了notify()方法,第二个线程才得以跳出循环,继续执行。

本文转载自: 掘金

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

java 自定义异常类

发表于 2021-10-19

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

1.常见的异常类

算术异常:ArithmeticExecption

类型强制转换异:ClassCastException

数组下标越界异:ArrayIndexOutOfBoundsException

操作数据库异常:SQLException

方法未找到异常:NoSuchMethodException

输入输出异常:IOException

空指针异常类:NullPointerException

2.查看一个异常类源码(以NullPointerException 为例子)

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
java复制代码public class NullPointerException extends RuntimeException {
private static final long serialVersionUID = 5162710183389028792L;

public NullPointerException() {
}

public NullPointerException(String s) {
super(s);
}
}

public class RuntimeException extends Exception {
static final long serialVersionUID = -7034897190745766939L;

public RuntimeException() {
}

public RuntimeException(String message) {
super(message);
}

public RuntimeException(String message, Throwable cause) {
super(message, cause);
}

public RuntimeException(Throwable cause) {
super(cause);
}

protected RuntimeException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

上面的这个异常是RuntimeException的子类,所以他是非检查异常类,他一般在编译时能过但是在代码运行过程中会出现异常,而我们一般自定义的异常是非检查类异常,需要继承RuntimeException.因此根据上面这个例子 我们自己来编写自定义的异常类,以使得我们在开发过程中写适合需求的异常类

3.自定义异常

(在上面你继承Exception也是可以的)自定义异常在我们日常开发中有时是需要的,这里我自定义了一个用户异常类,在抛出异常的时候能够灵活自定义错误信息,或者可以同时自定义错误码和错误信息 你也可以重写RuntimeException中的一些方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码public class UserException extends RuntimeException{
private static final long serialVersionUID = 1L;
private String code;

public UserException() {
super();
}

public UserException(String message) {
super(message);
this.code = ResultCode.EXISTING_ASSOCIATED_DATA.getCode();
}

public UserException(String code, String message){
super(message);
this.code = code;
}

public UserException(String message, Throwable cause) {
super(message, cause);
}

public UserException(Throwable cause) {
super(cause);
}
}

顺便提一下 一般处理异常我们有5个关键字:throws在方法上抛出一个异常,throw一般是在一个语句中抛出异常,而try-catch-finally: 将可能要出现异常的代码放入try中,catch 捕获 try 中的异常,并处理,不管有没有异常,finally中的代码都会执行。(finally不是必须)

本文转载自: 掘金

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

阿里巴巴10个顶级开源项目,确定不来看看?

发表于 2021-10-19

点赞再看,养成习惯。

本文已收录到 github-java3c ,里面有我的系列文章、面试题库、自学资料、架构师视频、电子书等。

一、创作由来

Hello 大家好,我是l拉不拉米,今天给大家分享10个阿里巴巴开源的顶级项目,都是Java开发必备利器。

这10个项目均来自Github上阿里巴巴公开的开源项目。

都是java3c在工作中用过或者之前学习过的项目,也有写过个别项目的文章,但是从没有整理到一起过。周末花了大概半天的时间,一一整理出来,方便大家阅读学习。

阿里巴巴的github地址

二、项目介绍

1、arthas

1.png

截至目前,github上有27.4k个星和6k的fork,是阿里巴巴最受欢迎的开源项目。

Arthas 是Alibaba开源的Java诊断工具,深受开发者喜爱。

arthas.png

当你遇到以下类似问题而束手无策时,Arthas可以帮助你解决:

  1. 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
  2. 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
  3. 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
  4. 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
  5. 是否有一个全局视角来查看系统的运行状况?
  6. 有什么办法可以监控到JVM的实时运行状态?
  7. 怎么快速定位应用的热点,生成火焰图?
  8. 怎样直接从JVM内查找某个类的实例?

Arthas 提供管理界面在线排查问题,无需重启;动态跟踪Java代码;实时监控JVM状态。

Arthas 支持JDK 6+,支持Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的 Tab 自动补全功能,进一步方便进行问题的定位和诊断。

  • Github: github.com/alibaba/art…
  • 文档: arthas.aliyun.com/doc/

超过120家公司登记了使用:

2.png

2、p3c

3.png

p3c是阿里巴巴开发的Java规约检查插件,可以继承到idea和eclipse中。

相信不少搞Java开发的同学都了解过阿里巴巴出品《Java开发手册》,主要整理了阿里内部使用的Java开发的规范。

p3c插件实现了开发手册中的的53条规则,大部分基于PMD实现,其中有4条规则基于IDEA实现,并且基于IDEA Inspection实现了实时检测功能。部分规则实现了Quick Fix功能(快速修复)。

目前插件检测有两种模式:实时检测、手动触发。

主要功能一览:

4.png

5.png

6.png

3、druid

7.png

Druid 是一个 JDBC 组件库,与c3p0、dbcp齐名,包含数据库连接池、SQL Parser 等组件,被大量业务和技术产品使用或集成,经历过最严苛线上业务场景考验。

Druid连接池

Druid连接池是阿里巴巴开源的数据库连接池项目。Druid连接池为监控而生,内置强大的监控功能,监控特性不影响性能。功能强大,能防SQL注入,内置Loging能诊断Hack应用行为。

  • Github项目地址 github.com/alibaba/dru…
  • 文档 github.com/alibaba/dru…
  • 下载 repo1.maven.org/maven2/com/…
  • 监控DEMO http://120.26.192.168/druid/index.html

竞品对比

9.png

4、fastjson

8.png

fastjson是阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean。

fastjson的优点

1、速度快

fastjson相对其他JSON库的特点是快,从2011年fastjson发布1.1.x版本之后,其性能从未被其他Java实现的JSON库超越。

2、使用广泛

fastjson在阿里巴巴大规模使用,在数万台服务器上部署,fastjson在业界被广泛接受。在2012年被开源中国评选为最受欢迎的国产开源软件之一。

3、测试完备

fastjson有非常多的testcase,在1.2.11版本中,testcase超过3321个。每次发布都会进行回归测试,保证质量稳定。

4、使用简单

fastjson的API十分简洁。

1
2
java复制代码String text = JSON.toJSONString(obj); //序列化
VO vo = JSON.parseObject("{...}", VO.class); //反序列化

5、easyexcel

10.png

EasyExcel是一个基于Java的简单、省内存的读写Excel的开源项目。在尽可能节约内存的情况下支持读写百万行级别的Excel。

Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。easyexcel重写了poi对07版Excel的解析,一个3M的excel用POI sax解析依然需要100M左右内存,改用easyexcel可以降低到几M,并且再大的excel也不会出现内存溢出;03版依赖POI的sax模式,在上层做了模型转换的封装,让使用者更加简单方便。

举个例子:64M内存1分钟内读取75M(46W行25列)的Excel

11.png

还有极速模式能更快,但是内存占用会在100M多一点。

6、canal

12.png

阿里巴巴 MySQL binlog 增量订阅&消费组件。

简介

主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费。

13.png

基于日志增量订阅和消费的业务包括

  • 数据库镜像
  • 数据库实时备份
  • 索引构建和实时维护(拆分异构索引、倒排索引等)
  • 业务 cache 刷新
  • 带业务逻辑的增量数据处理

当前的 canal 支持源端 MySQL 版本包括 5.1.x , 5.5.x , 5.6.x , 5.7.x , 8.0.x

工作原理

MySQL主备复制原理

14.jpg

  • MySQL master 将数据变更写入二进制日志( binary log, 其中记录叫做二进制日志事件binary log events,可以通过 show binlog events 进行查看)
  • MySQL slave 将 master 的 binary log events 拷贝到它的中继日志(relay log)
  • MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据
canal 工作原理
  • canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议
  • MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal )
  • canal 解析 binary log 对象(原始为 byte 流)

7、Spring Cloud Alibaba

15.png

Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。

依托 Spring Cloud Alibaba,您只需要添加一些注解和少量配置,就可以将 Spring Cloud 应用接入阿里微服务解决方案,通过阿里中间件来迅速搭建分布式应用系统。

主要功能

  • 服务限流降级:默认支持 WebServlet、WebFlux、OpenFeign、RestTemplate、Spring Cloud Gateway、Zuul、Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。
  • 服务注册与发现:适配 Spring Cloud 服务注册与发现标准,默认集成了 Ribbon 的支持。
  • 分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。
  • 消息驱动能力:基于 Spring Cloud Stream 为微服务应用构建消息驱动能力。
  • 分布式事务:使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题。
  • 阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。
  • 分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行。
  • 阿里云短信服务:覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。

组件

  • Sentinel:把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
  • Nacos:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
  • RocketMQ:一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。
  • Dubbo:Apache Dubbo™ 是一款高性能 Java RPC 框架。
  • Seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。
  • Alibaba Cloud OSS: 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。
  • Alibaba Cloud SchedulerX: 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。
  • Alibaba Cloud SMS: 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。

8、nacos

16.png

Nacos 简介

Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。

Nacos 支持几乎所有主流类型的“服务”的发现、配置和管理:Kubernetes Service、gRPC & Dubbo RPC Service、Spring Cloud RESTful Service

Nacos 特性

  • 服务发现和服务健康监测

Nacos 支持基于 DNS 和基于 RPC 的服务发现。服务提供者使用 原生SDK、OpenAPI、或一个独立的Agent TODO注册 Service 后,服务消费者可以使用DNS TODO 或HTTP&API查找和发现服务。

Nacos 提供对服务的实时的健康检查,阻止向不健康的主机或服务实例发送请求。Nacos 支持传输层 (PING 或 TCP)和应用层 (如 HTTP、MySQL、用户自定义)的健康检查。 对于复杂的云环境和网络拓扑环境中(如 VPC、边缘网络等)服务的健康检查,Nacos 提供了 agent 上报模式和服务端主动检测2种健康检查模式。Nacos 还提供了统一的健康检查仪表盘,帮助您根据健康状态管理服务的可用性及流量。

  • 动态配置服务

动态配置服务可以让您以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置。

动态配置消除了配置变更时重新部署应用和服务的需要,让配置管理变得更加高效和敏捷。

配置中心化管理让实现无状态服务变得更简单,让服务按需弹性扩展变得更容易。

Nacos 提供了一个简洁易用的UI (控制台样例 Demo) 帮助您管理所有的服务和应用的配置。Nacos 还提供包括配置版本跟踪、金丝雀发布、一键回滚配置以及客户端配置更新状态跟踪在内的一系列开箱即用的配置管理特性,帮助您更安全地在生产环境中管理配置变更和降低配置变更带来的风险。

  • 动态 DNS 服务

动态 DNS 服务支持权重路由,让您更容易地实现中间层负载均衡、更灵活的路由策略、流量控制以及数据中心内网的简单DNS解析服务。动态DNS服务还能让您更容易地实现以 DNS 协议为基础的服务发现,以帮助您消除耦合到厂商私有服务发现 API 上的风险。

Nacos 提供了一些简单的 DNS APIs TODO 帮助您管理服务的关联域名和可用的 IP:PORT 列表.

  • 服务及其元数据管理

Nacos 能让您从微服务平台建设的视角管理数据中心的所有服务及元数据,包括管理服务的描述、生命周期、服务的静态依赖分析、服务的健康状态、服务的流量管理、路由及安全策略、服务的 SLA 以及最首要的 metrics 统计数据。

Nacos 地图

nacosMap.jpg

  • 特性大图:要从功能特性,非功能特性,全面介绍我们要解的问题域的特性诉求
  • 架构大图:通过清晰架构,让您快速进入 Nacos 世界
  • 业务大图:利用当前特性可以支持的业务场景,及其最佳实践
  • 生态大图:系统梳理 Nacos 和主流技术生态的关系
  • 优势大图:展示 Nacos 核心竞争力
  • 战略大图:要从战略到战术层面讲 Nacos 的宏观优势

Nacos 生态图

17.png

9、Sentinel

18.png

Sentinel 介绍

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式服务架构的流量控制组件,主要以流量为切入点,从流量控制、熔断降级、系统自适应保护等多个维度来帮助您保障微服务的稳定性。

Sentinel 基本概念

资源

资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。在接下来的文档中,我们都会用资源来描述代码块。

只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。

规则

围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。

Sentinel 功能和设计理念

流量控制

流量控制在网络传输中是一个常用的概念,它用于调整网络包的发送数据。然而,从系统稳定性角度考虑,在处理请求的速度上,也有非常多的讲究。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状,如下图所示:

15.jpg

流量控制有以下几个角度:

  • 资源的调用关系,例如资源的调用链路,资源和资源之间的关系;
  • 运行指标,例如 QPS、线程池、系统负载等;
  • 控制的效果,例如直接限流、冷启动、排队等。

Sentinel 的设计理念是让您自由选择控制的角度,并进行灵活组合,从而达到想要的效果。

熔断降级

除了流量控制以外,降低调用链路中的不稳定资源也是 Sentinel 的使命之一。由于调用关系的复杂性,如果调用链路中的某个资源出现了不稳定,最终会导致请求发生堆积。这个问题和 Hystrix 里面描述的问题是一样的。

20.png

Sentinel 和 Hystrix 的原则是一致的: 当调用链路中某个资源出现不稳定,例如,表现为 timeout,异常比例升高的时候,则对这个资源的调用进行限制,并让请求快速失败,避免影响到其它的资源,最终产生雪崩的效果。

在限制的手段上,Sentinel 和 Hystrix 采取了完全不一样的方法。

Hystrix 通过线程池的方式,来对依赖(在我们的概念中对应资源)进行了隔离。这样做的好处是资源和资源之间做到了最彻底的隔离。缺点是除了增加了线程切换的成本,还需要预先给各个资源做线程池大小的分配。

Sentinel 对这个问题采取了两种手段:

  • 通过并发线程数进行限制

和资源池隔离的方法不同,Sentinel 通过限制资源并发线程的数量,来减少不稳定资源对其它资源的影响。这样不但没有线程切换的损耗,也不需要您预先分配线程池的大小。当某个资源出现不稳定的情况下,例如响应时间变长,对资源的直接影响就是会造成线程数的逐步堆积。当线程数在特定资源上堆积到一定的数量之后,对该资源的新请求就会被拒绝。堆积的线程完成任务后才开始继续接收请求。

  • 通过响应时间对资源进行降级

除了对并发线程数进行控制以外,Sentinel 还可以通过响应时间来快速降级不稳定的资源。当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的时间窗口之后才重新恢复。

系统负载保护

Sentinel 同时提供系统维度的自适应保护能力。防止雪崩,是系统防护中重要的一环。当系统负载较高的时候,如果还持续让请求进入,可能会导致系统崩溃,无法响应。在集群环境下,网络负载均衡会把本应这台机器承载的流量转发到其它的机器上去。如果这个时候其它的机器也处在一个边缘状态的时候,这个增加的流量就会导致这台机器也崩溃,最后导致整个集群不可用。

针对这个情况,Sentinel 提供了对应的保护机制,让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请求。

Sentinel 工作机制

  • 对主流框架提供适配或者显示的 API,来定义需要保护的资源,并提供设施对资源进行实时统计和调用链路分析。
  • 根据预设的规则,结合对资源的实时统计信息,对流量进行控制。同时,Sentinel 提供开放的接口,方便您定义及改变规则。
  • Sentinel 提供实时的监控系统,方便您快速了解目前系统的状态。

总体框架

19.png

10、COLA

21.png

COLA 是 Clean Object-Oriented and Layered Architecture的缩写,代表“整洁面向对象分层架构”。

目前COLA已经发展到COLA 4.0。

COLA分为两个部分,COLA架构和COLA组件。

22.png

好的应用架构,都遵循一些共同模式,不管是六边形架构、洋葱圈架构、整洁架构、还是COLA架构,都提倡以业务为核心,解耦外部依赖,分离业务复杂度和技术复杂度等。

为了能够快速创建满足COLA架构的应用,我们提供了两个archetype,位于cola-archetypes目录下:

  1. cola-archetype-service:用来创建纯后端服务的archetype。
  2. cola-archetype-web:用来创建adapter和后端服务一体的web应用archetype。

执行以下命令:

1
2
3
4
5
6
7
8
ini复制代码mvn archetype:generate  \
  -DgroupId=com.alibaba.cola.demo.web \
  -DartifactId=demo-web \
  -Dversion=1.0.0-SNAPSHOT \
  -Dpackage=com.alibaba.demo \
  -DarchetypeArtifactId=cola-framework-archetype-web \
  -DarchetypeGroupId=com.alibaba.cola \
  -DarchetypeVersion=4.0.1

命令执行成功的话,会看到如下的应用代码结构:

23.png

COLA架构的核心职责就是定义良好的应用结构,提供最佳应用架构的最佳实践。通过不断探索,我们发现良好的分层结构,良好的包结构定义,可以帮助我们治理混乱不堪的业务应用系统。

写在最后

收集这些项目的信息虽然算不上技术活,但却是实实在在的花了不少时间整理,希望日后您在工作中,出技术方案/技术选型,或者面试的时候还能有个印象,说不定能帮住您解决个问题。

喜欢的话,麻烦转评赞,作者tuo光一切给你看!

本文转载自: 掘金

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

重复了

发表于 2021-10-19

重复了

本文转载自: 掘金

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

重复了

发表于 2021-10-19

重复了

本文转载自: 掘金

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

Sentinel-Go 源码系列(一)|开篇

发表于 2021-10-19

大家好呀,打算写一个 Go 语言组件源码分析系列,一是为了能学习下 Go 语言,看下别人是怎么写 Go 的,二是也掌握一个组件。

本次选择了 Sentinel-Go,一是对 Java 版本的 Sentinel 算是有一些了解,也在生产上落地过,二是感觉他的代码应该不会太复杂(仅仅是感觉),三是在云原生越来越热的趋势下,用 Go 实现的限流降级容错应该是比较通用的。

源码阅读本身是枯燥的,我尽量用容易理解的语言来描述,希望大家也多支持我的文章,点个赞、在看和关注就是对我最大的支持。


背景

Sentinel 简介

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式服务架构的流量控制组件,主要以流量为切入点,从流量控制、熔断降级、系统自适应保护等多个维度来帮助您保障微服务的稳定性。

Sentinel 是阿里2018年开源的项目,最初是 Java 版本,截止目前有 17.6k 的star,项目地址为

github.com/alibaba/Sen…

2020年又开源了 Go 的版本,目的是朝云原生方向演进,截止目前 1.7k star,项目地址为

github.com/alibaba/sen…

Sentinel 的作用

在上面简介中也说了,Sentinel 是微服务时代保障稳定的神兵利器

举个例子:电商系统中用户浏览商品详情页,通常会通过 RPC 调用多个微服务,查询商品信息的同时还会查询用户的信息,也会展示优惠信息,通常下拉列表还会展示推荐,广告等信息,如下图

如果流量较大时,CouponService 容量不足,或者某种原因导致 RecomService 不可用,此时 AggrService 会被拖死,导致商品详情服务不可用,但仔细想想这些服务不是那么重要,可以进行限流或者直接降级(不再调用),总比直接服务不用要好吧

又或者流量实在太高,ProductService 也顶不住了,那是否可以采取限流措施,保住部分用户的请求时正常的,也比全部不可用要好

这些问题,Sentinel 都能解决

Sentinel 提供的能力

Sentinel 将要保护的对象(可以是某个服务或一段代码)抽象为资源,通过动态下发的规则,对资源进行

  • 流量控制
  • 熔断降级

针对这两个主要功能又有很多的玩法,比如限流是针对QPS还是并发数,控制的效果是直接拒绝还是排队等等。

当然 Sentinel 也提供一个开箱即用的 Dashboard,可扩展配中心进行下发规则,展示监控指标,调用关系链等等

快速开始

源码阅读环境准备

  • fork 源码到自己仓库,便于增加注释
  • 拉取源码到本地

git clone git@github.com:lkxiaolou/sentinel-golang.git

  • 导入 IDE,由于我既要写 Java 又要写 Go,所以用 IntelliJ IDEA 比较方便,只要装一个 Go plugin 就可以了
  • 导入后,一般 IDE 会自动下载依赖,如果没有自动下载,试试执行( Go 安装就不说了)

go mod download

目录结构介绍

  • sentinel-golang
    • api:对外暴露的接口
    • core:核心实现
    • example:使用例子
    • exporter:Prometheus的exporter
    • ext:扩展接口,主要是动态规则配置中心扩展接口
    • logging:日志模块
    • pkg:第三方插件的实现,比如各个组件适用 Sentinel 的 adapter,以及 Sentinel 对接各种第三方配置中心的扩展实现
    • tests:测试类代码,包括单元测试、benchmark
    • util:工具类

样例跑通

在 /example 目录下新建 mytests 目录,并创建一个 quick_start.go 文件,按照官网给出的例子,先用最简单的默认方式初始化

1
2
3
4
go复制代码if err := sentinel.InitDefault(); err != nil {
// 初始化失败
panic(err.Error())
}

再用写死的方式加载规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码// 资源名
resource := "test-resource"

// 加载流控规则,写死
_, err := flow.LoadRules([]*flow.Rule{
{
Resource: resource,
// Threshold + StatIntervalInMs 可组合出多长时间限制通过多少请求,这里相当于限制为 10 qps
Threshold: 10,
StatIntervalInMs: 1000,
// 暂时不用关注这些参数
TokenCalculateStrategy: flow.Direct,
ControlBehavior: flow.Reject,
},
})

最后写测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码// 修改这个看看效果吧
currency := 100

for i := 0; i < currency; i++ {
go func() {
e, b := sentinel.Entry(resource, sentinel.WithTrafficType(base.Inbound))
if b != nil {
// 被流控
fmt.Printf("blocked %s \n", b.BlockMsg())
} else {
// 通过
fmt.Println("pass...")
// 通过后必须调用Exit
e.Exit()
}
}()
}

这里限制了 10 qps,我们用 100 个协程并发测试跑一下,刚好通过10个请求

测试代码已上传到我的仓库

github.com/lkxiaolou/s…

总结

本文介绍了 Sentinel 的和它能解决的问题,以及源码阅读的一些准备工作,并跑通了一个最简单的例子,见识到了 Sentinel 限流的效果,本文先到这里,我们下一节见。


搜索关注微信公众号”捉虫大师”,后端技术分享,架构设计、性能优化、源码阅读、问题排查、踩坑实践。

本文转载自: 掘金

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

nestjs 集成 sentry

发表于 2021-10-19

最近做的一个公司项目,使用 nestjs 作为服务端,为了收集服务报错,尝试了一下接入 sentry。整个流程也是比较简单,sentry 的功能也真的很强大。

开始之前需要一个 sentry 账号。

创建项目

首先选择一个开发平台,这里没有 nestjs(可能还是比较小众吧),我们可以直接选择 node;然后填写项目名称,owner 等信息

B9E2B630-1AD3-4465-A241-38E1B763990E.png

客户端集成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
js复制代码import { NestFactory } from '@nestjs/core';
import { AppModule } from './app/app.module';
import * as Sentry from '@sentry/node';
import CONFIG from '@Root/config';

const { APP_PORT, APP_ENV } = CONFIG;

async function bootstrap() {
const app = await NestFactory.create(AppModule);

// 本地开发环境报错不上传至 sentry
if (APP_ENV !== 'dev') {
Sentry.init({
dsn: 'https://yourSentry.com/6',
tracesSampleRate: 1.0,
environment: APP_ENV,
});
}

await app.listen(APP_PORT);
}

bootstrap();
  • dsn:指定 sentry SDK 向哪里发送这些异常事件
  • tracesSampleRate:跟踪采样率,为 0 到 1 之间的数字。假如为 0.2,大约 20% 的错误将被记录和发送
  • environment:隔离不同环境,方便查询

68110814-B059-474E-BD4E-408B96EDDA84.png

错误收集

通过以上配置基本可以上报错误了,然后通过 sentry 控制台看到相关的错误信息及代码定位。
22BA9342-E102-4DC2-9E9C-0A3DE50A591A.png

0E4A3B3F-8F46-4A12-9AD2-30550B7960C9.png

🌴 注意:如果是前端项目,需要上传 sourceMap 至 sentry 服务器, 但是 Node 项目一般没有源码转换(压缩、合并文件和转译),并不需要这样操作

本文转载自: 掘金

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

1…482483484…956

开发者博客

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