探究 Spring Cloud 全局锁的实现
2021-11-241.9k 阅读
微服务架构下全局锁的重要性
在微服务架构中,随着业务的不断发展和系统规模的扩大,多个服务实例可能会同时对共享资源进行操作。为了保证数据的一致性和业务逻辑的正确性,就需要引入全局锁机制。全局锁能够在分布式环境下,对跨服务实例的资源访问进行有效控制,避免并发冲突。例如,在电商系统中,库存扣减操作可能会由多个订单服务实例同时触发,如果没有全局锁,就可能出现超卖的情况。
Spring Cloud 全局锁实现的常用方式
- 基于 Redis 的全局锁
- 原理:Redis 是一个高性能的键值对存储数据库,它提供了一些原子操作,如
SETNX
(Set if Not eXists)。基于 Redis 的全局锁就是利用SETNX
命令来实现的。当一个服务实例尝试获取锁时,它会向 Redis 发送SETNX
命令,如果该键不存在,则设置成功,代表获取到了锁;如果键已存在,则获取锁失败。 - 代码示例:
- 原理:Redis 是一个高性能的键值对存储数据库,它提供了一些原子操作,如
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
@Component
public class RedisLock {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public boolean tryLock(String key, String value, long expireTime) {
Boolean result = redisTemplate.opsForValue().setIfAbsent(key, value);
if (result != null && result) {
// 设置锁的过期时间,防止死锁
redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
return true;
}
return false;
}
public void unlock(String key) {
redisTemplate.delete(key);
}
}
- 优点:Redis 性能高,支持分布式部署,能够满足高并发场景下的全局锁需求。而且 Redis 的原子操作保证了锁的获取和释放的原子性。
- 缺点:存在锁过期时间设置的问题。如果设置的过期时间过短,可能导致业务还未执行完锁就过期了,从而出现并发问题;如果设置过长,在锁持有者出现故障时,会导致其他实例长时间无法获取锁。另外,Redis 的主从复制可能会导致锁的短暂丢失,在主从切换过程中,如果主节点上的锁还未同步到从节点,而此时从节点晋升为主节点,新的主节点上就不存在该锁,可能会导致其他实例获取到重复的锁。
- 基于 ZooKeeper 的全局锁
- 原理:ZooKeeper 是一个分布式协调服务,它以树形结构存储数据。基于 ZooKeeper 的全局锁是通过创建临时顺序节点来实现的。当一个服务实例尝试获取锁时,它会在 ZooKeeper 的某个指定节点下创建一个临时顺序节点。然后,获取该节点下所有子节点,并判断自己创建的节点是否是序号最小的节点。如果是,则获取到锁;否则,监听比自己序号小的前一个节点的删除事件,当前一个节点删除时,再次判断自己是否是最小序号节点,从而获取锁。
- 代码示例:
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class ZooKeeperLock {
private static final String ZK_SERVERS = "localhost:2181";
private static final int SESSION_TIMEOUT = 5000;
private static final int CONNECTION_TIMEOUT = 3000;
private static final String LOCK_PATH = "/lock";
private CuratorFramework client;
public ZooKeeperLock() {
client = CuratorFrameworkFactory.builder()
.connectString(ZK_SERVERS)
.sessionTimeoutMs(SESSION_TIMEOUT)
.connectionTimeoutMs(CONNECTION_TIMEOUT)
.retryPolicy(new ExponentialBackoffRetry(1000, 3))
.build();
client.start();
}
public boolean tryLock(String lockKey, long time, TimeUnit unit) {
InterProcessMutex mutex = new InterProcessMutex(client, LOCK_PATH + "/" + lockKey);
try {
return mutex.acquire(time, unit);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public void unlock(String lockKey) {
InterProcessMutex mutex = new InterProcessMutex(client, LOCK_PATH + "/" + lockKey);
try {
mutex.release();
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 优点:ZooKeeper 基于其数据一致性协议(ZAB 协议),能够保证锁的强一致性。而且通过临时节点的特性,在服务实例崩溃或网络故障时,锁会自动释放,避免死锁。
- 缺点:由于 ZooKeeper 采用的是树形结构存储数据,在高并发场景下,频繁的节点创建和删除操作会给 ZooKeeper 服务器带来较大的压力。而且与 Redis 相比,ZooKeeper 的性能相对较低,因为它需要进行更多的网络交互和一致性协调操作。
- 基于数据库的全局锁
- 原理:利用数据库的事务特性和唯一索引来实现全局锁。通常的做法是在数据库中创建一张锁表,表中包含一个唯一索引字段。当一个服务实例尝试获取锁时,它会向锁表中插入一条记录,如果插入成功,代表获取到了锁;如果因为唯一索引冲突插入失败,则获取锁失败。锁的释放则是通过删除这条记录来实现。
- 代码示例:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
@Component
public class DatabaseLock {
@Autowired
private JdbcTemplate jdbcTemplate;
public boolean tryLock(String lockKey) {
String insertSql = "INSERT INTO lock_table (lock_key) VALUES (?)";
try {
jdbcTemplate.update(insertSql, lockKey);
return true;
} catch (Exception e) {
return false;
}
}
public void unlock(String lockKey) {
String deleteSql = "DELETE FROM lock_table WHERE lock_key =?";
jdbcTemplate.update(deleteSql, lockKey);
}
}
- 优点:实现相对简单,对于已经使用数据库的项目来说,不需要额外引入其他中间件。而且数据库本身具备数据持久化的能力,能够保证锁的状态在重启后依然有效。
- 缺点:性能相对较低,数据库的写入操作性能瓶颈会影响全局锁的获取和释放效率。同时,数据库事务的开销较大,如果锁的持有时间较长,会占用数据库连接资源,影响其他业务操作。另外,在高并发场景下,数据库的压力会显著增加,可能导致数据库性能下降甚至崩溃。
Spring Cloud 整合全局锁的实践
- Spring Cloud Alibaba Sentinel 与全局锁的结合
- Sentinel 简介:Spring Cloud Alibaba Sentinel 是一个流量控制、熔断降级的框架。它可以在微服务架构中对服务的流量进行有效控制,保证系统的稳定性。
- 结合方式:在一些需要对流量和资源访问同时进行控制的场景下,可以将 Sentinel 与全局锁结合使用。例如,在一个抢购业务中,首先通过 Sentinel 对请求流量进行限流,确保在高并发情况下系统不会因为过多的请求而崩溃。然后,对于通过限流的请求,使用全局锁来保证库存扣减等关键操作的原子性。
- 示例代码:
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class抢购Controller {
@Autowired
private RedisLock redisLock;
@GetMapping("/抢购")
public String抢购() {
String key = "抢购锁";
String value = System.currentTimeMillis() + "";
try (Entry entry = SphU.entry("抢购资源")) {
if (redisLock.tryLock(key, value, 10)) {
try {
// 执行抢购业务逻辑,如库存扣减等
return "抢购成功";
} finally {
redisLock.unlock(key);
}
} else {
return "抢购失败,已无库存或锁获取失败";
}
} catch (BlockException e) {
return "抢购失败,系统繁忙,请稍后重试";
}
}
}
- Spring Cloud Gateway 与全局锁的应用
- Spring Cloud Gateway 简介:Spring Cloud Gateway 是 Spring Cloud 生态中的网关组件,它可以对请求进行路由、过滤等操作。
- 应用场景:在微服务架构中,Spring Cloud Gateway 可以作为全局锁的前置防线。例如,对于一些需要进行身份验证和全局锁控制的请求,可以先通过 Gateway 的过滤器进行身份验证,验证通过后再尝试获取全局锁。如果获取锁成功,则将请求转发到相应的微服务;如果获取锁失败,则返回相应的错误信息。
- 配置示例:
spring:
cloud:
gateway:
routes:
- id: 资源路由
uri: lb://资源服务
predicates:
- Path=/资源路径/**
filters:
- name: GlobalLockFilter
args:
lockKey: 资源锁键
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class GlobalLockFilter implements GlobalFilter, Ordered {
@Autowired
private RedisLock redisLock;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
String lockKey = "资源锁键";
String value = System.currentTimeMillis() + "";
if (redisLock.tryLock(lockKey, value, 10)) {
try {
return chain.filter(exchange);
} finally {
redisLock.unlock(lockKey);
}
} else {
response.setStatusCode(HttpStatus.SERVICE_UNAVAILABLE);
return response.setComplete();
}
}
@Override
public int getOrder() {
return -1;
}
}
- 在分布式事务中的全局锁应用
- 分布式事务背景:在微服务架构中,一个业务操作可能会涉及多个微服务之间的数据交互,这就需要引入分布式事务来保证数据的一致性。例如,在一个订单创建的业务中,可能会涉及订单服务、库存服务、支付服务等多个服务。
- 全局锁作用:在分布式事务中,全局锁可以用于保证事务操作的原子性和隔离性。以两阶段提交(2PC)为例,在准备阶段,各个参与事务的微服务可以先获取全局锁,确保在提交阶段不会有其他实例对相关资源进行修改。这样可以避免在分布式事务执行过程中出现数据不一致的情况。
- 代码示例(以 Seata 分布式事务框架结合 Redis 全局锁为例):
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
@Autowired
private RedisLock redisLock;
@Autowired
private OrderService orderService;
@Autowired
private StockService stockService;
@Autowired
private PaymentService paymentService;
@PostMapping("/createOrder")
@GlobalTransactional
public String createOrder(Order order) {
String lockKey = "订单创建锁";
String value = System.currentTimeMillis() + "";
if (redisLock.tryLock(lockKey, value, 10)) {
try {
orderService.createOrder(order);
stockService.deductStock(order.getProductId(), order.getQuantity());
paymentService.pay(order.getOrderId(), order.getTotalAmount());
return "订单创建成功";
} finally {
redisLock.unlock(lockKey);
}
} else {
return "订单创建失败,锁获取失败";
}
}
}
全局锁实现中的常见问题及解决方案
- 死锁问题
- 死锁原因:死锁是全局锁实现中常见的问题之一。在基于 Redis 的全局锁中,如果没有设置锁的过期时间,当持有锁的服务实例出现故障或异常,无法主动释放锁时,就会导致其他实例永远无法获取锁,从而形成死锁。在基于 ZooKeeper 的全局锁中,如果在监听节点删除事件时出现异常,导致监听逻辑无法正确执行,也可能出现死锁情况。
- 解决方案:对于基于 Redis 的全局锁,设置合理的锁过期时间是关键。可以根据业务执行时间的预估来设置过期时间,例如,如果一个业务操作通常在 5 秒内完成,可以将锁的过期时间设置为 10 秒。同时,在业务代码中,要确保在获取锁后及时释放锁,无论业务执行成功还是失败。对于基于 ZooKeeper 的全局锁,要确保监听逻辑的健壮性,对可能出现的异常进行捕获和处理,保证监听能够正常工作,及时获取锁。
- 锁的性能问题
- 性能瓶颈:在高并发场景下,全局锁的性能可能会成为系统的瓶颈。基于数据库的全局锁由于数据库的写入性能限制,在大量并发请求获取锁时,会导致数据库压力增大,响应时间变长。基于 ZooKeeper 的全局锁,由于频繁的节点创建和删除操作,也会给 ZooKeeper 服务器带来较大压力,影响锁的获取和释放效率。
- 解决方案:对于基于数据库的全局锁,可以考虑使用数据库连接池来提高数据库连接的复用率,减少连接创建和销毁的开销。同时,可以对锁表进行分库分表操作,分散数据库压力。对于基于 ZooKeeper 的全局锁,可以优化 ZooKeeper 的配置,如调整会话超时时间、心跳间隔等参数,提高其性能。另外,可以采用缓存机制,将一些频繁访问的锁信息缓存到内存中,减少对 ZooKeeper 的直接访问。
- 锁的一致性问题
- 一致性挑战:在分布式环境下,保证全局锁的一致性是一个挑战。基于 Redis 的主从复制模式下,可能会出现锁的短暂丢失问题,导致一致性被破坏。基于 ZooKeeper 的全局锁,虽然其自身具备较强的一致性保证,但在网络分区等异常情况下,也可能出现一致性问题。
- 解决方案:对于基于 Redis 的全局锁,可以采用 Redlock 算法。Redlock 算法通过向多个独立的 Redis 实例获取锁,只有当大多数实例都获取到锁时,才认为获取锁成功。这样可以在一定程度上提高锁的一致性。对于基于 ZooKeeper 的全局锁,要确保 ZooKeeper 集群的稳定性,及时处理网络分区等异常情况。同时,可以通过增加 ZooKeeper 节点数量,提高其容错能力,保证锁的一致性。
不同场景下全局锁实现的选择策略
- 高并发且对性能要求极高的场景
- 选择建议:在这种场景下,基于 Redis 的全局锁是一个较好的选择。Redis 的高性能和对分布式部署的支持,能够满足高并发情况下快速获取和释放锁的需求。例如,在一些实时性要求较高的金融交易系统中,大量的交易请求需要快速获取锁来保证交易的原子性,Redis 的高性能可以有效减少锁的获取时间,提高系统的吞吐量。
- 优化措施:为了进一步提高性能,可以采用集群模式部署 Redis,利用 Redis Cluster 来分担负载,提高系统的可用性和性能。同时,合理设置锁的过期时间,避免因过期时间设置不当导致的并发问题。
- 对数据一致性要求极高的场景
- 选择建议:基于 ZooKeeper 的全局锁更适合这种场景。ZooKeeper 的数据一致性协议(ZAB 协议)能够保证锁的强一致性,在一些对数据一致性要求极高的业务场景,如银行转账、库存管理等,使用 ZooKeeper 全局锁可以有效避免数据不一致的问题。
- 优化措施:为了减轻 ZooKeeper 服务器的压力,可以对锁的使用进行优化,减少不必要的锁操作。例如,尽量合并一些可以批量处理的业务操作,减少锁的获取和释放次数。同时,合理配置 ZooKeeper 集群的节点数量和参数,提高其性能和稳定性。
- 系统简单且对成本敏感的场景
- 选择建议:基于数据库的全局锁在这种场景下具有一定的优势。其实现相对简单,不需要额外引入复杂的中间件,对于一些规模较小、业务逻辑相对简单的系统来说,使用数据库全局锁可以降低开发和运维成本。
- 优化措施:为了提高数据库全局锁的性能,可以对数据库进行优化,如创建合适的索引,优化 SQL 语句等。同时,合理控制锁的持有时间,避免长时间占用数据库连接资源。
总结不同全局锁实现方式的特点
- 基于 Redis 的全局锁
- 性能:性能高,能够满足高并发场景下快速获取和释放锁的需求。
- 一致性:在主从复制模式下存在一致性问题,但可以通过 Redlock 算法等方式提高一致性。
- 实现复杂度:实现相对简单,利用 Redis 的原子操作即可实现。
- 适用场景:适用于高并发且对性能要求极高的场景,如实时性要求较高的交易系统。
- 基于 ZooKeeper 的全局锁
- 性能:性能相对较低,在高并发场景下频繁的节点操作会给 ZooKeeper 服务器带来压力。
- 一致性:具备强一致性,能够有效保证数据的一致性。
- 实现复杂度:实现相对复杂,需要对 ZooKeeper 的原理和操作有深入了解。
- 适用场景:适用于对数据一致性要求极高的场景,如银行转账、库存管理等业务。
- 基于数据库的全局锁
- 性能:性能较低,数据库的写入操作性能瓶颈会影响锁的效率。
- 一致性:通过数据库的事务特性保证一致性。
- 实现复杂度:实现简单,利用数据库的事务和唯一索引即可实现。
- 适用场景:适用于系统简单且对成本敏感的场景,如小型业务系统。
通过对 Spring Cloud 全局锁不同实现方式的深入探究,我们可以根据具体的业务场景和需求,选择合适的全局锁实现方式,从而保证微服务架构下系统的稳定性、一致性和高性能。在实际应用中,还需要不断优化和调整全局锁的使用,以适应业务的发展和变化。