1.分布式集群架构下怎么去保证并发安全
1.初出茅庐,信心满满
刚开始,我开发一个秒杀功能。经过一番需求分析,我觉得这个功能挺简单的:只需要判断商品的库存是否大于零,如果大于零就扣减库存,否则秒杀失败。我很快就完成了开发,并且信心满满地上了线。
2.第一次线上使用
然而,线上使用没多久,出现了问题。原来,有一个商品的库存明明只有一个,但却卖出了好几份!这下可好,我意识到,这个问题出在多线程并发的情况下,多个线程同时对一个共享资源进行读写,导致了数据错乱。
public class SeckillService {
private int stock = 100; // 假设库存为100
public boolean seckill() {
if (stock > 0) {
stock--;
return true; // 秒杀成功
}
return false; // 秒杀失败
}
}
3.加锁解决超卖问题
为了解决这个问题,我决定在代码中加入同步锁。这样一来,多个线程在访问共享资源时就会互斥,需要排队等待。上线前,我还特意进行了压测,确保这次不会再有超卖的问题。果然,上线后一切正常。
public class SeckillService {
private int stock = 100; // 假设库存为100
private final Object lock = new Object();
public boolean seckill() {
synchronized (lock) {
if (stock > 0) {
stock--;
return true; // 秒杀成功
}
return false; // 秒杀失败
}
}
}
4.用户量激增,性能瓶颈
随着用户量的增加,服务器的压力也越来越大,性能达到了瓶颈。不过,这次我不慌了,因为我学过Nginx负载均衡技术。我通过Nginx将服务器进行了水平扩展,实现了分布式集群部署。
5.分布式部署,超卖再现
然而,在压测时,我发现吞吐量虽然上来了,但秒杀功能又出现了超卖问题。经过一番研究,我发现问题出在同步锁上。因为同步锁是JVM级别的,只能锁住单个进程。在分布式部署后,每台服务器在并发情况下只能锁住一个线程,导致超卖问题再次出现。
6.分布式锁的引入
为了解决这个问题,我研究了一晚上,发现可以通过分布式锁来解决。主流的分布式锁解决方案有Redis和Zookeeper。由于我们的系统已经使用了Redis,我决定采用Redis来实现分布式锁。
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class SeckillService {
private int stock = 100; // 假设库存为100
private RedissonClient redisson;
public SeckillService() {
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
redisson = Redisson.create(config);
}
public boolean seckill() {
String lockKey = "seckill_lock";
RLock lock = redisson.getLock(lockKey);
try {
// 尝试加锁,最多等待100秒,上锁后10秒自动解锁
boolean isLocked = lock.tryLock(100, 10, java.util.concurrent.TimeUnit.SECONDS);
if (isLocked) {
if (stock > 0) {
stock--;
return true; // 秒杀成功
}
return false; // 秒杀失败
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 释放锁
}
return false; // 获取锁失败
}
}
7.Redis的SETNX命令
我发现,通过Redis的SETNX命令可以非常简单地实现分布式锁。SETNX的特性是:当一个线程往Redis中存储一个值时,如果这个键不存在,它会存储这个值并返回true;如果键已经存在,它会返回false。通过这个特性,我实现了分布式锁。
import redis.clients.jedis.Jedis;
public class SeckillService {
private int stock = 100; // 假设库存为100
private Jedis jedis = new Jedis("localhost", 6379);
public boolean seckill() {
String lockKey = "seckill_lock";
String requestId = String.valueOf(Thread.currentThread().getId());
int expireTime = 10000; // 锁的过期时间10秒
// 尝试获取锁
Long result = jedis.setnx(lockKey, requestId);
if (result == 1) {
jedis.expire(lockKey, expireTime); // 设置过期时间
try {
if (stock > 0) {
stock--;
return true; // 秒杀成功
}
return false; // 秒杀失败
} finally {
// 释放锁
if (requestId.equals(jedis.get(lockKey))) {
jedis.del(lockKey);
}
}
}
return false; // 获取锁失败
}
}
8.锁的过期时间
在测试过程中,我发现如果服务器挂掉了,其他服务器的请求会一直阻塞,因为挂掉的服务器一直持有锁,导致死锁。为了解决这个问题,我给锁加了一个过期时间。这样,即使服务器挂掉,锁也会在一定时间后自动释放,不影响其他服务器的正常请求。
9.业务处理时间超过锁的过期时间
随着业务的扩展,我又遇到了新的问题。当业务的处理时间超过了锁的过期时间时,锁会自动释放,其他线程就会趁虚而入,导致超卖问题再次出现。这其实是两个问题:一是锁的过期时间不够长,二是业务代码处理完后释放了其他线程的锁。
10.解决方案
针对第一个问题,我加长了锁的过期时间,并且增加了一个兜底方案:在业务代码中添加了一个子线程,每10秒去确认主线程是否在线。如果在线,就将过期时间重设,实现锁的续命。
针对第二个问题,我给锁增加了一个唯一ID,确保每把锁的key与当前线程绑定,从而不会释放其他线程的锁。
11.Redisson组件
不过,我发现要自己实现这些功能非常麻烦,还得保证代码的健壮性。于是,我找到了Redis提供的Redisson组件,它可以轻松实现分布式锁。只需要添加Redisson的依赖,然后通过Redisson客户端自动装配,调用lock()方法即可。
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.RedissonRedLock;
public class SeckillService {
private int stock = 100; // 假设库存为100
private RedissonClient redisson1;
private RedissonClient redisson2;
private RedissonClient redisson3;
public SeckillService() {
Config config1 = new Config();
config1.useSingleServer().setAddress("redis://localhost:6379");
redisson1 = Redisson.create(config1);
Config config2 = new Config();
config2.useSingleServer().setAddress("redis://localhost:6380");
redisson2 = Redisson.create(config2);
Config config3 = new Config();
config3.useSingleServer().setAddress("redis://localhost:6381");
redisson3 = Redisson.create(config3);
}
public boolean seckill() {
String lockKey = "seckill_lock";
RLock lock1 = redisson1.getLock(lockKey);
RLock lock2 = redisson2.getLock(lockKey);
RLock lock3 = redisson3.getLock(lockKey);
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
// 尝试加锁,最多等待100秒,上锁后10秒自动解锁
boolean isLocked = redLock.tryLock(100, 10, java.util.concurrent.TimeUnit.SECONDS);
if (isLocked) {
if (stock > 0) {
stock--;
return true; // 秒杀成功
}
return false; // 秒杀失败
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
redLock.unlock(); // 释放锁
}
return false; // 获取锁失败
}
}
12.Redisson的原理
Redisson的原理是:多个线程同时请求竞争锁,只有一个线程能获取到锁。获取锁的线程会有一个watchdog任务,每隔10秒检查当前线程是否还持有锁。如果持有,就延长锁的生存时间,实现锁的续命。
13.Redis主从集群的问题
在使用Redis主从集群时,如果主节点挂掉了,可能会导致锁丢失。为了解决这个问题,Redis提供了RedLock机制。RedLock会保证锁在所有节点上都存储成功后才返回成功,从而保证强一致性。
14.总结
通过一步步的探索和解决,我终于实现了稳定的分布式锁,解决了秒杀功能中的超卖问题
评论区