中间件 - redis

官网链接:https://redis.io/

 

首先简单理解一下

1、什么是redis

redis 是一种开源的、内存中数据结构存储,用作数据库缓存消息代理

redis 数据结构包含五大数据类型:字符串、散列、列表、集合、带范围查询的排序集合

以及三大特殊数据类型:位图、超级日志、地理空间索引。

redis 内置复制、Lua 脚本、LRU 驱逐、事务和不同级别的磁盘持久化,并通过 Redis Sentinel 和 Redis Cluster 自动分区提供高可用性。

redis 是单线程,基于内存操作,cpu不是性能瓶颈,redis的瓶颈是根据机器的内存和网络带宽,既然可以使用单线程来实现,所以就使用单线程。

redis由C语言编写,不比Mamecache差

 

说道单线程,这里有两个误区:

1、高性能的服务器一定是多线程的?

2、多线程(CPU上下文会切换)一定比单线程效率高?

 

核心:redis 是将所有的数据存放 到内存中,所以说用单线程是效率最高的,因为多线程之间CPU上下文切换,是一个耗时的操作,对于内存系统来说,没有数据上下文切换,效率就是最高的,多次读写都是在一个CPU上的,在内存情况下,单线程就是最优的方案

 

2、如何在项目中使用redis

第一种:原生 Jedis

1、什么是jedis?

jedis就是集成了redis的一些命令操作,封装了redis的java客户端。提供了连接池管理。一般不直接使用jedis,而是在其上在封装一层,作为业务的使用

 

2、导入jedis的maven依赖

<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.6.0</version>
</dependency>

 

3、使用测试

注:需要开启本地的redis服务

ping :是否连接上redis服务

package com.cai;

import redis.clients.jedis.Jedis;

