redis实际应用场景及并发问题的解决

news2024/11/10 17:01:17

业务场景

接下来要模拟的业务场景:

每当被普通攻击的时候,有千分之三的概率掉落金币,每回合最多爆出两个金币。

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;

    @Resource
    private RedisTemplate redisTemplate;

    /**
     * 默认线程池
     */
    @Resource
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;

    @Resource
    private 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在实际业务场景中的应用,并且看到高并发下会产生的数据错误的问题,可采取分布式锁和修改业务逻辑的方式解决,由于锁会影响到性能(请求对锁的竞争),所以更推荐后者。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1544075.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【科研基础】分布式信源编码与中继通信

[1] Bian, Chenghong, et al. “Deep joint source-channel coding over cooperative relay networks.” arXiv preprint arXiv:2211.06705 (2022). [2] Bian, Chenghong, et al. “Process-and-Forward: Deep Joint Source-Channel Coding Over Cooperative Relay Networks.”…

Linux系统 安装docker

安装&#xff1a; 1、Docker要求CentOS系统的内核版本高于 3.10 &#xff0c;通过 uname -r 命令查看你当前的内核版本是否支持安账docker 2、更新yum包&#xff1a; sudo yum -y update 3、安装需要的软件包&#xff0c;yum-util 提供yum-config-manager功能&#xff0c;另外…

PyCharm环境下Git与Gitee联动:本地与远程仓库操作实战及常见问题解决方案

写在前面&#xff1a;本博客仅作记录学习之用&#xff0c;部分图片来自网络&#xff0c;如需引用请注明出处&#xff0c;同时如有侵犯您的权益&#xff0c;请联系删除&#xff01; 文章目录 前言下载及安装GitGit的使用设置用户签名设置用户安全目录Git基本操作Git实操操作 Pyc…

Redis 大 Key 对持久化有什么影响?

资料来源 : 小林coding 小林官方网站 : 小林coding (xiaolincoding.com) Redis 的持久化方式有两种&#xff1a;AOF 日志和 RDB 快照。 所以接下来&#xff0c;针对这两种持久化方式具体分析分析 大 Key 对 AOF 日志的影响 先说说 AOF 日志三种写回磁盘的策略 Redis 提供了 3…

SpringBoot3+JPA+MySQL实现多数据源的读写分离(基于EntityManagerFactory)

1、简介 在Spring Boot中配置多个数据源并实现自动切换EntityManager&#xff0c;这里我编写了一个RoutingEntityManagerFactory和AOP&#xff08;面向切面编程&#xff09;的方式来实现。 这里我配置了两个数据源&#xff1a;primary和secondary&#xff0c;其中primary主数…

Unity连接MySQL踩坑,问题处理记录

用的unity2021版本&#xff0c;MySQL是官方下载的最新版8.0.36. 安装MySQL时&#xff0c;过去如果安装过&#xff0c;一定要删干净&#xff0c;单纯的卸载不行&#xff0c;网上有很多教程。 MySQL安装完成后&#xff0c;将安装目录的MySql.Data.dll文件放入unity项目的Plugin…

【Leetcode每日一题】模拟 - 替换所有的问号(难度⭐)

1. 题目解析 题目链接&#xff1a;1576. 替换所有的问号 这个问题的理解其实相当简单&#xff0c;只需看一下示例&#xff0c;基本就能明白其含义了。 2.算法原理 遍历字符串&#xff1a;从左到右逐个处理字符。 处理问号字符&#xff1a;对于每个问号字符&#xff0c;我们需…

[蓝桥杯 2020 省 AB1] 网络分析

一开始写的暴力合并 卡n^2过的不是正解 看正解是类似 虚拟点树形DP的思路 很巧妙 记录一下 #include<bits/stdc.h> using namespace std; using ll long long; using pii pair<int,int>; #define int long long const int N 3e510; const int inf 0x3f3f3f3f; …

MyBatis3源码深度解析(二十一)动态SQL实现原理(二)动态SQL解析过程、#{}和${}的区别

文章目录 前言8.5 动态SQL解析过程8.5.1 SQL配置转换为SqlSource对象8.5.2 SqlSource转换为静态SQL语句 8.6 #{}和${}的区别8.7 小结 前言 在【MyBatis3源码深度解析(二十)动态SQL实现原理(一)动态SQL的核心组件】中研究了MyBatis动态SQL相关的组件&#xff0c;如SqlSource用于…

