狂神說redis筆記(二)


四、三種特殊數據類型

Geospatial(地理位置)

使用經緯度定位地理坐標並用一個有序集合zset保存,所以zset命令也可以使用

  • geoadd key longitud(經度) latitude(緯度) member [..] 將具體經緯度的坐標存入一個有序集合
  • geopos key member [member..] 獲取集合中的一個/多個成員坐標
  • geodist key member1 member2 [unit] 返回兩個給定位置之間的距離。默認以米作為單位。
  • georadius key longitude latitude radius m|km|mi|ft [WITHCOORD][WITHDIST] [WITHHASH] [COUNT count] 以給定的經緯度為中心, 返回集合包含的位置元素當中, 與中心的距離不超過給定最大距離的所有位置元素。
  • GEORADIUSBYMEMBER key member radius... 功能與GEORADIUS相同,只是中心位置不是具體的經緯度,而是使用結合中已有的成員作為中心點。
  • geohash key member1 [member2..] 返回一個或多個位置元素的Geohash表示。使用Geohash位置52點整數編碼。

有效經緯度

  • 有效的經度從-180度到180度。
  • 有效的緯度從-85.05112878度到85.05112878度。

指定單位的參數 unit 必須是以下單位的其中一個:

m 表示單位為米。

km 表示單位為千米。

mi 表示單位為英里。

ft 表示單位為英尺。

關於GEORADIUS的參數

通過georadius就可以完成 附近的人功能

withcoord:帶上坐標

withdist:帶上距離,單位與半徑單位相同

COUNT n : 只顯示前n個(按距離遞增排序)

----------------georadius---------------------
127.0.0.1:6379> GEORADIUS china:city 120 30 500 km withcoord withdist # 查詢經緯度(120,30)坐標500km半徑內的成員
1) 1) "hangzhou"
   2) "29.4151"
   3) 1) "120.20000249147415"
      2) "30.199999888333501"
2) 1) "shanghai"
   2) "205.3611"
   3) 1) "121.40000134706497"
      2) "31.400000253193539"

------------geohash---------------------------
127.0.0.1:6379> geohash china:city yichang shanghai # 獲取成員經緯坐標的geohash表示

  1. "wmrjwbr5250"
  2. "wtw6ds0y300"

Hyperloglog(基數統計)

Redis HyperLogLog 是用來做基數統計的算法,HyperLogLog 的優點是,在輸入元素的數量或者體積非常非常大時,計算基數所需的空間總是固定的、並且是很小的。

花費 12 KB 內存,就可以計算接近 2^64 個不同元素的基數。

因為 HyperLogLog 只會根據輸入元素來計算基數,而不會儲存輸入元素本身,所以 HyperLogLog 不能像集合那樣,返回輸入的各個元素。

其底層使用string數據類型。

什么是基數?

數據集中不重復的元素的個數。

應用場景:

網頁的訪問量(UV):一個用戶多次訪問,也只能算作一個人。

傳統實現,存儲用戶的id,然后每次進行比較。當用戶變多之后這種方式及其浪費空間,而我們的目的只是計數,Hyperloglog就能幫助我們利用最小的空間完成。

  • PFADD key element1 [elememt2..] 添加指定元素到 HyperLogLog中
  • PFCOUNT key [key] 返回給定 HyperLogLog 的基數估算值。
  • PFMERGE destkey sourcekey [sourcekey..] 將多個 HyperLogLog 合並為一個 HyperLogLog

代碼示例

----------PFADD--PFCOUNT---------------------
127.0.0.1:6379> PFADD myelemx a b c d e f g h i j k # 添加元素
(integer) 1
127.0.0.1:6379> type myelemx # hyperloglog底層使用String
string
127.0.0.1:6379> PFCOUNT myelemx # 估算myelemx的基數
(integer) 11
127.0.0.1:6379> PFADD myelemy i j k z m c b v p q s
(integer) 1
127.0.0.1:6379> PFCOUNT myelemy
(integer) 11

----------------PFMERGE-----------------------
127.0.0.1:6379> PFMERGE myelemz myelemx myelemy # 合並myelemx和myelemy 成為myelemz
OK
127.0.0.1:6379> PFCOUNT myelemz # 估算基數
(integer) 17

