作者:老郑 2021-04-12 08:02:12
开发
前端
分布式 对于商品秒杀的场景,我们需要防止库存超卖或者重复扣款等并发问题,我们通常需要使用分布式锁,来解决共享资源竞争导致数据不一致的问题。本篇就讲解如何用分布式锁的来解决此类问题。
成都创新互联是专业的丰满网站建设公司,丰满接单;提供网站设计、成都网站制作,网页设计,网站设计,建网站,PHP网站建设等专业做网站服务;采用PHP框架,可快速的进行丰满网站开发网页制作和功能扩展;专业做搜索引擎喜爱的网站,专业的做网站团队,希望更多企业前来合作!
对于商品秒杀的场景,我们需要防止库存超卖或者重复扣款等并发问题,我们通常需要使用分布式锁,来解决共享资源竞争导致数据不一致的问题。
以手机秒杀的场景为例子,在抢购的过程中通常我们有三个步骤:
扣掉对应商品的库存;2. 创建商品的订单;3. 用户支付。
对于这样的场景我们就可以采用分布式锁的来解决,比如我们在用户进入秒杀 “下单“ 链接的过程中,我们可以对商品库存进行加锁,然后完成扣库存和其他操作,操作完成后。释放锁,让下一个用户继续进入保证库存的安全性;也可以减少因为秒杀失败,导致 DB 回滚的次数。整个流程如下图所示:
注:对于锁的粒度要根据具体的场景和需求来权衡。
对于 Zookeeper 的分布式锁实现,主要是利用 Zookeeper 的两个特征来实现:
对于非公平锁,我们在加锁的过程如下图所示。
其实上面的实现有优点也有缺点:
优点:
实现比较简单,有通知机制,能提供较快的响应,有点类似 ReentrantLock 的思想,对于节点删除失败的场景由 Session 超时保证节点能够删除掉。
缺点:
重量级,同时在大量锁的情况下会有 “惊群” 的问题。
“惊群” 就是在一个节点删除的时候,大量对这个节点的删除动作有订阅 Watcher 的线程会进行回调,这对Zk集群是十分不利的。所以需要避免这种现象的发生。
为了解决“惊群“问题,我们需要放弃订阅一个节点的策略,那么怎么做呢?
基于非公平锁的缺点,我们可以通过一下的方案来规避。
优点: 如上借助于临时顺序节点,可以避免同时多个节点的并发竞争锁,缓解了服务端压力。
缺点: 对于读写场景来说,无法解决一致性的问题,如果读的时候也去获取锁的话,这样会导致性能下降,对于这样的问题,我们可以通过读写锁来实现如类似 jdk 中的 ReadWriteLock
对于读写锁的特点:读写锁在如果多个线程都是在读的时候,是可以并发读的,就是一个无锁的状态,如果有写锁正在操作的时候,那么读锁需要等待写锁。在加写锁的时候,由于前面的读锁都是并发,所以需要监听最后一个读锁完成后执行写锁。步骤如下:
本文源码中使用环境:JDK 1.8 、Zookeeper 3.6.x
org.apache.curator curator-framework 2.13.0 org.apache.curator curator-recipes 2.13.0
由于 Zookeeper 非公平锁的 “惊群” 效应,非公平锁在 Zookeeper 中其实并不是最好的选择。下面是一个模拟秒杀的例子来使用 Zookeeper 分布式锁。
- public class MutexTest {
- static ExecutorService executor = Executors.newFixedThreadPool(8);
- static AtomicInteger stock = new AtomicInteger(3);
- public static void main(String[] args) throws InterruptedException {
- CuratorFramework client = getZkClient();
- String key = "/lock/lockId_111/111";
- final InterProcessMutex mutex = new InterProcessMutex(client, key);
- for (int i = 0; i < 99; i++) {
- executor.submit(() -> {
- if (stock.get() < 0) {
- System.err.println("库存不足, 直接返回");
- return;
- }
- try {
- boolean acquire = mutex.acquire(200, TimeUnit.MILLISECONDS);
- if (acquire) {
- int s = stock.decrementAndGet();
- if (s < 0) {
- System.err.println("进入秒杀,库存不足");
- } else {
- System.out.println("购买成功, 剩余库存: " + s);
- }
- }
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- try {
- if (mutex.isAcquiredInThisProcess())
- mutex.release();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- });
- }
- while (true) {
- if (executor.isTerminated()) {
- executor.shutdown();
- System.out.println("秒杀完毕剩余库存为:" + stock.get());
- }
- TimeUnit.MILLISECONDS.sleep(100);
- }
- }
- private static CuratorFramework getZkClient() {
- String zkServerAddress = "127.0.0.1:2181";
- ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 3, 5000);
- CuratorFramework zkClient = CuratorFrameworkFactory.builder()
- .connectString(zkServerAddress)
- .sessionTimeoutMs(5000)
- .connectionTimeoutMs(5000)
- .retryPolicy(retryPolicy)
- .build();
- zkClient.start();
- return zkClient;
- }
- }
读写锁可以用来保证缓存双写的强一致性的,因为读写锁在多线程读的时候是无锁的, 只有在前面有写锁的时候才会等待写锁完成后访问数据。
- public class ReadWriteLockTest {
- static ExecutorService executor = Executors.newFixedThreadPool(8);
- static AtomicInteger stock = new AtomicInteger(3);
- static InterProcessMutex readLock;
- static InterProcessMutex writeLock;
- public static void main(String[] args) throws InterruptedException {
- CuratorFramework client = getZkClient();
- String key = "/lock/lockId_111/1111";
- InterProcessReadWriteLock readWriteLock = new InterProcessReadWriteLock(client, key);
- readLock = readWriteLock.readLock();
- writeLock = readWriteLock.writeLock();
- for (int i = 0; i < 16; i++) {
- executor.submit(() -> {
- try {
- boolean read = readLock.acquire(2000, TimeUnit.MILLISECONDS);
- if (read) {
- int num = stock.get();
- System.out.println("读取库存,当前库存为: " + num);
- if (num < 0) {
- System.err.println("库存不足, 直接返回");
- return;
- }
- }
- } catch (Exception e) {
- e.printStackTrace();
- }finally {
- if (readLock.isAcquiredInThisProcess()) {
- try {
- readLock.release();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
- try {
- boolean acquire = writeLock.acquire(2000, TimeUnit.MILLISECONDS);
- if (acquire) {
- int s = stock.get();
- if (s <= 0) {
- System.err.println("进入秒杀,库存不足");
- } else {
- s = stock.decrementAndGet();
- System.out.println("购买成功, 剩余库存: " + s);
- }
- }
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- try {
- if (writeLock.isAcquiredInThisProcess())
- writeLock.release();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- });
- }
- while (true) {
- if (executor.isTerminated()) {
- executor.shutdown();
- System.out.println("秒杀完毕剩余库存为:" + stock.get());
- }
- TimeUnit.MILLISECONDS.sleep(100);
- }
- }
- private static CuratorFramework getZkClient() {
- String zkServerAddress = "127.0.0.1:2181";
- ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 3, 5000);
- CuratorFramework zkClient = CuratorFrameworkFactory.builder()
- .connectString(zkServerAddress)
- .sessionTimeoutMs(5000)
- .connectionTimeoutMs(5000)
- .retryPolicy(retryPolicy)
- .build();
- zkClient.start();
- return zkClient;
- }
- }
打印结果如下,一开始会有 8 个输出结果为 读取库存,当前库存为: 3 然后在写锁中回去顺序的扣减少库存。
- 读取库存,当前库存为: 3
- 读取库存,当前库存为: 3
- 读取库存,当前库存为: 3
- 读取库存,当前库存为: 3
- 读取库存,当前库存为: 3
- 读取库存,当前库存为: 3
- 读取库存,当前库存为: 3
- 读取库存,当前库存为: 3
- 购买成功, 剩余库存: 2
- 购买成功, 剩余库存: 1
- 购买成功, 剩余库存: 0
- 进入秒杀,库存不足
- 进入秒杀,库存不足
- 进入秒杀,库存不足
- 进入秒杀,库存不足
- 进入秒杀,库存不足
- 读取库存,当前库存为: 0
- 读取库存,当前库存为: 0
- 读取库存,当前库存为: 0
- 读取库存,当前库存为: 0
- 读取库存,当前库存为: 0
- 读取库存,当前库存为: 0
- 读取库存,当前库存为: 0
- 读取库存,当前库存为: 0
- 进入秒杀,库存不足
- 进入秒杀,库存不足
- 进入秒杀,库存不足
- 进入秒杀,库存不足
- 进入秒杀,库存不足
- 进入秒杀,库存不足
- 进入秒杀,库存不足
- 进入秒杀,库存不足
咱们最常用的就是 Redis 的分布式锁和 Zookeeper 的分布式锁,在性能方面 Redis 的每秒钟 TPS 可以上轻松上万。在大规模的高并发场景我推荐使用 Redis 分布式锁来作为推荐的技术方案。如果对并发要求不是特别高的场景可以使用 Zookeeper 分布式来处理。
https://www.cnblogs.com/leeego-123/p/12162220.html
http://curator.apache.org/
https://blog.csdn.net/hosaos/article/details/89521537
分享文章:分布式锁看了又看,优秀方案我来告诉你
网页网址:http://www.shufengxianlan.com/qtweb/news39/276939.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联