1. 程式人生 > >模仿知乎——實現一個多使用者線上問答平臺

模仿知乎——實現一個多使用者線上問答平臺

online-questioning 從零開始開發線上問答平臺, 這是我模仿知乎做的一個貼吧類問答交流平臺

專案github地址:https://github.com/guomzh/online-questioning , 歡迎各位star和與我交流

本文持續更新,未完待續…

使用到的技術棧:

1、spring/springboot
* intercepter攔截器實現登入許可權控制
* javax.mali郵件服務,如有新評論時發郵件通知使用者,註冊時郵件驗證
* ioc專案容器中物件管理,對容器中beans操作
* aop平臺日誌操作記錄
* maven管理整個專案依賴
2、mybatis操作資料庫主要業務資料
3、前端模板引擎freemarker ,渲染整個前端模板
4、演算法設計:trie字首樹實現網站敏感詞過濾
5、Redis實現非同步佇列,利用多執行緒實現非同步事件處理,主要針對一些耗時操作,進行非同步執行,如發郵件,評論後發站內私信通知等
6、使用 Redis 資料結構中的 set 集合實現使用者對問題評論的點贊點踩功能
7、solr匯入mysql資料,建立問題和標題文件庫,利用ik-analyzer進行中文分詞,使用者可以進行站內全文搜尋

開發通用的新模組流程:

1、資料庫設計
2、Model:模型定義,與資料庫相匹配
3、Dao層:資料操作
4、Service:服務包裝
5、Controller:業務入口,資料互動
6、單元測試

註冊模組:

  1. 使用者名稱合法性檢測(長度,敏感詞,重複,特殊字元)
  2. 密碼長度要求
  3. 密碼salt加密,密碼強度檢查(md5庫)
  4. 使用者註冊郵件啟用

我在實現使用者註冊郵箱啟用時自己的實現思路:
當用戶提交登錄檔單資訊時,把表單資訊存到redis的hash資料結構中,同時產生一個對應的key,
這時釋出一個非同步事件,傳送一封郵件,同時把這個key放到連結中發到使用者的註冊郵箱中,當用戶
訪問郵箱中這個連結時,在redis中查出這個key對應的註冊資訊,並存到資料庫中完成註冊

  //資訊存到redis中
        String register_ticket=OnlineQUtil.MD5(email);
        redisAdapter.hset(register_ticket,"email",email);
        redisAdapter.hset(register_ticket,"username",username);
        redisAdapter.hset(register_ticket,"password", password);
        redisAdapter.expire(register_ticket,60
*15); redisAdapter.sadd("email",email); //釋出非同步事件 eventProducer.fireEvent(new EventModel(EventType.REGISTER) .setExt("register_ticket",register_ticket) .setExt("email",email)); //傳送郵件 @Override public void doHandle(EventModel model) { Map<String ,Object> map=new HashMap<>(); map.put("url","http://127.0.0.1:8080/regVerify?p="+model.getExt("register_ticket")); mailSender.sendWithHTMLTemplate(model.getExt("email"),"<我的知乎——線上問答平臺>註冊啟用郵件", "mails/register_email.html", map); } //從redis中讀取註冊資訊,完成註冊 if(redisAdapter.exists(p)){ try { String email=redisAdapter.hget(p,"email"); String username=redisAdapter.hget(p,"username"); String password=redisAdapter.hget(p,"password"); } ... }

登入模組

1、伺服器密碼校驗/三方校驗,token(sessionId或者cookie的一個key)登記
* 伺服器端token關聯userId
* 客戶端儲存token(本地或者cookie)

2、伺服器端/客戶端token設定有效期(記住登入)
3、登出:伺服器端/客戶端token刪除或者session清理

問題釋出模組

  • HTML/敏感詞過濾
  //html過濾
  question.setContent(HtmlUtils.htmlEscape(question.getContent()));
  敏感詞過濾通過Trie樹儲存敏感詞彙,匹配文字串,對匹配到的敏感詞打碼或者刪除
  • 多執行緒

關於多執行緒的一些運用知識回顧如下:

  Future作用:進行執行緒與執行緒間通訊
  1. 返回非同步結果:Future<Integer>future =service.submit(new Callable<Integer>{ });
  2. 阻塞等待返回結果(future.get())
  3. timeout(future.get(100,TimeUnit.MILLISECONDS))
  4. 獲取執行緒中的Exception

評論中心

訊息中心(贊,評論通知,私信通知,回答採納等)

Redis資料結構使用場景

List Set SortedSet Hash KV
用途 棧操作,雙向列表,使用於最新列表,關注列表 適用於無順序的集合,點贊點踩,抽獎,已讀,共同好友 排行榜,優先佇列 物件屬性,不定長
api lpush sdiff zadd hset
api lpop smembers zscore hget
api blpop sinter zrange hgetAll
api lindex scard zcount hexists
api lrange zrank hkeys
api lrem zrevrank hvals
api linsert
api lset
api rpush

例如:實現PV(page views)點選量功能等

  • 點贊(Set) 操作:sadd, srem, sismembers等
  • 關注(Set)
  • 排行榜(SortedSet)
  • 驗證碼(KV)
  • 快取(序列化存KV)
  • 非同步佇列(中間層)
  • 判斷佇列(中間層)

非同步佇列實現

這裡寫圖片描述

例如:實現評論後給提問者發一條站內通知功能

問題評論郵件通知功能實現,手寫一個非同步實現程式碼

思路:
* 自己手動實現一個非同步佇列
* 用redis做非同步訊息佇列實現,利用多執行緒來實現非同步傳送郵件
* 用javax.mail傳送郵件,使用smtps協議
技術關鍵流程:
1、定義訊息事件介面

public interface EventHandler {

    void doHandle(EventModel model);

    List<EventType> getSupportEventTypes();
}

2、定義訊息模型,可以用map的資料結構來實現
3、定義生產者,釋出訊息到redis構建的非同步佇列中(利用redis的阻塞佇列操作)

public class EventProducer {
    @Autowired
    private RedisAdapter redisAdapter;
    public boolean fireEvent(EventModel eventModel) {
        try {
            String json = JSONObject.toJSONString(eventModel);
            String key = RedisKeyUtil.getEventQueueKey();
            redisAdapter.lpush(key, json);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}

4、釋出事件
5、定義消費者,開啟多執行緒非同步處理事件

         Thread thread =new Thread(new Runnable() {
             @Override
             public void run() {
                 while (true){
                     String key= RedisKeyUtil.getEventQueueKey();
                     List<String> events =redisAdapter.brpop(0,key);
                     for(String message:events){
                         //訊息佇列的第一個值可能是key,返回值的原因
                         if(message.equals(key)){
                             continue;
                         }
                         EventModel eventModel= JSON.parseObject(message,EventModel.class);
                         if(!config.containsKey(eventModel.getType())){
                             logger.error("不能識別的事件");
                             continue;
                         }
                         for(EventHandler handler :config.get(eventModel.getType())){
                             handler.doHandle(eventModel);
                         }
                     }
                 }
             }
         });
         thread.start();

關注服務,如A關注了某人,某問題

分一下功能點:
* 首頁問題關注數
* 詳情頁問題關注列表
* 粉絲/關注人列表
* 關注介面設計,關注列表分頁
* 關注非同步事件處理

儲存結構:
redis: zset / list

平臺內容排序演算法:

1、Score = (P-1)/(T+2)^G
P表示投票數,G表示分值根據時間降低速率,相當於重力加速度,T表示釋出到現在時間間隔,單位小時
2、f(t,x,y,z)=log z + yt/45000
t等於釋出到現在的時間差,如一個差86400秒
x等於贊數-踩數
如果x大於0則y=1,x小於0則y=-1,x=0則y=0
z等於x的絕對值,如果x=0,則z=1

未完待續。。。