微服务开发系列 第六篇:Redisson

news2024/12/23 14:41:04

总概

A、技术栈

  • 开发语言:Java 1.8
  • 数据库:MySQL、Redis、MongoDB、Elasticsearch
  • 微服务框架:Spring Cloud Alibaba
  • 微服务网关:Spring Cloud Gateway
  • 服务注册和配置中心:Nacos
  • 分布式事务:Seata
  • 链路追踪框架:Sleuth
  • 服务降级与熔断:Sentinel
  • ORM框架:MyBatis-Plus
  • 分布式任务调度平台:XXL-JOB
  • 消息中间件:RocketMQ
  • 分布式锁:Redisson
  • 权限:OAuth2
  • DevOps:Jenkins、Docker、K8S

B、本节实现目标

  • 用Redisson分布式锁控制并发

一、实现用户积分功能

1.1 功能说明

用户下单后,自动给用户增加对应订单金额的积分数(取整)。

1.2 用户积分表

增加两张表,用户总积分表:t_member_integral、积分明细表:t_member_integral_log

CREATE TABLE `t_member_integral` (
  `id` bigint NOT NULL COMMENT 'id',
  `member_id` bigint NOT NULL COMMENT '用户ID',
  `total_integral` bigint DEFAULT '0' COMMENT '用户总积分',
  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除 0未删除 1已删除',
  `update_time` datetime NOT NULL COMMENT '修改时间',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `UK_member_id` (`member_id`),
  KEY `IDX_member_id` (`member_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='用户积分'
CREATE TABLE `t_member_integral_log` (
  `id` bigint NOT NULL COMMENT 'id',
  `member_id` bigint NOT NULL COMMENT '用户ID',
  `integral` bigint DEFAULT '0' COMMENT '积分',
  `source_type` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '积分来源类型(下单奖励积分/签到积分)',
  `source_remark` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT '' COMMENT '积分来源描述(2023-02-23下单获得积分)',
  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除 0未删除 1已删除',
  `update_time` datetime NOT NULL COMMENT '修改时间',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `IDX_member_id` (`member_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='用户积分明细'

1.3 实现代码

Controller

@Api(tags = "用户")
@RestController
@RequestMapping("member")
public class MemberController {

    @Resource
    private MemberService memberServiceImpl;

    @Resource
    private MemberIntegralComponent memberIntegralComponent;

    @ApiOperation(value = "记录积分")
    @PostMapping("integral")
    public Boolean recordIntegral(@RequestBody @Valid IntegralLogEditVO logEditVO) {
         return memberIntegralComponent.recordIntegral(logEditVO);
    }
}

Component

@Component
public class MemberIntegralComponent {

    @Resource
    private MemberIntegralLogService memberIntegralLogServiceImpl;

    @Resource
    private MemberIntegralService memberIntegralServiceImpl;

    /**
     * 记录积分
     * 并发问题:出现死锁
     * 并发下相同的业务参数去执行,第一个事物还没提交后面的事物又来了,这种我们加分布式锁就好了
     *
     * @param logEditVO
     */
    @Transactional(rollbackFor = Exception.class)
    public boolean recordIntegral(IntegralLogEditVO logEditVO) {
        //记录积分明细
        memberIntegralLogServiceImpl.addIntegral(logEditVO);
        //更新用户总积分
        memberIntegralServiceImpl.updateTotalIntegral(logEditVO.getMemberId());
        return true;
    }
}

MemberIntegralLogService

@Slf4j
@Service
public class MemberIntegralLogServiceImpl implements MemberIntegralLogService {

    @Resource
    private MemberIntegralLogDao memberIntegralLogDaoImpl;

    @Override
    public void addIntegral(IntegralLogEditVO logEditVO) {
        MemberIntegralLog entity = new MemberIntegralLog();
        entity.setMemberId(logEditVO.getMemberId());
        entity.setIntegral(logEditVO.getIntegral());
        entity.setSourceType(logEditVO.getSourceType());
        entity.setSourceRemark(logEditVO.getSourceRemark());
        memberIntegralLogDaoImpl.save(entity);
    }
}

MemberIntegralService

@Slf4j
@Service
public class MemberIntegralServiceImpl implements MemberIntegralService {

    @Resource
    private MemberIntegralDao memberIntegralDaoImpl;

    /**
     * 更新用户积分
     *
     * @param memberId
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateTotalIntegral(Long memberId) {
        if (!memberIntegralDaoImpl.existsMemberIntegral(memberId)) {
            MemberIntegral defaultEntity = new MemberIntegral();
            defaultEntity.setMemberId(memberId);
            defaultEntity.setTotalIntegral(0L);
            memberIntegralDaoImpl.save(defaultEntity);
        }
        memberIntegralDaoImpl.freshTotalIntegral(memberId);
    }
}

二、JMeter并发测试

2.1 并发测试

使用JMeter工具对积分接口进行并发测试,启动20个线程进行并发,如下图:

积分接口

20个线程

2.2 并发测试结果

控制台显示死锁异常,20个线程数据没有全部正确执行成功。

控制台显示死锁异常

异常信息:

Error updating database.  Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction

查询数据库:

用户总积分

积分明细

三、Redisson分布式锁-控制并发

3.1 maven加Redis依赖包

在项目[mall-pom]的pom.xml里加入Redis依赖包

<redisson.version>3.20.1</redisson.version>

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>${redisson.version}</version>
</dependency>

3.2 common.yml配置Redisson参数

spring:
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
    password: 123abc
    jedis:
      pool:
        max-active: 500  #连接池的最大数据库连接数。设为0表示无限制
        max-idle: 20   #最大空闲数
        max-wait: -1
        min-idle: 5
    timeout: 1000
    redisson:
      password: 123abc
      cluster:
        nodeAddresses: ["redis://127.0.0.1:6379"]
      single:
        address: "redis://127.0.0.1:6379"
        database: 0

common.yml完整配置

spring:
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
    password: 123abc
    jedis:
      pool:
        max-active: 500  #连接池的最大数据库连接数。设为0表示无限制
        max-idle: 20   #最大空闲数
        max-wait: -1
        min-idle: 5
    timeout: 1000
    redisson:
      password: 123abc
      cluster:
        nodeAddresses: ["redis://127.0.0.1:6379"]
      single:
        address: "redis://127.0.0.1:6379"
        database: 0

  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.100.51:3306/ac_db?serverTimezone=Asia/Shanghai&useUnicode=true&tinyInt1isBit=false&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
    username: ac_u
    password: ac_PWD_123

    #hikari数据库连接池
    hikari:
      pool-name: YH_HikariCP
      minimum-idle: 10 #最小空闲连接数量
      idle-timeout: 600000 #空闲连接存活最大时间,默认600000(10分钟)
      maximum-pool-size: 100 #连接池最大连接数,默认是10
      auto-commit: true  #此属性控制从池返回的连接的默认自动提交行为,默认值:true
      max-lifetime: 1800000 #此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟
      connection-timeout: 30000 #数据库连接超时时间,默认30秒,即30000
      connection-test-query: SELECT 1

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

3.3 Redisson配置

在[mall-core] 里加入配置代码

@Data
public class RedissonCluster {

    private List<String> nodeAddresses;
}

@Data
public class RedissonSingle {

    private String address;

    private int database;
}

@Configuration
@ConfigurationProperties(prefix = "spring.redis.redisson")
@ConditionalOnProperty("spring.redis.redisson.password")
@Data
public class RedissonRepository {

    private String password;

    private RedissonCluster cluster;

    private RedissonSingle single;
}

import com.ac.core.properties.RedissonRepository;
import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.client.codec.StringCodec;
import org.redisson.config.Config;
import org.redisson.config.SingleServerConfig;
import org.redisson.config.TransportMode;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;

@Slf4j
@Configuration
public class RedissonConfig {

    @Resource
    private RedissonRepository redissonRepository;

    /**
     * Redisson单机配置
     *
     * @return
     */
    @Bean(destroyMethod = "shutdown")
    public RedissonClient singleRedisson() {
        log.info("redisSonRepository={}", redissonRepository);
        Config config = new Config();
        config.setCodec(StringCodec.INSTANCE);
        config.setTransportMode(TransportMode.NIO);
        SingleServerConfig singleServerConfig = config.useSingleServer();
        singleServerConfig.setPassword(redissonRepository.getPassword());
        singleServerConfig.setAddress(redissonRepository.getSingle().getAddress());
        singleServerConfig.setDatabase(redissonRepository.getSingle().getDatabase());
        return Redisson.create(config);
    }
}

3.4 接口加分布式锁

Redisson分布式不能放在@Transactional里,否则会失效。

@Api(tags = "用户")
@RestController
@RequestMapping("member")
public class MemberController {

    @Resource
    private MemberService memberServiceImpl;

    @Resource
    private MemberIntegralComponent memberIntegralComponent;

    @Resource
    private RedissonClient redissonClient;

    @ApiOperation(value = "记录积分")
    @PostMapping("integral")
    public Boolean recordIntegral(@RequestBody @Valid IntegralLogEditVO logEditVO) {
        RLock redisLock = redissonClient.getLock("integral:" + logEditVO.getMemberId());
        try {
            redisLock.lock(5, TimeUnit.SECONDS);
            return memberIntegralComponent.recordIntegral(logEditVO);
        } finally {
            // 释放锁
            if (redisLock.isLocked() && redisLock.isHeldByCurrentThread()) {
                redisLock.unlock();
            }
        }
    }
}

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

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

相关文章

动态规划-分割回文串 II

动态规划-分割回文串 II 1 题目描述2 示例2.1 示例 1&#xff1a;2.2 示例 2&#xff1a;2.3 示例 3&#xff1a;2.4 提示&#xff1a; 3 解题思路和方法3.1 解题思路3.1.1 确定状态3.1.2 转移方程3.1.3 初始条件和边界情况3.1.4 计算顺序3.1.5 回文串的判断方法 3.2 算法代码实…

day17 - 用形状包围图像

在进行图像轮廓提取时&#xff0c;有的情况下不需要我们提取出精确的轮廓&#xff0c;只要提取出一个接近于轮廓的近似多边形&#xff0c;就可以满足后续的操作。 本期我们来学习如何通过设置参数来找出图像的近似多边形。 完成本期内容&#xff0c;你可以&#xff1a; 了解…

《汇编语言》- 读书笔记 - 第5章- [BX]和 loop 指令

《汇编语言》- 读书笔记 - 第5章- [BX]和 loop 指令 5.1 [BX]问题 5.1 5.2 Loop 指令任务 1任务 2任务 3程序 5.1问题 5.2问题 5.2 5.3 在 Debug 中跟踪用 loop 指令实现的循环程序5.4 Debug 和汇编编译器 masm 对指令的不同处理DebugMASM 5.5 loop 和[bx]的联合应用程序 5.5问…

大数据项目实战之数据仓库:电商数据仓库系统——第9章 数仓开发之DWD层

文章目录 第9章 数仓开发之DWD层9.1 交易域加购事务事实表9.2 交易域下单事务事实表9.3 交易域取消订单事务事实表9.4 交易域支付成功事务事实表9.5 交易域退单事务事实表9.6 交易域退款成功事务事实表9.7 交易域购物车周期快照事实表9.8 工具域优惠券领取事务事实表9.9 工具域…

Python论文复现:VMD之自适应选择分解模态数K值

Python论文复现&#xff1a;《基于稀疏指标的优化变分模态分解方法》 信号分解方法中&#xff0c;虽然变分模态分解&#xff08;Variational Mode Decomposition, VMD&#xff09;有严格的数学推导&#xff0c;能有效抑制端点效应、模态混叠等问题&#xff0c;但其分解模态数需…

Docker笔记9 | Docker中网络功能知识梳理和了解

9 | Docker中网络功能知识梳理和了解 1 外部访问容器1.1 访问方式1.2 映射所有接口地址1.3 映射到指定地址的指定端口1.4 映射到指定地址的任意端口1.5 查看映射端口配置 2 容器互联2.1 新建网络2.2 连接容器 3 配置DNS 简单说&#xff1a;Docker 允许通过外部访问容器或容器互…

Python中的全局变量与局部变量

1 定义 全局变量指的是在整个python文件中定义的变量&#xff0c;而局部变量是在函数内部定义的变量。 a 1 def yang_func():b 2 从以上代码中可以看出&#xff0c;变量a是全局变量&#xff0c;变量b是定义在yang_func()函数的内部&#xff0c;因此b是局部变量。 2 使用范…

云原生之部署Docker管理面板SimpleDocker

云原生之部署Docker管理面板SimpleDocker 一、SimpleDocker介绍1. SimpleDocker简介2. SimpleDocker特点 二、本地环境介绍1. 本地环境规划2. 本次实践介绍 三、本地环境检查1.检查Docker服务状态2. 检查Docker版本3.检查docker compose 版本 四、下载SimpleDocker镜像五、部署…

【GD32开发】一、GD32F103 TIMER0 PWM死区时间计算

一、PWM死区时间如何计算&#xff1f; GD32F103的数据手册关于死区时间控制的公式如上图。 DTCFG的值为 设置死区结构体的寄存器值 tDTS_CK的值为 系统时钟的时钟周期。 如&#xff1a;GD32F103的系统时钟是108Mhz, 则tDTS_CK 1/108Mhz 9.26ns。( stm32的这个值跟定时器的…

如何从 Android 内部存储中恢复已删除的照片?

我们使用手机录制的照片和视频通常存储在手机的内存中。我们存储它们以记住我们生活的美丽。然而&#xff0c;在管理这些照片的过程中&#xff0c;一些用户却发现自己不小心删除了这些照片&#xff0c;这很尴尬。 如果您的 Android 设备遇到过此类问题&#xff0c;那么您来对地…

SpringBoot日记本系统小白部署指南

哈喽&#xff0c;大家好&#xff0c;我是兔哥。 之前写的SpringBoot日记本系统备受好评&#xff0c;考虑到还是有很多小伙伴不会部署&#xff0c;所以这一篇文章就单独来讲一下部署步骤吧。 需要资源 idea&#xff08;破不破解都行&#xff09; MySQL&#xff08;最好5.7以…

redis高级篇一

redis 是多线程还是单线程 redis单线程的操作 主要是指redis的网路IO和键值对的读写是由一个线程来完成的&#xff0c;Redis在处理客户端的请求时&#xff0c;包括获取&#xff08;socket 读&#xff09;&#xff0c;解析&#xff0c;执行&#xff0c;内容返回&#xff08;so…

linux(动静态库)

目录&#xff1a; 1.文件时间的概念 2.动态库和静态库 3.如何制作动静态库 -------------------------------------------------------------------------------------------------------------------------------- 1.文件时间的概念 我们现在就开始学习这三个时间分别表示了什…

AI百科:一个开启人工智能时代的综合性平台

无论是人工智能的快速发展还是AI技术在各个领域的广泛应用&#xff0c;都让我们对智能未来充满了期待和好奇。随着科技的进步&#xff0c;发现了一个好网站&#xff0c;一个集合了丰富AI工具和产品介绍的综合性网站—— AI百科。 在人工智能&#xff08;AI&#xff09;领域的快…

【实用工具 - vscode】实现Linux服务器的远程操控

&#x1f31f;hello&#xff0c;各位读者大大们你们好呀&#x1f31f; &#x1f36d;&#x1f36d;系列专栏&#xff1a;【计算机工具】 ✒️✒️本篇内容&#xff1a;vscode的下载安装&#xff0c;常见插件及详细安装方法&#xff0c;插件停用及卸载&#xff0c;vscode快捷键 …

如何将文件制作成二维码

日常生活和工作中&#xff0c;为了让大家更方便的查阅文件&#xff0c;可将文件生成二维码&#xff0c;打印在通知书、会议要求&#xff0c;或是直接用“二维码”分享。通过草料二维码可以将多种类型的文件快速生成文件二维码&#xff0c;微信扫码查看即可查看和下载文件。 ● …

项目风险管理的四个阶段,你了解多少?

项目风险管理是项目管理的一部分&#xff0c;目的是保证项目总目标的实现。 风险管理包括风险识别、风险估计、风险解决和风险监控。风险管理贯穿在项目中的每个环节&#xff0c;在项目的生命周期中监控风险是非常重要的手段。 风险管理包含四个阶段&#xff1a; 1、风险识别…

从需求分析到功能扩展:打破瓶颈,构建智能采购管理软件

在当今快速发展的商业环境下&#xff0c;采购管理已成为企业发展的关键环节之一。如何在采购管理中快速响应市场需求、协调供应链、提高效率和降低成本&#xff0c;是摆在企业面前的一个巨大挑战。因此&#xff0c;打破瓶颈&#xff0c;构建智能采购管理软件成为了迫切的需求。…

内蒙古自治区住房和城乡建设分析及解决方案

安科瑞 徐浩竣 江苏安科瑞电器制造有限公司 zx acrelxhj 摘 要&#xff1a;为深入贯彻落实《国务院办公厅关于印发新能源汽车产业发展规划&#xff08;2021—2035年&#xff09;的通知》&#xff08;国办发 ﹝2020﹞39号&#xff09;、《国家发展改革委等部门关于进一步提升…

一文读懂如何将 Rancher 下游集群升级到 Kubernetes v1.25

介 绍 最初在 Kubernetes v1.21 中被弃用的 PodSecurityPolicy API&#xff0c;已经在 Kubernetes v1.25 中被完全删除。由于 API 被移除&#xff0c;你无法在 Kubernetes v1.25 集群中创建、编辑或查询 PodSecurityPolicy 资源。此外&#xff0c;由于其准入控制器已被移除&am…