最近在做一个保险业务系统时遇到了一个典型的高并发问题:多个客服同时操作同一份保单,导致数据错乱。比如客服A正在修改保单信息,客服B同时也在修改,最后保存的数据就乱套了。这种场景在电商秒杀、金融交易等系统中也很常见。
传统单机锁(如synchronized)在分布式环境下完全失效,因为多个服务实例运行在不同机器上。这时候就需要分布式锁来保证同一时刻只有一个实例能操作共享资源。Redis凭借其高性能和原子性操作,成为实现分布式锁的首选方案。
我刚开始用Redis锁时踩过不少坑。有一次忘记设置过期时间,结果系统崩溃后锁永远无法释放,导致整个系统瘫痪。还有一次设置的过期时间太短,业务还没执行完锁就自动释放了,引发数据不一致。这些问题都源于对setIfAbsent和expire两个关键命令的理解不够深入。
setIfAbsent是Redis实现分布式锁的核心命令,它的作用是只有当key不存在时才设置值。用Java代码表示是这样的:
java复制Boolean result = redisTemplate.opsForValue()
.setIfAbsent("policy_lock_123", "request_456", 30, TimeUnit.SECONDS);
这个操作是原子性的,意味着判断key是否存在和设置值这两个动作不会被其他命令打断。如果多个线程同时执行这个命令,Redis会确保只有一个能成功返回true。
我在实际项目中发现,value应该设置为唯一标识(如UUID),这样在释放锁时可以验证是否是自己加的锁,避免误删别人的锁。曾经有个线上事故就是因为value设置成固定值,导致锁被错误释放。
expire命令用于设置key的过期时间,这是避免死锁的关键保障。即使持有锁的客户端崩溃,锁也会在过期后自动释放。但要注意两个细节:
在Spring Data Redis中,可以直接在setIfAbsent时设置过期时间,这是最推荐的做法:
java复制// 推荐:原子性设置值和过期时间
redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, 30, TimeUnit.SECONDS);
// 不推荐:分两步操作可能产生竞态条件
if(redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue)) {
redisTemplate.expire(lockKey, 30, TimeUnit.SECONDS);
}
下面是一个经过生产验证的保单处理锁实现:
java复制public class PolicyLockService {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String LOCK_PREFIX = "policy_lock_";
private static final int LOCK_EXPIRE = 30; // 秒
public boolean processPolicy(String policyId, PolicyProcessor processor) {
String lockKey = LOCK_PREFIX + policyId;
String lockValue = UUID.randomUUID().toString();
try {
// 尝试获取锁
Boolean acquired = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, LOCK_EXPIRE, TimeUnit.SECONDS);
if(Boolean.TRUE.equals(acquired)) {
// 执行业务逻辑
return processor.process();
} else {
throw new BusyException("保单正在被其他客服处理");
}
} finally {
// 释放锁
String script =
"if redis.call('get',KEYS[1]) == ARGV[1] then " +
" return redis.call('del',KEYS[1]) " +
"else " +
" return 0 " +
"end";
redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey),
lockValue);
}
}
public interface PolicyProcessor {
boolean process();
}
}
这个实现有几个关键点:
在高并发场景下,直接返回"系统繁忙"并不友好。我们可以采用以下策略优化用户体验:
java复制int retry = 0;
while(retry++ < 3) {
if(tryLock(policyId)) {
try {
return processPolicy();
} finally {
unlock(policyId);
}
}
Thread.sleep(100 * retry); // 指数退避
}
throw new BusyException("当前操作人数过多,请稍后再试");
在Redis集群环境下,需要考虑跨节点锁一致性问题。官方推荐的Redlock算法需要至少5个主节点才能保证可靠性,对大多数中小系统来说成本太高。实际项目中,如果业务可以接受偶尔的锁失效,单Redis节点+合理过期时间通常就够了。
我曾经遇到一个案例:某金融系统使用3节点Redis集群,由于网络分区导致多个客户端同时获取锁。后来我们通过以下措施降低风险:
分布式锁的异常必须及时发现。建议监控以下指标:
在Grafana中可以这样配置报警规则:
sql复制# 锁等待时间报警
avg(redis_lock_wait_seconds{application="$application"}) by (lock_type) > 1
# 锁冲突报警
increase(redis_lock_conflict_total{application="$application"}[1m]) > 50
在保单处理场景中,我们最终将平均处理时间从800ms优化到300ms,关键就是细化了锁粒度: