秒杀抢购场景下实战JVM级别锁与分布式锁

简介: 在电商系统中,秒杀抢购活动是一种常见的营销手段。它通过设定极低的价格和有限的商品数量,吸引大量用户在特定时间点抢购,从而迅速增加销量、提升品牌曝光度和用户活跃度。然而,这种活动也对系统的性能和稳定性提出了极高的要求。特别是在秒杀开始的瞬间,系统需要处理海量的并发请求,同时确保数据的准确性和一致性。为了解决这些问题,系统开发者们引入了锁机制。锁机制是一种用于控制对共享资源的并发访问的技术,它能够确保在同一时间只有一个进程或线程能够操作某个资源,从而避免数据不一致或冲突。在秒杀抢购场景下,锁机制显得尤为重要,它能够保证商品库存的扣减操作是原子性的,避免出现超卖或数据不一致的情况。


背景历史

在电商系统中,秒杀抢购活动是一种常见的营销手段。它通过设定极低的价格和有限的商品数量,吸引大量用户在特定时间点抢购,从而迅速增加销量、提升品牌曝光度和用户活跃度。然而,这种活动也对系统的性能和稳定性提出了极高的要求。特别是在秒杀开始的瞬间,系统需要处理海量的并发请求,同时确保数据的准确性和一致性。

为了解决这些问题,系统开发者们引入了锁机制。锁机制是一种用于控制对共享资源的并发访问的技术,它能够确保在同一时间只有一个进程或线程能够操作某个资源,从而避免数据不一致或冲突。在秒杀抢购场景下,锁机制显得尤为重要,它能够保证商品库存的扣减操作是原子性的,避免出现超卖或数据不一致的情况。

锁机制的发展经历了从单机锁到分布式锁的过程。早期的系统大多运行在单机环境中,因此主要使用JVM级别的锁,如synchronizedReentrantLock等。随着分布式系统的兴起,传统的JVM级别锁已经无法满足需求,于是分布式锁应运而生。分布式锁能够在多个节点之间协调对共享资源的访问,确保数据的一致性和系统的稳定性。

业务场景

秒杀抢购活动通常具有以下几个特点:

  1. 瞬时大流量:秒杀活动吸引大量用户参与,活动开始时会有海量的并发请求涌入系统。
  2. 热点数据:用户通常抢购的是同一商品,因此该商品的库存数据会成为热点数据,需要频繁读写。
  3. 数据一致性:秒杀活动需要确保数据的一致性,避免出现超卖或数据不一致的情况。

在秒杀抢购场景下,锁机制主要用于以下几个方面:

  1. 库存扣减:在用户下单时,需要确保库存扣减操作是原子性的,避免出现多个请求同时扣减库存导致超卖的情况。
  2. 订单生成:在用户下单成功后,需要生成订单并扣减库存,这个过程也需要确保原子性。
  3. 支付确认:在用户支付成功后,需要确认支付并释放库存,这个过程同样需要确保原子性。

底层原理

JVM级别锁

JVM级别锁是运行在单JVM进程中的锁机制,它主要通过Java对象头中的锁标记来实现。在Java中,每个对象都有一个对象头,对象头中包含了锁标记、哈希码等信息。根据锁的状态不同,锁标记的内容也会有所不同。

JVM级别锁主要包括以下几种:

  1. synchronizedsynchronized是Java中最基本的锁机制,它可以用于修饰方法或代码块。当线程进入synchronized修饰的方法或代码块时,会尝试获取对象的锁。如果锁已经被其他线程持有,则当前线程会进入阻塞状态,直到锁被释放。

synchronized锁的实现原理可以归纳为以下几个步骤:

  • 获取锁:当线程进入synchronized修饰的方法或代码块时,会检查对象头中的锁标记。如果锁标记为未加锁状态,则当前线程会尝试获取锁,并将锁标记设置为锁定状态。如果锁已经被其他线程持有,则当前线程会进入阻塞状态。
  • 释放锁:当线程退出synchronized修饰的方法或代码块时,会释放锁,并将锁标记设置为未加锁状态。其他等待的线程会重新尝试获取锁。

synchronized锁具有以下几个特点:

  • 可重入:同一个线程可以多次获取同一个锁,而不会导致死锁。
  • 不可中断:获取锁的线程在锁被释放之前无法被中断。
  • 公平锁/非公平锁synchronized锁默认是非公平锁,即线程获取锁的顺序不是按照请求的顺序来的。
  1. ReentrantLockReentrantLock是Java中另一种常用的锁机制,它提供了比synchronized更灵活的锁控制。ReentrantLock实现了Lock接口,支持显式地获取和释放锁,以及设置锁的超时时间等。

ReentrantLock的实现原理与synchronized类似,也是通过改变对象头中的锁标记来实现锁的控制。不过,ReentrantLock提供了更多的功能,如可重入性、公平锁/非公平锁、锁超时等。

  1. ReadWriteLockReadWriteLock是一种读写锁,它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。ReadWriteLock提供了更好的并发性能,适用于读多写少的场景。
  2. StampedLockStampedLock是Java 8中引入的一种新的锁机制,它提供了一种乐观读锁的机制。StampedLock允许线程在没有获取锁的情况下读取共享资源,但如果读取过程中资源被修改,则读取操作会失败。StampedLock适用于读多写少的场景,且读操作对性能要求较高的场景。
分布式锁

分布式锁是运行在多个节点之间的锁机制,它能够在多个节点之间协调对共享资源的访问。分布式锁的实现通常依赖于一些外部的、可靠的存储或服务,如Redis、ZooKeeper、数据库等。

分布式锁主要包括以下几种:

  1. 基于数据库的分布式锁:基于数据库的分布式锁通过在数据库中创建一个锁表来实现。锁表中包含锁的名称和锁的状态等信息。当一个节点需要获取锁时,它会在锁表中插入一条记录。如果插入成功,则表示该节点获取到了锁;如果插入失败(因为其他节点已经插入了相同的记录),则表示该节点获取锁失败。当节点使用完锁后,会删除锁表中的记录以释放锁。

基于数据库的分布式锁具有实现简单的优点,但性能较低,且如果数据库出现故障,可能会影响到锁的功能。

  1. 基于Redis的分布式锁:基于Redis的分布式锁利用Redis的SETNX命令和EXPIRE命令来实现。SETNX命令用于在key不存在时设置值,这可以确保在同一时间只有一个客户端能够获得锁。EXPIRE命令用于为key设置过期时间,这可以避免死锁的情况。当一个客户端需要获取锁时,它会尝试使用SETNX命令来设置锁。如果命令返回OK,则表示客户端成功获取了锁;如果返回nil,则表示锁已被其他客户端持有。客户端在获取锁后,可以使用EXPIRE命令为锁设置过期时间。当客户端完成操作后,需要使用DEL命令来释放锁。

基于Redis的分布式锁具有性能高、实现简单的优点,但默认是不可重入的,且如果Redis服务器出现故障,可能会导致锁无法正常工作。

  1. 基于ZooKeeper的分布式锁:基于ZooKeeper的分布式锁利用ZooKeeper的临时节点来实现。当一个节点需要获取锁时,它会尝试在ZooKeeper中创建一个临时节点。如果创建成功,则表示该节点获取到了锁;如果创建失败(因为其他节点已经创建了相同的临时节点),则表示该节点获取锁失败。当节点使用完锁后,会删除临时节点以释放锁。如果节点崩溃,ZooKeeper会自动删除临时节点,从而避免了死锁的问题。

基于ZooKeeper的分布式锁具有高效且可靠的优点,但实现相对复杂一些。

  1. 基于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级别锁和分布式锁有了更深入的了解,并能够在实际项目中灵活运用这些技术来解决并发访问和数据一致性问题。

