feat(ff-game): 添加创建成员接口的缓存锁功能
- 在 ApiMemberController 中添加 RedisCacheLock 依赖 - 在 createMember 方法中实现基于 Redis 的分布式锁- 新增 CacheLockConstants 类用于定义缓存锁的键常量 - 在 ff-base 中添加 Redisson 相关依赖 - 新增 RedisCacheLock 类用于实现 Redis 缓存锁功能 - 修改 application-druid.yml 配置,更新 Redis 连接信息main-cf
parent
170584861b
commit
59af0c07a7
|
@ -121,6 +121,13 @@
|
||||||
<artifactId>commons-io</artifactId>
|
<artifactId>commons-io</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.redisson</groupId>
|
||||||
|
<artifactId>redisson-spring-boot-starter</artifactId>
|
||||||
|
<version>3.23.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
<!-- excel工具 -->
|
<!-- excel工具 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.poi</groupId>
|
<groupId>org.apache.poi</groupId>
|
||||||
|
|
|
@ -61,4 +61,6 @@ public class CacheConstants
|
||||||
* pg游戏投注货币
|
* pg游戏投注货币
|
||||||
*/
|
*/
|
||||||
public static final String PG_GAMES_BET_CURRENCY= "pg_games:bet:currency";
|
public static final String PG_GAMES_BET_CURRENCY= "pg_games:bet:currency";
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -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(); // 释放锁
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -7,9 +7,11 @@ import com.ff.api.request.MemberInfoAllApiRequest;
|
||||||
import com.ff.api.request.MemberInfoApiRequest;
|
import com.ff.api.request.MemberInfoApiRequest;
|
||||||
import com.ff.api.response.MemberInfoAllResponse;
|
import com.ff.api.response.MemberInfoAllResponse;
|
||||||
import com.ff.api.response.MemberInfoResponse;
|
import com.ff.api.response.MemberInfoResponse;
|
||||||
|
import com.ff.base.constant.CacheLockConstants;
|
||||||
import com.ff.base.constant.Constants;
|
import com.ff.base.constant.Constants;
|
||||||
import com.ff.base.core.controller.BaseController;
|
import com.ff.base.core.controller.BaseController;
|
||||||
import com.ff.base.core.domain.AjaxResult;
|
import com.ff.base.core.domain.AjaxResult;
|
||||||
|
import com.ff.base.core.redis.RedisCacheLock;
|
||||||
import com.ff.base.enums.ErrorCode;
|
import com.ff.base.enums.ErrorCode;
|
||||||
import com.ff.base.exception.base.ApiException;
|
import com.ff.base.exception.base.ApiException;
|
||||||
import com.ff.base.exception.base.BaseException;
|
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.config.KeyConfig;
|
||||||
import com.ff.game.api.IGamesService;
|
import com.ff.game.api.IGamesService;
|
||||||
import com.ff.game.api.request.*;
|
import com.ff.game.api.request.*;
|
||||||
import com.ff.game.domain.GameSecretKey;
|
|
||||||
import com.ff.game.dto.GameSecretKeyCurrencyDTO;
|
import com.ff.game.dto.GameSecretKeyCurrencyDTO;
|
||||||
import com.ff.game.service.IGameSecretKeyCurrencyService;
|
import com.ff.game.service.IGameSecretKeyCurrencyService;
|
||||||
import com.ff.game.service.IGameSecretKeyService;
|
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.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
import org.springframework.transaction.annotation.Isolation;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.CollectionUtils;
|
|
||||||
import org.springframework.util.ObjectUtils;
|
import org.springframework.util.ObjectUtils;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
@ -84,6 +85,10 @@ public class ApiMemberController extends BaseController {
|
||||||
@Resource
|
@Resource
|
||||||
private IGameSecretKeyCurrencyService gameSecretKeyCurrencyService;
|
private IGameSecretKeyCurrencyService gameSecretKeyCurrencyService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RedisCacheLock redisCacheLock;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建成员
|
* 创建成员
|
||||||
*
|
*
|
||||||
|
@ -91,53 +96,60 @@ public class ApiMemberController extends BaseController {
|
||||||
* @return {@link AjaxResult }
|
* @return {@link AjaxResult }
|
||||||
*/
|
*/
|
||||||
@PostMapping("/create")
|
@PostMapping("/create")
|
||||||
@Transactional
|
@Transactional(isolation = Isolation.READ_COMMITTED)
|
||||||
public synchronized AjaxResult createMember(@Validated @RequestBody MemberCreateApiRequest memberCreateApiRequest) {
|
public AjaxResult createMember(@Validated @RequestBody MemberCreateApiRequest memberCreateApiRequest) {
|
||||||
|
|
||||||
IGamesService iGamesService = gamesService.get(memberCreateApiRequest.getPlatformCode() + Constants.SERVICE);
|
|
||||||
ApiException.notNull(iGamesService, ErrorCode.PLATFORM_NOT_EXIST.getCode());
|
|
||||||
|
|
||||||
TenantSecretKey tenantSecretKey = keyConfig.get();
|
TenantSecretKey tenantSecretKey = keyConfig.get();
|
||||||
GameSecretKeyCurrencyDTO gameSecretKey = gameSecretKeyCurrencyService.findByGameSecretKeyCurrencyDTO(GameSecretKeyCurrencyDTO.builder()
|
String lockName = CacheLockConstants.CREATE_MEMBER + memberCreateApiRequest.getAccount() + memberCreateApiRequest.getCurrencyCode() + memberCreateApiRequest.getPlatformCode() + tenantSecretKey.getTenantSn();
|
||||||
.platformCode(memberCreateApiRequest.getPlatformCode())
|
|
||||||
.systemCurrency(memberCreateApiRequest.getCurrencyCode()).build());
|
|
||||||
|
|
||||||
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());
|
||||||
|
|
||||||
|
|
||||||
// 获取用户信息
|
GameSecretKeyCurrencyDTO gameSecretKey = gameSecretKeyCurrencyService.findByGameSecretKeyCurrencyDTO(GameSecretKeyCurrencyDTO.builder()
|
||||||
Member gameMember = memberService.selectMemberByAccount(memberCreateApiRequest.getAccount(), memberCreateApiRequest.getCurrencyCode(), memberCreateApiRequest.getPlatformCode());
|
.platformCode(memberCreateApiRequest.getPlatformCode())
|
||||||
if (!ObjectUtils.isEmpty(gameMember)){
|
.systemCurrency(memberCreateApiRequest.getCurrencyCode()).build());
|
||||||
throw new ApiException(ErrorCode.GAME_ACCOUNT_CREATION_FAILED.getCode());
|
|
||||||
|
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());
|
ApiException.notNull(member, ErrorCode.ACCOUNT_NOT_EXIST.getCode());
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//向第三方查询账号
|
//向第三方查询账号
|
||||||
MemberInfoRequestDTO gamesBaseRequestDTO = MemberInfoRequestDTO.builder()
|
MemberInfoRequestDTO gamesBaseRequestDTO = MemberInfoRequestDTO.builder()
|
||||||
.accounts(member.getGameAccount())
|
.accounts(member.getGameAccount())
|
||||||
|
@ -191,9 +202,6 @@ public class ApiMemberController extends BaseController {
|
||||||
public AjaxResult infoAll(@Validated @RequestBody MemberInfoAllApiRequest memberInfoAllApiRequest) {
|
public AjaxResult infoAll(@Validated @RequestBody MemberInfoAllApiRequest memberInfoAllApiRequest) {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
List<GameSecretKeyCurrencyDTO> gameSecretKeys = gameSecretKeyCurrencyService.findByGameSecretKeyCurrencyDTOList(GameSecretKeyCurrencyDTO.builder()
|
List<GameSecretKeyCurrencyDTO> gameSecretKeys = gameSecretKeyCurrencyService.findByGameSecretKeyCurrencyDTOList(GameSecretKeyCurrencyDTO.builder()
|
||||||
.systemCurrency(memberInfoAllApiRequest.getCurrencyCode()).build());
|
.systemCurrency(memberInfoAllApiRequest.getCurrencyCode()).build());
|
||||||
|
|
||||||
|
|
|
@ -3,13 +3,13 @@ spring:
|
||||||
# redis 配置
|
# redis 配置
|
||||||
redis:
|
redis:
|
||||||
# 地址
|
# 地址
|
||||||
host: 127.0.0.1
|
host: 192.168.50.11
|
||||||
# 端口,默认为6379
|
# 端口,默认为6379
|
||||||
port: 6379
|
port: 26379
|
||||||
# 数据库索引
|
# 数据库索引
|
||||||
database: 1
|
database: 10
|
||||||
# 密码
|
# 密码
|
||||||
password:
|
password: reAa123456
|
||||||
# 连接超时时间
|
# 连接超时时间
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
lettuce:
|
lettuce:
|
||||||
|
|
Loading…
Reference in New Issue