learn.lianglianglee.com/专栏/SpringCloud微服务实战(完)/20 优惠券如何避免超兑——引入分布式锁.md.html
2022-05-11 18:52:13 +08:00

821 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>20 优惠券如何避免超兑——引入分布式锁.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/SpringCloud微服务实战/00 开篇导读.md">00 开篇导读.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/01 以真实“商场停车”业务切入——需求分析.md">01 以真实“商场停车”业务切入——需求分析.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/02 具象业务需求再抽象分解——系统设计.md">02 具象业务需求再抽象分解——系统设计.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/03 第一个 Spring Boot 子服务——会员服务.md">03 第一个 Spring Boot 子服务——会员服务.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/04 如何维护接口文档供外部调用——在线接口文档管理.md">04 如何维护接口文档供外部调用——在线接口文档管理.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/05 认识 Spring Cloud 与 Spring Cloud Alibaba 项目.md">05 认识 Spring Cloud 与 Spring Cloud Alibaba 项目.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/06 服务多不易管理如何破——服务注册与发现.md">06 服务多不易管理如何破——服务注册与发现.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/07 如何调用本业务模块外的服务——服务调用.md">07 如何调用本业务模块外的服务——服务调用.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/08 服务响应慢或服务不可用怎么办——快速失败与服务降级.md">08 服务响应慢或服务不可用怎么办——快速失败与服务降级.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/09 热更新一样更新服务的参数配置——分布式配置中心.md">09 热更新一样更新服务的参数配置——分布式配置中心.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/10 如何高效读取计费规则等热数据——分布式缓存.md">10 如何高效读取计费规则等热数据——分布式缓存.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/11 多实例下的定时任务如何避免重复执行——分布式定时任务.md">11 多实例下的定时任务如何避免重复执行——分布式定时任务.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/12 同一套服务如何应对不同终端的需求——服务适配.md">12 同一套服务如何应对不同终端的需求——服务适配.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/13 采用消息驱动方式处理扣费通知——集成消息中间件.md">13 采用消息驱动方式处理扣费通知——集成消息中间件.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/14 Spring Cloud 与 Dubbo 冲突吗——强强联合.md">14 Spring Cloud 与 Dubbo 冲突吗——强强联合.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/15 破解服务中共性问题的繁琐处理方式——接入 API 网关.md">15 破解服务中共性问题的繁琐处理方式——接入 API 网关.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/16 服务压力大系统响应慢如何破——网关流量控制.md">16 服务压力大系统响应慢如何破——网关流量控制.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/17 集成网关后怎么做安全验证——统一鉴权.md">17 集成网关后怎么做安全验证——统一鉴权.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/18 多模块下的接口 API 如何统一管理——聚合 API.md">18 多模块下的接口 API 如何统一管理——聚合 API.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/19 数据分库后如何确保数据完整性——分布式事务.md">19 数据分库后如何确保数据完整性——分布式事务.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/SpringCloud微服务实战/20 优惠券如何避免超兑——引入分布式锁.md">20 优惠券如何避免超兑——引入分布式锁.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/21 如何查看各服务的健康状况——系统应用监控.md">21 如何查看各服务的健康状况——系统应用监控.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/22 如何确定一次完整的请求过程——服务链路跟踪.md">22 如何确定一次完整的请求过程——服务链路跟踪.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/23 结束语.md">23 结束语.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>20 优惠券如何避免超兑——引入分布式锁</h1>
<p>会员办理月卡或签到累积的积分,可以在指定时间段内兑换商场优惠券,由于数量有限,时间有限,兑换操作相当集中,如果按正常流程处理的话,肯定会出现超兑的情况。比如只有 5000 张券,结果兑换出 8000 张,这对商场来说是一笔经济损失。</p>
<p>为防止超兑,自然做法是按总量一个接一个兑换,至到兑换完,但多并发的情况下如何保证还一个一个兑换呢?自然而然就会想到锁上面来。提及锁,你脑海是不是出现了一堆关于锁的场景:死锁、互斥锁、乐观锁、悲观锁等等,本节介绍分布式锁,它主要应用于分布式系统下面,单体应用基本不会涉及。</p>
<h3>两种实现机制介绍</h3>
<p>常见的实现方法分布式锁可以基于数据库、Redis、Zookeeper 等第三方工具来实现,各种不同实现方式需要引入第三方,截止目前 MySQL 及 Redis 已经引入到实战中,为降低系统复杂度,我们想办法基于这两个机制进行分布式锁实现。</p>
<ol>
<li>采用数据库实现分布式锁,还记得前面《分布式定时任务》章节吗?里面就用到分布式锁。为保证指定时刻下多实例定时任务的执行,优先通过 ShedLock 的方式获取锁,锁产生在公共存储库中,生成一条新记录来告诉其它集群中其它实例,我正在执行,其它实例获取到这个状态后,自动跳过不再执行,来保证同一时刻只有一个任务在执行。</li>
<li>采用 Redis 实现分布式锁。Redis 提供了 setnx 指令,保证同一时刻内只有一个请求针对同一 key 进行 setnx 操作,鉴于 Redis 是单线程模式,依旧是先到先得,晚到不得,通过这个操作可以实现排它性的操作。</li>
</ol>
<p>但此做法存在漏洞,操作 key 后,指令发起方挂掉的话,这个 key 就永远不能被操作了。稍做改进,给 key 设置失效时间,这样就可以到期自动释放,供其它操作。但依旧有漏洞,在 setnx 后,发起 expire 前服务挂了,这种方式依旧与第一处方式类似。</p>
<p>细查 Redis 官方指令后,发现 set 指令后还跟有 <code>[EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]</code> 等选项,可以针对第二种方式用此方式进一步改进。在单实例 Redis 的情况下,在实例可用的情况下,取锁、释放锁操作已经基本可用。</p>
<p>Redis 另外提供了一种 Redlock 算法来实现分面式锁,有兴趣的朋友可看<a href="https://redis.io/topics/distlock">原文</a><a href="http://www.redis.cn/topics/distlock.html">中文版本</a>),在单实例无法保证可用的情况下,通过集群中多实例来有效防止单点故障导致锁不可用。大致意思是同某一时刻,向所有实例发起加锁请求,如果获取到 N/2+1 个锁表示成功,否则失败并自动解锁所有实例,到达锁失效期后同样去解锁所有实例。</p>
<p>如果是自己去实现这一套算法的话,想必还是比较复杂的,庆幸的是有非常好的成品,已经帮我们完成了。这就是本篇要提到的 Redission 客户端,里面有 Redlock 分布式锁的完整实现。</p>
<h3>什么是 Redisson</h3>
<p>Redis 的三大 Java 客户端之一其它两个是Jedis 和 LettuceSpringBoot 2.x 之后就将默认集成的 Jedis 客户端替换成 Lettuce。不仅提供了一系列的分布式的 Java 常用对象还提供了许多分布式服务。Redisson 的宗旨是促进使用者对 Redis 的关注分离Separation of Concern从而让使用者能够将精力更集中地放在处理业务逻辑上。</p>
<p>更多介绍参见官网:<a href="https://github.com/redisson/redisson">redisson</a></p>
<h3>引入 Redisson</h3>
<p>由于我们使用的框架是 Spring Boot 搭建的,这里同样采用 starter 的方式引入(不再需要 spring-data-redis 模块):</p>
<pre><code class="language-xml">&lt;dependency&gt;
&lt;groupId&gt;org.redisson&lt;/groupId&gt;
&lt;artifactId&gt;redisson-spring-boot-starter&lt;/artifactId&gt;
&lt;version&gt;3.11.6&lt;/version&gt;
&lt;/dependency&gt;
</code></pre>
<p>配置文件采用 redis 的默认配置方式,可以兼容:</p>
<pre><code class="language-properties">#redis config
spring.redis.database=2
spring.redis.host=localhost
spring.redis.port=16479
#default redis password is empty
spring.redis.password=zxcvbnm,./
spring.redis.timeout=60000
spring.redis.pool.max-active=1000
spring.redis.pool.max-wait=-1
spring.redis.pool.max-idle=10
spring.redis.pool.min-idle=5
</code></pre>
<h3>代码编写、测试</h3>
<p>这里编写了一个启动类,将本次兑换优惠券总可兑换数量写入缓存,每次采用原子操作进行减少。</p>
<pre><code class="language-java">@Component
@Order(0)
public class StartupApplicatonRunner implements ApplicationRunner {
@Autowired
Redisson redisson;
@Override
public void run(ApplicationArguments args) throws Exception {
RAtomicLong atomicLong = redisson.getAtomicLong(ParkingConstant.cache.grouponCodeAmtKey);
atomicLong.set(ParkingConstant.cache.grouponCodeAmt);
}
}
</code></pre>
<p>在兑换逻辑中,判断优惠券可用数量,兑换结束后数量减 1</p>
<pre><code class="language-java"> @Autowired
Redisson redisson;
@Override
public int createExchange(String json) throws BusinessException {
Exchange exchange = JSONObject.parseObject(json, Exchange.class);
int rtn = 0;
// 兑换类型有两部分0 是商场优惠券1 是洗车券,这是作了简单区分
if (exchange.getCtype() == 0) {
RAtomicLong atomicLong = redisson.getAtomicLong(ParkingConstant.cache.grouponCodeAmtKey);
// 获取锁
RLock rLock = redisson.getLock(ParkingConstant.lock.exchangeCouponLock);
// 锁定,默认 10s 不主动解锁的话,自动解锁,防止出现死锁的情况。正常情况下可基于 redisson 获取 redLock 处理,更加安全,本测试基于单机 redis 测试。
rLock.lock(1000, TimeUnit.SECONDS);
log.info(&quot;lock it when release ...&quot;);
// 判定可兑换数量,如果有就兑换,兑换结束数量减一
if (atomicLong.get() &gt; 0) {
rtn = exchangeMapper.insertSelective(exchange);
atomicLong.decrementAndGet();
}
// 释放锁
rLock.unlock();
log.info(&quot;exchage coupon ended ...&quot;);
} else {
rtn = exchangeMapper.insertSelective(exchange);
}
log.debug(&quot;create exchage ok = &quot; + exchange.getId());
return rtn;
}
</code></pre>
<p>简单测试,将 lock 时间设置个较长时间,利用断点来测试(也可以采用前面介绍到的 Postman 的方式进行并发测试)。</p>
<ol>
<li>准备两个实例,一个实例构建成 jar 运行,一个实例在 IDE 中运行。</li>
<li>在 if 判定处打断点,在 IDE 中启动第一个实例,请求 lock 后,不向下运行。</li>
<li>启动 jar 实例,发起第二个请求,可以看到日志并未输出 <code>_lock it when release …_</code>,而是一直在等待。</li>
<li>将 IDE 中的断点跳过,执行结束,自动释放锁。回头看 jar 实例的日志输出,可以看到两个日志正常输出。</li>
</ol>
<p>这样就达到分布锁的目标,实际应用中锁定时间肯定比较短,否则服务会被拖垮,很类似秒杀的场景,但杀场景更复杂,还需要其它辅助手段,不能如此简单处理。文中只提到 Redission 的这一种锁的用法,文后留个小作业吧,你再研究下 Redission 还有没有其它场景下的用法。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/SpringCloud微服务实战/19 数据分库后如何确保数据完整性——分布式事务.md">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/SpringCloud微服务实战/21 如何查看各服务的健康状况——系统应用监控.md">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709975b73bbc3cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>