相关文章
|
3天前
|
存储 运维 安全
云上金融量化策略回测方案与最佳实践
2024年11月29日,阿里云在上海举办金融量化策略回测Workshop,汇聚多位行业专家,围绕量化投资的最佳实践、数据隐私安全、量化策略回测方案等议题进行深入探讨。活动特别设计了动手实践环节,帮助参会者亲身体验阿里云产品功能,涵盖EHPC量化回测和Argo Workflows量化回测两大主题,旨在提升量化投研效率与安全性。
云上金融量化策略回测方案与最佳实践
|
5天前
|
人工智能 自然语言处理 前端开发
从0开始打造一款APP:前端+搭建本机服务,定制暖冬卫衣先到先得
通义灵码携手科技博主@玺哥超carry 打造全网第一个完整的、面向普通人的自然语言编程教程。完全使用 AI,再配合简单易懂的方法,只要你会打字,就能真正做出一个完整的应用。
5716 18
|
16天前
|
人工智能 自动驾驶 大数据
预告 | 阿里云邀您参加2024中国生成式AI大会上海站,马上报名
大会以“智能跃进 创造无限”为主题,设置主会场峰会、分会场研讨会及展览区,聚焦大模型、AI Infra等热点议题。阿里云智算集群产品解决方案负责人丛培岩将出席并发表《高性能智算集群设计思考与实践》主题演讲。观众报名现已开放。
|
9天前
|
自然语言处理 数据可视化 API
Qwen系列模型+GraphRAG/LightRAG/Kotaemon从0开始构建中医方剂大模型知识图谱问答
本文详细记录了作者在短时间内尝试构建中医药知识图谱的过程,涵盖了GraphRAG、LightRAG和Kotaemon三种图RAG架构的对比与应用。通过实际操作,作者不仅展示了如何利用这些工具构建知识图谱,还指出了每种工具的优势和局限性。尽管初步构建的知识图谱在数据处理、实体识别和关系抽取等方面存在不足,但为后续的优化和改进提供了宝贵的经验和方向。此外,文章强调了知识图谱构建不仅仅是技术问题,还需要深入整合领域知识和满足用户需求,体现了跨学科合作的重要性。
|
4天前
|
人工智能 容器
三句话开发一个刮刮乐小游戏!暖ta一整个冬天!
本文介绍了如何利用千问开发一款情侣刮刮乐小游戏,通过三步简单指令实现从单个功能到整体框架,再到多端优化的过程,旨在为生活增添乐趣,促进情感交流。在线体验地址已提供,鼓励读者动手尝试,探索编程与AI结合的无限可能。
|
1月前
|
存储 人工智能 弹性计算
阿里云弹性计算_加速计算专场精华概览 | 2024云栖大会回顾
2024年9月19-21日,2024云栖大会在杭州云栖小镇举行,阿里云智能集团资深技术专家、异构计算产品技术负责人王超等多位产品、技术专家,共同带来了题为《AI Infra的前沿技术与应用实践》的专场session。本次专场重点介绍了阿里云AI Infra 产品架构与技术能力,及用户如何使用阿里云灵骏产品进行AI大模型开发、训练和应用。围绕当下大模型训练和推理的技术难点,专家们分享了如何在阿里云上实现稳定、高效、经济的大模型训练,并通过多个客户案例展示了云上大模型训练的显著优势。
|
8天前
|
Cloud Native Apache 流计算
PPT合集|Flink Forward Asia 2024 上海站
Apache Flink 年度技术盛会聚焦“回顾过去,展望未来”,涵盖流式湖仓、流批一体、Data+AI 等八大核心议题,近百家厂商参与,深入探讨前沿技术发展。小松鼠为大家整理了 FFA 2024 演讲 PPT ,可在线阅读和下载。
3491 10
PPT合集|Flink Forward Asia 2024 上海站
|
2天前
|
弹性计算 运维 监控
阿里云云服务诊断工具:合作伙伴架构师的深度洞察与优化建议
作为阿里云的合作伙伴架构师,我深入体验了其云服务诊断工具,该工具通过实时监控与历史趋势分析,自动化检查并提供详细的诊断报告,极大提升了运维效率和系统稳定性,特别在处理ECS实例资源不可用等问题时表现突出。此外,它支持预防性维护,帮助识别潜在问题,减少业务中断。尽管如此,仍建议增强诊断效能、扩大云产品覆盖范围、提供自定义诊断选项、加强教育与培训资源、集成第三方工具,以进一步提升用户体验。
605 242
|
22天前
|
人工智能 自然语言处理 前端开发
100个降噪蓝牙耳机免费领,用通义灵码从 0 开始打造一个完整APP
打开手机,录制下你完成的代码效果,发布到你的社交媒体,前 100 个@玺哥超Carry、@通义灵码的粉丝,可以免费获得一个降噪蓝牙耳机。
5934 16
|
4天前
|
消息中间件 人工智能 运维
12月更文特别场——寻找用云高手,分享云&AI实践
我们寻找你,用云高手,欢迎分享你的真知灼见!
480 37