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

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


背景历史

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

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

锁机制的发展经历了从单机锁到分布式锁的过程。早期的系统大多运行在单机环境中,因此主要使用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级别锁和分布式锁有了更深入的了解,并能够在实际项目中灵活运用这些技术来解决并发访问和数据一致性问题。

相关实践学习
基于MSE实现微服务的全链路灰度
通过本场景的实验操作,您将了解并实现在线业务的微服务全链路灰度能力。
目录
打赏
0
10
10
6
503
分享
相关文章
鸿蒙HarmonyOS应用开发 | 探索 HarmonyOS Next-从开发到实战掌握 HarmonyOS Next 的分布式能力
HarmonyOS Next 是华为新一代操作系统,专注于分布式技术的深度应用与生态融合。本文通过技术特点、应用场景及实战案例,全面解析其核心技术架构与开发流程。重点介绍分布式软总线2.0、数据管理、任务调度等升级特性,并提供基于 ArkTS 的原生开发支持。通过开发跨设备协同音乐播放应用,展示分布式能力的实际应用,涵盖项目配置、主界面设计、分布式服务实现及部署调试步骤。此外,深入分析分布式数据同步原理、任务调度优化及常见问题解决方案,帮助开发者掌握 HarmonyOS Next 的核心技术和实战技巧。
213 76
鸿蒙HarmonyOS应用开发 | 探索 HarmonyOS Next-从开发到实战掌握 HarmonyOS Next 的分布式能力
|
5天前
|
Java中的分布式缓存与Memcached集成实战
通过在Java项目中集成Memcached,可以显著提升系统的性能和响应速度。合理的缓存策略、分布式架构设计和异常处理机制是实现高效缓存的关键。希望本文提供的实战示例和优化建议能够帮助开发者更好地应用Memcached,实现高性能的分布式缓存解决方案。
32 9
【📕分布式锁通关指南 01】从解决库存超卖开始加锁的初体验
本文通过电商场景中的库存超卖问题,深入探讨了JVM锁、MySQL悲观锁和乐观锁的实现及其局限性。首先介绍了单次访问下库存扣减逻辑的正常运行,但在高并发场景下出现了超卖问题。接着分析了JVM锁在多例模式、事务模式和集群模式下的失效情况,并提出了使用数据库锁机制(如悲观锁和乐观锁)来解决并发问题。 悲观锁通过`update`语句或`select for update`实现,能有效防止超卖,但存在锁范围过大、性能差等问题。乐观锁则通过版本号或时间戳实现,适合读多写少的场景,但也面临高并发写操作性能低和ABA问题。 最终,文章强调没有完美的方案,只有根据具体业务场景选择合适的锁机制。
29 12
鸿蒙HarmonyOS应用开发 |鸿蒙技术分享HarmonyOS Next 深度解析:分布式能力与跨设备协作实战
鸿蒙技术分享:HarmonyOS Next 深度解析 随着万物互联时代的到来,华为发布的 HarmonyOS Next 在技术架构和生态体验上实现了重大升级。本文从技术架构、生态优势和开发实践三方面深入探讨其特点,并通过跨设备笔记应用实战案例,展示其强大的分布式能力和多设备协作功能。核心亮点包括新一代微内核架构、统一开发语言 ArkTS 和多模态交互支持。开发者可借助 DevEco Studio 4.0 快速上手,体验高效、灵活的开发过程。 239个字符
227 13
鸿蒙HarmonyOS应用开发 |鸿蒙技术分享HarmonyOS Next 深度解析:分布式能力与跨设备协作实战
分布式读写锁的奥义:上古世代 ZooKeeper 的进击
本文作者将介绍女娲对社区 ZooKeeper 在分布式读写锁实践细节上的思考,希望帮助大家理解分布式读写锁背后的原理。
104 11
|
1月前
|
什么场景下要使用分布式锁
分布式锁用于确保多节点环境下的资源互斥访问、避免重复操作、控制并发流量、防止竞态条件及任务调度协调,常见于防止超卖等问题。
50 4
基于Redis海量数据场景分布式ID架构实践
【11月更文挑战第30天】在现代分布式系统中,生成全局唯一的ID是一个常见且重要的需求。在微服务架构中,各个服务可能需要生成唯一标识符,如用户ID、订单ID等。传统的自增ID已经无法满足在集群环境下保持唯一性的要求,而分布式ID解决方案能够确保即使在多个实例间也能生成全局唯一的标识符。本文将深入探讨如何利用Redis实现分布式ID生成,并通过Java语言展示多个示例,同时分析每个实践方案的优缺点。
92 8
|
2月前
|
实战优化公司线上系统JVM:从基础到高级
【11月更文挑战第28天】Java虚拟机(JVM)是Java语言的核心组件,它使得Java程序能够实现“一次编写,到处运行”的跨平台特性。在现代应用程序中,JVM的性能和稳定性直接影响到系统的整体表现。本文将深入探讨JVM的基础知识、基本特点、定义、发展历史、主要概念、调试工具、内存管理、垃圾回收、性能调优等方面,并提供一个实际的问题demo,使用IntelliJ IDEA工具进行调试演示。
57 0
京东双十一高并发场景下的分布式锁性能优化
【10月更文挑战第20天】在电商领域,尤其是像京东双十一这样的大促活动,系统需要处理极高的并发请求。这些请求往往涉及库存的查询和更新,如果处理不当,很容易出现库存超卖、数据不一致等问题。
87 1
JVM进阶调优系列(6)一文详解JVM参数与大厂实战调优模板推荐
本文详述了JVM参数的分类及使用方法,包括标准参数、非标准参数和不稳定参数的定义及其应用场景。特别介绍了JVM调优中的关键参数,如堆内存、垃圾回收器和GC日志等配置,并提供了大厂生产环境中常用的调优模板,帮助开发者优化Java应用程序的性能。

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等