目录
一. redis 概述
- redis 基于内存的单线程非关系型数据库,每秒支持十万并发
- 架构模式: 单机版, 主从复制,哨兵模式,分片集群
- 使用场景: 热点数据缓存,生成令牌,网站计数器,通过redis实现接口幂,存储验证码,分布式锁,生成分布式ID
- redis集群使用哈希槽算法进行负载,redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置在哪个槽,集群的每个节点负责一部分hash槽,思考为什么哈希槽数量是16384(2 ^14)呢,CRC16算法产生的hash值是16bit,该算法可以产生2 ^16约等于65536,换句话来说值是分布在0-65535之间,那在mod运算是为什么不是要mod65535而选择mod16384,
防止发送心跳包过大,假设槽数未65536,发送心跳的消息头为65535/8/1024=8k,因为集群情况下redis每秒都会发送一个心跳包,这样就太大了
集群节点越多,心跳包消息体内携带的数据越多,如果超过了1000也会导致网络拥堵,对于节点在1000内的redis cluster集群,16384个槽位就够了,没必要拓展到65535
槽位越小,节点少的情况下,压缩比高,更容易传输,redis主节点的配置信息中他所负责的哈希槽是通过bitmap的形式保存的,在传输过程中会对bitmap进行压缩,如果bitmap的填充率 slots/节点数 很高,bitmap的压缩率就很低,如果节点数少,哈希槽数量很多,bitmap的压缩率就会很低
redis 主从同步执行流程
在集群环境中多个redis 分为master主库与Salve从库,当Salve启动后会连接到master主库,并发送一个sync命令,master接收到该命令后台启动一个进程,收集接收到的操作数据指令缓存为快照文件,当缓存完毕后,将这个文件发送给所有连接到该master的Salve从库,Salve将文件接收保存到磁盘上,然后加载到内存中,后续master主库接收到的修改数据指令都会通过这个后台进程发送给Salve
redis 淘汰策略
缓存常见问题
KEYS指令与SCAN指令
假设需要在reid中获取某个key的数据,或者匹配获取某些key的数据,需要考虑redis中数据大小的问题,如果数据过大,由于redis是基于内存的,使用keys指令去查询会读取所有数据进行匹配会造成服务器卡顿现象,推荐使用SCAK指令
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
@Component
public class RedisHelper {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* scan 实现
* @param pattern 表达式
* @param consumer 对迭代到的key进行操作
*/
public void scan(String pattern, Consumer<byte[]> consumer) {
this.stringRedisTemplate.execute((RedisConnection connection) -> {
try (Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions().count(Long.MAX_VALUE).match(pattern).build())) {
cursor.forEachRemaining(consumer);
return null;
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
});
}
/**
* 获取符合条件的key
* @param pattern 表达式
* @return
*/
public List<String> keys(String pattern) {
List<String> keys = new ArrayList<>();
this.scan(pattern, item -> {
//符合条件的key
String key = new String(item,StandardCharsets.UTF_8);
keys.add(key);
});
return keys;
}
}
SpringBoot 整合 redis
- 引入 resid 依赖
<!-- SpringBoot对Redis支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--如果使用jedis操作redis数据的话添加这个依赖 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
- yml 文件中配置redis连接
单机版:
spring:
redis:
#设置数据存放在redis的哪个库中,此处为默认的0库
database: 0
host: 132.232.44.194
port: 6379
password: 123456
jedis:
pool:
max-active: 8
max-wait: -1
max-idle: 8
min-idle: 0
timeout: 10000
集群版
spring:
redis:
database: 0
#集群版本不需这些设置(否则会走单机连接)
# host: 132.232.44.194
# port: 6379
# password: 123456
jedis:
pool:
max-active: 8
max-wait: -1
max-idle: 8
min-idle: 0
timeout: 10000
cluster:
nodes:
- 192.168.212.149:9001
- 192.168.212.149:9002
- 192.168.212.149:9003
- 192.168.212.149:9004
- 192.168.212.149:9005
- 192.168.212.149:9006
- 代码中使用 StringRedisTemplate 或 RedisTemplate 操作redis
@Autowired
private StringRedisTemplate stringRedisTemplate;
//StringRedisTemplate
//StringRedisTemplate.opsForValue().* //操作String字符串类型
//StringRedisTemplate.delete(key/collection) //根据key/keys删除
//StringRedisTemplate.opsForList().* //操作List类型
//StringRedisTemplate.opsForHash().* //操作Hash类型
//StringRedisTemplate.opsForSet().* //操作set类型
//StringRedisTemplate.opsForZSet().* //操作有序set
@Autowired
private RedisTemplate<String, String> redisTemplate;
/* RedisTemplate对象,可以理解为一个map集合对象,通过这个对象操作redis中的各种类型数据,
* redisTemplate.opsForValue() -> ValueOperations<String, Object>;// 操作字符串
* redisTemplate.opsForHash() -> HashOperations<String, String, String>;// 操作 hash
* redisTemplate.opsForList() -> ListOperations<String, Object>;// 操作 list
* redisTemplate.opsForSet() -> SetOperations<String, Object>;// 操作 set
* redisTemplate.opsForZSet() -> ZSetOperations<String, Object>;// 操作 SortedSet */
StringRedisTemplate 与 RedisTemplate
- StringRedisTemplate 继承了 RedisTemplate 但是连着操作的数据不是共通的,StringRedisTemplate只能管理StringRedisTemplate里面的数据,RedisTemplate只能管理RedisTemplate中的数据
- RedisTemplate 使用JdkSerializationRedisSerializer 序列化机制,存入数据会将数据先序列化成字节数组然后在存入Redis数据库。
- StringRedisTemplate使用的是StringRedisSerializer
- 推荐: 存储数据是字符串类型的时候,就算存储对象也是将对象转换为JSON串,使用StringRedisTemplate即可。如果存储的数据是复杂的对象类型,取出的时候又不想做数据转换,直接从Redis里面取出一个对象,使用RedisTemplate
- RedisTemplate 中存取数据都是字节数组。当redis中存入的数据是可读形式而非字节数组时,使用redisTemplate取值的时候会无法获取导出数据,获得的值为null。可以使用 StringRedisTemplate 试试
redis 支持事物控制与锁
- 以代码举例(在实际项目中可以通过拦截器,操作redis的事物)
- redis支持锁,在使用锁
redis 设置key失效
调用expire(),方法,指定哪个key,指定存活时间,时间单位
//设置redis中指定key的失效时间(毫秒)
public boolean setTime(String key, Long time){
if(null != key || time !=0){
return stringRedisTemplate.expire(key, time, TimeUnit.SECONDS);
}else {
return false;
}
}
redis 指定库
redis 默认有16个库,从0开始,默认使用0库,可以通过配置修改库的个数,可以手动指定选择哪个库
@Test
public void test2() {
//1.通过jedisPool获取连接
Jedis conn = jedisPool.getResource();
//2.通过stringRedisTemplate获取连接
JedisConnectionFactory factory = (JedisConnectionFactory) stringRedisTemplate.getConnectionFactory();
//3.通过redisTemplate获取连接
JedisConnectionFactory factory2 = (JedisConnectionFactory) redisTemplate.getConnectionFactory();
//指定使用redis中的哪个库(默认16个从0开始)
factory2.setDatabase(1);
}
redis 的 setnx() 与 getset()
- setnx() 向redis中存储一个key-value,如果redis库中已存在,当前存储失败返回0,如果不存在,存储成功,返回1
- get(key) 获取key的值,如果存在,则返回;如果不存在,则返回nil;
- getset()命令:这个命令主要有两个参数 getset(key, newValue)。该方法是原子的,对key设置newValue这个值,并且返回key原来的旧值。
@Autowired
public Jedis jedis;
@Autowired
public JedisPool jedisPool;
@Test
public void test() {
//1.建立redis连接
Jedis conn = jedisPool.getResource();
long currentTime = System.currentTimeMillis();//当前时
String lockTimeDuration = String.valueOf(currentTime);
//将当前时间作为value 存储到redis中,存储成功返回1,否则返回0
Long i = jedis.setnx("keys", lockTimeDuration);
}
封装操作 redis 数据的工具类
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.Set;
import java.util.concurrent.TimeUnit;
//封装向redis中存储获取数据的方法
//将工具类注入到容器中
@Component
public class RedisUtils {
//通过StringRedisTemplate对象操作redis,向redis中存储数据
@Autowired
private StringRedisTemplate stringRedisTemplate;
//公用的存储方法,方法中判断数据类型,根据数据类型
//向redis中存储,并设置有效时间
public void set(String key, Object object, Long time) {
// 存放String 类型
if (object instanceof String) {
setString(key, object);
}
// 存放 set类型
if (object instanceof Set) {
setSet(key, object);
}
// 设置redis中指定key的有效时间
stringRedisTemplate.expire(key, time, TimeUnit.SECONDS);
}
//向redis中存储String类型数据
public void setString(String key, Object object) {
// 如果是String 类型
String value = (String) object;
stringRedisTemplate.opsForValue().set(key, value);
}
//向redis中存储set类型数据
public void setSet(String key, Object object) {
Set<String> value = (Set<String>) object;
for (String oj : value) {
stringRedisTemplate.opsForSet().add(key, oj);
}
}
//通过key获取redis中String数据
public String getString(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
//设置redis中指定key的失效时间(毫秒)
public boolean setTime(String key, Long time){
if(null != key || time !=0){
return stringRedisTemplate.expire(key, time, TimeUnit.SECONDS);
}else {
return false;
}
}
}
二. java 操作 redis 各种类型数据示例
注意点: 此处使用 StringRedisTemplate 操作redis数据,存储与获取都是字符串类型,如果存储对象需要先将对象转换为JSON串,然后进行存储,如果存储与获取时不进行类型转换,例如对象就直接存储对象,获取时直接获取为该对象类型不进行转换则使用 RedisTemplate,该对象向redis中存储的是字节数组
通过命令操作
- 操作 String 类型,二进制安全的。意思是可以包含任何数据。比如jpg图片或者序列化的对象 ,最基本的数据类型,一个键最大能存储512MB。
@Test
public void test01() {
//创建操作redis中String类型数据的对象 ValueOperations
ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
//1.添加数据set()添加数据(如果添加这个数据在库中已经存在就变为了修改)
valueOperations.set("username", "mingming");
/*2.以层级关系目录形式添加数据*/
valueOperations.set("user:01", "wwww");
valueOperations.set("user:01", "cccc");
//3.添加多条数据multiSet()
//map中的key为redis的key,map中的值为redis的值
Map<String,String> stringMap=new HashMap<>();
stringMap.put("数据1","值1");
stringMap.put("数据2","值2");
stringMap.put("数据3","值3");
valueOperations.multiSet(stringMap);
//4.查询
Object username=valueOperations.get("username");
//5.查询多条,以集合中的数据为key进行查询
List<String> keyStr=new ArrayList<>();
keyStr.add("数据1");
keyStr.add("数据2");
keyStr.add("数据3");
List<String> valL=valueOperations.multiGet(keyStr);
//6.删除指定key数据
stringRedisTemplate.delete("username");
//6.模糊匹配
Set<String> keys = stringRedisTemplate.keys("noteUserListenedPoi:" + "*");
//7.批量删除
stringRedisTemplate.delete(keys);
}
- 操作 Hash 散列类型,是一个string类型的field和value的映射表,hash特别适合用于存储对象
@Test
public void test02(){
//1.创建操作 Hash类型数据的对象
HashOperations<String ,String,String> hashOperations = stringRedisTemplate.opsForHash();
//2.添加数据,第一个为redis的key,第二个为当前数据的field,点三个为值
hashOperations.put("userInfo","name","gggg");
//3.添加多条 map类型
Map<String ,String> map=new HashMap<>();
map.put("age","18");
map.put("sex","1");
//userInfo为redis的key,map中的键为field,值为value
hashOperations.putAll("userInfo", map);
//4.根据key,与field查询redis中的Hash类型数据
String name=hashOperations.get("userInfo","name");
//5.查询多条,创建查询的field集合
List<String> keyS=new ArrayList<>();
keyS.add("age");
keyS.add("sex");
//查询多条返回List集合
List<String> hashValL=hashOperations.multiGet("userInfo",keyS);
//查询多条返回Map集合
Map<String,String>hashValue= hashOperations.entries("userInfo");
//6.删除
hashOperations.delete("userInfo","name");
}
- 操作 List 列表类型,按照插入顺序排序,可以添加一个元素到列表的头部(左边)或者尾部(右边),通过list进行指定下标的查询,可以做到简单分页,
@Test
public void test03(){
//1.创建操作List类型数据的对象
ListOperations<String ,String> listOperations= stringRedisTemplate.opsForList();
//2.左上添加,第一个添加的会被后一个添加的挤压到下面
listOperations.leftPush("student","liuliuliu");
//3.右下追加,第一个添加的会被后一个添加的挤到上面
listOperations.rightPush("students", "Zhao Liu");
//4.查询,根据key与下标获取指定位置的数据,左闭右闭,两边包含,返回一个List
List<String> students = listOperations.range("student", 0,2);
//5.根据key与指定下标获取单个数据
String stu = listOperations.index("student", 1);
//6.获取当前List类型数据的长度
Long total = listOperations.size("student");
//7.删除List类型数据,key为student的,value值为"Li",在List中第二次出现的,
listOperations.remove("student", 2, "Li");
// 删除多条,有左删除一条,右删除一条等
stringRedisTemplate.delete("student");
//8.从左边开始获取并删除
String srt1 = listOperations.leftPop("student");
//9.右边开始获取并删除
String str2= listOperations.rightPop("student");
}
- 操作 Set 无序集合类型,不允许重复的成员,是通过哈希表实现的,所有添加,删除,查找的复杂度都是O(1),优点:天然去重,可以取交集,差集等
@Test
public void testSet() {
//1.创建操作Set类型数据的对象
SetOperations<String, String> setOperations = stringRedisTemplate.opsForSet();
//模拟数据
String[] letters = new String[]{"aaa", "bbb", "ccc", "ddd", "eee"};
//1.批量添加
setOperations.add("letters", letters);
//2.批量获取
Set<String> let = setOperations.members("letters");
//3.删除redis中key为letters的set类型数据中的aaa,bbb数据*
setOperations.remove("letters", "aaa", "bbb");
}
- 操作 SortedSet 有序集合,不允许重复的成员,每个元素都会关联一个double类型的分数,通过分数来为集合中的成员进行从小到大的排序,zset的成员是唯一的,但分数(score)却可以重复
@Test
public void testSortedSet() {
//1.创建操作sorted set数据的对象
ZSetOperations<String, String> zSetOperations = stringRedisTemplate.opsForZSet();
//2.向redis中添加 sorted set 类型数据,需要将数据封装到 ZSetOperations 中
//传递一个dubbo值指定该数据在sortedSet集合中的排序位置,
ZSetOperations.TypedTuple<String> objectTypedTuple1 = new DefaultTypedTuple<String>("zhangsan", 99D);
ZSetOperations.TypedTuple<String> objectTypedTuple2 = new DefaultTypedTuple<String>("lisi", 96D);
ZSetOperations.TypedTuple<String> objectTypedTuple3 = new DefaultTypedTuple<String>("wangwu", 92D);
ZSetOperations.TypedTuple<String> objectTypedTuple4 = new DefaultTypedTuple<String>("zhaoliu", 100D);
ZSetOperations.TypedTuple<String> objectTypedTuple5 = new DefaultTypedTuple<String>("tianqi", 95D);
Set<ZSetOperations.TypedTuple<String>> tuples = new HashSet<ZSetOperations.TypedTuple<String>>();
tuples.add(objectTypedTuple1);
tuples.add(objectTypedTuple2);
tuples.add(objectTypedTuple3);
tuples.add(objectTypedTuple4);
tuples.add(objectTypedTuple5);
//添加数据
zSetOperations.add("score", tuples);
//获取多条数据,左闭右闭,返回Set集合
Set<String> scores = zSetOperations.range("score", 0, 4);
//获取长度
Long total = zSetOperations.size("score");
//删除redis的key为score,value值为 zhangsan和lisi的两个数据
zSetOperations.remove("score", "zhangsan", "lisi");
}