From 59af0c07a7bb1bd14c8df68ffe7004ede6490240 Mon Sep 17 00:00:00 2001 From: shi Date: Tue, 18 Mar 2025 14:10:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(ff-game):=20=E6=B7=BB=E5=8A=A0=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E6=88=90=E5=91=98=E6=8E=A5=E5=8F=A3=E7=9A=84=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E9=94=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 ApiMemberController 中添加 RedisCacheLock 依赖 - 在 createMember 方法中实现基于 Redis 的分布式锁- 新增 CacheLockConstants 类用于定义缓存锁的键常量 - 在 ff-base 中添加 Redisson 相关依赖 - 新增 RedisCacheLock 类用于实现 Redis 缓存锁功能 - 修改 application-druid.yml 配置,更新 Redis 连接信息 --- ff-base/pom.xml | 7 ++ .../com/ff/base/constant/CacheConstants.java | 2 + .../ff/base/constant/CacheLockConstants.java | 17 +++ .../ff/base/core/redis/RedisCacheLock.java | 71 ++++++++++++ .../api/controller/ApiMemberController.java | 102 ++++++++++-------- .../src/main/resources/application-druid.yml | 8 +- 6 files changed, 156 insertions(+), 51 deletions(-) create mode 100644 ff-base/src/main/java/com/ff/base/constant/CacheLockConstants.java create mode 100644 ff-base/src/main/java/com/ff/base/core/redis/RedisCacheLock.java diff --git a/ff-base/pom.xml b/ff-base/pom.xml index dbf57ac..db88db5 100644 --- a/ff-base/pom.xml +++ b/ff-base/pom.xml @@ -121,6 +121,13 @@ commons-io + + org.redisson + redisson-spring-boot-starter + 3.23.0 + + + org.apache.poi diff --git a/ff-base/src/main/java/com/ff/base/constant/CacheConstants.java b/ff-base/src/main/java/com/ff/base/constant/CacheConstants.java index 3d96357..57d0452 100644 --- a/ff-base/src/main/java/com/ff/base/constant/CacheConstants.java +++ b/ff-base/src/main/java/com/ff/base/constant/CacheConstants.java @@ -61,4 +61,6 @@ public class CacheConstants * pg游戏投注货币 */ public static final String PG_GAMES_BET_CURRENCY= "pg_games:bet:currency"; + + } diff --git a/ff-base/src/main/java/com/ff/base/constant/CacheLockConstants.java b/ff-base/src/main/java/com/ff/base/constant/CacheLockConstants.java new file mode 100644 index 0000000..e0f1df7 --- /dev/null +++ b/ff-base/src/main/java/com/ff/base/constant/CacheLockConstants.java @@ -0,0 +1,17 @@ +package com.ff.base.constant; + +/** + * 缓存锁的key 常量 + * + * @author ff + */ +public class CacheLockConstants +{ + /** + * 登录用户 redis key + */ + public static final String CREATE_MEMBER = "CREATE:MEMBER:"; + + + +} diff --git a/ff-base/src/main/java/com/ff/base/core/redis/RedisCacheLock.java b/ff-base/src/main/java/com/ff/base/core/redis/RedisCacheLock.java new file mode 100644 index 0000000..aba978b --- /dev/null +++ b/ff-base/src/main/java/com/ff/base/core/redis/RedisCacheLock.java @@ -0,0 +1,71 @@ +package com.ff.base.core.redis; + +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.concurrent.TimeUnit; + + +/** + * redis锁 + * + * @author shi + * @date 2025/03/18 + */ +@Component +public class RedisCacheLock { + + @Resource + private RedissonClient redissonClient; + + /** + * 获取 Redis 分布式锁 + * + * @param key 锁的唯一标识(Redis 键),用于标识要获取的锁 + * @return 返回一个 `RLock` 对象,表示分布式锁 + * + * `RLock` 是 Redisson 提供的分布式锁对象,可以用于多线程或分布式环境中,确保某一时刻只有一个线程(或服务实例)可以执行某个操作。 + */ + public RLock getLock(String key) { + return redissonClient.getLock(key); + } + + /** + * 尝试加锁(可重入锁) + * + * @param key 锁的唯一标识(Redis 键) + * @param waitTime 等待获取锁的最大时间(单位:秒)。如果在指定的时间内未能获取锁,则返回 `false`。 + * @param leaseTime 锁的有效时间(单位:秒)。获取到锁之后,锁会在 `leaseTime` 时间后自动释放,避免死锁。 + * @return `true` 表示成功获取锁,`false` 表示未能在指定时间内获取到锁 + * + * 该方法是可重入锁,意味着同一个线程可以多次加锁而不会发生死锁。`tryLock` 方法不会阻塞,能够防止线程长时间等待。 + * 如果锁已经被其他线程获取,当前线程将会等待 `waitTime` 期间,如果在此时间内无法获取锁,方法将返回 `false`。 + */ + public boolean tryLock(String key, long waitTime, long leaseTime) { + RLock lock = redissonClient.getLock(key); + try { + return lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); // 恢复线程中断状态 + return false; + } + } + + /** + * 释放锁 + * + * @param key 锁的唯一标识(Redis 键) + * + * 该方法用于释放已经获取的分布式锁。如果当前线程持有锁(即 `RLock` 对象的 `isHeldByCurrentThread()` 方法返回 `true`), + * 则会释放锁。如果不是当前线程持有锁,则该方法不做任何操作,避免抛出异常。 + */ + public void unlock(String key) { + RLock lock = redissonClient.getLock(key); + if (lock.isHeldByCurrentThread()) { + lock.unlock(); // 释放锁 + } + } + +} diff --git a/ff-game/src/main/java/com/ff/api/controller/ApiMemberController.java b/ff-game/src/main/java/com/ff/api/controller/ApiMemberController.java index 2d42f5e..8b7432f 100644 --- a/ff-game/src/main/java/com/ff/api/controller/ApiMemberController.java +++ b/ff-game/src/main/java/com/ff/api/controller/ApiMemberController.java @@ -7,9 +7,11 @@ import com.ff.api.request.MemberInfoAllApiRequest; import com.ff.api.request.MemberInfoApiRequest; import com.ff.api.response.MemberInfoAllResponse; import com.ff.api.response.MemberInfoResponse; +import com.ff.base.constant.CacheLockConstants; import com.ff.base.constant.Constants; import com.ff.base.core.controller.BaseController; import com.ff.base.core.domain.AjaxResult; +import com.ff.base.core.redis.RedisCacheLock; import com.ff.base.enums.ErrorCode; import com.ff.base.exception.base.ApiException; import com.ff.base.exception.base.BaseException; @@ -18,7 +20,6 @@ import com.ff.base.system.domain.TenantSecretKey; import com.ff.config.KeyConfig; import com.ff.game.api.IGamesService; import com.ff.game.api.request.*; -import com.ff.game.domain.GameSecretKey; import com.ff.game.dto.GameSecretKeyCurrencyDTO; import com.ff.game.service.IGameSecretKeyCurrencyService; import com.ff.game.service.IGameSecretKeyService; @@ -30,9 +31,9 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -84,6 +85,10 @@ public class ApiMemberController extends BaseController { @Resource private IGameSecretKeyCurrencyService gameSecretKeyCurrencyService; + @Resource + private RedisCacheLock redisCacheLock; + + /** * 创建成员 * @@ -91,53 +96,60 @@ public class ApiMemberController extends BaseController { * @return {@link AjaxResult } */ @PostMapping("/create") - @Transactional - public synchronized AjaxResult createMember(@Validated @RequestBody MemberCreateApiRequest memberCreateApiRequest) { - - IGamesService iGamesService = gamesService.get(memberCreateApiRequest.getPlatformCode() + Constants.SERVICE); - ApiException.notNull(iGamesService, ErrorCode.PLATFORM_NOT_EXIST.getCode()); - + @Transactional(isolation = Isolation.READ_COMMITTED) + public AjaxResult createMember(@Validated @RequestBody MemberCreateApiRequest memberCreateApiRequest) { TenantSecretKey tenantSecretKey = keyConfig.get(); - GameSecretKeyCurrencyDTO gameSecretKey = gameSecretKeyCurrencyService.findByGameSecretKeyCurrencyDTO(GameSecretKeyCurrencyDTO.builder() - .platformCode(memberCreateApiRequest.getPlatformCode()) - .systemCurrency(memberCreateApiRequest.getCurrencyCode()).build()); + String lockName = CacheLockConstants.CREATE_MEMBER + memberCreateApiRequest.getAccount() + memberCreateApiRequest.getCurrencyCode() + memberCreateApiRequest.getPlatformCode() + tenantSecretKey.getTenantSn(); - ApiException.notNull(gameSecretKey, ErrorCode.CURRENCY_NOT_EXIST.getCode()); + //加锁防止重复 + boolean tryLock = redisCacheLock.tryLock(lockName, 10, 10); - String gameAccount = StringUtils.addSuffix(memberService.getMemberGameAccount(), tenantSecretKey.getTenantSn()); + try { + if (tryLock) { + IGamesService iGamesService = gamesService.get(memberCreateApiRequest.getPlatformCode() + Constants.SERVICE); + ApiException.notNull(iGamesService, ErrorCode.PLATFORM_NOT_EXIST.getCode()); - // 获取用户信息 - Member gameMember = memberService.selectMemberByAccount(memberCreateApiRequest.getAccount(), memberCreateApiRequest.getCurrencyCode(), memberCreateApiRequest.getPlatformCode()); - if (!ObjectUtils.isEmpty(gameMember)){ - throw new ApiException(ErrorCode.GAME_ACCOUNT_CREATION_FAILED.getCode()); + GameSecretKeyCurrencyDTO gameSecretKey = gameSecretKeyCurrencyService.findByGameSecretKeyCurrencyDTO(GameSecretKeyCurrencyDTO.builder() + .platformCode(memberCreateApiRequest.getPlatformCode()) + .systemCurrency(memberCreateApiRequest.getCurrencyCode()).build()); + + ApiException.notNull(gameSecretKey, ErrorCode.CURRENCY_NOT_EXIST.getCode()); + + String gameAccount = StringUtils.addSuffix(memberService.getMemberGameAccount(), tenantSecretKey.getTenantSn()); + + // 获取用户信息 + Member gameMember = memberService.selectMemberByAccount(memberCreateApiRequest.getAccount(), memberCreateApiRequest.getCurrencyCode(), memberCreateApiRequest.getPlatformCode()); + if (!ObjectUtils.isEmpty(gameMember)) { + throw new ApiException(ErrorCode.GAME_ACCOUNT_CREATION_FAILED.getCode()); + } + //注册本地账号 + Member member = Member.builder() + .tenantKey(tenantSecretKey.getTenantKey()) + .memberAccount(memberCreateApiRequest.getAccount()) + .gameAccount(gameAccount) + .platformCode(memberCreateApiRequest.getPlatformCode()) + .currencyCode(memberCreateApiRequest.getCurrencyCode()) + .build(); + int insertMember = memberService.insertMember(member); + Assert.isTrue(insertMember > 0, "建立游戏账号失败"); + + //向第三方注册账号 + CreateMemberRequestDTO gamesBaseRequestDTO = CreateMemberRequestDTO.builder() + .account(gameAccount) + .agentId(gameSecretKey.getCode()) + .agentKey(gameSecretKey.getKey()) + .currency(gameSecretKey.getCurrency()) + .build(); + Boolean result = iGamesService.createMember(gamesBaseRequestDTO); + Assert.isTrue(result, "建立游戏账号失败"); + } else { + throw new ApiException(ErrorCode.FREQUENT_INTERFACE_REQUESTS.getCode()); + } + return toAjax(Boolean.TRUE); + } finally { + redisCacheLock.unlock(lockName); } - - - - - //注册本地账号 - Member member = Member.builder() - .tenantKey(tenantSecretKey.getTenantKey()) - .memberAccount(memberCreateApiRequest.getAccount()) - .gameAccount(gameAccount) - .platformCode(memberCreateApiRequest.getPlatformCode()) - .currencyCode(memberCreateApiRequest.getCurrencyCode()) - .build(); - int insertMember = memberService.insertMember(member); - Assert.isTrue(insertMember > 0, "建立游戏账号失败"); - - //向第三方注册账号 - CreateMemberRequestDTO gamesBaseRequestDTO = CreateMemberRequestDTO.builder() - .account(gameAccount) - .agentId(gameSecretKey.getCode()) - .agentKey(gameSecretKey.getKey()) - .currency(gameSecretKey.getCurrency()) - .build(); - Boolean result = iGamesService.createMember(gamesBaseRequestDTO); - Assert.isTrue(result, "建立游戏账号失败"); - return toAjax(Boolean.TRUE); - } @@ -166,7 +178,6 @@ public class ApiMemberController extends BaseController { ApiException.notNull(member, ErrorCode.ACCOUNT_NOT_EXIST.getCode()); - //向第三方查询账号 MemberInfoRequestDTO gamesBaseRequestDTO = MemberInfoRequestDTO.builder() .accounts(member.getGameAccount()) @@ -191,9 +202,6 @@ public class ApiMemberController extends BaseController { public AjaxResult infoAll(@Validated @RequestBody MemberInfoAllApiRequest memberInfoAllApiRequest) { - - - List gameSecretKeys = gameSecretKeyCurrencyService.findByGameSecretKeyCurrencyDTOList(GameSecretKeyCurrencyDTO.builder() .systemCurrency(memberInfoAllApiRequest.getCurrencyCode()).build()); diff --git a/ff-game/src/main/resources/application-druid.yml b/ff-game/src/main/resources/application-druid.yml index 89db033..dd61f2a 100644 --- a/ff-game/src/main/resources/application-druid.yml +++ b/ff-game/src/main/resources/application-druid.yml @@ -3,13 +3,13 @@ spring: # redis 配置 redis: # 地址 - host: 127.0.0.1 + host: 192.168.50.11 # 端口,默认为6379 - port: 6379 + port: 26379 # 数据库索引 - database: 1 + database: 10 # 密码 - password: + password: reAa123456 # 连接超时时间 timeout: 10s lettuce: