背景历史
在电商系统中,秒杀抢购活动是一种常见的营销手段。它通过设定极低的价格和有限的商品数量,吸引大量用户在特定时间点抢购,从而迅速增加销量、提升品牌曝光度和用户活跃度。然而,这种活动也对系统的性能和稳定性提出了极高的要求。特别是在秒杀开始的瞬间,系统需要处理海量的并发请求,同时确保数据的准确性和一致性。
为了解决这些问题,系统开发者们引入了锁机制。锁机制是一种用于控制对共享资源的并发访问的技术,它能够确保在同一时间只有一个进程或线程能够操作某个资源,从而避免数据不一致或冲突。在秒杀抢购场景下,锁机制显得尤为重要,它能够保证商品库存的扣减操作是原子性的,避免出现超卖或数据不一致的情况。
锁机制的发展经历了从单机锁到分布式锁的过程。早期的系统大多运行在单机环境中,因此主要使用JVM级别的锁,如synchronized
和ReentrantLock
等。随着分布式系统的兴起,传统的JVM级别锁已经无法满足需求,于是分布式锁应运而生。分布式锁能够在多个节点之间协调对共享资源的访问,确保数据的一致性和系统的稳定性。
业务场景
秒杀抢购活动通常具有以下几个特点:
- 瞬时大流量:秒杀活动吸引大量用户参与,活动开始时会有海量的并发请求涌入系统。
- 热点数据:用户通常抢购的是同一商品,因此该商品的库存数据会成为热点数据,需要频繁读写。
- 数据一致性:秒杀活动需要确保数据的一致性,避免出现超卖或数据不一致的情况。
在秒杀抢购场景下,锁机制主要用于以下几个方面:
- 库存扣减:在用户下单时,需要确保库存扣减操作是原子性的,避免出现多个请求同时扣减库存导致超卖的情况。
- 订单生成:在用户下单成功后,需要生成订单并扣减库存,这个过程也需要确保原子性。
- 支付确认:在用户支付成功后,需要确认支付并释放库存,这个过程同样需要确保原子性。
底层原理
JVM级别锁
JVM级别锁是运行在单JVM进程中的锁机制,它主要通过Java对象头中的锁标记来实现。在Java中,每个对象都有一个对象头,对象头中包含了锁标记、哈希码等信息。根据锁的状态不同,锁标记的内容也会有所不同。
JVM级别锁主要包括以下几种:
- synchronized :
synchronized
是Java中最基本的锁机制,它可以用于修饰方法或代码块。当线程进入synchronized
修饰的方法或代码块时,会尝试获取对象的锁。如果锁已经被其他线程持有,则当前线程会进入阻塞状态,直到锁被释放。
synchronized
锁的实现原理可以归纳为以下几个步骤:
- 获取锁 :当线程进入
synchronized
修饰的方法或代码块时,会检查对象头中的锁标记。如果锁标记为未加锁状态,则当前线程会尝试获取锁,并将锁标记设置为锁定状态。如果锁已经被其他线程持有,则当前线程会进入阻塞状态。 - 释放锁 :当线程退出
synchronized
修饰的方法或代码块时,会释放锁,并将锁标记设置为未加锁状态。其他等待的线程会重新尝试获取锁。
synchronized
锁具有以下几个特点:
- 可重入:同一个线程可以多次获取同一个锁,而不会导致死锁。
- 不可中断:获取锁的线程在锁被释放之前无法被中断。
- 公平锁/非公平锁 :
synchronized
锁默认是非公平锁,即线程获取锁的顺序不是按照请求的顺序来的。
- ReentrantLock :
ReentrantLock
是Java中另一种常用的锁机制,它提供了比synchronized
更灵活的锁控制。ReentrantLock
实现了Lock
接口,支持显式地获取和释放锁,以及设置锁的超时时间等。
ReentrantLock
的实现原理与synchronized
类似,也是通过改变对象头中的锁标记来实现锁的控制。不过,ReentrantLock
提供了更多的功能,如可重入性、公平锁/非公平锁、锁超时等。
- ReadWriteLock :
ReadWriteLock
是一种读写锁,它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。ReadWriteLock
提供了更好的并发性能,适用于读多写少的场景。 - StampedLock :
StampedLock
是Java 8中引入的一种新的锁机制,它提供了一种乐观读锁的机制。StampedLock
允许线程在没有获取锁的情况下读取共享资源,但如果读取过程中资源被修改,则读取操作会失败。StampedLock
适用于读多写少的场景,且读操作对性能要求较高的场景。
分布式锁
分布式锁是运行在多个节点之间的锁机制,它能够在多个节点之间协调对共享资源的访问。分布式锁的实现通常依赖于一些外部的、可靠的存储或服务,如Redis、ZooKeeper、数据库等。
分布式锁主要包括以下几种:
- 基于数据库的分布式锁:基于数据库的分布式锁通过在数据库中创建一个锁表来实现。锁表中包含锁的名称和锁的状态等信息。当一个节点需要获取锁时,它会在锁表中插入一条记录。如果插入成功,则表示该节点获取到了锁;如果插入失败(因为其他节点已经插入了相同的记录),则表示该节点获取锁失败。当节点使用完锁后,会删除锁表中的记录以释放锁。
基于数据库的分布式锁具有实现简单的优点,但性能较低,且如果数据库出现故障,可能会影响到锁的功能。
- 基于Redis的分布式锁:基于Redis的分布式锁利用Redis的SETNX命令和EXPIRE命令来实现。SETNX命令用于在key不存在时设置值,这可以确保在同一时间只有一个客户端能够获得锁。EXPIRE命令用于为key设置过期时间,这可以避免死锁的情况。当一个客户端需要获取锁时,它会尝试使用SETNX命令来设置锁。如果命令返回OK,则表示客户端成功获取了锁;如果返回nil,则表示锁已被其他客户端持有。客户端在获取锁后,可以使用EXPIRE命令为锁设置过期时间。当客户端完成操作后,需要使用DEL命令来释放锁。
基于Redis的分布式锁具有性能高、实现简单的优点,但默认是不可重入的,且如果Redis服务器出现故障,可能会导致锁无法正常工作。
- 基于ZooKeeper的分布式锁:基于ZooKeeper的分布式锁利用ZooKeeper的临时节点来实现。当一个节点需要获取锁时,它会尝试在ZooKeeper中创建一个临时节点。如果创建成功,则表示该节点获取到了锁;如果创建失败(因为其他节点已经创建了相同的临时节点),则表示该节点获取锁失败。当节点使用完锁后,会删除临时节点以释放锁。如果节点崩溃,ZooKeeper会自动删除临时节点,从而避免了死锁的问题。
基于ZooKeeper的分布式锁具有高效且可靠的优点,但实现相对复杂一些。
- 基于Etcd的分布式锁:基于Etcd的分布式锁利用Etcd的键值存储系统来实现。当一个节点需要获取锁时,它会尝试在Etcd中创建一个带有TTL(Time To Live)的键值对。如果创建成功,则表示该节点获取到了锁;如果创建失败(因为其他节点已经创建了相同的键值对),则表示该节点获取锁失败。当节点使用完锁后,会删除键值对以释放锁。如果TTL过期而节点仍未释放锁,Etcd会自动删除键值对以释放锁。
基于Etcd的分布式锁具有实现简单、性能较高的优点,但同样需要处理Redis服务器故障等潜在问题。
Java代码实现
JVM级别锁实现
以下是一个使用synchronized
关键字实现秒杀抢购功能的Java代码示例:
java
public class SeckillService {
// 商品库存
private int stock = 10;
// 秒杀方法
public synchronized boolean seckill(String userId) {
if (stock <= 0) {
return false; // 库存不足
}
stock--; // 扣减库存
System.out.println(userId + " 秒杀成功!剩余库存:" + stock);
return true;
}
public static void main(String[] args) {
SeckillService service = new SeckillService();
// 模拟多个用户同时秒杀
for (int i = 0; i < 20; i++) {
new Thread(() -> {
String userId = "用户" + Thread.currentThread().getId();
service.seckill(userId);
}).start();
}
}
}
在这个示例中,SeckillService
类中的seckill
方法使用了synchronized
关键字进行修饰,以确保在同一时间只有一个线程能够执行该方法。当库存扣减成功后,会打印出秒杀成功的用户ID和剩余库存。
以下是一个使用ReentrantLock
实现秒杀抢购功能的Java代码示例:
java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SeckillService {
// 商品库存
private int stock = 10;
// ReentrantLock锁
private final Lock lock = new ReentrantLock();
// 秒杀方法
public boolean seckill(String userId) {
lock.lock(); // 获取锁
try {
if (stock <= 0) {
return false; // 库存不足
}
stock--; // 扣减库存
System.out.println(userId + " 秒杀成功!剩余库存:" + stock);
return true;
} finally {
lock.unlock(); // 释放锁
}
}
public static void main(String[] args) {
SeckillService service = new SeckillService();
// 模拟多个用户同时秒杀
for (int i = 0; i < 20; i++) {
new Thread(() -> {
String userId = "用户" + Thread.currentThread().getId();
service.seckill(userId);
}).start();
}
}
}
在这个示例中,SeckillService
类中使用了一个ReentrantLock
对象作为锁。在seckill
方法中,通过调用lock.lock()
方法来获取锁,并在finally
块中调用lock.unlock()
方法来释放锁。这样可以确保在出现异常时也能够正确释放锁。
分布式锁实现
以下是一个使用Redis实现分布式锁的Java代码示例:
java
import redis.clients.jedis.Jedis;
public class RedisDistributedLock {
private final Jedis jedis;
private final String lockKey;
private final String uniqueValue;
private final int lockTimeout;
public RedisDistributedLock(Jedis jedis, String lockKey, int lockTimeout) {
this.jedis = jedis;
this.lockKey = lockKey;
this.uniqueValue = UUID.randomUUID().toString(); // 生成唯一值作为锁的持有者标识
this.lockTimeout = lockTimeout;
}
// 尝试获取锁
public boolean tryLock() {
String result = jedis.set(lockKey, uniqueValue, "NX", "PX", lockTimeout);
return "OK".equals(result);
}
// 释放锁
public void unlock() {
// 使用Lua脚本确保只有锁的持有者才能释放锁
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(script, 1, lockKey, uniqueValue);
}
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
RedisDistributedLock lock = new RedisDistributedLock(jedis, "seckill_lock", 10000); // 锁超时时间为10秒
// 模拟多个用户同时秒杀
for (int i = 0; i < 20; i++) {
new Thread(() -> {
if (lock.tryLock()) {
try {
// 执行秒杀操作
System.out.println(Thread.currentThread().getId() + " 秒杀成功!");
} finally {
lock.unlock(); // 确保释放锁
}
} else {
System.out.println(Thread.currentThread().getId() + " 秒杀失败,锁已被占用");
}
}).start();
}
}
}
在这个示例中,RedisDistributedLock
类封装了Redis分布式锁的实现。构造函数中接收Jedis对象、锁键名、锁超时时间等参数。tryLock
方法尝试获取锁,如果获取成功则返回true,否则返回false。unlock
方法使用Lua脚本确保只有锁的持有者才能释放锁。在main
方法中,模拟了多个用户同时秒杀的场景,每个线程都会尝试获取锁并执行秒杀操作。
以下是一个使用ZooKeeper实现分布式锁的Java代码示例(需要引入ZooKeeper的客户端库):
java
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
public class ZookeeperDistributedLock {
private final ZooKeeper zooKeeper;
private final String lockPath;
private final CountDownLatch connectedSignal = new CountDownLatch(1);
public ZookeeperDistributedLock(String connectString, int sessionTimeout, String lockPath) throws IOException, InterruptedException {
zooKeeper = new ZooKeeper(connectString, sessionTimeout, event -> {
if (event.getState() == Event.KeeperState.SyncConnected) {
connectedSignal.countDown();
}
});
connectedSignal.await();
this.lockPath = lockPath;
}
// 尝试获取锁
public boolean tryLock() throws KeeperException, InterruptedException {
String createPath = zooKeeper.create(lockPath + "/lock_", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
String lockName = createPath.substring(lockPath.length() + 1);
List<String> children = zooKeeper.getChildren(lockPath, false);
children.sort(String::compareTo);
if (lockName.equals(children.get(0))) {
return true; // 获取锁成功
} else {
String previousSequenceNode = lockPath + "/" + children.get(0);
Stat stat = zooKeeper.exists(previousSequenceNode, false);
if (stat != null) {
zooKeeper.getData(previousSequenceNode, true, new Watcher() {
@Override
public void process(WatchedEvent event) {
if (event.getType() == Event.EventType.NodeDeleted) {
try {
tryLock();
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
}
}
});
}
return false; // 获取锁失败
}
}
// 释放锁
public void unlock() throws KeeperException, InterruptedException {
zooKeeper.delete(lockPath + "/lock_" + Thread.currentThread().getId(), -1);
}
public static void main(String[] args) throws Exception {
ZookeeperDistributedLock lock = new ZookeeperDistributedLock("localhost:2181", 3000, "/locks");
// 模拟多个用户同时秒杀
for (int i = 0; i < 20; i++) {
new Thread(() -> {
try {
if (lock.tryLock()) {
try {
// 执行秒杀操作
System.out.println(Thread.currentThread().getId() + " 秒杀成功!");
} finally {
lock.unlock(); // 确保释放锁
}
} else {
System.out.println(Thread.currentThread().getId() + " 秒杀失败,锁已被占用");
}
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
}
在这个示例中,ZookeeperDistributedLock
类封装了ZooKeeper分布式锁的实现。构造函数中接收ZooKeeper连接字符串、会话超时时间、锁路径等参数。tryLock
方法尝试获取锁,如果获取成功则返回true,否则返回false。在获取锁的过程中,会创建一个临时顺序节点,并根据节点的序号来判断是否获取到锁。如果当前节点是序号最小的节点,则表示获取锁成功;否则,会监听序号最小的节点的删除事件,以便在该节点被删除时重新尝试获取锁。unlock
方法用于释放锁,即删除当前节点。在main
方法中,模拟了多个用户同时秒杀的场景,每个线程都会尝试获取锁并执行秒杀操作。
总结
在秒杀抢购场景下,锁机制是确保数据一致性和系统稳定性的关键。JVM级别锁适用于单机环境,具有实现简单、性能较高等优点;而分布式锁则适用于分布式环境,能够在多个节点之间协调对共享资源的访问。在实际应用中,可以根据具体场景选择合适的锁机制来实现秒杀抢购功能。
通过本文的介绍,相信读者已经对JVM级别锁和分布式锁有了更深入的了解,并能够在实际项目中灵活运用这些技术来解决并发访问和数据一致性问题。