场景描述
在多线程访问下,用户在提现的时候,该怎么保证数据的线程安全以及幂等性?
分析
保证数据线程安全,可以使用分布式锁
而要实现同一段时间内请求的幂等性,可以同样借助分布式锁给我们提供的RedissonClient
解决方案
基于redisson分布式锁+redis键值对过期时间,双重校验锁
- 以下是使用的依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.3</version>
</dependency>
- spring容器中配置RedissonClient
@Configuration
@Slf4j
public class RedissonSingleConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private Integer redisPort;
@Value("${spring.redis.password}")
private String redisPassword;
@Value("${spring.redis.database}")
private Integer redisDatabase;
@Value("${spring.redis.timeout}")
private Duration redisTimeout;
@Bean
public RedissonClient redissonClient() {
RedissonClient redissonClient;
Config config = new Config();
//解决中文乱码问题
config.setCodec(new JsonJacksonCodec());
SingleServerConfig serverConfig = config.useSingleServer();
String url = "redis://" + redisHost + ":" + redisPort;
serverConfig.setAddress(url);
serverConfig.setPassword(StringUtils.isEmpty(redisPassword)?null:redisPassword);
serverConfig.setDatabase(redisDatabase);
serverConfig.setTimeout(Integer.parseInt(String.valueOf(redisTimeout.toMillis())));
serverConfig.setConnectTimeout(Integer.parseInt(String.valueOf(redisTimeout.toMillis())));
try {
redissonClient = Redisson.create(config);
return redissonClient;
} catch (Exception e) {
log.error("RedissonSingleClient init redis url:[{}], Exception:", url, e);
return null;
}
}
}
- 核心代码
//1.外层校验 防止用户连点 一次性产生多笔提现
RBucket<String> bucket = redissonClient.getBucket("Bucket"+UserId);
String check = (String)bucket.get();
if(check != null){
//避免重复点击
return s;
}
//2.获取分布式锁 利用redission分布式锁,防止同一用户并发提现
String lockKey = UserId;
RLock lock = redissonClient.getLock(lockKey);
lock.lock(30, TimeUnit.SECONDS);
//3.双重校验,防止连续提现
if(check != null){
//避免重复点击
//return null;
return s;
}
/**
* 4.提现业务代码
*/
//5.幂等性设置
check.set("提现中。。。请勿重复点击",30,TimeUnit.SECONDS);
//6.释放分布式锁
if(lock.isLocked() && lock.isHeldByCurrentThread()){
lock.unlock();
}