Redis
1、概述
1.1、为什么使用NoSQL
用户的个人信息,社交网络,地理位置等日志数据呈爆发式增长!
NoSQL就可以很好地处理这个事情
1.2、什么是NoSQL
NoSQL=not only sql
1.3、特点
解耦
- 方便扩展(数据之间没有关系,很好扩展)
- 大数据量高性能(Redis一秒写8万次,读取11万次,NoSQL的缓存记录级是细粒度的缓存,性能会比较高)
- 数据类型多样型(不需要事先设计数据库,随取随用)
- 传统RDBMS和NoSQL的区别
传统RDBMS
- 结构化组织
- SQL语句
- 数据和关系都存在单独的表中 row col
- 操作语言,数据定义语言
- 严格的一致性
- 基础的事务
- .......
NoSQL
- 不仅仅是数据
- 没有固定的查询语句
- 键值对存储,列存储,文档存储,图形数据库
- 最终一致性
- CAP和BASE定理(异地多服) :初级架构师
- 高性能,高可用,高可扩
- .....
了解:3V+3高
-
大数据时代的3V:主要是描述问题
- 海量Velume
- 多样Variety
- 实时Velocity
-
大数据时代的3高
- 高并发
- 高可扩
- 高性能
真正在公司的实践:一定是NoSQL和RDBMS一起使用的
架构师:没有什么是加一层解决不了的!
1.4、分类
- MongoDB是非关系数据库里面最像关系型数据库的
MongoDB数据只能在内存中存在,不会持久化
Redis是单线程+多路IO复用
2、Redis入门
Redis(Remote Dictionary Server ),即远程字典服务。
2.1、安装Redis
推荐在linux下安装
windows下的redis
- redis-server.exe启动redis服务
- redis-cli.exe打开redis窗口,测试连接
- ping命令测试连接是否成功
- get和set命令操作数据
- shutdown关闭redis,quit退出客户端(关闭弹窗)
2.2、测试性能
redis-benchmark
- redis自带的测试性能的工具
- 可选参数
简单测试
# 测试:100个并发连接,100000个请求
redis-benchmark -h localhost -p 6379 -c 100 -n 100000
- 查看分析
2.3、基础知识
redis默认16个数据库
- 默认使用的第0个
- 使用select切换数据库
select 3 # 切换数据库
dbsize # 查看DB大小
keys * #得到当前数据所有的key
flushall # 清空所有数据库
flushdb # 清空当前数据库
为什么redis端口是6379
- 是redis作者喜欢的女明星的名字在手机键盘上打出来的按键(粉丝效率)
- mysql是作者女儿的名字
redis是单线程的
- Redis很快,官方表示Redis是基于内存操作,CPU不是Redis的性能瓶颈,瓶颈是根据机器的内存和网络带宽,既然可以使用单线程,为什么要使用多线程呢
- 为什么单线程这么快?
- 误区1:高性能的数据库一定是多线程的
- 误区2:多线程(CPU会上下文切换)一定比单线程效率高
- 核心:redis将所有数据放在内存中,所以使用单线程操作效率就是最高,多线程(切换上下文是耗时的操作)(对于内存系统,没有上下文切换就是效率最高的,是最佳方案)
3、五大数据类型
官方介绍
3.1、Redis-Key
中文地址:http://www.redis.cn/commands.html
set name name# 设置name的值
get name #得到name的值
exists name # key是否存在
move name 1 # 将key移动到数据库1
expire name 10 #设置过期时间(秒)
ttl name # 查看当前key的剩余时间,-1表示永不过期,-2表示已过期
type name #查看类型
del name # 删除key
unlink name # 异步删除key
查找命令官网http://www.redis.cn/commands.html
3.2、String(字符串)
# 操作字符串
append name 123 # 追加操作,如果key不存在就相当于set name
strlen name #字符串长度
# 操作数字
set age 0 #设置初始值为0(可以配置浏览量)
incr age # 数字加1
incrby age 10 # 数字加10,设置步长
decr age # 数字减1
decrby age 10 # 数字减10
# 截取字符串
getrange name 0 2 #查看0到2位字符串,两边闭合 3位 (字符串从0开始)
getrange name 0 -1 #查看全部字符串 和 get name 一样
# 替换
setrange name 1 xxx #从第1位开始替换成 xxx
setex key 30 "hello" #(set with expire)设置过期时间,秒数
stepx key 3000 hello # 过期时间,毫秒
setnx key "hello" # (set if not exists)不存在再创建(存在就不会覆盖,设置失败)(在分布式锁中常用)
setxx key "hello" # 当存在才插入,与sexnx互斥
mset k1 v1 k2 v2 k3 v3 #设置多个
mget k1 k2 k3 #得到多个
msetnx k1 v1 k4 v4 #失败,具有原子性的操作(要么一起成功,要么一起失败),适合分布式
# 对象
set user:1 {name:zhangsan,age:3}
mset user:1:name zhangsan user:1:age 1
#组合命令
getset db redis #先get再set,不存在返回null(更新)
String的使用场景
- 计数器
- 统计多单位的数量
- 粉丝数
- 对象缓存存储
String是二进制安全(只要内容可以是二进制的,都可以放这里面去)的,可以包含任何数据,比如图片或者序列化对象;String的value最大可以是512M
原子性:不会被线程调度机制打断的操作;操作一旦开始,就运行到结束,不会有被打断的情况
单线程中在单条指令中完成的操作就是原子操作,因为中断只能发生在指令之间;
多线程不被其他线程打断的操作就是原子操作
String底层是动态字符串,是可以修改的字符串,内部结构类似java的ArrayList,扩容是成倍容量扩容的,最大512M
3.3、List
- 在redis中我们可以把list玩成:栈、队列、阻塞队列
- 所有的list的命令都是
l
开头 - 可以存在重复值
基本命令
# 插入值
lpush list one #存值 相当于从左边开始
lrange list 0 -1 #得到所有值
lrange list 0 1 #得到后存入的两个值(有点像栈)
rpush list two #从右开始插入值(相当于再栈底插入值)
# 移出值
lpop list # 从左开始(栈 最上面)
rpop list # 从右开始(最下面)
# 通过下标获取值
lindex list 0 #获取第一个值 (可以实现生产者消费者模式 没有值就等待,有值就获取)
llen list #长度
lrem list 1 one #移出1个指定值one ,精确匹配
ltrim list 1 2 #保留list中的第1到2个元素(从0开始)
rpoplpush list list1#移除list最下面的一个元素,添加至list1最上面去
linsert #在list某个字符前或后添加值
小结
- 实际上相当于一个链表,前后都可以插入值
- 如果key不存在,创建新的链表
- 如果key存在,新增内容
- 如果移出了所有值,空链表,代表也不存在
- 在两边插入或改动值,效率最高!中间元素,效率会低一点
可以实现
- 消息队列
- 栈
底层实现
快速链表quicklist, 当数量较少时,使用的压缩列表 ziplist,当数据量较多时,ziplist就会组成链表quicklist
3.4、Set
- set的值不可重复,无序
- 所有命令前加
s
命令
sadd set hello #添加值
smembers set #查看所有元素
sismember set hello #判断是否有某个值
scard set #长度
srem set hello #移出某个指定元素
sismember set hello # set里面是否存在这个元素
srandmember set #获取随机的一个元素 (可实现抽奖)
spop set #移出元素
smove set set1 hello # 将set的hello元素移动到set1中
#数学集合
sdiff set1 set2 #两个set的差集(set1中有的,set2中没有的)
sinter set1 set2 #交集(都有的) 实现共同好友(社交类软件)
sunion set1 set2 #并集
使用场景
- 微博一个用户的所有关注放在一个set中,所有粉丝也是;两个用户关注的交集就是共同关注,共同爱好;推荐好友(六度分割理论:通过六个人可以认识任何一个陌生人 实现)
- 把用户的关注放一个set、把关注这个用户的放在一个set,求交集就是互相关注
数据结构
dict字典,字典使用哈希表实现的
3.5、Hash(哈希)
- Map集合:key-map 值是一个map(key-value)
- 更适合存对象,和hashmap差不多,String->Object
- 本质和String没有太大的区别,是一个简单的key-value
命令
hset hash field1 lvbo #设置值
hget hash field1 # 获取值
hmset hash field1 a field2 b #设置多个值
hmget hash field1 field2 #得到多个值
hgetall hash #得到所有值
hdel hash field1 #删除指定字段
hlen hash #长度
hexists hash field1 #判断指定字段是否存在
hkeys hash #获取所有字段
hvals hash #获取所有值
hincrby
hdecrby #都行,和String使用一样 只能使用按步长增加
应用场景
- 存经常变动的数据,尤其是用户信息之类的,更适合于对象的存储
数据结构
数据量较少时使用ziplist,数据量较多时,使用hashtable
3.5、Zset(有序集合)
- 在set的基础上增加了一个值(排序),set k v ; zset k score v
命令
zadd myset 1 one #添加一个数据 ,中间的数字是用来排序的
zadd myset 2 two 3 three #添加两个数据
zrangebyscore salary -inf +inf #-inf:负无穷 +inf:正无穷 表示薪水从负无穷到正无穷升序排序
zrevrange salary 0 -1 #降序排序
zrangebyscore salary -inf 2500 #负无穷到2500的排序,大于2500的不排序
应用场景
- 班级成绩表
- 工资表
- 普通消息 1 ,重要消息 2,带权重进行判断
- 排行榜应用实现,取出top N
数据结构
- hash,hash的作用就是关联value和权重score,保障value的唯一性,可以通过score的范围获取元素
*跳表,跳表的目的在于给score排序,根据score的范围获取元素列表
链表:找51这个元素,比较6次
跳表:找51这个元素找了4次
4、三种特殊数据类型
4.1、geospatial地理位置
- 朋友的定位,附件的人,打车距离计算
- 可以推算地理位置的信息,两地距离和方圆几里的人
- 可以查询测试数据
- 只有6个命令
geoadd
#添加数据
#规则:两级不能添加,我们一般会下载城市数据,通过java程序一次性导入
# 有效的经度:-180-180 有效的纬度:-85.05112878-85.05~
geoadd china:city 116.40 39.90 beijing #添加(可添加多个) 经度 纬度
#获取指定城市的经度纬度数据
geopos china:city beijing # 可以获取多个
#返回两个位置的直线距离
geodist china:city beijing shanghai km #km为单位 默认为m
# 附近的人(首先获取所有的人,再去定位),通过半径来查询
# 以给定的经纬度和半径为中心
georadius china:city 110 30 1000 km #以经纬度为 110 30 半径为1000km查找周围的城市
count 1#设置查询出来的个数
#以一个元素为中心查找周围的元素
georadiusbymember china:city beijing 1000 km
#返回哈希值,将2维的经纬度转化成一维的字符串,如果两个字符串越相似,两个城市距离越近 11位hash值
geohash china:city beijing chengdu
geo实现原理
- 底层实现原理为
zset
- 可以使用zset的命令来操作geo(移出、排序等)
4.2、Hyperloglog计算基数
什么是基数
- 不重复的元素,可以接受误差
简介
- 是一种数据结构
- 用于基数统计的算法,优点:占用的内存固定,2^64个元素,只需要12KB的内存 有0.81%的错误率,可以忽略不计
- 比如说网页的用户访问量(一个用户访问多次网站,但还是算作一个人);传统方式,是使用set实现(不可重复):但是uuid这种id很长,计数是保存id会占内存,就会很垃圾,因为我们是为了计数,不是为了保存id
命令
pfadd key a b c d e f g #添加数据
pfcount key #计算基数
pfmerge key3 key key2#合并key和key2到key3中
4.3、Bitmaps位存储
- 通过位运算实现
- 是一种数据结构,专门进行位操作的字符串
- 一般写程序全用对象的话,效率是极其低下的,使用数据结构可以提示效率
命令
setbit sign 0 1
setbit sign 1 0
setbit sign 2 0
setbit sign 3 0
setbit sign 4 0
setbit sign 5 0
setbit sign 6 0 #记录周一到周日的打卡记录
getbit sign 3 #查看是否打卡
bitcount sign #统计为1的 (这周打卡记录)
bitop:可以统计两天都访问过网站的人
应用场景
- 统计疫情感染人数(所有人设置0 ,感染了就设置为1)
- 统计用户信息,活跃,不活跃! 登录,不登录!打卡,不打卡!等两个状态
5、事务
5.1、事务概述
事务:要么同时成功,要么同时失败。
- Redis单条命令是保证原子性的(要么同时成功,要么同时失败),但是事务不保证原子性
- Redis事务的本质:一组命令的集合!一个事务中所有命令都会被序列化,在事务执行过程中,会按照顺序执行!
- 一次性、顺序性、排他性(不允许被别人干扰)!
- Redis事务没有隔离级别的概念(没有关系型数据库的脏读、幻读、不可重复读),所有命令放入事务(放入一个队列中)中,并没有被执行,发起执行命令的时候才会去执行
exec
正常执行事务
multi #开启事务
#命令入队,加入事务,不会执行,发起执行命令才开始执行
set k1 v1
set k2 v2
get k1
exec #执行事务
discard #取消事务,事务队列中的命令都不会被执行
-
编译型异常(代码有问题,命令错误),事务中的所有命令都不会被执行
-
运行时异常(1/0),存在语法性错误,执行命令的时候,其他命令可以正常执行,错误命令抛出异常,也就是redis的事务不支持原子性
5.2、监控(watch)
Redis实现乐观锁
- 正常执行
乐观锁适用于读多写少的情况,悲观锁适用于写多读少的情况 - 测试多线程修改值,使用watch执行失败,实现乐观锁
watch
就相当于加乐观锁,unwatch
相当于解锁
- 可以实现秒杀等场景
6、Jedis
学习不能急躁,慢慢来
使用Java操作Redis
什么是Jedis
- 是Redis官方推荐的java连接开发工具,是java操作Redis的中间件
测试
- 导入依赖
<dependencies>
<!--jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.5.1</version>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.70</version>
</dependency>
</dependencies>
- 测试连接
public class TestPing {
public static void main(String[] args) {
//直接使用Jedis对象连接
Jedis jedis = new Jedis("127.0.0.1",6379);
//jedis的所有方法就是之前学习的命令,所以指令学习很重要
System.out.println(jedis.ping());
jedis.close();//关闭连接
}
}
-
jedis的所有方法就是之前学习的命令,一个都没有变
-
事务
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","lvboaa");
//开启事务
Transaction multi = jedis.multi();
//可以使用jedis.watch()为对象添加乐观锁
String result = jsonObject.toJSONString();
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();//关闭连接
}
}
}
7、SpringBoot整合Redis
7.1、基础
- SpringBoot操作数据:使用SpringData连接
- SpringData也是和SpringBoot齐名的项目
- 说明:在SpringBoot2.x之后,原来使用的jedis被替换成了lettuce
- jedis:采用直连,多个线程操作,不安全,如果避免不安全,使用jedis pool连接池技术!类似于BIO(同步阻塞)模式
- lettuce:采用netty(高性能的网络框架,异步请求,非常快),实例可以在多个线程中共享,不存在线程不安全的情况!(duboo底层也是使用这个),减少线程数(不需要使用池化技术),类似于NIO模式
源码分析
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")//当不存在这个id的bean时生效,相当于我们可以自定义redisTemplate来替换这个配置
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
//默认的RedisTemplate没有过多的设置,redis对象都是需要序列化
//两个类型时<Object, Object>,我们使用后需要强制转换<String, Object>
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class) //由于String是redis最常使用的方法,所以单独提出来了一个方法
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
整合测试
- 导入依赖
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.4.2</version>
</dependency>
- 配置
#SpringBoot所有配置类都有一个自动配置类 RedisAutoConfiguration
#所有的自动配置类都会绑定一个properties配置文件RedisProperties
#配置redis(SpringBoot默认配置lettuce,jedis有些类不存在,注入会失败)
spring.redis.host=127.0.0.1
spring.redis.port=6379
- 测试
@SpringBootTest
class Redis02SpringbootApplicationTests {
@Autowired
RedisTemplate redisTemplate;
@Test
void contextLoads() {
//opsForValue 操作字符串String
//opsForList 操作集合list
//opsForHash 操作哈希 hash
//......
//除了基本的操作,常用的方法都可以直接通过redisTemplate操作,比如事务和基本的CRUD等
//获取redis的连接对象
// RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
// connection.flushAll();
// connection.flushDb();
// connection.close();
redisTemplate.opsForValue().set("key","狂神说");
System.out.println(redisTemplate.opsForValue().get("key"));
}
}
配置RedisTemplate
对于对象的保存需要序列化
自己定义的RedisTemplate
- 主要是配置编码格式,在客户端可能正常编码显示
@Configuration
public class RedisConfig {
//编写我们自己的RedisTemplate
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
//为了自己方便,一般使用<String, Object>
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
//json序列化配置
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(mapper);
//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;
}
}
测试
@Autowired
@Qualifier("redisTemplate")
RedisTemplate redisTemplate;//这里需要使用自己配置的redisTemplate,点击能进入自己配置的
@Test
public void test() throws JsonProcessingException {
User user = new User("kuang", 3);
//将对象转成JSON字符串
//String jsonUser = new ObjectMapper().writeValueAsString(user);
redisTemplate.opsForValue().set("user",user);
System.out.println(redisTemplate.opsForValue().get("user"));
}
真实的企业开发一般都会写一个RedisUtils.java
- 用更简洁的方式调用API,封装底层的代码
7.2、Redis工具类
@Component
public final class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// =============================common============================
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(Arrays.asList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
* @param key 键
* @param delta 要增加几(大于0)
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
* @param key 键
* @param delta 要减少几(小于0)
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* HashGet
* @param key 键 不能为null
* @param item 项 不能为null
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
* @param key 键
* @param map 对应多个键值
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
* @param key 键
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0)
expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
8、Redis.conf详解
8.1、单位
- 配置文件对大小写不敏感
8.2、包含
- 相当于spring的import
8.3、网络
bind 127.0.0.1:限定只能本地连接
protected-mode yes:保护,只能本地连接
8.4、通用
daemonize yes:表示可以后台运行
守护进程就是后台运行
8.5、快照
- 持久化,在规定的时间内,执行了多少次操作,则会持久化到文件 .rdb .aof
- redis是内存数据库,如果没有持久化,数据就会断电即失
8.6、REPLICATION主从复制
8.7、SECURITY安全
- 可以设置密码,redis默认没有密码
8.8、限制CLIENTS,配置客户端
8.9、APPEND ONLY模式 AOF模式
- 默认不开启
8、Redis持久化
面试和工作都是重点
Redis是内存数据库(断电即失),如果不将数据保存在磁盘中,一旦服务器退出,服务器中的数据库状态也会消失,所有就有了持久化功能。
8.1、RDB(Redis DataBase)
在主从复制中,rdb用来备份,将数据从主机备份到从机上,不占用主机的内存,aof几乎不使用
- 在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot 快照,它恢复时是将快照文件直接读到内存里。
- Redis 会单独创建( fork )一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的。这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式要比 AOF方式更加的高效。 ROB 的缺点是最后一次持久化后的数据可能丢失。我们**默认的就是 RDB **,一般情况下不需要修改这个配置。
- rdb保存的文件是dump.rdb,在redis.conf文件中配置的
触发机制(生成dump.rdb文件)
- 在满足save的规则,会自动触发
- 执行flushall命令,也会
- 退出redis,也会产生rdb文件
持久化数据库
如何恢复rdb文件
- 只需要将rdb文件放到redis启动目录,redis启动的时候会自动检查dump.rdb,恢复其中的数据
- 生产环境一般都要把这个文件备份
几乎只需要自己的默认配置就行了,但我们还是需要去学习!
优点
- 适合大规模的数据恢复(恢复速度更快,相比于AOF)
- 对数据的完整性要求不高(比如60秒触发一次,如果59秒时宕机,那59秒的数据就不见了 )
缺点
- 需要一定的时间间隔操作,如果redis意外宕机,那么最后一次修改的数据就没有了
- fork进程的时候,会占用一定的内存空间
8.2、AOF(Append Only File)
将所有的命令都记录下来,history文件,恢复的时候就把这个文件的命令全部执行一次
- 以日志的形式来记录每个写操作(改写数据库),将 Redis 执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件, redis 启动之初会读取该文件重新构建数据(所有大数据时很慢),换言之, redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
- 也是开启一个子进程进行日志记录
- aof保存的文件是appendonly.aof文件
配置文件
- 如果人为地去破坏(修改)appendonly.aof文件,会导致redis数据库启动失败(aof文件有错误)
- 需要修复aof文件(
redis-check-aof.exe
):redis-check-aof --fix appendonly.aof
- rdb文件不能修复,是一个日志文件
优点
- 每一次修改都会同步,文件的完整性会更好
- 默认每秒同步一次,可能会丢失一秒的数据(宕机)
- 如果配置是no的话,从不同步,效率最高
缺点
- 相对于数据文件来说,aof文件远远大于rdb文件,修复的速度也比rdb慢
- aof是io操作,运行效率比rdb慢,所以redis的默认配置是rdb持久化
总结
- 只需要redis用作缓存的时候,可以不使用任何持久化
- 同时开启两种持久化方式的时候,redis重启的时候优先载入aof恢复原始文件,因为aof文件更加完整
- rdb文件只用作备份用户,在从机(slave)上持久化rdb文件,只需要15分钟备份一次(规则:
save 900 1
),一般不使用aof模式
9、Redis发布订阅
Redis发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送信息,订阅者(sub)接收信息。场景:微信公众号、微博、关注系统
订阅/发布消息图:
第一个:消息发送这 第二个:频道 第三个:消息订阅者
命令
测试
-
订阅端(自动监听)
-
发布端
简单原理
底层是pubsub.c实现的,把所有的频道(发布者)放到一个字典里面,将订阅了这个频道的接收者放到一个链表中,发送消息时通过遍历这个链表发送。
使用场景
- 实时消息系统
- 实时聊天系统(频道当做消息室,将消息回显给所有人)
- 订阅,关注系统
稍微复杂的场景都会使用消息中间件MQ、kafka等实现
Redis实现秒杀
一般步骤
- 用户和商品信息非空判断
- 拼接key:库存key,秒杀成功用户key(库存存一个string,可以incre和decre;秒杀用户的id存在set里面,防止重复)
- 1.获取库存,库存为null,表示秒杀还未开始
- 2.判断用户是否重复秒杀(set里面是否有重复id)
- 3.判断库存是否大于0,小于等于0表示秒杀结束
- 4.秒杀过程:库存-1,添加秒杀用户id到秒杀清单里面
问题
1.如果直接线性执行对应java代码,会导致超卖问题,库存变成负数(并发操作,在库存减至0之前就已经获取数据判断库存数量了,然后执行,导致库存变为负数)
解决办法:使用watch命令,乐观锁;获取值的时候,带上对应版本号,当库存更新至0时,版本号会更新,导致其他的线程执行失败。问题:会导致库存遗留和连接超时问题
连接超时使用连接池解决
库存遗留问题需要使用LUA脚本解决,将秒杀业务使用LUA脚本就可解决库存遗留问题。
LUA脚本:和redis的事务类似,有一定的原子性,执行多个命令的不会被其他命令插队;也能将多个脚本写成一个LUA脚本,减少连接redis的次数,提升性能。本身没有库,是一个嵌入式脚本语言,可以写游戏外挂。
secKillScript:lua脚本
local userid=KEYS[1];
local prodid=KEYS[2];
local qtkey="sk:"..prodid..":qt";
local userskey="sk:"..prodid..":user";
local userExists=redis.call("sismember",userskey,userid);
if tonumber(userExists)==1 then
return 2;
end
local num=redis.call("get",qtkey);
if tonumber(num)<=0 then
return 0;
else
redis.call("decr",qtkey);
redis.call("sadd",userskey,userid);
end
return 1
java代码
public boolean doSecKill(String userid, String prodid) {
JedisPool jedisPool = JedisPoolUtil.getJedisPool();
Jedis jedis = jedisPool.getResource();
String sha1 = jedis.scriptLoad(secKillScript);
Object result = jedis.evalsha(sha1, 2, userid, prodid);
String reString = String.valueOf(result);
if ("0".equals(reString)) {
System.out.println("已抢空!");
return false;
} else if ("1".equals(reString)) {
System.out.println("抢购成功");
return true;
} else if ("2".equals(reString)) {
System.out.println("该用户已抢过!");
return false;
} else {
System.out.println("抢购异常");
return false;
}
}
10、Redis主从复制
10.1、概念
主从复制,是指将一台 Redis 服务器的数据,复制到其他的 Redis服务器。前者称为主节点( master / leader ) ,后者称为从节点(slave / follower ) ;数据的复制是单向的,只能由主节点到从节点。 Master 以写为主,slave 以读为主。
默认情况下,每台 Redis 服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。
10.2、主从复制的作用
- 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
- 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
- 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写 Redis 数据时应用连接主节点,读 Redis 数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高 Redis 服务器的并发量。
- 高可用(一般用于集群)基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是 Redis 高可用的基础。
主从复制,读写分离!80%的情况下都是读取操作!减缓服务器的压力!架构中经常使用!一主二从(最低配置)
实际项目
- 一般来说,要将 Redis 运用于工程项目中,只使用一台 Redis 是万万不能的(会宕机)
原因如下:
1 、从结构上,单个 Redis 服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,
2 、从容量上,单个 RediS 服务器内存容量有限,就算一台 RediS 服务器内存容量为 256G ,一般来说,单台 Redis 最大使用内存不应该超过 20G 。
电商网站上的商品,一般都是一次上传,无数次浏览的,说专业点也就是”多读少写”。
对于这种场景,使用下面这种架构:
10.3、环境配置
只配置从库,不配置主库
复制三个配置文件,然后修改对应的信息
- 端口
- pid名字
- log文件名字
- dump.rdb文件名字
修改完毕,启动三个redis服务器,可通过进程信息查看
一主二从配置
默认情况下,每台 Redis 服务器都是主节点;一般情况,主要配置从机
认老大,主机(79),从机(80,81):slaveof host 6379
SLAVEOF NO ONE
:取消主机配置,变成自己当master
可以使用info replication
在主机和从机查看配置信息
真实的主从配置应该在配置文件中配置,我们这是通过命令配置的,是暂时的
在配置文件的replication下面配置
配置成功的话
- 主机能写读,从机只能读(写的话会报错),主机的所有数据和信息,都会自动被保存在从机
- 主机崩了,从节点还在
- 主机断开连接(宕机):从机依旧连接到主机的,只是没有写操作;这个时候,主机回来了,从机依旧可以获取到主机写的信息
- 如果使用命令进行主从配置,从机这个时候重启了,当前从机就会变回主机;再次配成主机的从机,数据立马就会写入从机
10.4、复制原理
- Slave启动成功连接到master 后会发送一个 sync 同步命令 ,把持久化到本地的数据发送到从机,即从机刚连上就会有主机的全部数据,进行全量复制,复制的时候是在主进程进行的,就会降低主进程的效率
- Master 接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后, master 将传送整个数据文件到slave ,并完成一次完全同步(全量复制),之后master每一次写操作都会把对应数据发送到从机,进行增量复制
全量复制:slave 服务在接收到数据库文件数据后,将其存盘并加载到内存中(从机复制主机的所有数据)
增量复制:Master 继续将新的所有收集到的修改命令依次传给slave ,完成同步
- 但是只要是重新连接 master ,一次完全同步(全量复制)将被自动执行!我们的数据一定可以在从机中看到!
另一种主从复制模型
- 在工作中,这两种模式一般都不会使用
薪火相传
有这样一种模式,master只连一个slave,然后slave也连一个slave,复制的时候master只需要复制一次就行了,然后从机再把数据复制给从机,就稍微降低主机效率的消耗;问题:如果中间连接的从机宕机了,就会导致没有从机了,主机没有连接其他从机。
谋朝窜位
如果主机断开了连接,使用slaveof no one
让自己变成主机!其他的结点可以手动连接到这个主节点(手动)!如果这个时候原来的主机回来了,就需要重新原来的主机
10.5、哨兵模式(重点)
自动选举老大,谋朝窜位
概述
- 当主机宕机时,手动的将从机配置成主机,费时费力,这时候就需要哨兵模式(架构)了
- 就是谋朝窜位的自动版,后台监控主机是否故障,如果故障了就根据哨兵投票数(从机的票数)自动将从机换成主库
哨兵
哨兵是一种特殊的模式,Redis提供了哨兵的命令,哨兵是一个独立的进程(需要重新开启一个哨兵线程),会独立运行;
原理是:哨兵通过发送命令,等待Redis服务器响应,从而监控多个Redis实例
哨兵的作用:
- 通过发送命令,让Redis服务器返回其运行状态,包括主服务器和从服务器
- 当哨兵检测到master宕机,会自动将一个slave切换成master,然后通过发布订阅模式通知其他的服务器,修改配置文件,切换主机
多哨兵模式
- 只有一个哨兵的话,哨兵宕机就直接裂开,就没了
- 因此使用多个哨兵,哨兵之间也会相互监督
假设主服务器master宕机,哨兵1先检测到这个结果,系统并不会马上进行failover(故障转移)过程,这仅仅是哨兵1认为主服务器不可用,这个现象称为主观下线。当后面的哨兵也检测主服务器不可用,并且数量达到一定数量,哨兵之间就会进行一次投票,投票由任意一个哨兵发起(票数高的变为主机),之后进行failover操作(故障转移)。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程成为客观下线
哨兵选举的依据:
- 优先级高的,在redis.conf中配置 slave-priority 100 ,值越小优先级越高
- 偏移量大的,获得原主机数据最全的
- runid最小的,每个redis实例启动后都会随机生成一个40位的runid
测试
状态:一主二从
- 配置哨兵配置文件(sentinel.conf)
#最核心的配置,真的哨兵模式不止这些
#sentinel monitor 被监控的名称 host post 1
sentinel monitor myredis 127.0.0.1 6379 1
后面的1表示:几个哨兵认为主机宕机(master挂了)时,可开始投票选举新的master
- 启动哨兵
redis-sentinel sentinel.conf #启动命令,sentinel.conf需要自己配置
注意
- 如果master节点断开(网络原因或断电),一定时间后(默认30s,可以在sentinel.conf中配置),就会从从机中选择一个服务器当主机(redis有一个投票算法);故障转移的时间默认3分钟,也可配置,把挂掉的主机直接当成从机
- 如果此时主机回来了,只能归并到新的主机下的从机,这就是哨兵模式的规则
优点
- 哨兵集群基于主从复制模式,所有主从复制的优点,他都有
- 主从可以切换,故障可以转移,系统的可用性更好
- 哨兵模式就是主从模式的升级,从手动到自动,更加健壮
缺点: - redis不好在线扩容,集群一旦达到上限,在线扩容十分麻烦
- 实现哨兵模式的配置是很麻烦的,有很多的选择
11、Redis缓存穿透和雪崩
服务器的高可用问题:
11.1、缓存穿透(查不到)
概念
缓存穿透的概念很简单,就是用户查询一个数据库,发现redis数据库中没有数据,也就是缓存没有命中,再向持久层数据库中查询,发现也没有,于是本次查询失败。当用户很多时,都没有查询成功,给持久层数据库造成了很大的压力,这就相当于出现了缓存穿透。一般是网站被攻击了
解决方案
- 布隆过滤器
- 是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合就丢弃,从而避免了对底层存储系统的查询压力
- 缓存空对象
- 当存储层不命中后,即使返回的空对象也将其缓存起来,需要设置一个过期时间,之后再访问这个数据会从缓存中取出来,保护了后端数据源
3.设置可访问的白名单
在控制层进行校验,放到redis中、或者配置
4.实时监控
当redis命中率降低的时候,和运维人员配合排查访问的对象,设置黑名单限制服务
缓存空对象会存在两个问题
- 空值会缓存起来,这就意味着需要更多的空间储存更多的键,因为这当中可能会有很多的空值的键
- 即使空值设置了过期时间,还是会存在数据时间上的不一致,一致性的业务也会有影响
11.2、缓存击穿(查的人太多了,缓存过期)
概述
注意和缓存穿透的区别,就指一个key非常热门,在不停地扛着大量并发,大量并发对这个点进行访问,当这个key失效的时候,持续的大并发就穿破缓存,直接请求数据库,就像在一面墙上凿了个洞(比如说微博热搜导致服务器宕机)
解决方案
- 设置热点数据永不过期,或者加大热点数据的过期时间
- 不过期就只走缓存,不会出现缓存击穿
- 实时调整,现场监控热点key,把时间调大
- 加互斥锁(setnx)
- 保证每次只有一个线程去访问数据库,其他线程等待,这时压力就来到了分布式锁上面(就算进来了多个线程,setnx成功的也只会有一个);数据查出来只会缓存,然后删除nx的key
- 加锁会降低效率
11.3、缓存雪崩
概念
在某一个时间段,缓存集中失效,redis服务器宕机(也就是redis数据大批量消失)
其中数据集中过期,不致命;致命的是服务器宕机或断电;因为自然形成的缓存雪崩(数据过期),这个时候数据库是可以顶住压力的(会对数据库产生周期性的压力);但是宕机对数据库造成的压力不可预知,很有可能把数据库瞬间压垮
双十一:停掉一些服务(保证主要的服务可用,也是实现高可用)
解决方案
- 设置过期标志更新缓存,当缓存过期触发其他的线程更新缓存。
- 数据预热
- 在正式部署之前,先把可能的数据先访问一遍,把可能会大量访问的数据加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀
- 和缓存击穿的加锁一样,不适用于高并发操作
分布式锁
问题描述
单机部署发展成分布式集群系统后,多线程、多进程并且分布在不同的服务器上,单机的控制策略不行了,java api也不能提供分布式锁的能力;为了解决这个问题需要加个互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题。
分布式锁的实现方案:
- 基于数据库实现
- 基于缓存,redis(性能最好)等
- 基于zookeeper(可靠性最高)
redis一般使用setnx上锁,del进行解锁;但是如果锁忘了解锁,就会造成死锁,给setnx的key设置一个过期时间,但是上锁加过期时间需要是原子操作,使用命令set key val nx ex 100
加锁并设置过期时间,最好的100表示过期时间,再使用这条命令如果已经设置会返回null,没设置返回ok;后面加上 xx 表示已经存在才对key进行操作
程序里面设置成功 执行其他业务(业务执行完成需要删除锁的key),锁失败就等待一会重新获取锁(设置)
可能会产生的问题及解决方案
- 误删其他线程的锁
当同时进来了线程a、b、c,a获取到锁时,执行业务的时候突然卡住了,a设置的锁自动过期(设置了过期时间),然后b获取到了锁,执行b的业务,这时a又不卡了执行完之后删除当前key,导致其他线程获取到锁,就误删了其他线程的锁
解决方案:
上锁的时候设置锁的value为uuid,释放锁时删除key之前先判断当前线程value,一样才删除(需要使用lua脚本保证原子性,不然也会误删),就可以防止误删操作
为了确保分布式锁可用,需要满足四个条件
- 互斥性,在任意时刻,只有一个客户端会持有锁
- 不会发生死锁,即使客户端发送崩溃,也需要保证其他客户端可用
- 不能误删别人的锁
- 加锁和解锁需要保证原子性
Redis新功能
ACL
对权限进行更细粒度的控制
可以添加用户、删除、编辑和查看用户,已经赋权等操作
IO 多线程
IO多线程指客户端交互部分的网络IO交互处理模块多线程,而非执行命令多线程,Redis6依旧是单线程的
需要设置redis的配置文件:
io-thread-do-reads yes
io-threads 4 #多线程数量