用户注册
用户注册支持 用户名+密码 进行注册,且用户名唯一、用户注销后用户名可复用
问题
若用户名已被注册,在数据库中存在数据,并且已经将数据写入到redis缓存,若用户名未注册,则数据库和缓存均无数据,当新用户进行注册,首先调用查询接口,查询用户名是否存在:具体而言是先查缓存,如果未命中,不能说明数据库中就不存在该用户名(因为redis缓存数据有可能过期 或者 可能由于服务问题导致没有将数据库数据写入缓存),因此还需要去查数据库,若数据库也未命中,则最后说明该用户名是可用的。然而,查询用户名是否可用,并不一定就使用该用户名注册(根据业务分析)
那么,如果同一时刻有大量查询请求打到数据库上,就给数据库造成巨大压力。
解决方案
1、 对不存在的key进行缓存,value设置为null,并设置短暂过期时间,如60s
缺点:用户A查询了一次可用的用户名,但没使用,且这时把该用户名作为key,null作为value存入redis缓存,短暂过期时间为60s。则在这60s内该用户名实际上是没人注册的,但因为存入缓存,导致该用户名短暂地不可用,其他用户不能用这个用户名注册
2、将已注册的用户名存入布隆过滤器:
优点:当用户发起查询请求时,先从布隆过滤器中查询,如果未命中,则说明数据库一定不存在该用户名,不会去查数据库,避免造成巨大压力
缺点:布隆过滤器无法删除元素,如果删除,可能影响其他元素的判断
3、将已注册的用户名存入Redis Set里:
缺点:无法存储大量数据。如果Set里未命中,仍然需要查数据库,给数据库造成巨大压力。
4、高并发时,依旧先查缓存,未命中则使用分布式锁限制只能有一个线程查询数据库:
缺点:其他线程阻塞,请求缓慢或超时
最终解决方案
将已注册的用户名存入布隆过滤器中,防止未命中时给数据库造成巨大压力
然后每次有用户注销时,将注销的用户名存入Redis Set里,在布隆过滤器命中时,由于布隆过滤器无法删除的特点(无法保证该用户名是否是已注销的,即可能误判),为了解决误判的问题,可以继续查询 注销用户名Set集合,如果命中了,说明前面的布隆过滤器误判了,该用户名是可用的,如果未命中,说明布隆过滤器未误判,该用户名不可用
优化
随着时间推移,注销的用户可能越来越多,Set集合不足以支持大量数据的存储,因此采取两种方案:
1、可以限制一个用户最多只能注销5次
2、即使限制了,又随着时间推移,依旧可能产生大量数据,这就是Redis 大Key问题,因此考虑将Set分片,对用户名hashcode取模,将数据分散存储在1024个Set结构中
实操
用户注册请求体
public class UserRegisterReqDTO {
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 真实姓名
*/
private String realName;
/**
* 证件类型
*/
private Integer idType;
/**
* 证件号
*/
private String idCard;
/**
* 手机号
*/
private String phone;
/**
* 邮箱
*/
private String mail;
/**
* 旅客类型
*/
private Integer userType;
/**
* 审核状态
*/
private Integer verifyState;
/**
* 邮编
*/
private String postCode;
/**
* 地址
*/
private String address;
/**
* 国家/地区
*/
private String region;
/**
* 固定电话
*/
private String telephone;
}
用户信息实体
@Data
@TableName("t_user")
public class UserDO extends BaseDO {
/**
* id
*/
private Long id;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 真实姓名
*/
private String realName;
/**
* 国家/地区
*/
private String region;
/**
* 证件类型
*/
private Integer idType;
/**
* 证件号
*/
private String idCard;
/**
* 手机号
*/
private String phone;
/**
* 固定电话
*/
private String telephone;
/**
* 邮箱
*/
private String mail;
/**
* 旅客类型
*/
private Integer userType;
/**
* 审核状态
*/
private Integer verifyStatus;
/**
* 邮编
*/
private String postCode;
/**
* 地址
*/
private String address;
/**
* 注销时间戳
*/
private Long deletionTime;
}
用户手机号实体
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@TableName("t_user_phone")
public class UserPhoneDO extends BaseDO {
/**
* id
*/
private Long id;
/**
* 用户名
*/
private String username;
/**
* 手机号
*/
private String phone;
/**
* 注销时间戳
*/
private Long deletionTime;
}
用户邮箱实体
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@TableName("t_user_mail")
public class UserMailDO extends BaseDO {
/**
* id
*/
private Long id;
/**
* 用户名
*/
private String username;
/**
* 手机号
*/
private String mail;
/**
* 注销时间戳
*/
private Long deletionTime;
}
controller
/**
* 注册用户
*/
@PostMapping("/api/user-service/register")
public Result<UserRegisterRespDTO> register(@RequestBody @Valid UserRegisterReqDTO requestParam) {
return Results.success(userLoginService.register(requestParam));
}
serviceImpl
首先使用责任链设计模式进行基础校验
@Transactional(rollbackFor = Exception.class)
@Override
public UserRegisterRespDTO register(UserRegisterReqDTO requestParam) {
abstractChainContext.handler(UserChainMarkEnum.USER_REGISTER_FILTER.name(), requestParam);
RLock lock = redissonClient.getLock(LOCK_USER_REGISTER + requestParam.getUsername());
boolean tryLock = lock.tryLock();
if (!tryLock) {
throw new ServiceException(HAS_USERNAME_NOTNULL);
}
try {
try {
int inserted = userMapper.insert(BeanUtil.convert(requestParam, UserDO.class));
if (inserted < 1) {
throw new ServiceException(USER_REGISTER_FAIL);
}
} catch (DuplicateKeyException dke) {
log.error("用户名 [{}] 重复注册", requestParam.getUsername());
throw new ServiceException(HAS_USERNAME_NOTNULL);
}
UserPhoneDO userPhoneDO = UserPhoneDO.builder()
.phone(requestParam.getPhone())
.username(requestParam.getUsername())
.build();
try {
userPhoneMapper.insert(userPhoneDO);
} catch (DuplicateKeyException dke) {
log.error("用户 [{}] 注册手机号 [{}] 重复", requestParam.getUsername(), requestParam.getPhone());
throw new ServiceException(PHONE_REGISTERED);
}
if (StrUtil.isNotBlank(requestParam.getMail())) {
UserMailDO userMailDO = UserMailDO.builder()
.mail(requestParam.getMail())
.username(requestParam.getUsername())
.build();
try {
userMailMapper.insert(userMailDO);
} catch (DuplicateKeyException dke) {
log.error("用户 [{}] 注册邮箱 [{}] 重复", requestParam.getUsername(), requestParam.getMail());
throw new ServiceException(MAIL_REGISTERED);
}
}
String username = requestParam.getUsername();
userReuseMapper.delete(Wrappers.update(new UserReuseDO(username)));
StringRedisTemplate instance = (StringRedisTemplate) distributedCache.getInstance();
instance.opsForSet().remove(USER_REGISTER_REUSE_SHARDING + hashShardingIdx(username), username);
userRegisterCachePenetrationBloomFilter.add(username);
} finally {
lock.unlock();
}
return BeanUtil.convert(requestParam, UserRegisterRespDTO.class);
}
最后的一部分还不是很理解 TODO