Redis學習筆記——黑馬點評 附近商鋪到UV統計 完結

前言:

今天完結了Redis的所有實戰篇。

學習收獲:

GEO數據結構:

GEO就是Geolocation的簡寫形式,代表地理坐標。Redis在3.2版本中加入對Geo的支持,存儲、管理和操作地理空間數據的特殊數據結構,它能高效處理與位置、空間關系相關的信息。常見的命令有:

地理坐標數據結構其實底層就是基于sortset實現的,地理坐標被轉為數字作為score存儲。

  • 添加了兩個地理空間信息:
GEOADD cities 116.404 39.915 "Beijing" 121.473 31.230 "Shanghai"
  • 計算兩個地理位置的距離,默認單位是米:?
GEODIST cities Beijing Shanghai km  # 返回約 1068.11 千米
  • 獲取地理坐標位置:?
GEOPOS cities Beijing  # 返回 [116.40400149041748, 39.91500072454179]
  • ?附近查詢:
GEORADIUS key longitude latitude radius unit [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC]
GEORADIUSBYMEMBER key member radius unit [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC]

WITHCOORD:返回經緯度。WITHDIST:返回距離。COUNT count:限制返回數量。ASC|DESC:按距離升序 / 降序排列

實現附近商戶搜索功能:

?要把店鋪坐標的經緯度信息導入Redis中的GEO數據類型結構中,member存店鋪id。將來我們要做店鋪的篩選時,根據經緯度的到店鋪id,再根據id在數據庫中查詢店鋪;

?分離不同類型的商戶,再把不同類型的商戶分到不同的key中:

  • 查詢點鋪信息
  • 把店鋪分組,按照typeId分組,id一致的放到一個集合

通過收集的groupingBy來實現分組,通過TypeId來分組

  • 分批完成寫入Redis

獲取類型id;獲取同類型的點鋪集合;寫入redis GEOADD key 經度 維度 member

  • 最后導入店鋪數據到GEO
    /*** 預熱店鋪數據,按照typeId進行分組,用于實現附近商戶搜索功能*/@Testpublic void loadShopListToCache() {// 1、獲取店鋪數據List<Shop> shopList = shopService.list();// 2、根據 typeId 進行分類
//        Map<Long, List<Shop>> shopMap = new HashMap<>();
//        for (Shop shop : shopList) {
//            Long shopId = shop.getId();
//            if (shopMap.containsKey(shopId)){
//                // 已存在,添加到已有的集合中
//                shopMap.get(shopId).add(shop);
//            }else{
//                // 不存在,直接添加
//                shopMap.put(shopId, Arrays.asList(shop));
//            }
//        }// 使用 Lambda 表達式,更加優雅(優雅永不過時)Map<Long, List<Shop>> shopMap = shopList.stream().collect(Collectors.groupingBy(Shop::getTypeId));// 3、將分好類的店鋪數據寫入redisfor (Map.Entry<Long, List<Shop>> shopMapEntry : shopMap.entrySet()) {// 3.1 獲取 typeIdLong typeId = shopMapEntry.getKey();List<Shop> values = shopMapEntry.getValue();// 3.2 將同類型的店鋪的寫入同一個GEO ( GEOADD key 經度 維度 member )String key = SHOP_GEO_KEY + typeId;// 方式一:單個寫入(這種方式,一個請求一個請求的發送,十分耗費資源,我們可以進行批量操作)
//            for (Shop shop : values) {
//                stringRedisTemplate.opsForGeo().add(key, new Point(shop.getX(), shop.getY()),
//                shop.getId().toString());
//            }// 方式二:批量寫入List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>();for (Shop shop : values) {locations.add(new RedisGeoCommands.GeoLocation<>(shop.getId().toString(),new Point(shop.getX(), shop.getY())));}stringRedisTemplate.opsForGeo().add(key, locations);}}

?實現附近商鋪功能:

  1. 判斷是否要根據坐標查詢
  2. 計算分頁參數
  3. 查詢redis,按照距離排序并分頁。
  4. 解析出shopid
  5. 根據id查詢redis
  6. 返回

因為前端不一定是按照距離來查詢商鋪信息的,所以x和y的坐標不是必須的,所以通過required=false來設置可以有也可以沒有。沒傳就按數據庫查,傳了就按geo查。

List也要做非空判斷,因為stream流做了跳過,可能把有的數據跳過去了。

?

?ShopServiceImpl中的代碼:

@Overridepublic Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {// 1.判斷是否需要根據坐標查詢if (x == null || y == null) {// 不需要坐標查詢,按數據庫查詢Page<Shop> page = query().eq("type_id", typeId).page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));// 返回數據return Result.ok(page.getRecords());}// 2.計算分頁參數int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;int end = current * SystemConstants.DEFAULT_PAGE_SIZE;// 3.查詢redis、按照距離排序、分頁。結果:shopId、distanceString key = SHOP_GEO_KEY + typeId;GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo() // GEOSEARCH key BYLONLAT x y BYRADIUS 10 WITHDISTANCE.search(key,GeoReference.fromCoordinate(x, y),new Distance(5000),RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end));// 4.解析出idif (results == null) {return Result.ok(Collections.emptyList());}List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();if (list.size() <= from) {// 沒有下一頁了,結束return Result.ok(Collections.emptyList());}// 4.1.截取 from ~ end的部分List<Long> ids = new ArrayList<>(list.size());Map<String, Distance> distanceMap = new HashMap<>(list.size());list.stream().skip(from).forEach(result -> {// 4.2.獲取店鋪idString shopIdStr = result.getContent().getName();ids.add(Long.valueOf(shopIdStr));// 4.3.獲取距離Distance distance = result.getDistance();distanceMap.put(shopIdStr, distance);});// 5.根據id查詢ShopString idStr = StrUtil.join(",", ids);List<Shop> shops = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();for (Shop shop : shops) {shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());}// 6.返回return Result.ok(shops);}

用戶簽到:

但用戶的簽到狀態無非就兩種,簽了或者沒簽,我們可以用二進制位表示簽到卡,而這種二進制位可以通過BitMap來實現。

把每一個bit位對應當月的每一天,形成了映射關系。用0和1表示業務狀態,這種思路就稱為位圖(BitMap)。Redis中是利用String類型數據結構實現BitMap,因此最大上限是512M,轉換為bit則是2^32個bit位。

?核心思想就是把bit位與業務的某種核心狀態進行映射。

BitMap用法:

# 讀取所有的bit位
BITFIELD key
# 查找第一個數出現的位置
BITPOS key value
BITPOS bm1 1 # 返回的就是0,11100111 offset位0的位置就是1
BITPOS bm1 0 # 返回的就是0,11100111 offset位0的位置就是1
# 讀取指定位數的bit位
BITFIELD key GET type offset
# 獲取的數據是3
BITFIELD bm1 get u2 0

實現簽到功能:?

  1. 獲取當前登錄用戶
  2. 獲取日期
  3. 拼接key
  4. 獲取今天是本月的第幾天
  5. 寫入redis: setbit key offset 1
    /*** 用戶簽到** @return*/@Overridepublic Result sign() {// 獲取當前登錄用戶Long userId = ThreadLocalUtls.getUser().getId();// 獲取日期LocalDateTime now = LocalDateTime.now();// 拼接keyString keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));String key = USER_SIGN_KEY + userId + keySuffix;// 獲取今天是本月的第幾天int dayOfMonth = now.getDayOfMonth();// 寫入Redis SETBIT key offset 1stringRedisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true);return Result.ok();}

實現簽到統計功能:?

從當前時間開始向前逐個遍歷,依次判斷每個bit位,知道遇到第一次為0為止。并且定義一個計數器來計算總的簽到次數。

這里有三個問題來實現這個簽到統計功能:

    /*** 記錄連續簽到的天數** @return*/@Overridepublic Result signCount() {// 1、獲取簽到記錄// 獲取當前登錄用戶Long userId = ThreadLocalUtls.getUser().getId();// 獲取日期LocalDateTime now = LocalDateTime.now();// 拼接keyString keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));String key = USER_SIGN_KEY + userId + keySuffix;// 獲取今天是本月的第幾天int dayOfMonth = now.getDayOfMonth();// 獲取本月截止今天為止的所有的簽到記錄,返回的是一個十進制的數字 BITFIELD sign:5:202203 GET u14 0List<Long> result = stringRedisTemplate.opsForValue().bitField(key,BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0));// 2、判斷簽到記錄是否存在if (result == null || result.isEmpty()) {// 沒有任何簽到結果return Result.ok(0);}// 3、獲取本月的簽到數(List<Long>是因為BitFieldSubCommands是一個子命令,可能存在多個返回結果,這里我們知識使用了Get,// 可以明確只有一個返回結果,即為本月的簽到數,所以這里就可以直接通過get(0)來獲取)Long num = result.get(0);if (num == null || num == 0) {// 二次判斷簽到結果是否存在,讓代碼更加健壯return Result.ok(0);}// 4、循環遍歷,獲取連續簽到的天數(從當前天起始)int count = 0;while (true) {// 讓這個數字與1做與運算,得到數字的最后一個bit位,并且判斷這個bit位是否為0if ((num & 1) == 0) {// 如果為0,說明未簽到,結束break;} else {// 如果不為0,說明已簽到,計數器+1count++;}// 把數字右移一位,拋棄最后一個bit位,繼續下一個bit位num >>>= 1;}return Result.ok(count);}

?UV統計:

首先我們搞懂兩個概念:

  • UV:全稱Unique Visitor,也叫獨立訪客量,是指通過互聯網訪問、瀏覽這個網頁的自然人。1天內同一個用戶多次訪問該網站,只記錄1次。
  • PV:全稱Page View,也叫頁面訪問量或點擊量,用戶每訪問網站的一個頁面,記錄1次PV,用戶多次打開頁面,則記錄多次PV。往往用來衡量網站的流量。

UV統計在服務端做會比較麻煩,因為要判斷該用戶是否已經統計過了,需要將統計過的用戶信息保存。但是如果每個訪問的用戶都保存到Redis中,數據量會非常恐怖。

所以我們提出了HyperLogLog:

HyperLogLog用法:

Hyperloglog(HLL)是從LogLog算法派生的概率算法,用于確定非常大的集合的基數,而不是要存儲其所有值。相關算法原理可以參考:https://juejin.cn/post/6844903785744056333#heading-0

Redis中的HLL是基于string結構實現的,單個HLL的內存永遠小于16kb,內存占用低的令人發指。作為代價,其測量結果是概率性的,有小于0.81%的誤差。不過對于UV統計來說,這完全可以忽略。

?HLL對于重復的元素只記錄一次,可以方便的解決了獨立訪客量的問題。

由于當前系統并沒有足夠的用戶數據量,所以這里我們只是模擬實現UV統計。

    /*** 測試 HyperLogLog 實現 UV 統計的誤差*/@Testpublic void testHyperLogLog() {String[] values = new String[1000];// 批量保存100w條用戶記錄,每一批1個記錄int j = 0;for (int i = 0; i < 1000000; i++) {j = i % 1000;values[j] = "user_" + i;if (j == 999) {// 發送到RedisstringRedisTemplate.opsForHyperLogLog().add("hl2", values);}}// 統計數量Long count = stringRedisTemplate.opsForHyperLogLog().size("hl2");System.out.println("count = " + count);}

?可以看到,存儲100w條用戶記錄,但是內存至多占用了0.02MB

?

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/89036.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/89036.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/89036.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【客戶端排查】mac電腦怎么查看客戶端的實時運行日志

先退出客戶端&#xff1b;打開訪達里的應用程序&#xff1b; 打開【顯示包內容】&#xff1b; 找到MacOS 雙擊里面的終端程序&#xff1b; 雙擊后&#xff0c;客戶端會自動啟動&#xff0c;且可以在終端中查看客戶端的實時日志啦~

HarmonyOS NEXT倉頡開發語言實戰案例:健身App

各位好&#xff0c;今日分享一個健身app的首頁&#xff1a; 這個頁面看起比之前的案例要稍微復雜一些&#xff0c;主要在于頂部部分&#xff0c;有重疊的背景&#xff0c;還有偏移的部分。重疊布局可以使用Stack容器實現&#xff0c;超出容器范圍的偏移可以使用負數間距來實現&…

TreeMap源碼分析 紅黑樹

今天嘗試刨一下TreeMap的祖墳。 底層結構對比 先來看一下與HashMap、LinkedHashMap和TreeMap的對比&#xff0c;同時就當是復習一下&#xff1a; HashMap使用數組存儲數據&#xff0c;并使用單向鏈表結構存儲hash沖突數據&#xff0c;同一個沖突桶中數據量大的時候&#xff…

華為云Flexus+DeepSeek征文|基于Dify構建拍照識題智能學習助手

華為云FlexusDeepSeek征文&#xff5c;基于Dify構建拍照識題智能學習助手 一、構建拍照識題智能學習助手前言二、構建拍照識題智能學習助手環境2.1 基于FlexusX實例的Dify平臺2.2 基于MaaS的模型API商用服務 三、構建拍照識題智能學習助手實戰3.1 配置Dify環境3.2 配置Dify工具…

題解:CF2120E Lanes of Cars

根據貪心&#xff0c;不難想到每次會把最長隊伍末尾的那輛車移動到最短隊伍的末尾。但由于 k k k 的存在&#xff0c;會導致一些冗余移動的存在。設需要挪動 C C C 輛車&#xff0c;則怒氣值可以表示為 f ( C ) k C f(C) kC f(C)kC&#xff0c;其中 f ( C ) f(C) f(C) 是…

Excel基礎:選擇和移動

本文演示Excel中基礎的選擇和移動操作&#xff0c;并在最后提供了一張思維導圖&#xff0c;方便記憶。 文章目錄 一、選擇1.1 基礎選擇1.1.1 選擇單個單元格1.1.2 選擇連續范圍 1.2 行列選擇1.2.1 選擇整行整列1.2.2 選擇多行多列 1.3 全選1.3.1 全選所有單元格1.3.2 智能選擇…

Java面試寶典:基礎四

80. int vs Integer 維度intInteger類型基本數據類型(8種之一)包裝類默認值0null應用場景性能敏感場景(計算密集)Web表單、ORM框架(區分null和0)特殊能力無提供工具方法(如parseInt())和常量(如MAX_VALUE)示例:

RabbitMQ + JMeter 深度集成指南:中間件性能優化全流程解析!

在 2025 年的數字化浪潮中&#xff0c;中間件性能直接決定系統的穩定性和用戶體驗&#xff0c;而 RabbitMQ 作為消息隊列的“老大哥”&#xff0c;在分布式系統中扮演著關鍵角色。然而&#xff0c;高并發場景下&#xff0c;消息堆積、延遲激增等問題可能讓系統不堪重負&#xf…

uniapp image引用本地圖片不顯示問題

1. uniapp image引用本地圖片不顯示問題 在uniapp 開發過程中采用image引入本地資源圖片。 1.1. 相對路徑和絕對路徑問題 在UniApp中開發微信小程序時&#xff0c;引入圖片時&#xff0c;相對路徑和絕對路徑可能會有一些差異。這差異主要涉及到小程序和UniApp框架的文件結構、…

論文閱讀:arxiv 2025 ThinkSwitcher: When to Think Hard, When to Think Fast

總目錄 大模型安全相關研究&#xff1a;https://blog.csdn.net/WhiffeYF/article/details/142132328 ThinkSwitcher: When to Think Hard, When to Think Fast https://arxiv.org/pdf/2505.14183#page2.08 https://www.doubao.com/chat/10031179784579842 文章目錄 速覽一、…

智能體記憶原理-prompt設計

智能體記憶的管理與設計開發分為以下幾步&#xff1a; 1.記憶的抽取&#xff1b; 2.記憶的存儲&#xff1b; 3.記憶的搜索&#xff1b; 一、記憶抽取一&#xff1a; FACT_RETRIEVAL_PROMPT f"""你是一位個人信息整理助手&#xff0c;專門負責準確存儲事實、用…

026 在線文檔管理系統技術架構解析:基于 Spring Boot 的企業級文檔管理平臺

在線文檔管理系統技術架構解析&#xff1a;基于Spring Boot的企業級文檔管理平臺 在企業數字化轉型的進程中&#xff0c;高效的文檔管理系統已成為提升協作效率的核心基礎設施。本文將深入解析基于Spring Boot框架構建的在線文檔管理系統&#xff0c;該系統整合公告信息管理、…

AWTK-MVVM的一些使用技巧總結(1)

在項目中用了一段時間的AWTK-MVVM框架&#xff0c;由于AWTK-MVVM本身的文檔十分欠缺&#xff0c;自己經過一段時間的研究折騰出了幾個技巧&#xff0c;在此記錄總結。 用fscript啟用傳統UI代碼 AWTK-MVVM里面重新設計了navigator機制&#xff0c;重定位了navigator_to的調用方…

openwrt使用quilt工具制作補丁

前言&#xff1a;簡單聊一下為什么需要制作補丁&#xff0c;因為openwrt的編譯是去下載很多組件放到dl目錄下面&#xff0c;這些組件都是壓縮包。如果我們要修改這些組件里面的源碼&#xff0c;就需要對這些組件打pacth&#xff0c;也就是把我們的差異點在編譯的時候合入到對應…

強化學習 (1)基本概念

grid-world example 一個由多個格子組成的二維網格 三種格子&#xff1a;accessible可通行的&#xff1b; forbidden禁止通行的&#xff1b; target目標 state狀態 state是智能體相對于環境的狀態&#xff08;情況&#xff09; 在grid-world example里&#xff0c;state指的…

【Typst】縱向時間軸

概述 6月10日實驗了一個縱向時間軸排版效果&#xff0c;當時沒有做成單獨的模塊&#xff0c;也存在一些Bug。 今天(6月29日)在原基礎上進行了一些改進&#xff0c;并總結為模塊。 目前暫時發布出來&#xff0c;可用&#xff0c;后續可能會進行大改。 使用案例 導入模塊使用…

【Visual Studio Code上傳文件到服務器】

在 Visual Studio Code (VS Code) 中上傳文件到 Linux 系統主要通過 SSH 協議實現&#xff0c;結合圖形界面&#xff08;GUI&#xff09;或命令行工具操作。以下是具體說明及進度查看、斷點續傳的實現方法&#xff1a; ?? 一、VS Code 上傳文件到 Linux 的機制 SSH 遠程連接 …

手機控車一鍵啟動汽車智能鑰匙

手機一鍵啟動車輛的方法 手機一鍵啟動車輛是一種便捷的汽車啟動方式&#xff0c;它通過智能手機應用程序實現對車輛的遠程控制。以下是詳細的步驟&#xff1a; 完成必要的認證與激活步驟。打開手機上的相關移動管家手機控車APP&#xff0c;并與車載藍牙建立連接。在APP的主界面…

基于深度學習的語音增強技術:時間增強多尺度頻域卷積網絡模型解析

基于深度學習的語音增強技術&#xff1a;時間增強多尺度頻域卷積網絡模型解析 近年來&#xff0c;隨著語音處理技術的不斷發展&#xff0c;語音增強&#xff08;Speech Enhancement&#xff09;逐漸成為研究熱點。語音增強的主要目標是通過消除噪聲和改善信噪比來提高語音質量…

計算機組成原理-數據表示與運算(三)

### 文字提取結果&#xff1a; #### 題目內容&#xff1a; 34. 【2009 統考真題】浮點數加、減運算過程一般包括對階、尾數運算、規格化、舍入和判斷溢出等步驟。設浮點數的階碼和尾數均采用補碼表示&#xff0c;且位數分別為 5 和 7&#xff08;均含 2 位符號位&#xff09;。…