Ruiyeclub

  • HOME
  • ARCHIVES
  • ABOUT
  • LINKS
🐤SpringBoot学习入门Demo,持续更新中...: https://github.com/ruiyeclub/SpringBoot-Hello

当使用synchronized和分布式锁还是出现并发问题

Posted on 2025-06-01

📝记录一下,尽管使用了同步锁甚至是分布式锁还是查询并发问题的情况。

对同一条数据进行”查询状态->更新状态”操作时,出现多次更新状态。

这个是我业务代码:

1
2
3
4
5
6
7

@Transactional(rollbackFor = Exception.class)
public void update(Long modelId) throws GseException {
synchronized (this) {
//业务逻辑代码
}
}

使用redis分布式锁的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

@Transactional(rollbackFor = Exception.class)
public void update(Long modelId) throws GseException {
RLock lock = redissonClient.getLock(REDIS_COST_MODEL_ID_LOCK + modelId);
try {
if (lock.tryLock(20, 2, TimeUnit.SECONDS)) {
//业务逻辑代码
} else {
log.error("获取分布式锁失败");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 判断当前线程是否持有锁
if (lock.isHeldByCurrentThread()) {
//释放当前锁
lock.unlock();
}
}
}

一、存在的问题以及原因

问题就是以上两种写法都没有生效,但是为什么呢? 在解释这个问题之前,我们首先要弄清楚两个问题:

  1. @Transactional的底层实现原理,开启事务和提交事务的时机是什么?

  2. 分布式锁,和本地锁机制释放锁的时机是什么时候?3.1 @Transactional的底层实现原理,开启事务和提交事务的时机是什么?

它的底层实现原理主要依赖于 Spring 的面向切面编程(AOP)机制。 底层实现原理

  • AOP 代理:当一个类或方法被 @Transactional 注解标记时,Spring 容器在初始化 Bean 时会检测到这个注解。对于使用 Spring
    的代理模式(如 JDK 动态代理或 CGLIB),Spring 会为该 Bean
    创建一个代理对象。这个代理对象会在调用实际方法前后插入事务管理相关的代码,即在方法执行前开启事务,在方法执行完毕后根据执行情况提交或回滚事务。
  • 解析注解:Spring 通过扫描 Bean 定义,识别出带有 @Transactional 注解的方法或类,并配置相应的事务属性,如传播行为、隔离级别、超时时间、是否只读等。
  • 事务拦截器:Spring 使用 AOP 机制中的拦截器(Interceptor)或Advice(通常为 TransactionInterceptor 或 AspectJ
    的切面),在方法调用前后织入事务处理逻辑。在方法调用前,根据事务属性设置事务的开始;在方法正常结束时提交事务,如果方法抛出未检查异常(继承自
    RuntimeException 的异常)或已检查异常(被 @Transactional 的 rollbackFor 属性指定的异常)则回滚事务。

开启事务和提交事务的时机

  • 开启事务:事务通常在进入被 @Transactional 注解的方法之前立即开始。这意味着在执行业务逻辑之前,Spring
    会确保与当前环境匹配的事务上下文已经建立。这包括选择合适的事务管理器,根据事务属性配置事务的隔离级别、传播行为等,并在数据库中实际开启事务。
  • 提交事务:如果被注解的方法正常执行结束,没有抛出任何异常,Spring 会在离开该方法之前提交事务
    。提交事务意味着将所有挂起的更改永久化到数据库中,使事务中的所有操作对外可见。
  • 回滚事务:如果在被注解的方法执行过程中抛出了异常,并且该异常未被 @Transactional 的 noRollbackFor 属性豁免,Spring
    将在捕获到异常后立即回滚事务,撤销所有在事务中已完成但未提交的操作,保持数据的一致性。

二、分布式锁,和本地锁机制释放锁的时机是什么时候?

答案是:本地锁,如果是synchronized,看你包裹起来的范围。Lock的话 看你手动释放锁的时候。 分布式锁:看你手动释放锁的时候。

那么造成问题的原因就出来了,如下图:

5b139d52fdd74a7f81d6455270c34e5f~tplv-73owjymdk6-jj-mark-v1_0_0_0_0_5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd3Vr_q75

也就是说最终提交事务和释放锁的顺序有问题,按照上面的代码写法,因为当只有方法执行完了,AOP切面才会提交事务,那么如果你将上锁的代码写到被@Transactional注解的方法里面,那么提交事务永远都会处于释放锁之后,那么在释放锁之后,提交事务之前的这段时间,就会有并发问题。

三、正确的写法

  1. 本地锁
1
2
3
4
5
6
7
8
9
10
11
12
   public void addLock(Long modelId) throws GseException {
synchronized (this) {
update(modelId);
}
}

@Transactional(rollbackFor = Exception.class)
public void xxxCopy(Long modelId) throws GseException {
synchronized (this) {
//业务逻辑代码
}
}
  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
public void addLock(Long modelId) throws GseException {
RLock lock = redissonClient.getLock(REDIS_COST_MODEL_ID_LOCK + modelId);
try {
if (lock.tryLock(20, 2, TimeUnit.SECONDS)) {
update(modelId);
} else {
log.error("获取分布式锁失败");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 判断当前线程是否持有锁
if (lock.isHeldByCurrentThread()) {
//释放当前锁
lock.unlock();
}
}
}

@Transactional(rollbackFor = Exception.class)
public void update(Long modelId) throws GseException {
synchronized (this) {
//业务逻辑代码
}
}

这样的写法,成功避免了并发问题,被@Transactional注解的方法,在执行完毕以后,就会提交事务,然后到了调用方法里面,再去释放锁。

四、使用悲观锁和乐观锁

问题分析

  1. 请求A:查询状态 → 符合条件 → 更新状态(但数据库卡顿,更新缓慢)
  2. 请求B:在A完成前查询状态 → 看到的状态未被修改 → 也执行更新

这会导致:

  • 数据不一致(如重复扣减库存)
  • 业务逻辑错误(如订单重复支付)
  • 资源超用(如优惠券被多个用户同时使用)

解决方案

  1. 事务 + SELECT FOR UPDATE(推荐)

在事务中使用SELECT FOR UPDATE锁定行,确保更新操作的原子性:

1
2
3
4
5
6
7
8
9
10
11
12
13

@Transactional
public void updateWithLock(Long id) {
// 使用SELECT FOR UPDATE锁定行
MyData data = myDataMapper.selectForUpdate(id);

// 检查状态
if (data.getStatus() == TARGET_STATUS) {
// 执行更新
data.setStatus(NEW_STATUS);
myDataMapper.update(data);
}
}

SQL映射:

1
2
3
4
<!-- 使用FOR UPDATE锁定行 -->
<select id="selectForUpdate" resultType="MyData">
SELECT * FROM my_table WHERE id = #{id} FOR UPDATE
</select>
  1. 乐观锁(使用版本号)

添加版本号字段,每次更新检查版本号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public boolean updateWithVersion(Long id) {
MyData data = myDataMapper.findById(id);

if (data.getStatus() != TARGET_STATUS) {
return false;
}

// 带版本号的更新
int rows = myDataMapper.updateWithVersion(
id,
NEW_STATUS,
data.getVersion(), // 当前版本
data.getVersion() + 1 // 新版本
);

return rows > 0;
}

SQL映射:

1
2
3
4
5
6
7
<update id="updateWithVersion">
UPDATE my_table
SET status = #{newStatus},
version = #{newVersion}
WHERE id = #{id}
AND version = #{oldVersion}
</update>

参考文章:记录一次Redisson使用synchronized和分布式锁不生效的原因
参考文章:悲观锁、乐观锁、mybatis-plus实现乐观锁


Docker安装nacos使用mysql

Posted on 2025-05-08

要在 Docker 中安装并运行 Nacos 服务器并使用 MySQL 作为其数据库,您可以遵循以下步骤。Nacos 是一个更易于构建云原生应用的动态服务发现、配置和服务管理平台,它支持多种数据库作为其存储后端,包括 MySQL。

步骤 1: 安装 Docker

确保您的系统上已安装 Docker。您可以通过以下命令检查 Docker 是否已安装:

1
docker --version

如果未安装,请访问 Docker 官网 获取安装指南。

步骤 2: 启动 MySQL 容器

首先,您需要运行一个 MySQL 容器。可以使用官方的 MySQL 镜像。创建一个名为 nacos-mysql 的容器,并为它设置一些环境变量,例如 root 用户的密码和数据库名:

1
docker run --name nacos-mysql -e MYSQL_ROOT_PASSWORD=root -p 3306:3306 -d mysql:5.7

这条命令会启动一个 MySQL 5.7 容器,root 密码设置为 root,并且将容器的 3306 端口映射到宿主机的 3306 端口。

步骤 3: 创建 Nacos 所需的数据库和用户

登录到 MySQL 容器中,并创建一个数据库和用户供 Nacos 使用:

1
docker exec -it nacos-mysql mysql -uroot -p

输入密码后,执行以下 SQL 命令:

1
CREATE DATABASE nacos DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;CREATE USER 'nacos'@'%' IDENTIFIED BY 'nacos';GRANT ALL PRIVILEGES ON nacos.* TO 'nacos'@'%';FLUSH PRIVILEGES;EXIT;

步骤 4: 拉取并运行 Nacos 容器

接下来,拉取 Nacos 的官方 Docker 镜像并运行它,指定使用 MySQL 作为存储:

1
docker run --name nacos -e MODE=standalone -e MYSQL_SERVICE_HOST=nacos-mysql -e MYSQL_SERVICE_PORT=3306 -e MYSQL_SERVICE_DB_NAME=nacos -e MYSQL_SERVICE_USER=nacos -e MYSQL_SERVICE_PASSWORD=nacos -p 8848:8848 -d nacos/nacos-server:latest

这条命令会启动一个 Nacos 容器,配置为使用上面创建的 MySQL 数据库。MYSQL_SERVICE_HOST 应与 MySQL 容器的名称相同(这里是 nacos-mysql),而 MYSQL_SERVICE_PORT 是 MySQL 容器的端口(这里是 3306)。确保这些值与您在步骤 2 中设置的相匹配。

步骤 5: 验证 Nacos 是否正常运行

打开浏览器,访问 http://localhost:8848/nacos,默认用户名和密码都是 nacos。如果一切设置正确,您应该能够看到 Nacos 的管理界面。

通过以上步骤,您就可以在 Docker 中成功安装并运行 Nacos 服务器,使用 MySQL 作为其存储后端了。


Docker如何通过容器名称进行连接

Posted on 2025-04-18

在Docker环境中,容器间的通信是构建微服务架构的基础。使用容器名称进行连接相比直接使用IP地址具有显著优势。

一、为什么需要使用容器名称连接?

  1. IP地址不稳定性问题:
    • 容器每次重启可能获得不同的IP地址(容器启动时会从子网中动态获取可用IP)
    • 在集群环境中IP地址变化更为频繁
    • 硬编码IP地址会导致连接失效

  2. 管理复杂性降低:
    • 使用有意义的名称比记忆IP更直观
    • 无需维护IP地址映射表
    • 配置文件中使用名称更具可读性

  3. 动态扩展支持:
    • 服务扩容时无需修改连接配置
    • 自动适应容器替换和迁移
    • 与负载均衡和服务发现机制更好配合

二、实现容器名称连接的核心条件

1. 必须使用自定义网络

Docker的默认bridge网络(docker0)不支持通过容器名称连接,只有自定义网络才提供内置DNS服务:

1
2
# 创建支持名称解析的自定义网络
docker network create my-app-network

2. 容器必须加入同一网络

所有需要互相通信的容器必须连接到同一个自定义网络:

1
2
docker run -d --name service-a --network my-app-network my-image
docker run -d --name service-b --network my-app-network my-image

三、实战演示:通过名称连接MySQL与应用

场景准备

假设我们需要部署一个Web应用连接MySQL数据库

步骤1:创建专用网络

1
docker network create app-network

步骤2:启动MySQL容器

1
2
3
4
5
6
docker run -d \
--name mysql-db \
--network app-network \
-e MYSQL_ROOT_PASSWORD=secret \
-e MYSQL_DATABASE=myapp \
mysql:8.0

步骤3:启动应用容器

1
2
3
4
5
6
7
docker run -d \
--name web-app \
--network app-network \
-e DB_HOST=mysql-db \
-e DB_PASSWORD=secret \
-p 8080:8080 \
my-web-app

连接验证

在web-app容器中:

1
2
3
4
5
6
# 进入容器shell
docker exec -it web-app sh

# 测试连接MySQL
ping mysql-db # 应能成功解析
nc -zv mysql-db 3306 # 检查端口连通性

四、特殊场景处理:1Panel环境下的网络集成

1Panel等管理面板会创建自己的网络(如1panel-network),需要将自定义容器加入该网络:

查看1Panel管理的MySQL网络

1
docker inspect 1Panel-mysql-Zhb7 | grep NetworkMode

将应用容器加入同一网络

1
docker network connect 1panel-network my-app-container

验证连接

1
2
# 在应用容器中测试
ping 1Panel-mysql-Zhb7

五、常见问题排查

  1. 无法解析容器名:
    • 确认容器在同一自定义网络
    • 检查网络连接状态:docker network inspect <network>
    • 验证DNS解析:docker exec -it container-name nslookup target-name

  2. 连接超时:
    • 检查目标服务是否监听正确端口
    • 验证防火墙/安全组设置
    • 测试基础连通性:ping和telnet

  3. 1Panel环境特殊问题:
    • 确认容器已加入1panel-network
    • 检查1Panel的网络隔离策略
    • 查看1Panel的日志获取连接失败详情

六、踩坑记录

关于mysql的容器应用连接


使用Redis和RedisTemplate设计当日访问量和访问人数统计

Posted on 2025-04-01

在Java中使用Redis和RedisTemplate来统计当日访问量和访问人数(UV)是一个常见的需求。下面我将提供一个完整的实现方案。

1. 设计思路

• **访问量(PV)**:使用Redis的INCR命令统计总访问次数
• **访问人数(UV)**:使用Redis的HyperLogLog数据结构统计独立访客数
• 过期时间:设置当日24点自动过期

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.concurrent.TimeUnit;

@Service
public class VisitStatisticsService {

private final RedisTemplate<String, String> redisTemplate;

public VisitStatisticsService(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}

// 获取当日PV的key
private String getTodayPVKey() {
return "stat:pv:" + LocalDate.now().toString();
}

// 获取当日UV的key
private String getTodayUVKey() {
return "stat:uv:" + LocalDate.now().toString();
}

// 获取当天剩余的秒数(到24点的秒数)
private long getRemainingSecondsToday() {
ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault());
ZonedDateTime midnight = now.toLocalDate().plusDays(1).atStartOfDay(ZoneId.systemDefault());
return midnight.toEpochSecond() - now.toEpochSecond();
}

/**
* 记录访问
* @param userId 用户ID(可以为IP地址或其他唯一标识)
*/
public void recordVisit(String userId) {
// 获取当天剩余的秒数
long remainingSeconds = getRemainingSecondsToday();

// 记录PV(访问量)
ValueOperations<String, String> valueOps = redisTemplate.opsForValue();
String pvKey = getTodayPVKey();
valueOps.increment(pvKey);
redisTemplate.expire(pvKey, remainingSeconds, TimeUnit.SECONDS);

// 记录UV(独立访客)
String uvKey = getTodayUVKey();
redisTemplate.opsForHyperLogLog().add(uvKey, userId);
redisTemplate.expire(uvKey, remainingSeconds, TimeUnit.SECONDS);
}

/**
* 获取当日访问量(PV)
*/
public Long getTodayPV() {
String pvKey = getTodayPVKey();
String value = redisTemplate.opsForValue().get(pvKey);
return value == null ? 0L : Long.parseLong(value);
}

/**
* 获取当日访问人数(UV)
*/
public Long getTodayUV() {
String uvKey = getTodayUVKey();
return redisTemplate.opsForHyperLogLog().size(uvKey);
}

/**
* 获取历史某天的访问量(PV)
* @param date 日期
*/
public Long getPVByDate(LocalDate date) {
String pvKey = "stat:pv:" + date.toString();
String value = redisTemplate.opsForValue().get(pvKey);
return value == null ? 0L : Long.parseLong(value);
}

/**
* 获取历史某天的访问人数(UV)
* @param date 日期
*/
public Long getUVByDate(LocalDate date) {
String uvKey = "stat:uv:" + date.toString();
return redisTemplate.opsForHyperLogLog().size(uvKey);
}
}

3. 使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RestController
@RequestMapping("/api/visit")
public class VisitController {

private final VisitStatisticsService visitStatisticsService;

public VisitController(VisitStatisticsService visitStatisticsService) {
this.visitStatisticsService = visitStatisticsService;
}

@GetMapping("/record")
public String recordVisit(@RequestParam String userId) {
visitStatisticsService.recordVisit(userId);
return "Visit recorded for user: " + userId;
}

@GetMapping("/stats")
public Map<String, Object> getStats() {
Map<String, Object> result = new HashMap<>();
result.put("pv", visitStatisticsService.getTodayPV());
result.put("uv", visitStatisticsService.getTodayUV());
return result;
}
}

4. 设计说明

  1. PV统计:
    • 使用简单的计数器(INCR命令)
    • 每个访问请求调用一次INCR
    • 键格式:stat:pv:yyyy-MM-dd

  2. UV统计:
    • 使用HyperLogLog数据结构
    • 内存占用小(每个HyperLogLog约12KB)
    • 误差率约0.81%
    • 键格式:stat:uv:yyyy-MM-dd

  3. 过期时间:
    • 自动计算到当天24点的剩余秒数
    • 设置键的过期时间
    • 避免数据无限增长

  4. 扩展性:
    • 可以轻松扩展为按小时、周、月统计
    • 可以添加用户行为分析(如访问页面、停留时间等)

5. 性能优化

  1. 批量操作:如果有批量记录需求,可以使用pipeline提高性能
  2. 持久化:如果需要长期保存数据,可以定期将Redis数据持久化到数据库
  3. 分布式:此方案天然支持分布式环境

6. 注意事项

  1. HyperLogLog有约0.81%的误差率,如果要求精确计数,可以使用Set数据结构(但内存消耗更大)
  2. Redis重启可能导致数据丢失,重要数据应考虑持久化方案
  3. 在高并发场景下,INCR命令是原子操作,无需担心并发问题

这个实现方案简单高效,适合大多数Web应用的访问统计需求。


绕过cloudflare 5秒盾的技术方案解析

Posted on 2025-02-18

Cloudflare 的 5秒盾(Under Attack Mode)是网站防护的常见手段,通过 JavaScript 挑战验证访客真实性。很多网站会使用Cloudflare提供安全服务,导致爬虫的时候显示403状态。

55187d199c2a9079caff09622c6f78f9933ed773

一、解决方案及代码示例

1. 使用 cloudscraper 库

原理:模拟 Cloudflare 预期的请求特征

1
2
3
4
5
import cloudscraper

scraper = cloudscraper.create_scraper()
response = scraper.get("https://target-site.com")
print(response.text)

2. 使用 curl_cffi 库

优势:支持多浏览器指纹模拟

1
2
3
4
5
6
7
from curl_cffi import requests

response = requests.get(
"https://target-site.com",
impersonate="chrome110" # 支持 chrome99-110 / edge99-110
)
print(response.text)

3. 第三方 API 服务

推荐服务:

  • ScrapingBee(自带代理轮转)
  • ScraperAPI
  • ZenRows
1
2
3
4
5
6
import requests

API_KEY = "your_api_key"
response = requests.get(
f"https://api.scrapingbee.com/v1/?api_key={API_KEY}&url=target-site.com"
)

4. FlareSolverr 服务

架构:基于 Puppeteer 的中间件服务

部署步骤:

1
2
3
4
5
6
docker run -d \
--name flaresolverr \
-p 8191:8191 \
-e LOG_LEVEL=info \
--restart unless-stopped \
ghcr.io/flaresolverr/flaresolverr:latest

调用示例:

1
2
3
4
5
6
7
8
import requests

payload = {
"cmd": "request.get",
"url": "https://target-site.com",
"maxTimeout": 60000
}
response = requests.post("http://localhost:8191/v1", json=payload)

二、方案对比分析

方案 执行速度 维护成本 隐匿性 适用场景
cloudscraper ★★★★ 低 中 中小规模常规采集
curl_cffi ★★★★★ 低 高 需要最新指纹场景
第三方API ★★★ 中 高 企业级高频采集
FlareSolverr ★★ 高 极高 复杂JS验证场景

三、对抗升级建议

  1. IP轮换策略:结合住宅代理使用(推荐 BrightData)
  2. 指纹随机化:定期更换 User-Agent/TLS 指纹
  3. 请求限速:设置 3-10 秒随机延迟
  4. 头信息校验:携带完整 Accept-Encoding/Cookie 等头

RabbitMQ常见面试题技术文档

Posted on 2025-02-07

RabbitMQ 常见面试题技术文档


一、基础概念

1. 什么是消息队列(Message Queue)?RabbitMQ 的核心作用是什么?

答案:
消息队列是一种异步通信机制,用于解耦生产者和消费者,实现系统间可靠的消息传递。
RabbitMQ 的核心作用包括:

  • 异步处理:提升系统响应速度和吞吐量。
  • 应用解耦:通过中间件隔离服务间的直接依赖。
  • 流量削峰:缓冲突发流量,避免系统过载。
  • 消息广播:支持一对多消息分发模式。

2. AMQP 协议是什么?RabbitMQ 如何基于 AMQP 工作?

答案:
AMQP(Advanced Message Queuing Protocol)是应用层协议,定义了消息中间件的统一通信标准。
RabbitMQ 基于 AMQP 模型:

  • 生产者(Producer)发送消息到 Exchange。
  • Exchange 根据路由规则(Binding Key)将消息分发到 Queue。
  • 消费者(Consumer)从 Queue 中获取消息。

二、核心组件

3. RabbitMQ 的核心组件有哪些?

答案:

  • Exchange:消息路由中心,决定消息如何分发到队列。
  • Queue:存储消息的缓冲区,消费者从中订阅消息。
  • Binding:定义 Exchange 和 Queue 之间的映射规则。
  • Channel:复用 TCP 连接的轻量级通信通道。
  • Virtual Host:逻辑隔离单元,类似命名空间。

4. Exchange 的四种类型及其区别?

答案:

类型 路由规则 典型场景
Direct 精确匹配 Routing Key 点对点消息(如订单处理)
Topic 通配符匹配 Routing Key(* 和 #) 分类消息广播(如日志分类)
Fanout 忽略 Routing Key,广播到所有队列 全局通知(如系统配置更新)
Headers 基于消息头键值对匹配 复杂路由逻辑(较少使用)

三、消息机制

5. RabbitMQ 如何保证消息不丢失?

答案:

  • 生产者确认模式(Publisher Confirm):确保消息到达 Broker。
  • 消息持久化:Exchange、Queue 和消息均设置为持久化(durable=true)。
  • 消费者手动应答(Manual Acknowledgement):消息处理完成后发送 ACK。

6. 什么是死信队列(Dead Letter Queue, DLQ)?如何触发?

答案:
死信队列用于存储无法被正常消费的消息。触发条件:

  1. 消息被消费者拒绝(basic.reject 或 basic.nack)且不重新入队。
  2. 消息 TTL(存活时间)过期。
  3. 队列达到最大长度限制。

四、高级特性

7. 如何实现延迟队列?

答案:
RabbitMQ 原生不支持延迟队列,但可通过以下方式实现:

  1. 消息 TTL + 死信队列:设置消息的 TTL,过期后转发到 DLQ。
  2. 插件(如 rabbitmq-delayed-message-exchange):使用延迟交换机。

8. 如何避免消息重复消费?

答案:

  • 幂等性设计:业务逻辑天然支持重复处理(如数据库唯一约束)。
  • 去重表:记录已处理消息的唯一标识(如消息 ID)。
  • Redis 分布式锁:在消费前加锁。

五、可靠性与性能

9. RabbitMQ 的集群模式有哪些?

答案:

  • 普通集群:队列元数据同步,但消息存储在单个节点(无高可用)。
  • 镜像队列(Mirrored Queues):队列数据跨节点复制,实现高可用。

10. 如何提升 RabbitMQ 的吞吐量?

答案:

  • 批量发送(Batch Publish)。
  • 多线程 Channel(避免单 Channel 阻塞)。
  • 优化 ACK 机制:批量确认或异步确认。
  • 调整预取数量(prefetchCount)提升消费者并发能力。

六、实战场景

11. 如何保证消息顺序性?

答案:

  • 单队列单消费者:牺牲并发性保证顺序。
  • 消息分组:通过 Routing Key 将需要顺序的消息路由到同一队列。

12. 设计一个秒杀系统时,如何用 RabbitMQ 应对瞬时高并发?

答案:

  • 削峰填谷:将请求写入队列,后端服务按处理能力消费。
  • 队列容量限制:避免内存溢出。
  • 快速失败机制:队列满时直接拒绝新请求。

七、监控与管理

13. 如何监控 RabbitMQ 的运行状态?

答案:

  • 管理界面:通过 Web UI 查看连接、队列、吞吐量等指标。
  • 命令行工具:rabbitmqctl list_queues。
  • Prometheus + Grafana:集成监控告警。

八、对比与其他中间件

14. RabbitMQ 与 Kafka 的核心区别是什么?

答案:

维度 RabbitMQ Kafka
设计目标 消息可靠传输与复杂路由 高吞吐量日志流处理
消息保留 消费后删除 按时间或大小保留
吞吐量 万级 QPS 百万级 QPS
适用场景 业务解耦、延迟消息 日志收集、实时流处理

文档说明

本文档涵盖 RabbitMQ 的核心概念、工作机制及典型应用场景,适用于面试准备或技术方案设计参考。
持续更新建议:结合 RabbitMQ 官方文档(https://www.rabbitmq.com)和实际项目经验补充案例。

PS: 该内容有DeepSeek R1深度学习总结(deepseek牛逼!!!)


使用1Panel作为服务器管理面板

Posted on 2025-01-15

📝记录一下使用1panel作为服务器管理面板,之前基本是使用手搓命令构建服务,了解过宝塔面板,感觉太过臃肿且需要注册账户,后续考虑服务部署都是用1panel面板进行操作。

官方文档:https://1panel.cn/docs/

一、产品介绍

1Panel 是一个现代化、开源的 Linux 服务器运维管理面板。
UI展示

二、安装部署

  1. Ubuntu安装:
1
curl -sSL https://resource.fit2cloud.com/1panel/package/quick_start.sh -o quick_start.sh && sudo bash quick_start.sh

image-20250115134821023

  1. 通过访问地址和面板用户密码进行登录

三、功能使用

  1. 前端页面创建网址后选择静态网址进行配置,后台选择反向代理

image-20250115140517787

  1. 通过网址设置配置HTTPS和设置NGINX

图像_副本

四、计划任务

可以通过linux定时任务自动执行shell等脚本命令

截屏2025-01-15 14.16.51


Docker搭建和部署禅道

Posted on 2024-10-16

禅道项目管理工具提供了丰富的功能模块,包括任务管理、需求管理、Bug跟踪、版本控制、团队协作、项目报表等,这些功能覆盖了项目管理的各个方面。全面的功能模块使得项目团队可以在一个统一的平台上进行协同工作,无需再整合多个系统,从而提高了工作效率。

公司使用禅道来管理项目需求、进度以及Bug测试,新版禅道添加了很多功能,这里我搭建使用的是老版本12.5.3。

Docker拉取禅道镜像

1
docker pull idoop/zentao:12.5.3

image-20241016134837422

运行禅道容器

1
2
3
4
5
docker run --privileged=true -d -p 8082:80 -p 3316:3306 \
-e ADMINER_USER="admin" -e ADMINER_PASSWD="123456" \
-v /dockerdatafile/chandao/:/opt/zbox/ \
--name zentao-server \
idoop/zentao:12.5.3

参数说明:

  • ADMINER_USER:指定禅道管理员账号

  • ADMINER_PASSWD:指定禅道管理员首次登录密码

访问禅道

浏览器访问宿主机器地址加8082端口,登录使用用户名 admin,密码 123456

image-20241016135156614

使用1Panel创建网站

可通过域名进行访问

image-20241016135651301


Pandas转JSON无法显示中文的问题

Posted on 2024-09-18

问题解释:

在使用pandas转换DataFrame到JSON格式时,如果DataFrame中含有中文字符,可能会遇到无法正确显示中文的问题。这通常是因为默认情况下,pandas会将字符串编码为Unicode Escape形式,这不是JSON的标准格式,而是Python内部的表现形式。

解决方法:

  1. 在转换为JSON之前,确保DataFrame的编码是正确的,通常使用UTF-8编码。

  2. 使用to_json方法时,设置参数ensure_ascii=False,这样可以避免ASCII编码的问题,正确显示中文。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
import pandas as pd

# 创建一个包含中文的DataFrame

df = pd.DataFrame({'data': ['中文内容', '更多中文', '最后一条']})

# 转换为JSON,确保使用utf-8编码,并且不使用ASCII编码

json_str = df.to_json(orient='records', force_ascii=False)

print(json_str)

这段代码会创建一个包含中文的DataFrame,并将其转换为JSON格式,正确显示中文字符。


使用ohttps自动部署https证书

Posted on 2024-08-12

在使用第三方托管网站做个人网站,例如:Github Pages,想要给网站加上https,可以通过ohttps来创建Let’s Encrypt的https证书,单纯想给网站加上https也可以。

安装Nginx

先确保服务器(我这里是win服务器)已经安装了nginx,选择适合自己的版本,然后下载解压到服务器上,双击运行nginx.exe,访问服务器ip地址,出现Welcome to Nginx!则表示安装启动成功。

使用ohttps部署ssl证书

  1. 创建证书

image-20240812124550577

  1. 需要在自己域名管理解析中加入ohttps的记录

image-20240812124657919

  1. 在nginx配置中需要ssl_certificate_key和ssl_certificate

ssl_certificate_key: 私钥文件通常包含与SSL证书配对的私钥,用于加密和解密通信中的数据,文件后缀.key,内容开头—–BEGIN RSA PRIVATE KEY—–

ssl_certificate: SSL证书文件包含用于验证服务器身份的公钥以及相关信息,如服务器的域名和证书颁发机构(CA)的签名,文件后缀为.crt或者是.pem,内容开头—–BEGIN CERTIFICATE—–

image-20240812125505357

下载这两个文件,然后存放到服务器上

配置Nginx.conf

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
worker_processes  1;

events {
worker_connections 1024;
}


http {
server_names_hash_bucket_size 64;
include mime.types;
default_type application/octet-stream;

sendfile on;

keepalive_timeout 65;

#gzip on;

server {
listen 443 ssl;
server_name wechat.ruiyeclub.cn;

ssl_certificate 绝对路径/key/fullchain.pem;
ssl_certificate_key 路径/key/cert.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;

location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

server {
listen 80;
server_name wechat.ruiyeclub.cn;

ssl_certificate 绝对路径/key/fullchain.pem;
ssl_certificate_key 路径/key/cert.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;

location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}

Nginx常用命令

  • 启动Nginx服务 nginx

  • 停止Nginx服务 nginx -s stop

  • 检测配置文件的正确性 nginx -t

  • 重新加载Nginx配置文件 nginx -s reload

  • 查看Nginx版本信息 nginx -v

  • 平滑停止Nginx服务 nginx -s quit


12…5

  •   GitHub
  •   Ray4j.top
  •   Springboot-Hello
  •    Search
© 2022 — 2025 Cr.    |   
UV PV
TOP