public class TestPing {
    public static void main(String[] args) {

        // 实例化 jedis 
     // 无参构造方法会自动链接本地的redis服务,也就是说如果连接的是本地的redis服务,则可以不用写地址和端口号
Jedis jedis = new Jedis("127.0.0.1",6379); // ping ,如果返回pong 就代表链接成功 System.out.println(jedis.ping()); } }

运行响应

PONG

Process finished with exit code 0

返回 PONG 说明redis服务连接上了,就可以后续的命令操作

 

五大基本类型

① string(字符串)

package com.redis.basicType;

import redis.clients.jedis.Jedis;

import java.util.Set;

// 字符串类型
public class TpeString {
    public static void main(String[] args) {

        Jedis jedis = new Jedis("127.0.0.1",6379);

        // 数据库的大小
        System.out.println(jedis.dbSize());

        // 切换数据库
        jedis.select(2);

        // 设置字符串
        System.out.println("设置了key:ey1->value:value1:"+jedis.set("key1", "value1"));

        // 获取字符串 key
        System.out.println("get ke1:"+jedis.get("key1"));

        System.out.println("当前数据库中所有的key数量:"+jedis.dbSize());
        // 清空所有数据库
        jedis.flushAll();

        // 清空当前数据库
        jedis.flushDB();

        System.out.println("设置了key:name->value:test:"+jedis.set("name","test"));
        System.out.println("设置了key:name1->value:张三:"+jedis.set("name1","张三"));
        System.out.println("设置了key:name2->value:李四:"+jedis.set("name2","李四"));

        // 获取当前数据库下所有的key        
        Set<String> keys = jedis.keys("*");
        System.out.println("获取当前数据库中所有的key:"+keys);


        // 判断是否存在
        System.out.println("key->name 是否存在"+jedis.exists("name"));

        // 删除某个key
        System.out.println("删除了键name:"+jedis.del("name"));
        // 判断是否存在
        System.out.println("key->name 是否存在"+jedis.exists("name"));

        // 设置key的过期时间,10s之后
        System.out.println("设置name的过期时间为10s后:"+jedis.expire("name", 10));

        // 获取key的剩余时间
        System.out.println("key->name 剩余时间"+jedis.ttl("name"));

        // 查看key的类型
        System.out.println("key->name 类型"+jedis.type("name"));

        // 向 value值后面追加
        jedis.append("name","追加");

        // 追加之后的
        System.out.println("追加之后的值:"+jedis.get("name"));

        // 获取value的长度
        Long length = jedis.strlen("name");
        System.out.println("name的value长度"+length);

        // 追加的key如果不存在,则新增
        jedis.append("name2","张三");

        // 设置一个数字类型
        jedis.set("number", "100");
        System.out.println("起始number:"+jedis.get("name"));


        // 递增1
        jedis.incr("number");
        System.out.println("递增number:"+jedis.get("number"));

        // 递减1
        jedis.decr("number");
        System.out.println("递减number:"+jedis.get("number"));

        // 递增指定值
        jedis.incrBy("number",10);
        System.out.println("递增10 number:"+jedis.get("number"));

        // 递减指定值
        jedis.decrBy("number",5);
        System.out.println("递减10 number:"+jedis.get("number"));

        jedis.flushDB();
        jedis.set("key1","三生三世十里桃花");
        System.out.println("设置key1的值为:"+jedis.get("key1"));

        // 查看key1值 从 第几位到第几位s
        String rangeValue = jedis.getrange("key1", 1, 3);
        System.out.println("查看key1值 从 第1位到第3位:"+rangeValue);

        // 获取key1全部长度的值 和 get 功能一样
        jedis.getrange("key1",0,-1);

        // 替换某个字符串
        jedis.set("key2","hello world,redis");
        System.out.println("key2的值:"+jedis.get("key2"));

        // 将某个位置的值替换成某个值
        Long setRangeValue = jedis.setrange("key2", 7, "xx");
        System.out.println("将key2第7位的值换成xx:"+setRangeValue);

        // 当key存在设置过期时间
        jedis.setex("key2",30,"存在设置值,30s后过期");
        System.out.println("key2的值:"+jedis.get("key2"));
        System.out.println("key2的过期时间:"+jedis.ttl("key2"));

        // 当key不存在才去设置值
        jedis.setnx("noHaveKey","不存在的key设置值");
        System.out.println("设置noHaveKey的值:"+jedis.get("noHaveKey"));

        jedis.setnx("noHaveKey","再次设置值");
        System.out.println("设置noHaveKey存在时的值:"+jedis.get("noHaveKey"));

        jedis.flushDB();

        // 批量设置key-value
        jedis.mset("key1","value1","key2","value2","key3","value3");

        // 批量获取key的值
        System.out.println("批量获取值:"+jedis.mget("key1", "key2", "key3"));

        // 批量设置key不存在设置值 [原子性]
        System.out.println(jedis.msetnx("key1", "value11", "key4", "key4"));

        jedis.flushDB();

        // 设置对象 对象名-id-属性
        jedis.mset("user:1:name","张三","user:1:age","3");
        jedis.mget("user:1:name","user:1:age");

        // 先获取db的值再去设置db的值 不存在的key则返回null
        System.out.println("getSet:"+jedis.getSet("db", "redis"));

        // 再去获取db的值
        System.out.println(jedis.get("db"));


    }
}

总结:

msetnx :原子性的操作,要不全部成功,要不全部失败

 

使用场景:

计数器(incyby)

统计多单位的数量(user:1:like -> 1)

对象缓存存储

 

② list(列表)

在redis中 可将list玩成 栈(先进后出) 和 队列(先进先出)

命令都是用 l 或者 r 开头

package com.cai;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.ListPosition;

public class TypeList {
    public static void main(String[] args) {

        Jedis jedis = new Jedis("127.0.0.1", 6379);

        jedis.flushAll();

        // 将一个值或者多个值插入到列表的头部
        jedis.lpush("list","one","two","three");

        // 获取全部的值
        System.out.println("获取list第一位到最后一位的值:"+jedis.lrange("list", 0, -1));

        // 获取 第一位和 第二位的值
        System.out.println("获取list第一位到第二位的值:"+jedis.lrange("list", 0, 1));

        // 将值插入到列表的右边
        jedis.rpush("list","right");

        System.out.println("list插入右边值:"+jedis.lrange("list", 0, -1));

        // 移除左边的值
        jedis.lpop("list");
        System.out.println("list移除左边的值:"+jedis.lrange("list", 0, -1));

        // 移除右边的值
        jedis.rpop("list");
        System.out.println("list移除右边的值:"+jedis.lrange("list", 0, -1));

        // 通过下标获取list的某个值
        System.out.println("通过下标1获取list的值:"+jedis.lindex("list", 1));

        // 获取列表的长度
        System.out.println("获取list的长度:"+jedis.llen("list"));

        // 插入重复值
        jedis.lpush("list","one","two","three");

        System.out.println("list的值:"+jedis.lrange("list", 0, -1));

        // 移除list集合中指定的个数
        jedis.lrem("list",1,"one");
        System.out.println("移除list中1个one:"+jedis.lrange("list", 0, -1));

        jedis.flushDB();

        jedis.rpush("myList","hello","hello1","hello2","hello3","hello4");
        System.out.println("myList:"+jedis.lrange("myList", 0, -1));

        // 通过下标截取指定的长度,list被改变,截断了只剩下截取的元素
        jedis.ltrim("myList",1,3);
        System.out.println("myList截取1 到2 :"+jedis.lrange("myList", 0, -1));

        // 将一个list右边的一个元素移到另一个list的左边
        jedis.rpoplpush("myList","giveList");

        System.out.println("给值的myList:"+jedis.lrange("myList", 0, -1));
        System.out.println("拿到值得giveList:"+jedis.lrange("giveList", 0, -1));

        // 判断是否存在
        System.out.println("key->list是否存在:"+jedis.exists("list"));

        jedis.lpush("list","item01");
        System.out.println("list:"+jedis.lrange("list", 0, -1));

        // 将list指定下标的值替换成一个新的值 ,当 list 索引值不存在 设置会报错【ERR no such key】
        System.out.println(jedis.lset("list", 0, "item1"));
        System.out.println("修改之后的list:"+jedis.lrange("list", 0, -1));

        // 将某个value插入到列表中某个值的前面
        jedis.linsert("list", ListPosition.BEFORE,"item1","item0");
        System.out.println("insert->before的list:"+jedis.lrange("list", 0, -1));

        // 将某个value插入到列表中某个值的后面
        jedis.linsert("list", ListPosition.AFTER,"item1","item2");
        System.out.println("insert->after的list:"+jedis.lrange("list", 0, -1));


    }
}

 

总结:

list是一个链表,可以在left 和 right 进行插入

如果key不存在,创建新的链表

如果key存在,新增内容

如果移除了所有的值,空链表。,也代表不存在

在两边插入或者改变值,效率最高,中间元素,相对效率会低一点

 

使用场景: 

消息队列:lpush -> rpop

栈:lpush->lpop

 

③ set(集合)

set 所有的命令都是以 s 开头

set 是无序和不可重复的

package com.cai;


import redis.clients.jedis.Jedis;

// set 类型:无序、不可重复
public class TypeSet {

    public static void main(String[] args) {

        Jedis jedis = new Jedis("127.0.0.1", 6379);

        // sadd 添加值
        jedis.sadd("key1", "test");

        // 添加重复值:会添加失败,但是不会报错
        System.out.println("向key1中添加重复的值test:" + jedis.sadd("key1", "test"));

        // sadd 可一次性设置多个值,在这里一次性添加的value 中存在重复值,但是并没有报错,说明sadd方法不是原子性
        jedis.sadd("key1", "test1", "test2", "test", "hello set", "小菜", "study", "永无止境", "干巴得");

        // smembers 获取指定key中的所有值
        System.out.println("获取key1所有的值:" + jedis.smembers("key1"));

        // 判断某个值是都在某个key中存在
        System.out.println("key1中是否存在hello:" + jedis.sismember("key1", "hello"));
        System.out.println("key1中是否存在test:" + jedis.sismember("key1", "test"));

        // 查看元素个数
        System.out.println("key1中元素个数:" + jedis.scard("key1"));

        // 移除指定元素
        jedis.srem("key1", "test2");
        System.out.println("移除test2后key1的值:" + jedis.smembers("key1"));

        // 随机取值
        System.out.println("随机取key1中的1个值:" + jedis.srandmember("key1"));
        System.out.println("随机取key1中的2个值:" + jedis.srandmember("key1", 2));

        // 随机删除元素
        jedis.spop("key1");
        System.out.println("随机删除key1中的1个值:" + jedis.smembers("key1"));

        System.out.println("-----------------------------------");

        jedis.sadd("key2", "张三", "李四", "王五", "赵六", "田七");
        System.out.println("key1:" + jedis.smembers("key1"));
        System.out.println("key2:" + jedis.smembers("key2"));

        // 将一个key中的值移动到另一个key中
        jedis.smove("key2", "key1", "张三");
        System.out.println("张三转移之后的key1:" + jedis.smembers("key1"));
        System.out.println("张三转移之后的key2:" + jedis.smembers("key2"));

        System.out.println("-----------------------------------");

        // 清空当前redis数据库
        jedis.flushDB();

        // 设置两组set
        jedis.sadd("key1", "1", "3", "5", "7", "9");
        jedis.sadd("key2", "2", "4", "5", "8", "9");

        System.out.println("key1:" + jedis.smembers("key1"));
        System.out.println("key2:" + jedis.smembers("key2"));

        // 查询一个set与另一个set的差集
        System.out.println("key1对比key2的差集:"+jedis.sdiff("key1", "key2"));

        // 查询一个set与另一个set的交集
        System.out.println("key1对比key2的交集:"+jedis.sinter("key1", "key2"));

        // 查询一个set与另一个set的并集
        System.out.println("key1对比key2的并集:"+jedis.sunion("key1", "key2"));

    }


}

 

总结:

set具有不可重复的特点,在向set中添加重复值时会插入失效,不会报错,查询多个set的并集时,也会去重

 

使用场景:共同好友

 

④ Hash(哈希)

想象成Map集合,key ->Map(key-value)。

命令就是以 h 开头

package com.cai;

import redis.clients.jedis.Jedis;

import java.util.HashMap;
import java.util.Map;

public class TypeHash {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);

        // hash set : key:myHash value:(key:field1 - value:张三)
        jedis.hset("myHash", "field1", "123");
        System.out.println("myHash中field1的值:" + jedis.hget("myHash", "field1"));

        Map<String, String> map = new HashMap<>();
        map.put("field1", "张三");
        map.put("field2", "李四");
        map.put("field3", "王五");
        map.put("field4", "赵六");
        map.put("field5", "田七");
        map.put("field6", "1");

        // hmset 存多个,存在的值会被覆盖
        jedis.hmset("myHash", map);

        // hmget 获取某个key的多个字段值
        System.out.println("获取myHash的filed1、filed2的值:" + jedis.hmget("myHash", "filed1", "filed2"));

        // hgetAll 获取某个key 的所有值
        System.out.println("获取myHash所有的值:" + jedis.hgetAll("myHash"));

        // 删除key中指定的字段
        jedis.hdel("myHash", "field1");
        System.out.println("删除myHash中指定的字段field1:" + jedis.hgetAll("myHash"));

        // 获取某个key的长度
        System.out.println("获取myHash的长度:" + jedis.hlen("myHash"));

        // 判断某个key中某个field是否存在
        System.out.println("myHash中field1是否存在:" + jedis.hexists("myHash", "field1"));
        System.out.println("myHash中field2是否存在:" + jedis.hexists("myHash", "field2"));

        // 获取key中所有的filed
        System.out.println("myHash中所有的field:"+jedis.hkeys("myHash"));

        // 获取key中所有的value
        System.out.println("myHash中所有的value:"+jedis.hvals("myHash"));

        // 对某个key的field自增
        jedis.hincrBy("myHash","field6",1);
        System.out.println("myHash中的field6->hincrBy自增1之后的值:"+jedis.hget("myHash", "field6"));

        // 对某个key的field自增
        jedis.hincrBy("myHash","field6",-1);
        System.out.println("myHash中的field6->hincrBy自增-1之后的值:"+jedis.hget("myHash", "field6"));

        // 当某个key中某个field不存在的时候设置值
        System.out.println("hsetnx->field6存在时设置值:"+jedis.hsetnx("myHash", "field6", "test"));
        System.out.println("hsetnx->field7不存在时设置值:"+jedis.hsetnx("myHash", "field7", "test"));

        System.out.println("获取myHash所有的值:"+jedis.hgetAll("myHash"));
    }
}

 