BitMaps(位圖)

使用位存儲,信息狀態只有 0 和 1

Bitmap是一串連續的2進制數字(0或1),每一位所在的位置為偏移(offset),在bitmap上可執行AND,OR,XOR,NOT以及其它位操作。

應用場景: 簽到統計、狀態統計

  • setbit key offset value 為指定key的offset位設置值
  • getbit key offset 獲取offset位的值
  • bitcount key [start end] 統計字符串被設置為1的bit數,也可以指定統計范圍按字節
  • bitop operration destkey key[key..] 對一個或多個保存二進制位的字符串 key 進行位元操作,並將結果保存到 destkey 上。
  • BITPOS key bit [start] [end] 返回字符串里面第一個被設置為1或者0的bit位。start和end只能按字節,不能按位

代碼示例

------------setbit--getbit--------------
127.0.0.1:6379> setbit sign 0 1 # 設置sign的第0位為 1 
(integer) 0
127.0.0.1:6379> setbit sign 2 1 # 設置sign的第2位為 1  不設置默認 是0
(integer) 0
127.0.0.1:6379> setbit sign 3 1
(integer) 0
127.0.0.1:6379> setbit sign 5 1
(integer) 0
127.0.0.1:6379> type sign
string

127.0.0.1:6379> getbit sign 2 # 獲取第2位的數值
(integer) 1
127.0.0.1:6379> getbit sign 3
(integer) 1
127.0.0.1:6379> getbit sign 4 # 未設置默認是0
(integer) 0

-----------bitcount----------------------------
127.0.0.1:6379> BITCOUNT sign # 統計sign中為1的位數
(integer) 4

五、事務

Redis的單條命令是保證原子性的,但是redis事務不能保證原子性

Redis事務本質:一組命令的集合。

----------------- 隊列 set set set 執行 -------------------

事務中每條命令都會被序列化,執行過程中按順序執行,不允許其他命令進行干擾。

一次性
順序性
排他性
Redis事務沒有隔離級別的概念
Redis單條命令是保證原子性的,但是事務不保證原子性!

Redis事務操作過程

  • 開啟事務(multi)
  • 命令入隊
  • 執行事務(exec)

所以事務中的命令在加入時都沒有被執行,直到提交時才會開始執行(Exec)一次性完成。

正常執行

127.0.0.1:6379> multi # 開啟事務
OK
127.0.0.1:6379> set k1 v1 # 命令入隊
QUEUED
127.0.0.1:6379> set k2 v2 # ..
QUEUED
127.0.0.1:6379> get k1
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> keys *
QUEUED
127.0.0.1:6379> exec # 事務執行
1) OK
2) OK
3) "v1"
4) OK
5) 1) "k3"
   2) "k2"
   3) "k1"

取消事務(discurd)

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> DISCARD # 放棄事務
OK
127.0.0.1:6379> EXEC 
(error) ERR EXEC without MULTI # 當前未開啟事務
127.0.0.1:6379> get k1 # 被放棄事務中命令並未執行
(nil)

事務錯誤

代碼語法錯誤(編譯時異常)所有的命令都不執行

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> error k1 # 這是一條語法錯誤命令
(error) ERR unknown command `error`, with args beginning with: `k1`, # 會報錯但是不影響后續命令入隊 
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors. # 執行報錯
127.0.0.1:6379> get k1 
(nil) # 其他命令並沒有被執行

代碼邏輯錯誤 (運行時異常) **其他命令可以正常執行 ** >>> 所以不保證事務原子性

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> INCR k1 # 這條命令邏輯錯誤(對字符串進行增量)
QUEUED
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
3) (error) ERR value is not an integer or out of range # 運行時報錯
4) "v2" # 其他命令正常執行

雖然中間有一條命令報錯了,但是后面的指令依舊正常執行成功了。

所以說Redis單條指令保證原子性,但是Redis事務不能保證原子性。

監控
悲觀鎖:

  • 很悲觀,認為什么時候都會出現問題,無論做什么都會加鎖

樂觀鎖:

  • 很樂觀,認為什么時候都不會出現問題,所以不會上鎖!更新數據的時候去判斷一下,在此期間是否有人修改過這個數據
  • 獲取version
  • 更新的時候比較version

