用户重复注册分析(用户重复注册分析怎么写)

  本篇文章为你整理了用户重复注册分析(用户重复注册分析怎么写)的详细内容,包含有用户重复注册分析怎么做 用户重复注册分析怎么写 重复注册内容 重复注册是什么意思 用户重复注册分析,希望能帮助你了解 用户重复注册分析。

  博主github地址:github.com/wayn111

  一 复现过程

  线上客户端用户使用微信扫码登陆时需要再绑定一个手机号,在绑定手机后,用户购买客户端商品下线再登录,发现用户账号ID被变更,已经不是用户刚绑定手机号时自动登录的用户账号ID,查询线上数据库,发现同一个手机生成了多个账号id,至此问题复现

  二 分析过程

  发现数据库中一个手机号生成了多个用户账号,第一反应是用户在绑定手机号过程中,多次点击绑定按钮,导致绑定接口被调用多次,造成多线程并发调用用户注册接口,进而生成多个账号。为了验证我们的猜想,直接查看绑定手机后的用户注册方法

  

/**

 

   * 根据用户手机号进行注册操作

  // 启动@Transactional事务注解

  @Transactional(rollbackFor = Exception.class)

  public boolean userRegister(LoginReqBody body, BaseReqHeader header, BaseResp BaseRespHeader, LoginRespBody resp) {

   RedisLock redisLock = redisCache.getRedisLock(RedisNameEnum.USER_REGISTER_LOCK.get(""), 10);

   boolean lock;

   try {

   lock = redisLock.lock();

   // 使用redis分布式锁

   if (lock) {

   // 查询数据库该用户手机号是否插入成功,已存在则退出操作

   MemberDO member = mapper.findByMobile(body.getAccount(), body.getRegRes());

   if (Objects.nonNull(member)) {

   resp.setResultFail(ReturnCodeEnum.USER_EXIST);

   return false;

   // 执行用户注册操作,包含插入用户表、订单表、是否被邀请

   } catch (Exception e) {

   log.error("用户注册失败:", e);

   throw new Exception("用户注册失败");

   } finally {

   redisLock.unLock();

   // 添加注册日志,上报到数据分析平台...

   return true;

  

 

  初看代码,在分布式环境中,先加分布式锁保证同时只能被一个线程执行,然后判断数据库中是否存在用户手机信息,已存在则退出,不存在则执行用户注册操作,咋以为逻辑上没有问题,但是线上环境确实就是出现了相同手机号重复注册的问题,首先代码被 @Transactional 注解包含,就是在自动事务中执行注册逻辑

  现在博主带大家回忆一下,MySQL 事务的隔离级别有4个

  Read uncommitted:读取未提交,其他事务只要修改了数据,即使未提交,本事务也能看到修改后的数据值。

  Read committed:读取已提交,其他事务提交了对数据的修改后,本事务就能读取到修改后的数据值。

  Repeatable read:可重复读,无论其他事务是否修改并提交了数据,在这个事务中看到的数据值始终不受其他事务影响。

  Serializable:串行化,一个事务一个事务的执行。

  MySQL数据库默认使用可重复读( Repeatable read)。

  隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大,MySQL的默认隔离级别是读可重复读。在上述场景里,也就是说,无论其他线程事务是否提交了数据,当前线程所在事务中看到的数据值始终不受其他事务影响

  说人话(划重点):就是在 MySQL 中一个线程所在事务是读不到另一个线程事务未提交的数据的

  下面结合上述代码给出分析过程:上述注册逻辑都包含在 Spring 提供的自动事务中,整个方法都在事务中。而加锁也在事务中执行。最终导致我们注册 线程B 在当前事物中查询不到另一个注册 线程A 所在事物未提交的数据, 举个例子

  eg:

  当用户执行注册操作,重复点击注册按钮时,假设线程A和B同时执行到 redisLock.lock()时,假设线程A获取到锁,线程B进入自旋等待,线程A执行mapper.findByMobile(body.getAccount(), body.getRegRes())操作,发现用户手机不存在数据库中,进行注册操作(添加用户信息入库等),执行完毕,释放锁。执行后续添加注册日志,上报到数据分析平台操作,注意此时事务还未提交。

  
线程B终于获取到锁,执行mapper.findByMobile(body.getAccount(), body.getRegRes())操作,在我们一开始的假设中,以为这里会返回用户已存在,但是实际执行结果并不是这样的。原因就是线程A的事务还未提交,线程B读不到线程A未提交事务的数据也就是说查不到用户已注册信息,至此,我们知道了用户重复注册的原因。

  三 解决方案:

  给出三种解决方案

  3.1 修改事务范围,将事务的操作代码最小化,保证在加锁结束前完成事务提交,代码如下开启手动事务,这样其他线程在加锁代码块中就能看到最新数据

  

@Autowired

 

  private PlatformTransactionManager platformTransactionManager;

  @Autowired

  private TransactionDefinition transactionDefinition;

  private boolean userRegister(LoginReqBody body, BaseReqHeader header, BaseResp BaseRespHeader, LoginRespBody resp) {

   RedisLock redisLock = redisCache.getRedisLock(RedisNameEnum.USER_REGISTER_LOCK.get(""), 10);

   boolean lock;

   TransactionStatus transaction = null;

   try {

   lock = redisLock.lock();

   // 使用redis分布式锁

   if (lock) {

   // 查询数据库该用户手机号是否插入成功,已存在则退出操作

   MemberDO member = mapper.findByMobile(body.getAccount(), body.getRegRes());

   if (Objects.nonNull(member)) {

   resp.setResultFail(ReturnCodeEnum.USER_EXIST);

   return false;

   // 手动开启事务

   transaction = platformTransactionManager.getTransaction(transactionDefinition);

   // 执行用户注册操作,包含插入用户表、订单表、是否被邀请

   // 手动提交事务

   platformTransactionManager.commit(transaction);

   } catch (Exception e) {

   log.error("用户注册失败:", e);

   if (transaction != null) {

   platformTransactionManager.rollback(transaction);

   return false;

   } finally {

   redisLock.unLock();

   // 添加注册日志,上报到数据分析平台...

   return true;

  

 

  3.2 在用户注册时针对注册接口添加防重复提交处理

  下面给出一个基于 AOP 切面 + 注解实现的限流逻辑

  

/**

 

   * 限流枚举

  public enum LimitType {

   // 默认

   CUSTOMER,

   // by ip addr

   * 自定义接口限流

   * @author jacky

  @Target(ElementType.METHOD)

  @Retention(RetentionPolicy.RUNTIME)

  public @interface Limit {

   boolean useAccount() default true;

   String name() default "";

   String key() default "";

   String prefix() default "";

   int period();

   int count();

   LimitType limitType() default LimitType.CUSTOMER;

   * 限制器切面

  @Slf4j

  @Aspect

  @Component

  public class LimitAspect {

   @Autowired

   private StringRedisTemplate stringRedisTemplate;

   @Pointcut("@annotation(com.dogame.dragon.sparrow.framework.common.annotation.Limit)")

   public void pointcut() {

   @Around("pointcut()")

   public Object around(ProceedingJoinPoint joinPoint) throws Throwable {

   ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

   HttpServletRequest request = attrs.getRequest();

   MethodSignature signature = (MethodSignature) joinPoint.getSignature();

   Method signatureMethod = signature.getMethod();

   Limit limit = signatureMethod.getAnnotation(Limit.class);

   boolean useAccount = limit.useAccount();

   LimitType limitType = limit.limitType();

   String key = limit.key();

   if (StringUtils.isEmpty(key)) {

   if (limitType == LimitType.IP) {

   key = IpUtils.getIpAddress(request);

   } else {

   key = signatureMethod.getName();

   if (useAccount) {

   LoginMember loginMember = LocalContext.getLoginMember();

   if (loginMember != null) {

   key = key + "_" + loginMember.getAccount();

   String join = StringUtils.join(limit.prefix(), key, "_", request.getRequestURI().replaceAll("/", "_"));

   List String strings = Collections.singletonList(join);

   String luaScript = buildLuaScript();

   RedisScript Long redisScript = new DefaultRedisScript (luaScript, Long.class);

   Long count = stringRedisTemplate.execute(redisScript, strings, limit.count() + "", limit.period() + "");

   if (null != count count.intValue() = limit.count()) {

   log.info("第{}次访问key为 {},描述为 [{}] 的接口", count, strings, limit.name());

   return joinPoint.proceed();

   } else {

   throw new DragonSparrowException("短时间内访问次数受限制");

   * 限流脚本

   private String buildLuaScript() {

   return "local c" +

   "\nc = redis.call(get,KEYS[1])" +

   "\nif c and tonumber(c) tonumber(ARGV[1]) then" +

   "\nreturn c;" +

   "\nend" +

   "\nc = redis.call(incr,KEYS[1])" +

   "\nif tonumber(c) == 1 then" +

   "\nredis.call(expire,KEYS[1],ARGV[2])" +

   "\nend" +

   "\nreturn c;";

  

 

  3.3 前端针对绑定手机按钮添加防止连点处理

  线上项目对于 Spring 提供的自动事务注解使用要多加思考,尽可能减少事务影响范围,针对注册等按钮要在前后端添加防重复点击处理

  以上就是用户重复注册分析(用户重复注册分析怎么写)的详细内容,想要了解更多 用户重复注册分析的内容,请持续关注盛行IT软件开发工作室。

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: