Rails方式实时匹配两个用户

时间:2019-02-04 12:11:17

标签: ruby-on-rails database-design

我正在通过JSON API开发Rails应用程序作为移动应用程序的后端。我已经对几乎所有事物进行了建模,但是需要设计一个(核心)过程,而我没有找到一种清晰的方法来实现它。

除了其他功能外,该应用还必须匹配两个满足一定条件(主要是地理位置)的用户。出于问题的考虑和简化起见,假设它需要匹配彼此接近且当前正在寻找匹配项的用户(这是一种同步体验),例如:

  • 用户A点击“搜索合作伙伴”,然后出现加载屏幕
  • 用户B点击“搜索合作伙伴”,然后出现加载屏幕

假设用户相距5公里。经验应该是:

  • 他们都看到加载屏幕5秒钟,而“系统”正在寻找附近的比赛(3公里)。 5秒后,它将半径扩大到6km,并且与两个用户匹配。他们两个应该导航到“找到匹配项”屏幕。

我这里的主要问题是如何在Rails中为“寻找匹配”状态建模。我已经考虑过创建一个表格和模型,其中包括对用户及其位置的引用。但是后来我不知道如何处理“匹配查询”而不陷入主从状态。

基本上,理想的情况是两个用户的应用程序都处于一种空闲状态,并且后端在发生匹配的情况下可以同时通知它们,但是在那种情况下,后端的进程应该ve不是基于请求的,而是可能是一个工作人员...我正在将Postgres与Postgis结合使用,因此可以存储用户的职位,但是由于行数的变化,不确定是否Redis是更好的选择...

我知道我对我的问题含糊不清,但这实际上是采用哪种方法的问题,不仅仅是代码级​​解决方案。

非常感谢您!

2 个答案:

答案 0 :(得分:0)

免责声明:我对您想到的功能没有任何经验。但是,我会做类似以下的事情。

假设:

  • 下面的所有代码都使用Postgis

  • PartnersLocation模型具有t.st_point :geolocation, geographic: true属性。 Reference

可能的解决方案:

  • 客户端通过ActionCable(网络套接字)连接到Rails后端

    示例:

    // JS
    // when "Search Partner" is clicked, perform the following:
    
    var currentRadius = 5;
    
    // let XXX.XX and YYY.YY below to be the "current" location
    // Request:
    //   POST /partners_locations.json
    //     { partners_location: { radius: 5, latitude: XXX.XX, longitude: YYY.YY } }
    // Response:
    //   201 Created
    //     { id: 14, user_id: 6, radius: 5, latitude: XXX.XX, longitude: YYY.YY }
    
    // Using the `partners_location` ID above ^
    App.cable.subscriptions.create(
      { channel: 'PartnersLocationsSearchChannel',  id: 14 },
      {
        connected: function() {
          this.search()
          // search every 5 seconds, and incrementally increase the radius
          setInterval(this.search, 5000)
        },
        // this function will be triggered when there is a match
        received: function(data) {
          console.log(data)
          // i.e. will output: { id: 22, user_id: 7, latitude: XXX.XX, longitude: YYY.YY  }
        }
        search: function() {
          this.perform('search', { radius: currentRadius })
          currentRadius += 1
        }
      }
    )
    
  • 后端类似于:

    app / controllers / partners_locations_controller.rb:

    class PartnersLocationsController < ApplicationController
    
      def create
        @partners_location = PartnersLocation.new(partners_location_params)
        @partners_location.user = current_user
    
        if @partners_location.save
          render @partners_location, status: :created, location: @partners_location
        else
          render json: @partners_location.errors, status: :unprocessable_entity
        end
      end
    
      private
    
      def partners_location_params
        params.require(:partners_location).permit(:radius, :latitude, :longitude)
      end
    end
    

    app / channels / application_cable / partners_locations_search_channel.rb:

    class PartnersLocationsSearchChannel < ApplicationCable::Channel
      def subscribed
        @current_partners_location = PartnersLocation.find(params[:id])
        stream_for @current_partners_location
      end
    
      def unsubscribed
        @current_partners_location.destroy
      end
    
      def search(data)
        radius = data.fetch('radius')
    
        @current_partners_location.update!(radius: radius)
    
        partners_locations = PartnersLocation.where.not(
          id: @current_partners_location.id
        ).where(
          # TODO: update this `where` to do a Postgis two-circle (via radius) intersection query to get all the `partners_locations` of which their "radiuses" have already been taken accounted for.
          # ^ haven't done this "circle-intersect" before, but probably the following will help: https://gis.stackexchange.com/questions/166685/postgis-intersect-two-circles-each-circle-must-be-built-from-long-lat-degrees?rq=1
        )
    
        partners_locations.each do |partners_location|
          PartnersLocationsSearchChannel.broadcast_to(
            partners_location,
            @current_partners_location.as_json
          )
    
          PartnersLocationsSearchChannel.broadcast_to(
            @current_partners_location,
            partners_location.as_json
          )
        end
      end
    end
    