使用watch key監控指定數據,相當於樂觀鎖加鎖。

正常執行

127.0.0.1:6379> set money 100 # 設置余額:100
OK
127.0.0.1:6379> set use 0 # 支出使用:0
OK
127.0.0.1:6379> watch money # 監視money (上鎖)
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> DECRBY money 20
QUEUED
127.0.0.1:6379> INCRBY use 20
QUEUED
127.0.0.1:6379> exec # 監視值沒有被中途修改,事務正常執行
1) (integer) 80
2) (integer) 20

測試多線程修改值,使用watch可以當做redis的樂觀鎖操作(相當於getversion)

我們啟動另外一個客戶端模擬插隊線程。

線程1:

127.0.0.1:6379> watch money # money上鎖
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> DECRBY money 20
QUEUED
127.0.0.1:6379> INCRBY use 20
QUEUED
127.0.0.1:6379>     # 此時事務並沒有執行

模擬線程插隊,線程2:

127.0.0.1:6379> INCRBY money 500 # 修改了線程一中監視的money
(integer) 600

回到線程1,執行事務:

127.0.0.1:6379> EXEC # 執行之前,另一個線程修改了我們的值,這個時候就會導致事務執行失敗
(nil) # 沒有結果,說明事務執行失敗

127.0.0.1:6379> get money # 線程2 修改生效
"600"
127.0.0.1:6379> get use # 線程1事務執行失敗,數值沒有被修改
"0"

解鎖獲取最新值,然后再加鎖進行事務。

unwatch進行解鎖。

注意:每次提交執行exec后都會自動釋放鎖,不管是否成功

六、Jedis

使用Java來操作Redis,Jedis是Redis官方推薦使用的Java連接redis的客戶端。

1.導入依賴

<!--導入jredis的包-->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.2.0</version>
</dependency>
<!--fastjson-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.70</version>
</dependency>

2.編碼測試

連接數據庫
操作命令
斷開連接

代碼示例

public class TestPing {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.xx.xxx", 6379);
        String response = jedis.ping();
        System.out.println(response); // PONG
    }
}

輸出PONG

常用的API

string、list、set、hash、zset

所有的api命令,就是我們對應的上面學習的指令,一個都沒有變化!

public class TestTX {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        jedis.flushDB();
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("hello","world");
        jsonObject.put("name","kuangshen");
        // 開啟事務
        Transaction multi = jedis.multi();
        String result = jsonObject.toJSONString();
        // jedis.watch(result)
        try {
            multi.set("user1",result);
            multi.set("user2",result);
            int i = 1/0 ; // 代碼拋出異常事務,執行失敗!
            multi.exec(); // 執行事務!
        } catch (Exception e) {
            multi.discard(); // 放棄事務
            e.printStackTrace();
        } finally {
            System.out.println(jedis.get("user1"));
            System.out.println(jedis.get("user2"));
            jedis.close(); // 關閉連接
        }
    }
}

七、SpringBoot整合

SpringBoot 操作數據:spring-data jpa jdbc mongodb redis!

SpringData 也是和 SpringBoot 齊名的項目!

說明: 在 SpringBoot2.x 之后,原來使用的jedis 被替換為了 lettuce?

jedis : 采用的直連,多個線程操作的話,是不安全的,如果想要避免不安全的,使用 jedis pool 連接池! 更像 BIO 模式

lettuce : 采用netty,實例可以再多個線程中進行共享,不存在線程不安全的情況!可以減少線程數據了,更像 NIO 模式

源碼分析:

@Bean
@ConditionalOnMissingBean(name = "redisTemplate") 
// 我們可以自己定義一個redisTemplate來替換這個默認的!
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
// 默認的 RedisTemplate 沒有過多的設置,redis 對象都是需要序列化!
// 兩個泛型都是 Object, Object 的類型,我們后使用需要強制轉換 <String, Object>
    RedisTemplate<Object, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(redisConnectionFactory);
    return template;
}
@Bean
@ConditionalOnMissingBean // 由於 String 是redis中最常使用的類型,所以說單獨提出來了一個bean!
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
    StringRedisTemplate template = new StringRedisTemplate();
    template.setConnectionFactory(redisConnectionFactory);
    return template;
}

