背景前提

作为一个新闻媒体社交平台,浏览数/播放数是考量一个新闻或者一个内容创作的重要技术指标,往往在DAU数十万,数百万的大型互联网公司,都会部署数百甚至数千数万台服务器进行正常的生产运行。

简单举例

这里就先抛砖引余一个生产的事例:四台微服务实现都是count+1计数的功能 如何保证4台服务器最终count值在分布式高并发的情况下保持一致并用具体的代码实现

技术选型与原理

  1. Redis的原子操作:Redis提供INCR指令是线程安全的,即便多个客户端同时执行INCR操作,也能保持结果一致性
  2. 分布式锁(如Redis或者ZK实现):在高并发场景下,如果需要对count进行更为复杂操作(如条件判断/事务性逻辑),也可使用分布式锁来保护临界区代码

技术实现(伪代码)

Redis 原子操作实现

  1. 场景描述
  • 四台微服务分别处理客户端的count+1请求
  • 所有微服务共享一个计数器
  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,而是涉及复杂的计算或检查,可以使用分布式锁保护临界区。

  1. 场景描述
  • 对 count 值进行条件检查(例如,只允许计数值在某个范围内递增)
  • 在执行 count+1 操作时需要读取和更新计数器的其他关联数据
  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);
        }
    }
}

代码解析

  1. 分布式锁核心逻辑:
  • setIfAbsent:Redis 提供的原子性操作,只有键不存在时才能设置成功。
  • 自动过期时间:避免死锁问题,即使服务意外崩溃,锁也会在到期后释放。
  • 检查锁所有权:释放锁时需要验证当前持有者,避免误删其他服务的锁。
  1. 计数逻辑:
  • 在获取分布式锁后执行 count+1 操作。
  • 检查计数器是否满足业务条件(如上限约束)。
  1. 锁的过期时间设计:
  • 锁的有效期应略大于预期的操作时间,防止未完成任务时锁被释放。

分布式锁实现(基于Zookeeper)

Zookeeper分布式锁的原理

  1. 临时有序节点:
  • 每个客户端在注定路径下创建一个临时节点
  • Zookeeper自动分配节点序号,确保节点的顺序性
  1. 锁的获取
  • 客户端检查他创建的节点是否是序列号最小的节点。如果是,则获取锁
  • 如果不是,客户端监听比自己序列号小的节点删除事件
  1. 锁的释放
  • 客户端完成任务后删除其临时节点,触发监听器,通知下一个节点获取锁

代码实现

用Curator Framework实现,这是一个用于简化 Zookeeper 操作的高层 Java 库

  1. 引入依赖
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>5.5.1</version><!-- 最新版本号,请根据需要更新 -->
</dependency>
  1. 服务端代码

使用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;
    }    

}

代码解析

  1. Zookeeper客户端:
  • 使用Curator使用 Curator Framework 初始化 Zookeeper 客户端。
  • ExponentialBackoffRetry 是一种带指数回退的重试策略,处理连接中断的情况。
  1. 分布式锁
  • InterProcessMutex是Curator提供的分布式互斥锁
  • 锁路径 LOCK_PATH 是锁的标识。多个实例使用相同路径来共享锁。
  1. 锁的获取与释放:
  • lock.acquire(5, TimeUnit.SECONDS) 尝试获取锁,并设置 5 秒的超时。
  • lock.release() 在完成计数操作后释放锁。
  1. Redis 计数:
  • Redis 中存储共享计数器值,使用 StringRedisTemplate 操作。
  • 保证在分布式锁保护下对计数器进行一致性更新。

性能分析

  1. 性能:
  • Zookeeper 分布式锁在获取和释放锁时需要一定的网络开销,性能较 Redis 锁略低。
  • 适合需要严格一致性的场景。
  1. 高可用性:
  • Zookeeper 是分布式一致性存储系统,提供高可用性和数据持久化能力,适用于跨数据中心的分布式锁。
  1. 容错性:
  • 如果某个实例崩溃,其临时节点会自动删除,锁会被释放,避免死锁。

推荐在高一致性和容错性要求较高的场景下使用 Zookeeper 锁;对于更注重性能的场景,可选择 Redis 分布式锁。


爱跑步的猕猴桃
1 声望0 粉丝