上面的代码仍然需要调整:

  • 只需更新以上任何代码即可使用JSON API

  • 相应地更新客户端(它可能不是JS)。即您可以使用:

  • partners_locations#create仍需要进行调整,以将latitudelongitude都保存到geolocation属性中

  • 仍然需要调整上面的
  • current_partner_location.as_json以返回latitudelongitude而不是属性geolocation

  • 上面的
  • def search需要更新:where Postgis 2圆相交的条件。老实说,我不知道该如何处理。如果有人,请告诉我。这是closest I could find on the web

  • 上面的JS代码仍需要进行调整,以妥善处理连接错误,并在成功匹配后停止setInterval

  • 一旦关闭了“加载屏幕”,或者已经找到“匹配项”或类似的内容,仍然需要调整上面的
  • JS代码以从PartnersLocationsSearchChannel“退订”取决于您的要求)

答案 1 :(得分:0)

Sidekiq + WebSockets + psql应该可以。为了避免出现主从关系,匹配应该基于两个寻找匹配用户之间的邀请。解决方案非常简单:当用户通过websocket连接到您的Rails应用时,它将启动FindMatchJob。作业会检查5公里距离范围内是否还有其他用户邀请了我们并接受了第一个邀请。否则,它将邀请具有给定范围的其他用户,并以1秒的延迟再次显示自己。我添加了一些代码作为概念验证,但是就并发问题而言,它并不是防弹的。我也简化了范围扩展,因为我很懒:)

class FindMatchJob < ApplicationJob
  def perform(user, range: 5)
    return unless user.looking_for_a_match?

    invites = find_pending_invites(user, range)
    return accept_invite(invite.first) if invites.any?

    users_in_range = find_users_in_range(user, range)
    users_in_range.each do |other_user|
      Invite.create!(
        inviting: user,
        invited: other_user,
        distance: other_user.distance_from(user)
      )
    end

    self.class.set(wait: 1.second).perform_later(user, range: range + 1)
  end

  private

  def find_pending_invites(user)
    Invite.where('distance <= ?', distance).find_by(invited: user)
  end

  def accept_invite(invite)
    notify_users(invite)
    clear_other_invites(invite)
  end

  def find_users_in_range(user, range)
    # somehow find those users
  end

  def notify_users(invite)
    # Implement logic to notify users about a match via websockets
  end

  def clear_other_invites(invite)
    Invite.where(
      inviting: match.inviting
    ).or(
      invited: match.inviting
    ).or(
      inviting: match.invited
    ).or(
      invited: match.invited
    ).delete_all
  end
end

还有一个便条。您可能需要考虑使用其他技术堆栈来解决此问题。就并发而言,Rails并不是最好的方法。我会尝试使用GoLang,在这里会更好。