总结:hash 可以比string 更好的存储对象类型

 

使用场景:对象的存储,数据经常发生变化

 

⑤ Zset (有序集合)

顾名思义,就是在set的基础上增加了排序功能,满足有序,不可重复

命令都是以 z 开头

package com.cai;

import redis.clients.jedis.Jedis;

import java.util.HashMap;

import static jdk.nashorn.internal.objects.Global.Infinity;

public class TypeZset {

    public static void main(String[] args) {

        Jedis jedis = new Jedis("127.0.0.1", 6379);

        // 添加数据
        jedis.zadd("myZset",1,"张三");

        HashMap<String, Double> map = new HashMap<>();
        map.put("李四",2.0);
        map.put("王五",2.5);
        map.put("赵六",3.0);

        // 添加多个值
        jedis.zadd("myZset",map);

        // 通过索引获取区间值
        System.out.println("根据索引获取myZset的所有值:"+jedis.zrange("myZset", 0, -1));

        // 根据score正序来获取区间值
        System.out.println("获取myZset的score从负无穷到正无穷的区间值正序:"+jedis.zrangeByScore("myZset", -Infinity, Infinity));

        // 删除指定元素
        jedis.zrem("myZset","张三");

        // 根据score正序来获取区间值
        System.out.println("获取myZset的score从0到2.5的区间值正序:"+jedis.zrangeByScore("myZset", 0, 2.5));

        // 获取元素个数
        System.out.println("获取myZset的元素个数:"+jedis.zcard("myZset"));

        // 根据score倒序来获取区间值
        System.out.println("获取myZset的score->0到3的区间值倒序:"+jedis.zrevrange("myZset", 0, 3));

        // 获取score 区间值的元素个数
        System.out.println("获取score从0到2.5的元素个数:"+jedis.zcount("myZset", 0, 2.5));

    }

}

 