git在单分支(自己分支)上的操作流程

文章目录 一、git命令整体操作流程&#xff08;了解&#xff09;二、idea中git操作流程&#xff08;常用-图文&#xff09;1、add2、commit&#xff0c;提交代码3、pull 拉取最新代码4、push 推送代码到远程仓库5、最后就可以在远程仓库中看你提交的代码了。 平时在idea中&…

MySQL数据库的下载及安装教程

MySQL是一个数据库管理系统&#xff0c;允许您管理关系数据库。它是Oracle支持的开源软件&#xff0c;本文介绍如何下载、安装和启动&#xff0c;便于新手快速启动学习之旅&#xff0c;具体如下&#xff1a; 一、下载MySQL 1.打开MySQL官网&#xff08;https://www.mysql.com…

第九届蓝桥杯大赛个人赛省赛(软件类)真题C 语言 A 组-第几个幸运数字

幸运数字是可以被3,5,7任一整除的数字&#xff0c;列举小明号码内的所有可能组合并计数。注意别忘了把1占的一位减去。 #include<stdio.h> typedef long long ll; int main(){long long ans 0, n 59084709587505LL;for(ll i 1; i < n; i * 3){//计算小于等于n的数…

面试笔记——Redis(分布式锁的使用场景及实现原理)

分布式锁的使用场景 资源竞争控制&#xff1a;多个客户端同时访问共享资源时&#xff0c;可以使用分布式锁来控制资源的并发访问&#xff0c;防止多个客户端同时对同一资源进行修改造成数据不一致的问题。 避免重复操作&#xff1a;在分布式环境中&#xff0c;可能会出现多个客…

rmvb是什么文件格式?rmvb格式怎么改成mp4?

RMVB&#xff0c;全称RealMedia Variable Bitrate&#xff0c;是由RealNetworks公司开发的一种视频文件格式。其产生背景可追溯至上世纪90年代&#xff0c;为了解决传输和存储上的挑战&#xff0c;RealNetworks公司致力于推出一种更为高效的解决方案。于是&#xff0c;RMVB问世…

还在用传统知识库?AI知识库才是企业的最优选择

在数字化和信息化日趋严重的时代&#xff0c;企业不仅要处理海量的数据&#xff0c;同时还要有效地管理和利用它们。这就使得知识库&#xff0c;作为一种集中存储、管理和共享知识资源的工具&#xff0c;被越来越多的企业所重视。然而&#xff0c;随着技术的快速迭代&#xff0…

Django之Celery篇(一)

一、介绍 Celery是由Python开发、简单、灵活、可靠的分布式任务队列,是一个处理异步任务的框架,其本质是生产者消费者模型,生产者发送任务到消息队列,消费者负责处理任务。 Celery侧重于实时操作,但对调度支持也很好,其每天可以处理数以百万计的任务。特点: 简单:熟悉…

ElasticSearch8 - 基本操作

前言 本文记录 ES 的一些基本操作&#xff0c;就是对官方文档的一些整理&#xff0c;按自己的习惯重新排版&#xff0c;凑合着看。官方的更详细&#xff0c;建议看官方的。 下文以 books 为索引名举例。 新增 添加单个文档 (没有索引会自动创建) POST books/_doc {"n…

消息队列八股

RabbitMQ 确保消息不丢失 重复消费问题 延迟队列 消息堆积 高可用 很少使用 Kafka 如何保证消息不丢失 回调接口保证生产者发送到brocker消息不丢失 保证消息顺序性 高可用机制 数据清理机制 实现高性能的设计

mysql - 缓存

缓存 InnoDB存储引擎在处理客户端的请求时&#xff0c;当需要访问某个页的数据时&#xff0c;就会把完整的页的数据全部加载到内存中&#xff0c;也就是说即使我们只需要访问一个页的一条记录&#xff0c;那也需要先把整个页的数据加载到内存中。将整个页加载到内存中后就可以…

学习vue3第十一节(依赖注入:provide/inject)

本机介绍&#xff1a;provide/inject 注意&#xff1a;大家在看此小节时候&#xff0c;默认大家已经了解一些组件的使用方法 1、依赖注入的用途&#xff1a; 当嵌套层级多的时候&#xff0c;某个子组件需要较远层级的父组件数据时候&#xff0c;如果我们依然使用props 传递数…