侧边栏壁纸
博主头像
博客技术 博主等级

行动起来,活在当下

  • 累计撰写 42 篇文章
  • 累计创建 4 个标签
  • 累计收到 3 条评论

目 录CONTENT

文章目录

分布式锁 死锁问题

Administrator
2025-03-02 / 0 评论 / 0 点赞 / 5 阅读 / 0 字

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.总结

通过一步步的探索和解决,我终于实现了稳定的分布式锁,解决了秒杀功能中的超卖问题

0

评论区