四、三種特殊數據類型
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表示
- "wmrjwbr5250"
"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("mykey","kuangshen"); System.out.println(redisTemplate.opsForValue().get("mykey")); }
}
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的思想和每一種數據結構的用處和作用場景!