官网链接:Redis
首先简单理解一下
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 :原子性的操作,要不全部成功,要不全部失败
使用场景:
1计数器(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;
}
}