总结:

set:sadd key value

Zset:sadd key score value

 

使用场景:排名、等级

 

三种特殊数据类型

① geospatial(地理位置)

用于存放地点经纬度

命令都是 geo 开头

package com.cai;

import redis.clients.jedis.GeoUnit;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.GeoRadiusParam;

//
public class TypeGeospatial {
    public static void main(String[] args) {

        Jedis jedis = new Jedis("127.0.0.1", 6379);

        // 添加数据
        jedis.geoadd("city",116.23128,40.22077,"beijing");
        jedis.geoadd("city",121.48941,31.40527,"shanghai");
        jedis.geoadd("city",113.27324,23.15792,"guangzhou");
        jedis.geoadd("city",113.88308,22.55329,"shenzhen");

        // 获取key中指定元素的经纬度
        System.out.println("获取指定地点的经纬度:"+jedis.geopos("city","beijing","shanghai","guangzhou","shenzhen"));

        // 查询key中两个元素之间的距离
        System.out.println("北京到上海的直线距离约为:"+jedis.geodist("city", "beijing", "shanghai", GeoUnit.KM)+"/km");
        System.out.println("上海到深圳的直线距离约为:"+jedis.geodist("city", "shanghai", "shenzhen", GeoUnit.M)+"/m");

        // 模拟附近的人 以经纬度110,30为中心,半径为1000km附近的城市
        System.out.println("经纬度110,30附近1000km的地点:"+jedis.georadius("city", 110, 30, 1000, GeoUnit.KM));

        // 以元素为中心,查询指定距离的其他元素
        System.out.println("北京附近1000km的地点:"+jedis.georadiusByMember("city", "beijing", 1000, GeoUnit.KM));

        // 降维打击 ,hash值越像,距离越近
        System.out.println("将二维的经纬度转为一维的字符串:"+jedis.geohash("city", "beijing", "shanghai"));

        // 可以通过Zset的命令操作
        System.out.println("通过zrange获取元素:"+jedis.zrange("city", 0, -1));

        System.out.println("通过zrem删除元素:"+jedis.zrem("city", "shenzhen"));

        System.out.println("通过zrange获取元素(删除shenzhen之后):"+jedis.zrange("city", 0, -1));

    }
}

 

总结:

地球两极无法添加

有效经度:-180 到 +180

有效纬度:-85 到 +85

geo底层的实现远离其实就是Zset,我们可以使用Zset命令来操作geo

 

使用场景:

计算两地的直线距离

查询附近的人

 

② hyperloglog (数据结构)

用于做基数(不重复的元素)统计的算法

package com.cai;

import redis.clients.jedis.Jedis;

public class TypeHyperloglog {

    public static void main(String[] args) {
        Jedis jedis = new Jedis();

        // 添加元素
        jedis.pfadd("myKey","a","a","b","b","c");

        // 统计元素个数
        System.out.println("myKey中基数个数:"+jedis.pfcount("myKey"));

        jedis.pfadd("myKey2","1","2","3","4","4");

        System.out.println("myKey2中基数个数:"+jedis.pfcount("myKey2"));

        // 合并两个key 生成新的key
        jedis.pfmerge("myKey3","myKey","myKey2");

        System.out.println("myKey3中基数个数:"+jedis.pfcount("myKey3"));

    }
}

 

 

总结:

可以接受误差,占用的内存是固定的(2^64的元素,只占用12kb)

 

使用场景:

网站 UV(一个人访问一个网站多次,但是还是算一个人) 统计

 

③ bitmaps(位图)

操作二进制来进行记录,只有true(1)和false(0)两个状态,占用内存非常小,八个bit才一个字节

所以命令比较少,也比较简单

import redis.clients.jedis.Jedis;

public class TypeBitmaps {
    public static void main(String[] args) {

        Jedis jedis = new Jedis("127.0.0.1", 6379);

        // 签到 : 第一天 签到了
        jedis.setbit("sign",1,true);
        // 第二天 未签到
        jedis.setbit("sign",2,false);
        // 第三天 签到了
        jedis.setbit("sign",3,true);

        // 获取key中指定天数的状态
        System.out.println("获取sign中第1天是否打卡:"+jedis.getbit("sign", 1));
        System.out.println("获取sign中第2天是否打卡:"+jedis.getbit("sign", 2));
        System.out.println("获取sign中第3天是否打卡:"+jedis.getbit("sign", 3));

        // 查询key中value为true的总数
        System.out.println("三天内打卡次数:"+jedis.bitcount("sign"));
        
    }
}

 

总结:

bitmaps占用的内存很小,bit类型,只有两种状态,true和false

 

使用场景:

一般用于记录两种状态的数据,比如:是否签到、是否登录、是否活跃

 

 

事务

事务的ACID特性,在redis中,只保证单条命令具有原子性,事务是不保证原子性和隔离性的

 

redis的事务如果命令中出错是不会回滚的,造成不具有原子性。

 

redis的事务可以配合着监视器(watch),当监视的key的中值发生了变化时,事务中的命令则不会执行

 

redis的事务分为三个阶段:

1、开启事务(multi)

2、命令入队(set...)

3、执行事务(exec)

也就是说,所有的命令在事务中,并没有直接被执行,只有发起了执行事务命令,才会去执行。

package com.redis.transaction;

import com.alibaba.fastjson.JSONObject;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

import java.util.concurrent.TimeUnit;

public class Trans {
    public static void main(String[] args) {
        Jedis jedis = new Jedis();

        jedis.flushDB();

        // 创建一个json
        JSONObject jsonObject=new JSONObject();
        jsonObject.put("name","lucky");
        jsonObject.put("age","18");

        // 开启事务
        Transaction multi = jedis.multi();

        String user = jsonObject.toJSONString();

        try {
            System.out.println("事务插入user-json:"+multi.set("user", user));

            System.out.println("事务插入key1-张三:"+multi.set("key1", "张三"));
            System.out.println("事务插入key2-李四:"+multi.mset("key2", "李四"));
            System.out.println("事务插入key3-王五:"+multi.lpush("key3", "王五"));
            System.out.println("事务插入key4-赵六:"+multi.sadd("key4", "赵六"));
            System.out.println("事务插入key5-田七:"+multi.zadd("key5", 6, "田七"));
            System.out.println("事务插入key6-周八:"+multi.hset("key6", "name", "周八"));

            // 执行事务
            System.out.println("执行结果:"+multi.exec());

        }catch (Exception e){
            // 出现异常,取消事务
            multi.discard();
        }

        System.out.println("------------------------------------");


        // 获取事务插入的值
        System.out.println("事务中字符串set的user:"+jedis.get("user"));
        System.out.println("事务中字符串set的key1:"+jedis.get("key1"));
        System.out.println("事务中字符串mset的key2:"+jedis.mget("key2"));
        System.out.println("事务中列表push的key3:"+jedis.lrange("key3",0,-1));
        System.out.println("事务中集合set的key4:"+jedis.srandmember("key4"));
        System.out.println("事务中有序集合add的key5:"+jedis.zrange("key5",0,-1));
        System.out.println("事务中哈希set的key6:"+jedis.hgetAll("key6"));

        System.out.println("------------------------------------");

        // 创建事务,测试取消事务,是否还能查询到值
        Transaction multi1 = jedis.multi();

        System.out.println("事务插入key7-九九:"+multi1.set("key7", "九九"));

        // 取消事务
        System.out.println("取消事务:"+multi1.discard());

        // 查询事务取消之后是否还能查询中key7
        System.out.println("事务被取消添加的key7:"+jedis.get("key7"));


        System.out.println("-------------------------------------");
        // 测试事务执行的命令中出现错误,是否保证原子性
        Transaction multi2 = jedis.multi();

        System.out.println("事务插入key8-字符串:"+multi2.set("key8", "字符串"));
        System.out.println("事务将key8的值字符串自增1:"+multi2.incr("key8"));

        System.out.println("执行结果:"+multi2.exec());

        // 获取key8的值,发现有值,说明redis的事务不保证原子性
        System.out.println("获取key8的值:"+jedis.get("key8"));


        System.out.println("--------------------------------------");
        // 设置一个key
        System.out.println("设置money的值:"+jedis.set("money", "1000"));
        System.out.println("设置out的值:"+jedis.set("out", "0"));

        // 监视【watch】 相当于mysql中的乐观锁【version】
        System.out.println("监视money:"+jedis.watch("money"));

        // 测试正常情况,事务期间,数据没有发生变动
        Transaction multi3 = jedis.multi();

        System.out.println("测试正常情况money自减200:"+multi3.decrBy("money", 200));
        System.out.println("测试正常情况out自增200:"+multi3.incrBy("out", 200));

        System.out.println("执行结果:"+multi3.exec());
        System.out.println("测试正常情况money执行后的值:"+jedis.get("money"));
        System.out.println("测试正常情况out执行后的值:"+jedis.get("out"));

        System.out.println("--------------------------------------");


        jedis.watch("money");

        new Thread(()->{
            // 插入扣除money800,money变为0
            System.out.println("插队扣除money的800:"+jedis.decrBy("money", 800));
        },"b").start();
        new Thread(()->{
            Transaction multi4 = jedis.multi();

            try {

                System.out.println("测试money插队修改后money自减200:"+multi4.decrBy("money", 200));
                System.out.println("测试money插队修改后out自增200:"+multi4.incrBy("out", 200));

            }catch (Exception e){
                System.out.println("异常情况:"+multi4.discard());
            }

            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 得出结果,监视到money的值发生了变化,事务中的命令操作并没有执行
            System.out.println("执行结果:"+multi4.exec());
            System.out.println("测试money插队修改后money执行后的值:"+jedis.get("money"));
            System.out.println("测试money插队修改后out执行后的值:"+jedis.get("out"));

        },"a").start();


    }
}

 

 

 

第二种:SpringBoot整合

1、导入依赖,我们可以再选择在创建springboot项目的时候勾选上redis,也可以在创建完成项目之后,再导入依赖

 

 

 

 

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

 

2、了解 Redis 配置

redis既然集成springboot中,那就应该存在一个 RedisAutoConfiguration 的自动配置类,并绑定一个 properties 文件

我们在SpringBootAutoConfigure中查找关于redis的配置

 

 

  

找到了这个 RedisAutoConfiguration 的配置类,看到绑定了RedisProperties类。

以及注入的两个方法:

第一个就是和MangoDB和RabbMq一样熟悉的模板类:RedisTemplate,我们之后就可以直接使用这个模板类简单的来使用redis了

第二个就是StringRedisTemplate ,为什么会有两个Template 呢?这是因为在redis 中大部分使用的就是 String类型。

默认的Template并没有过多的配置,也没有使用 序列化 的配置,而我们的redis对象都是需要序列化的,之后会在配置中加上,RedisTemplate 默认用的都是两个Object 类型,我们一般使用 String,Object 类型,会造成强制转换,通过注解了解当不存在 redisTempla 类的时候生效,也就是说我们可以自定义一个redisTemplate的类替换掉它。

 

 

我们点进RedisTemplate类中

 

 

 

 这里有数据结构值的序列化配置,我们可以找一下默认的序列化

 

 

 

发现如果没有配置序列化方式,默认会使用jdk的序列化,这会让字符串转义,我们一般使用json来序列化。

 

我们再看看 RedisProperties 类,可以看到属性和一些默认的配置,比如 redis 默认使用的是0号数据库、采用本地的服务以及使用的6379端口

 

 

  

3、在Application文件中配置 Redis 

# host默认是本地连接
spring.redis.host=127.0.0.1
# 端口号
spring.redis.port=6379
# redis的数据库,默认是0
spring.redis.database=1

 

4、测试使用

在我们测试类中注入RedisTemplate

@Autowired
private RedisTemplate redisTemplate;

 

redisTemplate数据类型,对应关系如下:

操作字符串,String --> opsForValue()
操作列表,List --> opsForList()
操作集合,Set --> opsForSet()
操作有序集合,Zset --> opsForZSet()
操作哈希,Hash --> opsForHash()
操作位置,Geo --> opsForGeo()
操作基数,Hypeloglog --> opsForHyperLogLog()
操作位图,bitmaps --> opsForValue().setBit()

 

@Test
    void contextLoads() {

        // 操作字符串,String --> opsForValue()
        System.out.println("-----------------------string------------------------");
        redisTemplate.opsForValue().set("string01", "test01");
        System.out.println("string01:" + redisTemplate.opsForValue().get("string01"));

        // 操作列表,List --> opsForList()
        System.out.println("-----------------------list------------------------");
        redisTemplate.opsForList().leftPush("list01", "leftValue01");
        redisTemplate.opsForList().rightPush("list01", "rightValue02");
        System.out.println("list01:" + redisTemplate.opsForList().range("list01", 0, -1));

        // 操作集合,Set --> opsForSet()
        System.out.println("-----------------------Set------------------------");
        redisTemplate.opsForSet().add("set01", "value01", "value02", "value03", "value04");
        redisTemplate.opsForSet().add("set02", "value01", "value03", "value05", "value07");
        System.out.println("set01:" + redisTemplate.opsForSet().members("set01"));
        System.out.println("set02:" + redisTemplate.opsForSet().members("set02"));
        System.out.println("set01和set02的差集:" + redisTemplate.opsForSet().difference("set01", "set02"));
        System.out.println("set01和set02的并集:" + redisTemplate.opsForSet().union("set01", "set02"));
        System.out.println("set01和set02的交集:" + redisTemplate.opsForSet().intersect("set01", "set02"));

        // 操作有序集合,Zset --> opsForZSet()
        System.out.println("----------------------Zset-------------------------");
        redisTemplate.opsForZSet().add("Zset01", "a", 1);
        redisTemplate.opsForZSet().add("Zset01", "b", 2);
        redisTemplate.opsForZSet().add("Zset01", "c", 3);
        redisTemplate.opsForZSet().add("Zset01", "d", 4);
        System.out.println("Zset正序:" + redisTemplate.opsForZSet().rangeByScore("Zset01", 1, 4));
        System.out.println("Zset倒序:" + redisTemplate.opsForZSet().reverseRange("Zset01", 0, 4));

        // 操作哈希,Hash --> opsForHash()
        System.out.println("-----------------------hash------------------------");
        redisTemplate.opsForHash().put("hash01", "name", "zhangsan");
        redisTemplate.opsForHash().put("hash01", "age", 18);
        System.out.println("hash01的name:" + redisTemplate.opsForHash().get("hash01", "name"));
        System.out.println("hash01中是否存在age:" + redisTemplate.opsForHash().hasKey("hash01", "age"));

        // 操作位置,Geo --> opsForGeo()
        System.out.println("-------------------------geo----------------------");
        HashMap<String, Point> map = new HashMap<>();
        map.put("beijing", new Point(116.23128, 40.22077));
        map.put("shanghai", new Point(121.48941, 31.40527));
        redisTemplate.opsForGeo().add("geo01", map);
        System.out.println("geo01中beijing到shanghia的直线距离(km):" + redisTemplate.opsForGeo().distance("geo01", "beijing", "shanghai", Metrics.KILOMETERS));

        Circle circle = new Circle(new Point(119.31315, 36.33333), new Distance(1000, Metrics.KILOMETERS));
        System.out.println("geo01中距离坐标119.31315,36.3333的距离为10s00km的城市:" + redisTemplate.opsForGeo().radius("geo01", circle));

        // 操作基数,Hypeloglog --> opsForHyperLogLog()
        System.out.println("-------------------------hypeloglog----------------------");
        redisTemplate.opsForHyperLogLog().add("hype01", "1", "2", "2", "3", "4", "4", "5");
        System.out.println("hype01去重之后的数量:" + redisTemplate.opsForHyperLogLog().size("hype01"));

        // 操作位图,bitmaps --> opsForValue().setBit()
        System.out.println("-------------------------bitmaps----------------------");
        redisTemplate.opsForValue().setBit("bit01", 1, true);
        redisTemplate.opsForValue().setBit("bit01", 2, false);
        redisTemplate.opsForValue().setBit("bit01", 3, true);
        System.out.println("bit01的第1天状态:" + redisTemplate.opsForValue().getBit("bit01", 1));
        System.out.println("bit01的第2天状态:" + redisTemplate.opsForValue().getBit("bit01", 2));


        // 获取redis的连接对象,操作数据库
        RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
        connection.flushAll();
        connection.flushDb();


    }

 

 

5、自定义 RedisTemplate