整合測試

1.導入依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

springboot 2.x后 ,原來使用的 Jedis 被 lettuce 替換。

jedis:采用的直連,多個線程操作的話,是不安全的。如果要避免不安全,使用jedis pool連接池!更像BIO模式

lettuce:采用netty,實例可以在多個線程中共享,不存在線程不安全的情況!可以減少線程數據了,更像NIO模式

我們在學習SpringBoot自動配置的原理時,整合一個組件並進行配置一定會有一個自動配置類xxxAutoConfiguration,並且在spring.factories中也一定能找到這個類的完全限定名。Redis也不例外。

那么就一定還存在一個RedisProperties類

@ConditionalOnClass注解中有兩個類是默認不存在的,所以Jedis是無法生效的

然后再看Lettuce:

完美生效。

現在我們回到RedisAutoConfiguratio

只有兩個簡單的Bean

  • RedisTemplate
  • StringRedisTemplate

當看到xxTemplate時可以對比RestTemplat、SqlSessionTemplate,通過使用這些Template來間接操作組件。那么這倆也不會例外。分別用於操作Redis和Redis中的String數據類型。

在RedisTemplate上也有一個條件注解,說明我們是可以對其進行定制化的

說完這些,我們需要知道如何編寫配置文件然后連接Redis,就需要閱讀RedisProperties

這是一些基本的配置屬性。

還有一些連接池相關的配置。注意使用時一定使用Lettuce的連接池。

2.編寫配置文件

# 配置redis
spring.redis.host=39.99.xxx.xx
spring.redis.port=6379

3.使用RedisTemplate

@SpringBootTest
class Redis02SpringbootApplicationTests {
@Autowired
private RedisTemplate redisTemplate;

@Test
void contextLoads() {

    // redisTemplate 操作不同的數據類型,api和我們的指令是一樣的
    // opsForValue 操作字符串 類似String
    // opsForList 操作List 類似List
    // opsForSet
    // opsForHash
    // opsForZSet
    // opsForGeo
    // opsForHyperLog

    // 除了基本的操作,我們常用的方法都可以直接通過redisTemplate操作,比如事務和基本的CRUD

    // 獲取連接對象
    //RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
    //connection.flushDb();
    //connection.flushAll();

    redisTemplate.opsForValue().set(&quot;mykey&quot;,&quot;kuangshen&quot;);
    System.out.println(redisTemplate.opsForValue().get(&quot;mykey&quot;));
}

}

4.測試結果

此時我們回到Redis查看數據時候,驚奇發現全是亂碼,可是程序中可以正常輸出。這時候就關系到存儲對象的序列化問題,在網絡中傳輸的對象也是一樣需要序列化,否者就全是亂碼。

RedisTemplate內部的序列化配置是這樣的

默認的序列化器是采用JDK序列化器

后續我們定制RedisTemplate就可以對其進行修改。

RedisSerializer提供了多種序列化方案:

我們來編寫一個自己的 RedisTemplete

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
    // 這是我給大家寫好的一個固定模板,大家在企業中,拿去就可以直接使用!
    // 自己定義了一個 RedisTemplate
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        // 我們為了自己開發方便,一般直接使用 <String, Object>
        RedisTemplate<String, Object> template = new RedisTemplate<String,
        Object>();
        template.setConnectionFactory(factory);
    // Json序列化配置
    Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new
    Jackson2JsonRedisSerializer(Object.class);
    ObjectMapper om = new ObjectMapper();
    om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
    jackson2JsonRedisSerializer.setObjectMapper(om);
    // String 的序列化
    StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

    // key采用String的序列化方式
    template.setKeySerializer(stringRedisSerializer);
    // hash的key也采用String的序列化方式
    template.setHashKeySerializer(stringRedisSerializer);
    // value序列化方式采用jackson
    template.setValueSerializer(jackson2JsonRedisSerializer);
    // hash的value序列化方式采用jackson
    template.setHashValueSerializer(jackson2JsonRedisSerializer);
    template.afterPropertiesSet();

    return template;
}

}

所有的redis操作,其實對於java開發人員來說,十分的簡單,更重要是要去理解redis的思想和每一種數據結構的用處和作用場景!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM