redis实际应用场景及并发问题的解决
业务场景
接下来要模拟的业务场景:
每当被普通攻击的时候,有千分之三的概率掉落金币,每回合最多爆出两个金币。
1.每个回合只有15秒。
2.每次普通攻击的时间间隔是0.5s
3.这个服务是一个集群(这个要求暂时不实现)
编写接口,实现上述需求。
核心问题
可以想到要解决的主要问题是,
1.如何保证一个回合是15秒的时间?
2.如何保证如果一个回合掉落最大金币数量之后,不再掉落金币。
对于问1,我们可以选择设置回合开始的时间或者回合结束的时间,这里采用回合结束的时间。如果发现已经超过结束的时间,那么不做处理。
代码如下,second是一个回合的时间,这里就是十五秒。
private Boolean checkRound(String id, LocalDateTime now) {if (Boolean.TRUE.equals(redisTemplate.hasKey(id))) {LocalDateTime endTime = (LocalDateTime) redisTemplate.boundValueOps(id).get();if (now.isAfter(endTime)) {log.info("该回合已经结束:回合id:{}", id);return false;}}redisTemplate.boundValueOps(id).set(now.plusSeconds(second));return true;}
对于问2,处理的方式和1一样,redis存储已经掉落的金币,若掉落金币超过最大值,则不予处理。
private Boolean checkMoney(String id) {String moneyKey = buildMoneyKey(id);if (Boolean.TRUE.equals(stringRedisTemplate.hasKey(moneyKey))) {int money = Integer.parseInt(stringRedisTemplate.boundValueOps(moneyKey).get());if (money > maxMoney) {log.info("金钱超限。回合id:{}", id);return false;}}return true;}
如果当前回合未结束,并且掉落的金币也没有到达最大值,我们将随机生成金币返回去。
private Boolean money(String id){Random random = new Random();int i = random.nextInt(9);if (i <= 2) {log.info("获得到了金币:{}", id);stringRedisTemplate.boundValueOps(buildMoneyKey(id)).increment();return true;}log.info("未获得到金币:{}", id);return false;}
整体代码逻辑:
@RestController
@Slf4j
public class GameController {@Value("${second:15}")private Long second;@Value("${money:2}")private Integer maxMoney;@Resourceprivate RedisTemplate redisTemplate;/*** 默认线程池*/@Resourceprivate ThreadPoolTaskExecutor threadPoolTaskExecutor;@Resourceprivate StringRedisTemplate stringRedisTemplate;@GetMapping("/attack")public Boolean attack(AttackParam attackParam) {String id = attackParam.getRoundId();log.info("攻击了一次,回合id:{}", id);LocalDateTime now = LocalDateTime.now();/**前置检查**/if (!preCheck(id, now)) {return false;}return money(id);}/*** 检测是否获得金币,获得--true ,未获得--false** @param id id* @return {@link Boolean}*/private Boolean money(String id){Random random = new Random();int i = random.nextInt(9);if (i <= 2) {log.info("获得到了金币:{}", id);stringRedisTemplate.boundValueOps(buildMoneyKey(id)).increment();return true;}log.info("未获得到金币:{}", id);return false;}private String buildMoneyKey(String id) {return "attack:money:" + id;}/*** 预检查** @param id id* @param now 现在* @return {@link Boolean}*/private Boolean preCheck(String id, LocalDateTime now) {if (!checkRound(id, now)) {//检查回合return false;}if (!checkMoney(id)) {//检查本回合是否钱已经给够两次了return false;}return true;}/*** 校验回合是否结束** @param id id* @return {@link Boolean}*/private Boolean checkRound(String id, LocalDateTime now) {if (Boolean.TRUE.equals(redisTemplate.hasKey(id))) {LocalDateTime endTime = (LocalDateTime) redisTemplate.boundValueOps(id).get();if (now.isAfter(endTime)) {log.info("该回合已经结束:回合id:{}", id);return false;}}redisTemplate.boundValueOps(id).set(now.plusSeconds(second));return true;}/*** 校验金钱是够超限** @param id id* @return {@link Boolean}*/private Boolean checkMoney(String id) {String moneyKey = buildMoneyKey(id);if (Boolean.TRUE.equals(stringRedisTemplate.hasKey(moneyKey))) {int money = Integer.parseInt(stringRedisTemplate.boundValueOps(moneyKey).get());if (money > maxMoney) {log.info("金钱超限。回合id:{}", id);return false;}}return true;}/*** 使用线程池模拟并发测试** @return {@link String}*/@GetMapping("/test")public String test(){AttackParam attackParam = new AttackParam();attackParam.setRoundId(UUID.randomUUID().toString());for (int i = 0; i <= 10000; i++) {CompletableFuture.runAsync(() -> {this.attack(attackParam);}, threadPoolTaskExecutor);}return "aa";}
}
结果测试
接下来编写代码模拟高并发场景下是否有问题,
本次测试的并发量是1w。
@GetMapping("/test")public String test(){AttackParam attackParam = new AttackParam();attackParam.setRoundId(UUID.randomUUID().toString());for (int i = 0; i <= 10000; i++) {CompletableFuture.runAsync(() -> {this.attack(attackParam);}, threadPoolTaskExecutor);}return "aa";}
测试结束,查询本回合掉落金币数量。

为什么我们设置的最大掉落金币数量是2,结果却是4呢?
好吧,进行第二次测试查看结果。
这一次居然是7。

说明上面这串代码在并发情况下会出现问题,即使这个并发量几十的情况依然会出问题。
问题分析
那我们就来分析一下是哪里出现了问题,出现这种原因无非就是满足写后读,那就找到读写金币的位置。
举个例子,假设线程A正在获取金币,但是这个增加的操作还没有写到redis。另外有线程B,线程C....走到了图二中查询金币数量的位置。那么这一堆线程获得仍是oldValue,这就相当于线程A的写操作是“无效的”。那么导致的结果就是金币比预期多了很多,至于多多少,取决于金币掉落的概率。


解决方案
如何解决这个问题呢?
这个问题本质上是读写分离,导致了“脏数据”。
第一个想到的也是最直接的方法肯定是加锁,但是需要考虑到这种加锁的方式只适合单体应用,如果是多个程序呢,就无法解决了。
可以将synchronized换成分布式锁。
但是加锁的方式不推荐,锁的竞争会严重影响性能。如果可以通过业务逻辑来解决,就不要去加锁。那么我们需要将读写操作放在一起,使其具有原子性。
redis中的incr操作本身就是原子的,所以我们可以将检查金币数量这个操作提前,读写放到一起。
代码如下,checkMoney就可以注掉了。
private Boolean money(String id) {Random random = new Random();int i = random.nextInt(9);if (i <= 2) {Long increment = stringRedisTemplate.boundValueOps(buildMoneyKey(id)).increment();//将读和写放到一起 这是个原子性的if (increment > maxMoney) {log.info("金钱超限,回合{}", id);return false;}log.info("获得到了金币:{}", id);stringRedisTemplate.boundValueOps(id+"money").increment();return true;}log.info("未获得到金币:{}", id);return false;}
再次测试,可以看到数据已经是准确的了。

总结
本文讲述了redis在实际业务场景中的应用,并且看到高并发下会产生的数据错误的问题,可采取分布式锁和修改业务逻辑的方式解决,由于锁会影响到性能(请求对锁的竞争),所以更推荐后者。
相关文章:
redis实际应用场景及并发问题的解决
业务场景 接下来要模拟的业务场景: 每当被普通攻击的时候,有千分之三的概率掉落金币,每回合最多爆出两个金币。 1.每个回合只有15秒。 2.每次普通攻击的时间间隔是0.5s 3.这个服务是一个集群(这个要求暂时不实现) 编写接口&…...
考研数学|汤家凤《1800》基础部分什么时候做完?
从我个人的经验来看,做完汤家凤1800的基础部分在第一轮复习中并不是必须的,但是可以作为一个有效的复习工具。 我认为汤家凤1800的基础部分确实涵盖了考研高数的基础知识点,并且题目难度适中,适合用来巩固基础。在第一轮复习中&a…...
JS的设计模式(23种)
JavaScript设计模式是指在JavaScript编程中普遍应用的一系列经过验证的最佳实践和可重用的解决方案模板,它们用来解决在软件设计中频繁出现的问题,如对象的创建、职责分配、对象间通信以及系统架构等。 设计模式并不特指某个具体的代码片段,…...
[自研开源] MyData v0.7.5 更新日志
开源地址:gitee | github 详细介绍:MyData 基于 Web API 的数据集成平台 部署文档:用 Docker 部署 MyData 使用手册:MyData 使用手册 试用体验:https://demo.mydata.work 交流Q群:430089673 介绍 MyData …...
3月份的倒数第二个周末有感
坐在图书馆的那一刻,忽然感觉时间的节奏开始放缓。今天周末因为我们两都有任务需要完成,所以就选了嘉定图书馆,不得不说嘉定新城远香湖附近的图书馆真的很有感觉。然我不经意回想起学校的时光,那是多么美好且短暂的时光。凝视着窗…...
Java 变得越来越像 Rust
Java 变得越来越像 Rust 介绍 随着编程的增强和复杂性越来越流行,许多编程语言也相互效仿。 Java 也不例外。 尽管社区内部存在问题,Rust 仍逐年赢得了开发人员的喜爱。并且有充分的理由:由于编译器,Rust 使开发人员能够避免整…...
通过git bash 或命令行ssh访问服务器 sftp上传下载文件
上传下载文件 sftp -P 端口 appywIP 示例:sftp -P 10022 appyw25.222.133.222 然后输入密码即可 ls 查看文件 lls 查看本地文件 cd 跳转 lcd 本地跳转 get ... 下载文件 put 本地文件名 远程文件夹 //上传文件 put -r 本地文件夹 远程文件夹 //上传文件夹服务器…...
27 OpenCV 凸包
文章目录 概念Graham扫描算法convexHull 凸包函数示例 概念 什么是凸包(Convex Hull),在一个多变形边缘或者内部任意两个点的连线都包含在多边形边界或者内部。 正式定义: 包含点集合S中所有点的最小凸多边形称为凸包 Graham扫描算法 首先选择Y方向最低…...
【GPT概念04】仅解码器(only decode)模型的解码策略
一、说明 在我之前的博客中,我们研究了关于生成式预训练转换器的整个概述,以及一篇关于生成式预训练转换器(GPT)的博客——预训练、微调和不同的用例应用。现在让我们看看所有仅解码器模型的解码策略是什么。 二、解码策略 在之前…...
蔚来-安全开发一面/二面
基本不怎么会渗透测试,本科期间有过大数据隐私保护(密码)的项目,硕士期间有个华为合作的项目一篇在投的ai安全论文 一面(45min) 1.介绍自己 2.介绍一下实习 3.场景题轰炸,主要针对实习中的场景,主要考察…...
Redis Cluster集群模式容器化部署
Redis Cluster集群模式容器化部署 安装Docker和docker-compose准备docker-compose文件准备Redis配置文件Linux内核参数优化启停Redis实例Redis集群搭建 环境准备: IP版本角色端口172.x.x.11RHEL 7.9master6379172.x.x.12RHEL 7.9master6379172.x.x.13RHEL 7.9maste…...
网络原理(6)——IP协议
目录 一、网段划分 现在的网络划分: 1、一般情况下的家庭网络环境 2、IP地址 3、子网掩码 4、网关 以前的网络划分: 二、特殊IP 1、环回 IP 2、主机号为全 0 的IP 3、广播地址IP 三、路由选择(路线规划) 一、网段划分…...
淘宝商品详情API接口:快速获取商品信息的高效工具
淘宝商品详情API接口:快速获取商品信息的高效工具 请求示例,API接口接入Anzexi58 在信息化、数字化的今天,数据已成为商业决策的重要依据。对于电商行业而言,快速准确地获取商品信息对于商家和消费者都至关重要。淘宝作为中国最大…...
一分钟学习Markdown语法
title: 一分钟学习Markdown语法 date: 2024/3/24 19:33:29 updated: 2024/3/24 19:33:29 tags: MD语法文本样式列表结构链接插入图片展示练习实践链接问题 欢迎来到Markdown语法的世界!Markdown是一种简单而直观的标记语言,让文本排版变得轻松有趣。接下…...
Power Apps 学习笔记 -- OrganizationRequestCollection
文章目录 1. OrganizationRequestCollection 简介2. OrganizationRequestCollection2.1 OrganizationRequest 使用2.2 OrganizationRequestCollection 使用 1. OrganizationRequestCollection 简介 OrganizationRequestCollection 链接 : OrganizationRequestCollection Orga…...
python绘图matplotlib——使用记录1
本博文来自于网络收集,如有侵权请联系删除 使用matplotlib绘图 1 常用函数汇总1.1 plot1.2 legend1.3 scatter1.4 xlim1.5 xlabel1.6 grid1.7 axhline1.7 axvspan1.8 annotate1.9 text1.10 title 2 常见图形绘制2.1 bar——柱状图2.2 barh——条形图2.3 hist——直…...
Spring Data访问Elasticsearch----创建存储库实例
Spring Data访问Elasticsearch----创建存储库实例 一、Java配置二、XML配置三、使用过滤器四、独立使用 本文介绍如何为已定义的存储库接口创建实例和bean定义。 一、Java配置 在Java配置类上使用特定于存储的EnableElasticsearchRepositories注解来定义用于存储库激活的配置。…...
Wireshark TS | DNS 案例分析之外的思考
前言 承接之前一篇《Packet Challenge 之 DNS 案例分析》,在数据包跟踪文件 dnsing.pcapng 中,关于第 4 题(What is the largest DNS response time seen in this trace file? )的分析过程中曾经碰到一个小问题,主要…...
nginx集群部署访问不了怎么解决
如果你的Nginx集群部署无法访问,可能有多种原因导致,以下是一些常见的解决方法: 检查网络连接:确保服务器之间的网络连接是正常的,可以通过ping命令或telnet命令检查服务器之间的网络连通性。 检查防火墙设置ÿ…...
抖音小程序开发资质认证流程和资料
开发小程序前,你需要先入驻抖音开发平台开发者平台。包含注册账号、主体认证和对公认证,具体操作可参考注册开发者平台账号。 基础信息提交需要资料 1、营业执照。 本文介绍开发者在抖音开放平台的入驻流程。 入驻标准 入驻主体主要为中国或海外…...
Android Wi-Fi 连接失败日志分析
1. Android wifi 关键日志总结 (1) Wi-Fi 断开 (CTRL-EVENT-DISCONNECTED reason3) 日志相关部分: 06-05 10:48:40.987 943 943 I wpa_supplicant: wlan0: CTRL-EVENT-DISCONNECTED bssid44:9b:c1:57:a8:90 reason3 locally_generated1解析: CTR…...
VB.net复制Ntag213卡写入UID
本示例使用的发卡器:https://item.taobao.com/item.htm?ftt&id615391857885 一、读取旧Ntag卡的UID和数据 Private Sub Button15_Click(sender As Object, e As EventArgs) Handles Button15.Click轻松读卡技术支持:网站:Dim i, j As IntegerDim cardidhex, …...
Go 语言接口详解
Go 语言接口详解 核心概念 接口定义 在 Go 语言中,接口是一种抽象类型,它定义了一组方法的集合: // 定义接口 type Shape interface {Area() float64Perimeter() float64 } 接口实现 Go 接口的实现是隐式的: // 矩形结构体…...
蓝牙 BLE 扫描面试题大全(2):进阶面试题与实战演练
前文覆盖了 BLE 扫描的基础概念与经典问题蓝牙 BLE 扫描面试题大全(1):从基础到实战的深度解析-CSDN博客,但实际面试中,企业更关注候选人对复杂场景的应对能力(如多设备并发扫描、低功耗与高发现率的平衡)和前沿技术的…...
基于Docker Compose部署Java微服务项目
一. 创建根项目 根项目(父项目)主要用于依赖管理 一些需要注意的点: 打包方式需要为 pom<modules>里需要注册子模块不要引入maven的打包插件,否则打包时会出问题 <?xml version"1.0" encoding"UTF-8…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
以光量子为例,详解量子获取方式
光量子技术获取量子比特可在室温下进行。该方式有望通过与名为硅光子学(silicon photonics)的光波导(optical waveguide)芯片制造技术和光纤等光通信技术相结合来实现量子计算机。量子力学中,光既是波又是粒子。光子本…...
JAVA后端开发——多租户
数据隔离是多租户系统中的核心概念,确保一个租户(在这个系统中可能是一个公司或一个独立的客户)的数据对其他租户是不可见的。在 RuoYi 框架(您当前项目所使用的基础框架)中,这通常是通过在数据表中增加一个…...
AI+无人机如何守护濒危物种?YOLOv8实现95%精准识别
【导读】 野生动物监测在理解和保护生态系统中发挥着至关重要的作用。然而,传统的野生动物观察方法往往耗时耗力、成本高昂且范围有限。无人机的出现为野生动物监测提供了有前景的替代方案,能够实现大范围覆盖并远程采集数据。尽管具备这些优势…...
Linux系统部署KES
1、安装准备 1.版本说明V008R006C009B0014 V008:是version产品的大版本。 R006:是release产品特性版本。 C009:是通用版 B0014:是build开发过程中的构建版本2.硬件要求 #安全版和企业版 内存:1GB 以上 硬盘…...