上面通过源码我们得知了默认的RedisTemplate使用的是jdk的序列化。所以我们重新定义一个RedisTemplate,使用json的序列化

我们先建一个简单的user对象

package com.redis.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;

@Component
@AllArgsConstructor
@NoArgsConstructor
@Data
public class User {

    private String name;
    private Integer age;

}

 

我们再建一个config的包,新建一个 redisTemplate 的类

package com.redis.config;

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 {

    // redis默认的序列化使用的是jdk的方式,我们一般会使用json格式序列化,这个时候,我们就需要编写自己的配置,让默认的配置不生效
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // 方便开发使用<string,object>
        RedisTemplate<String, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);

        // jackson的序列化设置
        Jackson2JsonRedisSerializer<Object> objectJackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);

        ObjectMapper objectMapper = new ObjectMapper();

        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator());

        objectJackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        // string的序列化设置
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // key采用string的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key采用string的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);

        // value的序列化方式采用jackson
        template.setValueSerializer(objectJackson2JsonRedisSerializer);
        // hash的value序列化方式采用Jackson
        template.setHashValueSerializer(objectJackson2JsonRedisSerializer);

        template.afterPropertiesSet();

        return template;
    }
}

 

测试看效果:

@Test
    void UserTest() throws JsonProcessingException {

        redisTemplate.opsForValue().set("user_toString",new User("zhangsan",18).toString());
        System.out.println("对象toString的数据:"+redisTemplate.opsForValue().get("user_toString"));

        User user = new User("张三", 18);

        // 对象格式化json
        String jsonUser = new ObjectMapper().writeValueAsString(user);

        redisTemplate.opsForValue().set("user",jsonUser);
        System.out.println("对象json格式化的数据:"+redisTemplate.opsForValue().get("user"));

    }

 

 

Redis.config 详解

在我们安装redis的目录下找到我们的 redis.config 文件,来探究一下 redis 可以配置什么,有什么功能

 

1、配置内存单位,配置不区分大小写

# Note on units: when memory size is needed, it is possible to specify
# it in the usual form of 1k 5GB 4M and so forth:
#
# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes
#
# units are case insensitive so 1GB 1Gb 1gB are all the same.

 

2、INCLUDES 包含,可以导入其他的配置

# Include one or more other config files here.  This is useful if you
# have a standard template that goes to all Redis servers but also need
# to customize a few per-server settings.  Include files can include
# other files, so use this wisely.
#
# Note that option "include" won't be rewritten by command "CONFIG REWRITE"
# from admin or Redis Sentinel. Since Redis always uses the last processed
# line as value of a configuration directive, you'd better put includes
# at the beginning of this file to avoid overwriting config change at runtime.
#
# If instead you are interested in using includes to override configuration
# options, it is better to use include as the last line.
#
# include /path/to/local.conf
# include /path/to/other.conf

 

3、NETWORK 网络配置

bind 127.0.0.1 ::1    # 绑定ip

protected-mode yes    # 开启受保护模式

port 6379          # 端口号

 

4、GENERAL 通用配置

daemonize no               # 以守护进程(后台)的方式运行,默认是no,我们需要自己开启为yes

 pidfile /var/run/redis_6379.pid   # 已后台方式运行,需要指定一个 pid 文件

 

# Specify the server verbosity level.

# This can be one of:

# debug (a lot of information, useful for development/testing)

# verbose (many rarely useful info, but not a mess like the debug level)

# notice (moderately verbose, what you want in production probably)

# warning (only very important / critical messages are logged)

loglevel notice              # 日志级别

logfile ""                 # 日志文件目录

databases 16                # 数据库数量,默认是16个数据库

always-show-logo no            # 是否显示log

 

5、SNAPSHOTTING 快照,持久化

# Unless specified otherwise, by default Redis will save the DB:
#   * After 3600 seconds (an hour) if at least 1 key changed
#   * After 300 seconds (5 minutes) if at least 100 keys changed
#   * After 60 seconds if at least 10000 keys changed
#
# You can set these explicitly by uncommenting the three following lines.
#
# save 3600 1    # 在指定时间内操作了多少次,则会持久化到文件 .RDB/.AOP
# save 300 100
# save 60 10000

stop-writes-on-bgsave-error yes  # 持久化出错,是否继续工作

rdbcompression yes          # 是否压缩RDB文件,需要消耗CPU资源

rdbchecksum yes            # 保存RDB文件时,校验是否正确

dbfilename dump.rdb          # RDB文件的名称

dir /opt/homebrew/var/db/redis/   # RDB文件存放的目录

 

6、REPLICATION 复制 

replica-serve-stale-data yes  # 使用主从复制,从节点可以接收访问请求

 

7、SECURITY 安全

requirepass 123465    # 密码

密码设置之后,使用redis需要授权

> config set requirepas "123456"
> config get requirepas
> auth 123456

 

8、CLIENTS 客户端

# maxclients 10000    # 可连接最大数

 

9、内存管理

# maxmemory <bytes>        # 最大内存

  # maxmemory-policy noeviction  # 内存到达上限的策略,例如:移除一些过期的key、报错等等

 

mybatis 整合 redis 实现二级缓存

mybatis 有一级缓存和二级缓存之分,通俗的理解如下

一级缓存:

SqlSession 级别的缓存,在同一个连接(connection)中,相同的查询语句,第二次不会走数据库,默认开启

二级缓存:

夸SqlSession级别的缓存,在多个连接(connection)中,相同的查询语句,第二次不会走数据库,需要手动开启

开启二级缓存的方式:

1、springboot的配置文件中添加 :

mybatis.configuration.cache-enabled=true

2、在对应的Mapper配置文件中,加入 <cache /> 标签即可

 

整合 redis 实现二级缓存步骤

首先,我们需要知道mybatis如何实现二级缓存的,为什么加了一个配置就可以实现二级缓存,我们知道了这一点,就可以用类似的方式实现redis 的缓存

