背景前提
作为一个新闻媒体社交平台,浏览数/播放数是考量一个新闻或者一个内容创作的重要技术指标,往往在DAU数十万,数百万的大型互联网公司,都会部署数百甚至数千数万台服务器进行正常的生产运行。
简单举例
这里就先抛砖引余一个生产的事例:四台微服务实现都是count+1计数的功能 如何保证4台服务器最终count值在分布式高并发的情况下保持一致并用具体的代码实现
技术选型与原理
- Redis的原子操作:Redis提供INCR指令是线程安全的,即便多个客户端同时执行INCR操作,也能保持结果一致性
- 分布式锁(如Redis或者ZK实现):在高并发场景下,如果需要对count进行更为复杂操作(如条件判断/事务性逻辑),也可使用分布式锁来保护临界区代码
技术实现(伪代码)
Redis 原子操作实现
- 场景描述
- 四台微服务分别处理客户端的count+1请求
- 所有微服务共享一个计数器
- 示例代码
- 使用Redis的INCR实现分布式计数
- 设置countKey作为Redis中计数器的键命
@RestController
public class CountController {
@Autowired
public StringRedisTemplate redisTemplate;
private static final String COUNT_KEY = "disturbuted:counter";
@PostMapping("/increment")
public ResponseEntity<String> incrementCount(){
Long updatedCount = redisTemplate.opsForValue.increment(COUNT_KEY);
return ResponseEntity.ok("Count incremented successfully, current value: " + updatedCount);
}
@GetMapping("/count")
public ResponseEntity<Long> getCount() {
String count = redisTemplate.opsForValue.get(COUNT_KEY);
return ResponseEntity.ok(Long.valueof(count != null ? count : "0"));
}
}
解释:
1. redisTemplate.opsForValue().increment(COUNT_KEY):原子递增操作。
2. Redis 保证 INCR 操作的线程安全性,因此无需额外加锁。
3. 所有微服务共享同一个 Redis 实例,这样就能保证一致性。
分布式锁实现(基于Redis)
如果计数逻辑不仅仅是简单的 +1,而是涉及复杂的计算或检查,可以使用分布式锁保护临界区。
- 场景描述
- 对 count 值进行条件检查(例如,只允许计数值在某个范围内递增)
- 在执行 count+1 操作时需要读取和更新计数器的其他关联数据
- 示例代码
@RestController
public class CounterController {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String COUNT_KEY = "distributed:counter";
private static final String LOCK_KEY = "distributed:counter:lock";
@PostMapping("/increment")
public ResponseEntity<String> incrementCount() {
String lockValue = UUID.randomUUID().toString();
boolean lockAcquired = tryLock(LOCK_KEY, lockValue, 10); // 尝试获取锁,锁有效期10秒
if (!lockAcquired) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("Unable to acquire lock");
}
try {
// 获取当前计数值
String currentCount = redisTemplate.opsForValue().get(COUNT_KEY);
long count = currentCount != null ? Long.parseLong(currentCount) : 0;
// 检查条件并递增
if (count < 1000) { // 示例条件:计数器不能超过 1000
redisTemplate.opsForValue().set(COUNT_KEY, String.valueOf(count + 1));
} else {
return ResponseEntity.ok("Count reached its maximum limit.");
}
return ResponseEntity.ok("Count incremented successfully, current value: " + (count + 1));
} finally {
// 释放锁
releaseLock(LOCK_KEY, lockValue);
}
}
// 获取分布式锁
private boolean tryLock(String key, String value, long expireTimeInSeconds) {
Boolean success = redisTemplate.opsForValue().setIfAbsent(key, value, Duration.ofSeconds(expireTimeInSeconds));
return success != null && success;
}
// 释放分布式锁
private void releaseLock(String key, String value) {
String currentValue = redisTemplate.opsForValue().get(key);
if (value.equals(currentValue)) {
redisTemplate.delete(key);
}
}
}
代码解析
- 分布式锁核心逻辑:
- setIfAbsent:Redis 提供的原子性操作,只有键不存在时才能设置成功。
- 自动过期时间:避免死锁问题,即使服务意外崩溃,锁也会在到期后释放。
- 检查锁所有权:释放锁时需要验证当前持有者,避免误删其他服务的锁。
- 计数逻辑:
- 在获取分布式锁后执行 count+1 操作。
- 检查计数器是否满足业务条件(如上限约束)。
- 锁的过期时间设计:
- 锁的有效期应略大于预期的操作时间,防止未完成任务时锁被释放。
分布式锁实现(基于Zookeeper)
Zookeeper分布式锁的原理
- 临时有序节点:
- 每个客户端在注定路径下创建一个临时节点
- Zookeeper自动分配节点序号,确保节点的顺序性
- 锁的获取
- 客户端检查他创建的节点是否是序列号最小的节点。如果是,则获取锁
- 如果不是,客户端监听比自己序列号小的节点删除事件
- 锁的释放
- 客户端完成任务后删除其临时节点,触发监听器,通知下一个节点获取锁
代码实现
用Curator Framework实现,这是一个用于简化 Zookeeper 操作的高层 Java 库
- 引入依赖
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.5.1</version><!-- 最新版本号,请根据需要更新 -->
</dependency>
- 服务端代码
使用Curator的分布式锁实现Zookeeper锁逻辑
@RestController
public class CounterController {
private static final String ZK_CONNECT_STRING = "loaclhost:2181";
private static final String LOCK_PATH = "/distuributed/lock";
private static final String COUNT_KEY ="distuributed:counter";
private final CuratorFramework client;
private final InterProcessMutex lock;
@Autowired
private StringRedisTemplate redisTemplate;
public CounterController() {
client = CuratorFrameworkFactory.builder()
.connnectString(ZK_CONNECT_STRING)
.retryPolicy(new ExponetialBackoffRetry(1000,3)) //重试策略
.build();
client.start();
lock = new InterProcessMutex(client, LOCK_PATH);
}
@PostMapping("/increment")
public String incrementCount(){
boolean lockAcquired = false;
try {
lockAcquired = lock.acquire(5, TimeUnit.SECONDS);
if (!lockAcquired) {
return "Unable to acquire lock";
}
String currentCount = redisTemplate.opsForValue.get(COUNT_KEY);
long count = currentCount != null ? Long.parseLong(currentCount) : 0;
if (count < 1000) {
redisTemplate.opsForValue.set(COUNT_KEY, String.valueOf(count) + 1);
return "Count incremented successfully, current value: " + (count + 1);
} else {
return "Count reached its maximum limit.";
}
} catch (Exception e) {
e.printStackTrace();
return "Error occurred while incrementing count.";
} finally {
if (lockAcquired) {
try {
lock.release();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
@GetMapping("/count")
public long getCount() {
String count = redisTemplate.opsForValue.get(COUNT_KEY);
return count != null ? Long.parseLong(count) : 0;
}
}
代码解析
- Zookeeper客户端:
- 使用Curator使用 Curator Framework 初始化 Zookeeper 客户端。
- ExponentialBackoffRetry 是一种带指数回退的重试策略,处理连接中断的情况。
- 分布式锁
- InterProcessMutex是Curator提供的分布式互斥锁
- 锁路径 LOCK_PATH 是锁的标识。多个实例使用相同路径来共享锁。
- 锁的获取与释放:
- lock.acquire(5, TimeUnit.SECONDS) 尝试获取锁,并设置 5 秒的超时。
- lock.release() 在完成计数操作后释放锁。
- Redis 计数:
- Redis 中存储共享计数器值,使用 StringRedisTemplate 操作。
- 保证在分布式锁保护下对计数器进行一致性更新。
性能分析
- 性能:
- Zookeeper 分布式锁在获取和释放锁时需要一定的网络开销,性能较 Redis 锁略低。
- 适合需要严格一致性的场景。
- 高可用性:
- Zookeeper 是分布式一致性存储系统,提供高可用性和数据持久化能力,适用于跨数据中心的分布式锁。
- 容错性:
- 如果某个实例崩溃,其临时节点会自动删除,锁会被释放,避免死锁。
推荐在高一致性和容错性要求较高的场景下使用 Zookeeper 锁;对于更注重性能的场景,可选择 Redis 分布式锁。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。