在mapper配置文件中 <cache/> 标签 是有一个type的属性,我们可以去 Cache 这个接口中查询一下实现类 

 

 

  

<cache type="org.apache.ibatis.cache.impl.PerpetualCache" > 

实现类是 PerpetualCache 

然后再来想一想是不是 我们也可以写一个自定义缓存的类实现 Cahe 接口,放到 cache 标签中的 type属性中呢?下面就是实现步骤

 

1、新建一个 cache 的包,创建一个 MyCache的类,实现 Cache 的接口中的方法,要注意的是Cache这个接口的包要是mybatis的

 

 

 

类创建好之后, 我们将 cache 中的 type 换成我们编写的 MyCache 这个类

<cache type="com.redis.cache.MyCache"/>

在测试类中定义一个方法


@Test
public void testFindOne(){
userService.findUserById(1);
System.out.println("======================");
userService.findUserById(1);
}

然后运行,看看会不会报错

Caused by: org.apache.ibatis.cache.CacheException: Invalid base cache implementation (class com.redis.cache.MyCache).  Base cache implementations must have a constructor that takes a String id as a parameter.  Cause: java.lang.NoSuchMethodException: com.redis.cache.MyCache.<init>(java.lang.String)
    at org.apache.ibatis.mapping.CacheBuilder.getBaseCacheConstructor(CacheBuilder.java:202)
    at org.apache.ibatis.mapping.CacheBuilder.newBaseCacheInstance(CacheBuilder.java:190)
    at org.apache.ibatis.mapping.CacheBuilder.build(CacheBuilder.java:94)
    at org.apache.ibatis.builder.MapperBuilderAssistant.useNewCache(MapperBuilderAssistant.java:139)
    at org.apache.ibatis.builder.xml.XMLMapperBuilder.cacheElement(XMLMapperBuilder.java:213)
    at org.apache.ibatis.builder.xml.XMLMapperBuilder.configurationElement(XMLMapperBuilder.java:117)
    ... 84 more
Caused by: java.lang.NoSuchMethodException: com.redis.cache.MyCache.<init>(java.lang.String)
    at java.lang.Class.getConstructor0(Class.java:3082)
    at java.lang.Class.getConstructor(Class.java:1825)
    at org.apache.ibatis.mapping.CacheBuilder.getBaseCacheConstructor(CacheBuilder.java:200)
    ... 89 more

发现报错了,错误信息说我们的构造方法中需要一个id 的参数,

 

 

如果不知道是什么意思,也可以直接去看 Cache 的实现类,也就是 PerpetualCache 这个类中是怎么写的

 

 

 

这个 id 相当于 mapper文件中的namespace ,我们也可以参照这种方法来

// 相当于mapper的namespace
private final String id;
// 必须存在一个构造方法 public MyCache(String id) { System.out.println("id=" + id); this.id = id; }
@Override
public String getId() { return this.id; }

 

再次运行,发现运行成功,没有报错。

 

2、在 MyCache 中使用 RedisTemplate

由于 RedisTemplate 是工厂模式创建的,所以我们要拿到springboot 的工厂对象来创建redisTemplate

所以我们创建一个 ApplicationContextUtil 的工具类

package com.redis.utils;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

// 用来获取springboot创建好的工厂
@Component
public class ApplicationContextUtil implements ApplicationContextAware {

    // 保留下来的工厂
    private static ApplicationContext applicationContext;

    // 将创建好的工厂以参数的样式传递给这个类
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    // 提供在工厂中获取对象的方法 RedisTemplate -> redisTemplate
    public static Object getBean(String beanName){
        return applicationContext.getBean(beanName);
    }
}

  

接着在我们的MyCache 类中 使用,我们先编写一个方法

private RedisTemplate getRedisTemplate() {

        RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtil.getBean("redisTemplate");
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());

        return redisTemplate;
}

  

然后就可以在 MyCache 中 调用方法getRedisTemplate() 来使用了

package com.redis.cache;


import com.redis.utils.ApplicationContextUtil;
import org.apache.ibatis.cache.Cache;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.util.DigestUtils;

public class MyCache implements Cache {

    // 相当于mapper的namespace
    private final String id;

    // 必须存在一个构造方法
    public MyCache(String id) {
        System.out.println("id=" + id);
        this.id = id;

    }

    @Override
    public String getId() {
        return this.id;
    }

    // 缓存中放入值
    @Override
    public void putObject(Object key, Object value) {
        System.out.println("==========key:" + key.toString());
        System.out.println("==========value:" + value);
        String id = DigestUtils.md5DigestAsHex(this.id.getBytes());
        getRedisTemplate().opsForHash().put(id, key.toString(), value);

    }

    // 缓存中取出值
    @Override
    public Object getObject(Object key) {
        System.out.println("==========key:" + key.toString());

        String id = DigestUtils.md5DigestAsHex(this.id.getBytes());
        return getRedisTemplate().opsForHash().get(id, key.toString());
    }

    // 为mybatis的方法,目前没有实现
    @Override
    public Object removeObject(Object key) {
        System.out.println("==========删除:" + key);
        return null;

    }

    @Override
    public void clear() {
        System.out.println("=========清除:");

        String id = DigestUtils.md5DigestAsHex(this.id.getBytes());
        getRedisTemplate().delete(id);
    }

    // 计算缓存的数量
    @Override
    public int getSize() {

        String id = DigestUtils.md5DigestAsHex(this.id.getBytes());

        // 获取hash中的key value 数量
        return getRedisTemplate().opsForHash().size(id).intValue();
    }


    private RedisTemplate getRedisTemplate() {

        RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtil.getBean("redisTemplate");
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());

        return redisTemplate;
    }
}

 

posted @ 2021-08-07 20:21  幸运小菜  阅读(593)  评论(0编辑  收藏  举报