1,概念
为了达到内存访问的速度,又能控制成本像磁盘一样低廉。
1)场景
- 性能。
执行时间久且结果不频繁变动的SQL,适合将运行结果放入缓存。 - 并发。
在大并发的情况下,所有的请求直接访问数据库,数据库会出现连接异常。这个时候,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问数据库。
2)常用shell命令
redis常用命令在redis安装目录的src目录以下,如果没有安装到usr/bin
路径下,那么使用的时候需要带上路径参数,如./redis-cli
常用shell命令
命令 | 说明 | 备注 |
---|---|---|
./redis-cli | 访问6379默认端口的redis | -h ip 登录ip;默认本机ip;-p port 登录端口;默认为6379;-a psw 密码; --raw 中文乱码处理 -c 直接输入cmd() |
exit | 退出 | |
clear | 清屏幕 | |
Flushall | 清空整个 Redis 服务器的数据 | 删除所有数据库的所有 key |
redis-cli -h | 查看帮助 | |
help | 查看帮助 | |
help set | 查看set命令帮助 | |
select 8 | 进入8号库 | 默认进入0号库。redis有0-15共16个库,互相隔离 |
EXPIRE key seconds | 设置key的过期时间,以秒计 | |
PEXPIRE key milliseconds | 设置key的过期时间,以毫秒计 | |
EXPIREAT key timestamp | 设置key的过期时间,时间参数是 UNIX 时间戳(unix timestamp) | |
PERSIST key | 移除 key 的过期时间,key 将持久保持 | |
PTTL key | 返回 key 的剩余的过期时间,以毫秒计 | |
TTL key | 返回 key 的剩余的过期时间,以秒计 | 0 :立即过期 -1 :最小为0,小于0会处理成0 |
DEL keyName | 删除key | |
EXISTS keyName | 判断key是否存在 | 存在返回 1 ,否则返回 0 |
GET keyName | 读取key | |
keys * | 查看所有key | |
keys apple* | 列出匹配的key | |
type k1 | 查看value基础数据类型 |
3)基础数据类型
类型多,且每种类型都有很多api支持,客户端代码会很轻盈。–>计算向数据移动(redis数据保持不动,client请求数据时,redis通过函数计算获取到少量数据返回给client,减少网络开销。)
1>String(字符串、byte)
- 支持:字符类型、数值类型、bitmap
string 类型是二进制安全的(传输时只有字节流、没有字符流)。意思是 redis 的 string 可以包含任何数据,并且直接以二进制形式存储。比如jpg图片或者序列化的对象、c语言的结束字符\0等。
==》redis客户端要统一编码,不然读和存数据不一致。
二进制安全的意义:数据安全。读取必须编码格式相同才可以读取,单纯的01字符不容易在传输过程中被篡改、破译,如果被攻击也能及时检测出来。 - string 类型的值最大能存储 512MB。
- redis是C编写的,没有使用C的String(以\0结尾,这样就不能存\0了),而是自己实现了SDS(简单动态字符串),通过len来确定字符串长度及结尾。
a)字符类型(embstr)
不区分大小写。数值、bitmap通用。
字符串命令 | 说明 | 备注 |
---|---|---|
set k v | ||
get k | nil表示不存在的值 | |
set key value nx | nx表示不存在就set,存在则返回nil | 常用语分布式锁的实现,只能新建 |
set key value xx | xx表示存在才能set,不存在返回nil | 只能更新 |
append k v | 如果key存在则给之前的value追加数据 | |
setrange k offset v | 如果key存在给之前的value从offset开始覆盖v | SET key1 "Hello World" SETRANGE key1 6 "Redis" “Hello Redis” |
getrange k 0 3 | 截取字符串[0:3] 正向索引:从左到右依次为0 1 2; 反向索引:从右到左依次为-1 -2 -3 | GETRANGE mykey 0 -1 表示取出所有 |
getset k v | set k v并返回旧的value | 减少1次io通信 |
strlen k | 获取存储的value的值长度 | 根据当前编码方式返回的数据不同。 比如“中”在utf-8中是3个自己,gbk中是2个字节。 |
mset k1 v1 k2 v2 | 同时设置k1=v1, k2=v2 | |
mget k1 k2 | 同时取出k1 k2的值 | |
msetNx k1 k2 | set k v nx 多个值 | 原子操作,如果一个k设置失败则都失败 |
object encoding k1 | 查看字符串类型的具体编码类型(embstr、int、raw) |
b)数值类型(int)
场景:一般做一些复杂的计数功能的缓存。
如:抢购、秒杀、点赞、评论数、好友数等数量的控制(精准但是不要求可靠性),规避并发下对数据库事务的操作,完全由redis内存操作替代。
int支持范围:0-225
数值命令 | 说明 | 备注 |
---|---|---|
incr k | value++ | |
incr k 5 | value+5 | 相当于 incrby k 5 |
decr k | value– | |
decr k 5 | value+5 |
d)bitmap(raw 二进制)
bitmap命令 | 说明 | 备注 |
---|---|---|
setbit k offset v | 给k对应的value设置偏移量offset位置的值为v,并返回之前的值; 偏移量通过二进制位计(ASCII)算,而不是字节; | setbit k 12 1 设置第12位为1 |
getbit k offset | 获取第offset位置的值 | 正反向索引按二进制位来算 |
bitop and/or destKey k1 k2 | 取出k1 k2的值并进行与、或操作然后塞入目标key中 | |
bitcount k1 startIndex endIndex | 统计bit=1总共出现的次数 | 范围是字节索引的:startIndex endIndex |
bitpos k1 bit startIndex endIndex | 查找k1的值的二进制中bit(0或者1)的数量,范围是字节索引的:startIndex endIndex,返回vaule的二进制索引偏移量 | 只找第一个 |
场景:
- 统计用户登录天数,且窗口随机。==》速度快、存储小
# 假设每一天记一个二进制位,366天只需要365/8=46个字节存储一个用户的登录。
setbit sean 1 1
...
setbit sean 7 1
...
setbit sean 33 1
...
strlen sean # 总共占用字节数 8位1字节,strlen算的是字节数
bitcount sean -2 -1 #最后2周1的个数即最后2周用户登录的次数 最后2个字节刚好14位
- 统计活用户。
区分用户:僵尸用户、忠诚用户、忠诚用户
# 比如618三天用户登录过的才算活跃用户,统计它的数量并 去重
setbit 20230601 1 1
setbit 20230602 1 1
setbit 20230602 7 1 # 7是用户id
#618活跃用户数量
bitop or destKey 20230601 20230602
bitcount destKey 0 -1 #活用户为2
2>hashes(散列、字典)
a)特性
- redis的哈希值是字符串字段和字符串之间的映射,是表示对象的完美数据类型。
- 哈希中的字段数量没有限制,所以可以在你的应用程序以不同的方式来使用哈希。
b)场景
这里value存放的是结构化的对象,比较方便的就是操作其中的某个字段。
举例:单点登录中用于存储用户信息,以cookieId作为key,设置30分钟为缓存过期时间,能很好的模拟出类似session的效果。
c)使用
d)存储结构
k-map
3>lists(列表)
双向链表。
a)特性
- 排序为插入的顺序,使用链表实现(增删快,查找慢)。
- 列表的最大长度为2^32-1。
b)场景
最新消息排行等功能(比如朋友圈的时间线) ;
简单的消息队列的功能:
- 可以用列表获取最新的内容(像帖子,微博等),用ltrim很容易就会获取最新的内容,并移除旧的内容。
- 用列表可以实现生产者消费者模式,生产者调用lpush添加项到列表中,消费者调用rpop从列表中提取,如果没有元素,则轮询去获取,或者使用brpop等待生产者添加项到列表中。
- 利用lrange命令,做基于redis的分页功能,性能极佳,用户体验好。
c)使用
4>sets(集合)
a)特性
- 无序的字符串集合,集合中的值是唯一的,无序的。
- Redis 中 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。集合中最大的成员数为 2的32次方 - 1 (4294967295, 每个集合可存储40多亿个成员)。
- 使用@EnableCaching开启声明式缓存支持,这样就可以使用基于注解的缓存技术。注解缓存是一个对缓存使用的抽象,通过在代码中添加下面的一些注解,达到缓存的效果。
b)场景
- 可以对集合执行很多操作,测试元素是否存在,对多个集合执行交集、并集和差集等等。
如:共同好友;
如:好友推荐:根据tag求交集,大于某个阈值就可以推荐。 - 我们通常可以用集合存储一些无关顺序的,表达对象间关系的数据,例如用户的角色,可以用sismember很容易就判断用户是否拥有某个角色。
- 在一些用到随机值的场合是非常适合的,可以用 srandmember/spop 获取/弹出一个随机元素。
- 利用唯一性,统计访问网站的所有独立ip
d)使用
- add 添加
redisTemplate.opsForSet().add("setValue","A","B","C","B","D","E","F");
- 删除
//弹出变量中的元素: pop(K key) 弹出并删除
Object popValue = redisTemplate.opsForSet().pop("setValue");
//批量移除变量中的元素: remove(K key, Object... values),返回移除变量中的元素个数
long removeCount = redisTemplate.opsForSet().remove("setValue","E","F","G");
- 查
//members(K key) 取值
Set set = redisTemplate.opsForSet().members("setValue");
//获取变量中值的长度
long setLength = redisTemplate.opsForSet().size("setValue");
//随机获取变量中的元素
Object randomMember = redisTemplate.opsForSet().randomMember("setValue");
//随机获取变量中指定个数的元素
List randomMembers = redisTemplate.opsForSet().randomMembers("setValue",2);
//检查给定的元素是否在变量中
boolean isMember = redisTemplate.opsForSet().isMember("setValue","A");
//匹配获取键值对:
//ScanOptions.NONE为获取全部键值对;
//ScanOptions.scanOptions().match("C").build()匹配获取键位map1的键值对,不能模糊匹配。
Cursor<Object> cursor1 = redisTemplate.opsForSet().scan("setValue", ScanOptions.NONE);
Cursor<Object> cursor = redisTemplate.opsForSet().scan("setValue", ScanOptions.scanOptions().match("C").build());
while (cursor.hasNext()){
Object object = cursor.next();
System.out.println("通过scan(K key, ScanOptions options)方法获取匹配的值:" + object);
}
//通过集合求差值:difference(K key, Collection<K> otherKeys)
//通过给定的key求2个set变量的差值:difference(K key, K otherKey)
//将求出来的差值元素保存:differenceAndStore(K key, K otherKey, K destKey)
//将求出来的差值元素保存:differenceAndStore(K key, Collection<K> otherKeys, K destKey)
List list = new ArrayList();
list.add("destSetValue");
Set differenceSet = redisTemplate.opsForSet().difference("setValue",list);
System.out.println("通过difference(K key, Collection<K> otherKeys)方法获取变量中与给定集合中变量不一样的值:" + differenceSet);
//获取去重的随机元素。distinctRandomMembers(K key, long count)
set = redisTemplate.opsForSet().distinctRandomMembers("setValue",2);
System.out.println("通过distinctRandomMembers(K key, long count)方法获取去重的随机元素:" + set);
//获取2个变量中的交集。intersect(K key, K otherKey);intersect(K key, Collection<K> otherKeys)
set = redisTemplate.opsForSet().intersect("setValue","destSetValue");
System.out.println("通过intersect(K key, K otherKey)方法获取交集元素:" + set);
//获取2个变量交集后保存到最后一个参数上。intersectAndStore(K key, K otherKey, K destKey)
//intersectAndStore(K key, Collection<K> otherKeys, K destKey)
redisTemplate.opsForSet().intersectAndStore("setValue","destSetValue","intersectValue");
set = redisTemplate.opsForSet().members("intersectValue");
System.out.println("通过intersectAndStore(K key, K otherKey, K destKey)方法将求出来的交集元素保存:" + set);
//获取2个变量的合集。 union(K key, K otherKey);union(K key, Collection<K> otherKeys);
//获取2个变量合集后保存到最后一个参数上。unionAndStore(K key, K otherKey, K destKey);unionAndStore(K key, Collection<K> otherKeys, K destKey)
set = redisTemplate.opsForSet().union("setValue","destSetValue");
System.out.println("通过union(K key, K otherKey)方法获取2个变量的合集元素:" + set);
- 修改
//转移变量setValue的元素值A到目的变量destSetValue
boolean isMove = redisTemplate.opsForSet().move("setValue","A","destSetValue");
if(isMove){
set = redisTemplate.opsForSet().members("setValue");
System.out.print("通过move(K key, V value, K destKey)方法转移变量的元素值到目的变量后的剩余元素:" + set);
set = redisTemplate.opsForSet().members("destSetValue");
System.out.println(",目的变量中的元素值:" + set);
}
5>zset(sorted sets 有序集合)
将Set中的元素增加一个权重参数score,元素按score有序排列。
a)特性
- 不重复的有序集合
- 支持范围查询
跳表。 - 查询元素score权重:O(1)
有序集合中的每个元素都关联了一个浮点值,称为分数。==》可以做topN操作
通过哈希表组织索引(score有序)。
b)场景
1、排行榜
2、带权重的消息队列
c)使用
d)原理:跳表
3)场景
- 消息队列
Redis支持发布订阅模式和Stream,可以作为轻量级消息队列使用,用于异步处理任务和处理高并发请求。 - 排行榜
利用有序集合和列表结构,设计实时排行榜。 - 分布式锁
- 地理位置应用
Redis支持GEO,支持地理位置定位和查询。 - 分布式限流
Redis提供了令牌桶和漏桶算法的实现。 - 分布式session管理
保证多台服务器之间的会话同步; - 布隆过滤器
Redis提供了Bloom Filter数据结构的实现,可以高效的检测一个元素是否存在于集合中。
4)通信协议:RESP(REdis Serialization Protocol)
- 简单高效、易于解析。
- 基于TCP的协议。
以命令名称作为第一个参数;
请求和响应都以行结束符\r\n
作为分隔符。
包含:参数个数,参数长度,参数数据。
5)CAP
redis是AP。
redis的一致性模型是最终一致性,某个时间点的数据不是最新的。另外,在分布式设计中采用的是异步复制,导致节点之间数据同步延迟和不一致的可能性。
CAP理论:
一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)、分区容错性(Partitiontolerance)这三项中的两项。
6)发布/订阅(publish/notify)
1>redis-cli
##订阅者:创建了订阅频道
SUBSCRIBE runoobChat
##发送者:在同频道发布消息;此时订阅者会收到消息
PUBLISH runoobChat "Redis PUBLISH test"
2>java实现
/**
* redis配置
* @author liudongtian
* @date 2024-07-01
*/
@Configuration("myRedisConfig")
public class RedisConfig {
/**
* 池配置
* @param redisProperties 连接配置
* @return
*/
@Bean("myJedisPoolConfig")
public JedisPoolConfig redisPoolFactory(RedisProperties redisProperties) {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(redisProperties.getJedis().getPool().getMaxIdle());
jedisPoolConfig.setMaxWait(redisProperties.getJedis().getPool().getMaxWait());
return jedisPoolConfig;
}
/**
* 连接工厂
* @param redisProperties 连接配置
* @param redisPoolFactory 池配置
* @return
*/
@Bean("myJedisConnectionFactory")
JedisConnectionFactory jedisConnectionFactory(RedisProperties redisProperties, @Qualifier("myJedisPoolConfig") JedisPoolConfig redisPoolFactory) {
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setHostName(redisProperties.getHost());
redisStandaloneConfiguration.setPort(redisProperties.getPort());
redisStandaloneConfiguration.setPassword(RedisPassword.of(redisProperties.getPassword()));
JedisClientConfiguration.JedisClientConfigurationBuilder jedisClientConfiguration = JedisClientConfiguration.builder();
jedisClientConfiguration.connectTimeout(redisProperties.getTimeout());
jedisClientConfiguration.usePooling().poolConfig(redisPoolFactory);
return new JedisConnectionFactory(redisStandaloneConfiguration,
jedisClientConfiguration.build());
}
/**
* 配置RedisTemplate
*
* @param redisConnectionFactory 连接工厂
* @return RedisTemplate
*/
@Bean("myRedisTemplate")
public RedisTemplate<String, Object> redisTemplate(@Qualifier("myJedisConnectionFactory") RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
//设置序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
RedisSerializer stringSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringSerializer);//key序列化
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);//value序列化
redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);//Hash key序列化
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);//Hash value序列化
//设置连接工厂
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
//订阅通知
/**
* Redis消息监听器容器
*
* @param redisConnectionFactory 连接工厂
* @param listener 消息监听器
* @return Redis消息监听容器
*/
@Bean
public RedisMessageListenerContainer container(@Qualifier("myJedisConnectionFactory") RedisConnectionFactory redisConnectionFactory,
@Qualifier("myRedisMessageListener") RedisMessageListener listener) {
// 订阅主题
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
// 设置连接工厂
container.setConnectionFactory(redisConnectionFactory);
// 订阅频道
container.addMessageListener(listener, new PatternTopic(MY_CHANNEL));
//发送通知
redisTemplate.convertAndSend(channel, message);
//数据监听与接收
@Slf4j
@Component("myRedisMessageListener")
public class RedisMessageListener implements MessageListener {
@Resource(name = "myRedisTemplate")
private RedisTemplate<String, Object> redisTemplate;
@Override
public void onMessage(Message message, byte[] pattern) {
String channel = new String(message.getChannel());
log.info("myRedisMessageListener subscribe channel is {}", channel);
// 获取消息
byte[] messageBody = message.getBody();
// 序列化对象
MessageDto messageDTO = (MessageDto)redisTemplate.getValueSerializer().deserialize(messageBody);
if(Objects.isNull(messageDTO)){
return;
}
//
if(MY_CHANNEL.equals(channel)){
}
}
}
3>与kafka对比
kafka优于redis。
对比 | redis | kafka |
---|---|---|
工作原理 | 生产者发送消息,redis将消息推送给所有订阅者。 订阅者必须始终启动并保持活跃连接 | 生产者发送消息后,kafka将数据归类存储在partion中,由使用者自己去拉取数据。 |
消息大小 | 数据存储在ram中,只适合发送少量数据 | 分布式存储在磁盘中,最大可处理1G消息。太大的数据也不支持 |
消息传输 | 只发一次,可能重复消费 | 通过偏移量检测和避免重复消费 |
消息保留 | 发送消息后就丢掉数据;如果订阅者未连接redis,后续连接时无法收到之前的消息。 | 消息被读后依然保存(可配置保存策略),消费者可从订阅分区再次请求数据。 |
吞吐量 | 多个使用者连接时,redis要等待每个使用者确认,吞吐量降低。 | 针对流式处理,异步读/写,吞吐量高。当消费堆积时,缓存中的未读消息被移除,从磁盘读取消息时吞吐量下降。 |
容错性 | 无备份,开启RDB才能避免丢数据 | 高可用 |
并行性 | 不支持并行性。 | Kafka 支持并行性。多个使用者可以同时检索同一条消息。 |
延迟 | 分发较小的消息时延迟极低。 | 低延迟。由于默认会进行数据复制,因此比 Redis 稍慢一些。 |
2,原理
1)事务
-
原子性(Atomic)
不提供回滚,只保证原子性。
1)为了保证高性能,牺牲回滚。
2)原子性的保证:命令作为一个Lua脚本执行,检查每一个事务命令是否正确,不正确就不执行。脚本作为一个独立的事务,由其它命令过来会暂存,等当前脚本执行完毕再执行。执行时如果报错,不会回滚,将会影响后续命令。 -
一致性(Consistency)
如果执行一半也可以恢复。 -
隔离性(Isolation)
单线程的,串行执行。 -
持续性(永久性,Durability)
RDB、AOF
事务实现:
1>事务开始(MULTI命令)
MULTI命令将客户端状态的flags属性中打开REDIS_MULTI标识。
REDIS_MULTI标识之后表示一个事务开始。
2>命令入队
根据发送来的指令执行不同的操作。
- 如果是MULTI、EXEC、WATCH、DISCARD命令,立即执行;
- 其他命令,入事务队列。
检查命令是否正确,不正确直接关闭对应客户端的REDIS_MULTI标识,返回错误信息给客户端。
放入队列,返回QUEUED给客户端。
如果执行过程中出现逻辑错误,继续执行,全部执行完。==》为了性能不做逻辑检查
WATCH命令
乐观锁,提供check and set(CAS)行为。
监控一个或多个key,一旦有1个变更,之后的事务就不再执行。直到EXEC命令停止监控。
UNWATCH命令:放弃key的监控。
DISCARD命令
情况事务队列,放弃事务。
3>事务执行(EXEC命令)
客户端发送EXEC命令,服务器执行EXEC逻辑。
- 如果客户端不包含REDIS_MULTI,或者包含REDIS_DIRTY_CAS、REDIS_DIRTY_EXEC命令,直接取消事务执行。
- 否则遍历客户端事务队列,FIFO执行所有命令。最后返回结果。
4>缓存的并发竞争key的问题
通过客户端去set一个key,由于redis cluster使用分片,多个key都不一定在同一个结点上,因此事务十分鸡肋。
处理并发竞争key的方案:
- 如果对这个key操作,不要求顺序
加锁,谁抢到谁干活。 - 如果对这个key操作,要求顺序
1)时间戳机制
客户端A在set完数据之后保存时间戳(valueA 3:00),客户端2如果必须在A的后面执行,发现A的时间戳比现在晚,那么不执行。
2)其他方法,比如利用队列,将set方法变成串行访问也可以。
2)持久化机制
1>RDB(redis Database,数据快照)
redis快的主要原因是内存存储,持久化机制:数据快照。
a>原理
在指定的时间间隔内将内存中的数据集快照写入磁盘。通过fork一个子进程,将当前内容写入dump.rdb文件。==》先将数据集写入临时文件,写入成功之后再替换之前的文件,用二进制压缩文件。
b>优点
- 性能最大化,fork子进程完成写操作,让主进程继续处理命令 ==》IO最大化。
- 整个redis只有一个dump.rdb文件,方便持久化、备份。
- 数据量大时,比AOF启动效率更高。
c>缺点
- 数据安全性低
RDB间隔一段时间才做持久化工作,容易丢失数据。 - 如果数据量很大,可能导致fork子进程长时间占用cpu,导致整个服务器停止服务几百毫秒甚至几秒。
2>AOF(Append Only File,推荐!)
如果两个都配了,优先使用AOF。
a>原理
以日志形式处理服务器每一次的写、删除操作,查询不会记录;==》类似mysql的binlog日志。
b>优点
- 数据安全;
每次修改的数据都会记录到磁盘中间。提供了三种同步策略:每秒同步、每修改同步、不同步,同步都是异步完成,性能也很高。 - append写文件,即使中途宕机也不会影响已存在的内容。
- AOF的rewrite模式,定期重写文件,对文件进行压缩。
c>缺点
- 相比RDB,文件存储更大,恢复更慢。
- 数据集大时,启动慢。
- 运行效率比RDB低。
3> 文件存储形式
- redis通过key来分文件夹。
如key:abc:mdr:save
在redis中会自动划分文件夹abc/mdr,并将save作为key存储在abc/mdr文件夹下。
删除的时候删除根key可以做到批量删除。
3)过期策略以及内存淘汰机制
redis是key-value数据库,可以设置key的过期时间,缺省时表示永不过期。
redis采用的是定期删除+惰性删除策略。
1>惰性删除
每次获取key时,redis检查key的过期了就删除。
节省cpu资源,但会占用大量内存。
内存淘汰机制:长期占用大量内存需要采用内存淘汰机制。
redis.conf中有一行配置:# maxmemory-policy volatile-lru
- noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。(不推荐)
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(推荐使用)
- allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。(不推荐)
- volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。这种情况一般是把redis既当缓存,又做持久化存储的时候才用。(不推荐)
- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。(不推荐)
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。(不推荐)
2>定时过期
定时器到了一定时间就删除key。
cpu占用多,节省内存。
3>定期删除(折中方案)
redis默认每隔100ms检查过期key,有则删除。==》不是全盘扫描,随机抽取进行检查。
通过调整扫描间隔时间和每次扫描耗时,达到cpu和内存资源最优的平衡效果。
4)IO模型
对于Linux来说 “
everything is a file
”,包括外部设备都看作文件夹来操作。
fd(File descriptor,文件描述符)
非负整数,本质是一个索引值。
open系统调用文件时,内核向进程返回一个文件描述符;
read、write文件时作为参数传入fd即可确定具体的文件;
fd范围:0-OPEN_MAX-1
特殊fd:
fd=0(STDIN_FILENO,标准输入);
fd=1(STDOUT_FILENO,标准输出);
fd=2(STDERR_FILENO,标准错误)。
1>BIO(Blocking IO,阻塞IO)
早期的Socket是blocking的,fd到了read命令才能返回,cpu要等待处理完成才能处理下一个。==》cpu利用率低
2>NIO(nonblock IO,同步非阻塞IO)
socket开始支持nonblock,此时可以单线程请求内核了(减少线程切换),处理完一个fd再处理下一个。==>问题:大批量的请求需要轮询很多次请求内核,内核态用户态频繁切换成本很大
3>多路复用的NIO
内核增加了select系统调用命令,一个线程批量处理多个tcp连接,每接收到一个fd(writefds(写)、readfds(读)、和 exceptfds(异常))就返回进程,然后查询获取到就绪的fd进行IO操作。==》问题:用户态内核态数据(fd)来回拷贝浪费资源
Linux中常见的IO多路复用技术包括:
IO多路复用技术 | 版本吧 | 优点 | 缺点 |
---|---|---|---|
select | 最原始 | 只能监听1024个fd; 返回的fd通过轮询筛选出就绪fd | |
poll | fd无限制; | 监听的fd越多复杂度越高; 返回的fd通过轮询筛选出就绪fd | |
epoll | linux2.4.5 | fd无限制; 优化了就绪fd查询复杂度O(1),更高效; |
epoll(Linux内核的可扩展I/O事件通知机制)
- 每次注册新事件调用epoll_ctl时,把所有fd拷贝进内核,而不是在epoll_wait时重复拷贝。==》保证每个fd只拷贝一次;
通过内核态和用户态之间的共享空间(红黑树、链表)来存之前需要来回拷贝的fd(和0拷贝不同)。 - 通过epoll_wait查询就绪fd,如果有直接使用o(1),不需要遍历。
- 支持的fd上限是最大可打开文件的数目(句柄一般默认4096);可以通过命令查看:
cat /proc/sys/fs/file-max
5>零拷贝(zero-copy,sendfile)
传统IO 发送一个文件需要4次IO:
一、read() 函数:2次
读取硬盘数据到内核缓冲区(read buffer);
从read buffer拷贝到用户进程的页内存;
二、write() 方法:2次
将数据从用户进程的页内存拷贝到网络缓冲区(socket buffer);
把socket buffer中的数据输出到网卡进行传输
零拷贝
直接调用sendfile方法,在内核中读取文件并发送,不需要用户态和内核态的切换。
==》kafka:用了零拷贝,直接从网卡读数据,并写入文件(mmap)。消费者读数据也通过sendfile获取
5)线程模型(单线程、I/O多路复用)

redis 基于 reactor 模式开发的高性能网络事件处理器。
采用 IO多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器来处理这个事件。
如果被监听的 socket 准备好执行 accept、read、write、close 等操作的时候,与操作对应的文件事件就会产生,这时候文件事件处理器就会调用之前关联好的事件处理器来处理这个事件。
文件事件处理器的结构包含 4 个部分:
- 多个socket
- IO多路复用程序
- 文件事件分派器
- 事件处理器
1)命令请求处理器:写数据到 redis
2)命令回复处理器:客户端要从 redis 读数据
3)连接应答处理器:客户端要连接 redis
6)缓存雪崩、缓存穿透、缓存击穿
1>缓存雪崩问题
缓存同一时间大面积的失效(或者redis故障导致重启、第一次启动服务),后面的所有请求都怼到数据库上,数据库短期内承受大量请求而崩掉。
解决方案:
- 缓存过期时间设置为随机值,避免集体失效。
- 给每个缓存加上缓存标记,若缓存失效则更新缓存数据。==》耗性能
- 缓存预热
服务器启动前把数据一次性写入缓存。预防第一次启动服务导致缓存雪崩。 - 使用互斥锁
key失效了需要去数据库查数据时,添加互斥锁, 查完再释放。
让同一个key的请求排队,但是吞吐量明显下降了。 - 双缓存。我们有两个缓存,缓存A和缓存B。缓存A的失效时间为20分钟,缓存B不设失效时间。自己做缓存预热操作。然后细分以下几个小点
I 从缓存A读数据库,有则直接返回
II A没有数据,直接从B读数据,直接返回,并且异步启动一个更新线程。
III 更新线程同时更新缓存A和缓存B。
2>缓存穿透
缓存和数据库中都不存在数据,导致所有的请求都怼到数据库上,导致数据库崩了。一般是黑客攻击方式。
解决方案:
- 接口增加校验,从业务上直接拦截非法值。
- 数据库没有的数据存储为key-null,缓存有效期设置为30s。
- 采用布隆过滤器
将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被布隆过滤器过滤掉。
3>缓存击穿
缓存不存在数据但数据库中有(一般是缓存过期),由于并发用户多数据库同时请求同一条数据导致数据库请求压力激增。
解决方案:
- 互斥锁
缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试 - 设置热点数据永不过期
7)缓存和数据库双写一致性问题
一致性问题是分布式常见问题,还可以再分为最终一致性和强一致性。
如果对数据有强一致性要求,不能放缓存。我们所做的一切,只能保证最终一致性。另外,我们所做的方案其实从根本上来说,只能说降低不一致发生的概率,无法完全避免。因此,有强一致性要求的数据,不能放缓存。
策略:先处理数据库再同步redis。
- 增:写库成功后,将返回的对象存入redis。
- 删:数据库删除成功后,删除redis。
- 查:查redis,没查到,去数据库查询。
- 改:数据库修改成功后,再修改。
- 另外,对于前端接收到的数据,应该进行数据校验,确保数据的合法性。
上述策略再多请求并发执行的情况下,依然会造成脏数据问题。新的策略如下:
6. 查:查缓存,没查到,去数据库查询。
7. 更新(增删改):先更新数据库,成功后再删除缓存。–《Cache-Aside pattern》
这种策略基本上避免了脏数据问题,因为可能存在删除缓存失败的问题,提供一个补偿措施即可,例如利用消息队列重试删除。《缓存的正确使用方式,你都会了吗?》中提供的方法如下图所示:
8. 因为可能存在删除缓存失败的问题,提供一个补偿措施即可,例如利用消息队列。
延迟双删策略:
9. 读取缓存时,如果缓存不存在则读取数据库
10. 更新之前,先del缓存
11. 更新数据库
12. 删除缓存
8)虚拟内存机制
redis是内存存储的,通过虚拟内存机制将部分不常用的数据存储到磁盘,从而节省内存。可以通过参数控制这个阈值。
9)大key问题
1>概念
- value比较大;
- 占内存比较多;
- 元素数量比较多。
2>big key识别
redis-cli -bigkeys
没有绝对的衡量标准。
- String类型的Value,超过10M;
- Set、List类型的Vlue,成员超过1w;
- Hash类型的Value,成员超过1k,但是Values体积超过1000M。
3>影响
- 影响性能
读取慢。 - 占用内存
- 内存空间不均匀
集群分片存储,big key所在节点内存消耗大; - 搜索困难
- Redis备份、恢复、迁移困难
- 过期key删除耗时
4> 解决方案
- 删除;
- 设置缓存TTL;
- 拆key:
如根据日期拆分;
将大key分布在不同的服务器中。‘ - 迁移至单独的数据库中。
10)热key问题
1>概念
redis中同一个key被大量访问,就会导致流量过于集中,使得很多物理资源无法支撑,如网络带宽、物理存储空间、数据库链接等。
2>热key识别
- 通过JD框架的hotkey等工具识别热key,阈值需要结合业务和服务器性能不断的调优。
- redis 4.0.3提供了redis-cli的热key发现功能。
执行redis-cli -hotkeys
3>解决方案
通过事前预测和事中解决:
- 热点key拆分;
通过key的后缀将一个key拆成多个并分布到不同结点。 - 多级缓存;
通过缓存的方式尽量减少系统交互,使得用户请求可以提前返回。==》缓存在浏览器、距离用户最近的CDN中、Redis、服务器本地缓存等多级缓存。
提升用户体验的同时,减少系统压力。
CDN (内容分发网络) 指的是一组分布在各个地区的服务器。这些服务器存储着数据的副本,因此服务器可以根据哪些服务器与用户距离最近,来满足数据的请求。
- 热key备份;
- 限流
4,集群方案
0)安装及部署
1>安装
# wget http://download.redis.io/releases/redis-6.0.8.tar.gz
# tar -xzvf redis-6.0.8.tar.gz
# cd redis-6.0.8
# make 之后出现redis-server目录
2>启动
./redis-server ../redis.conf
3>文件说明
文件(夹) | 说明 |
---|---|
redis-server | redis 服务程序 |
redis-cli | 客户端程序 |
redis.conf | redis配置 |
redis-benchmark | |
redis-check-aof | |
redis-check-rdb |
redis.conf配置项说明(修改后需要重启):
配置项 | 说明 | 备注 |
---|---|---|
port 6379 | 监听端口 | |
daemonize no | 默认不是以守护进程的方式运行。使用 yes 启用守护进程 | |
pidfile /var/run/redis.pid | 当 Redis 以守护进程方式运行时,Redis 默认会把 pid 写入 /var/run/redis.pid 文件,可以通过 pidfile 指定 | |
loglevel notice | 指定日志记录级别,Redis 总共支持四个级别:debug、verbose、notice、warning,默认为 notice | |
logfile /opt/redis.log | 日志记录方式,默认为标准输出,如果配置 Redis 为守护进程方式运行,而这里又配置为日志记录方式为标准输出,则日志将会发送给 /dev/null | |
databases 16 | 设置数据库的数量,默认数据库为0,可以使用SELECT 命令在连接上指定数据库id | |
save <seconds> <changes> | 指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合 | 默认内容:save 900 1 save 300 10 save 60 10000 |
rdbcompression yes | 指定存储至本地数据库时是否压缩数据,默认为 yes,Redis 采用 LZF 压缩,如果为了节省 CPU 时间,可以关闭该选项,但会导致数据库文件变的巨大 | |
dbfilename dump.rdb | 指定本地数据库文件名 | |
dir /opt/data/redis/ | 本地数据库存放目录 | |
requirepass foobared | 设置 Redis 连接密码,默认关闭。 | |
masterauth | 当 master 服务设置了密码保护时,slave 服务连接 master 的密码 | |
slaveof | 设置当本机为 slave 服务时,设置 master 服务的 IP 地址及端口,在 Redis 启动时,它会自动从 master 进行数据同步 | |
maxclients 128 | 设置同一时间最大客户端连接数,默认无限制,Redis 可以同时打开的客户端连接数为 Redis 进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。达到最大限制会返回:max number of clients reached | |
maxmemory | 指定 Redis 最大内存限制。达到最大内存后,Redis 会先尝试清除已到期或即将到期的 Key;如果仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis 新的 vm 机制,会把 Key 存放内存,Value 会存放在 swap 区 | |
appendonly no | 指定是否在每次更新操作后进行日志记录,Redis 在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为 redis 本身同步数据文件是按上面 save 条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为 no | |
appendfsync everysec | 指定更新日志条件,共有 3 个可选值: no:表示等操作系统进行数据缓存同步到磁盘(快); always:表示每次更新操作后手动调用 fsync() 将数据写到磁盘(慢,安全); everysec:表示每秒同步一次(折中,默认值) |
1)主从模式
增量copy:
- 复制offset
主从都维护一个复制偏移量offset。 - 复制积压缓冲区
master维护,固定长度、FIFO队列。如果复制offset超过这个长度,只能进行全量数据。 - 服务器运行id(runid)
每个redis节点都有runid,每次请求:主节点runid保存在从结点。
断线重连后,如果runid与当前不一致,说明发生了master的重新选举,需要全量复制数据。
2)哨兵模式(sentinel)
基于主从模式进行升级。
1>特点
- 保证高可用,不保证零丢失。
即使部分哨兵挂掉了,也可以正常工作。 - 主节点负责读写,从结点仅可读不可写。
2>主要功能
- 集群监控
通过心跳监测的方式,监控master、slave是否在正常工作。 - 消息通知
如果某个redis实例故障了,通知给管理员。 - 故障转移
故障时进行主从切换。
故障转移时,需要大部分的哨兵都同意当前master宕机。==》分布式选举 - 配置中心
如果主从切换了,通知所有的client客户端新的master地址。
3>查看主节点
两种方式:
- 通过
info replicatio
n命令
# 进入src目录
cd /redis/redis-6.2.12/src
# 启动redis客户端,如果端口号使用默认的6379,可以不带后边的-p参数
./redis-cli -p 6379
# 简单密码认证: 输入redis密码
auth 密码
# 查看主从信息
info replication
显示内容-主结点
# Replication
role:master
connected_slaves:0
master_replid:4372c81a9cc00877xxxxxxx764579b660aa29
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
显示内容-从结点:
# Replication
role:slave
master_host:10.x.x.x
master_port:6379
master_link_status:up
master_last_io_seconds_ago:0
master_sync_in_progress:0
slave_repl_offset:242789600231
slave_priority:100
slave_read_only:0
connected_slaves:0
master_replid:799614901dd840b84950d097dcaf1498252ab78e
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:242789600231
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:242788551656
repl_backlog_histlen:1048576
- 进入主节点的哨兵目录,查看哨兵文件看配置
sentinel monitor mymaster
:
vi /opt/luo/etc/redis/redis_sentinel.conf
4>主从切换
停掉主节点,从节点自动切换为主节点;
主结点故障转移后,Sentinel仍然会进行故障监测,等其重上线后变为从结点工作。
主节点选举:
- 否决长时间没心跳的、长时间没通信;否决优先级为0的服务器;
- 选择优先级最高的服务器;
- 相同优先级,选择复制偏移量最大的服务器;
- 优先级相同、复制偏移量相同;选择运行ID最小的服务器。
5>Sentinel管理命令
Redis为 Sentinel 提供了相应的管理命令,用于对 Sentinel 执行各种各样的管理操作。
./redis-cli -h 【哨兵结点ip】 -p 26379 -c 【Sentinel管理命令】
Sentinel管理命令 | 说明 | 备注 |
---|---|---|
Sentinel masters | 获得主结点信息 | |
Sentinel slaves <master-name> | 获得主结点下的所有从结点信息 | slave-priority:从服务器优先级 ; slave-repl-offset:复制偏移量 |
Sentinel sentinels <master-name> | 获取其他所有Sentinel的信息 | voted-leader:当前票选出的leader,?表示无leader |
Sentinel get-master-addr-by-name <master-name> | 获取主服务器IP地址和端口号 | |
Sentinel reset <pattern> | 重置主服务器状态 | |
Sentinel failover <master-name> | 强制执行故障转移 | |
Sentinel ckquorum <master-name> | 检查可用的Sentinel数量 | |
Sentinel flushconfig | 强制写入配置文件到硬盘 |
6>java访问
import org.springframework.data.redis.connection.RedisNode;
import org.springframework.data.redis.connection.RedisPassword;
/**
* 获取哨兵配置
* @param master master_name eg:mymaster
* @param nodes ip:port 哨兵端口; eg:10.x.x.x:26379
* @return
*/
private RedisSentinelConfiguration createSentinelConfig(String master, List<String> nodes) {
RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration();
redisSentinelConfiguration.setMaster(master);
List<RedisNode> sentinelNode = (List)nodes.stream().map((hosts) -> {
String[] args = StringUtils.split(hosts, ":");
Assert.notNull(args, "HostAndPort need to be seperated by ':'.");
Assert.isTrue(args.length == 2, "Host and Port String needs to specified as host:port");
return new RedisNode(args[0], Integer.parseInt(args[1]));
}).collect(Collectors.toList());
redisSentinelConfiguration.setSentinels(sentinelNode);
redisSentinelConfiguration.setPassword(RedisPassword.of(EnvUtil.getRedisPassword()));
return redisSentinelConfiguration;
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory());
// 配置序列化方式等其他参数
return template;
}
7>部署
- (主从节点)上传redis-6.2.12.tar.gz到服务器指定地址
/redis
; - (主从节点)解压redis-6.2.12.tar.gz到当前目录;
- (主从节点)进入解压后的redis-6.2.12目录,执行
make
命令;
不执行make命令,src目录下没有redis-server、redis-cli等可执行命令。当出现如下所示内容时,表示make成功:
Hint: It's a good idea to run 'make test'
此时,进入src目录,发现已经出现了redis-server以及redis-cli命令。
4. (主节点配置)vim修改redis-6.2.12目录下的redis.conf
文件
# 注释bind属性,该属性只有在指定特定ip访问redis时才需要指定
# bind 127.0.0.1 ::1
# daemonize 属性值从no改为yes,表示后台启动redis
daemonize no
#设置redis密码
requirepass "123456"
# masterauth 主从节点之间通信时的密码,主从节点必须保持一致,否则在出现slave节点切换为master节点时,会出现无法验证密码从而主从不同步的情况
masterauth "123456"
# logfile 设置redis的日志文件名称
logfile "/opt/logs/redis/redis.log"
- (从节点的redis.conf配置),在主节点配置属性的基础上,再加上
replicaof
属性(也可以不加)
# 设置主节点信息。
replicaof 10.10.10.10 6379
- 启动redis节点
通过日志可以查看启动情况。
# 进入src目录
cd /redis/redis-6.2.12/src
# 指定配置文件启动redis
./redis-server ../redis.conf
- 验证主从关系
info replication
- 哨兵节点部署
哨兵节点是比较特殊的redis节点,只是用来做监控,并不实际存储业务数据。
哨兵节点数建议为奇数个,防止在主从切换时出现脑裂的情况;不能是单个结点(单点故障导致无必要的主从切换)。
与部署redis主从节点一致,只不过目录不同(/sentinel);
sentinel.conf
配置:
# daemonize 属性值从no改为yes,表示后台启动redis
daemonize yes
# logfile 设置redis的日志文件名称 也可以单独放在sentinel.log中
logfile "/opt/logs/redis/redis.log"
# 设置哨兵监控
# mymaster是集群名称 在springboot集成sentinel以及查询sentinel状态时需要用到该名字,可以任意修改,但是多个sentinel节点必须保持一致
# 后面跟主节点ip和端口
# 2表示:当有2个sentinel节点认定主节点失效时,启动选举主节点的进程,根据sentinel节点的个数确定该值,本文是3个sentinel节点,因此设置为2;
sentinel monitor mymaster 10.10.10.10 6379 2
# 设置密码 mymaster为集群名称
sentinel auth-pass mymaster redis密码
- 启动sentinel节点
使用同样的方式,启动其他两个sentinel节点。
# 进入src目录
cd /sentinel/redis.6.2.12/src
# 指定配置文件启动
./redis-sentinel ../sentinel.conf
- 验证sentinel节点
从三个sentinel节点中任意选择一个,执行以下命令
# 进入src目录
cd /sentinel/redis.6.2.12/src
# 指定端口号启动客户端(sentinel默认端口号26379,此处必须指定)
./redis-cli -p 26379
# 查看sentinel信息
sentinel master mymaster
返回值都很好理解,其中:
quorum:2 表示当有2个sentinel节点认定主节点失效时,启动选举master进程;
- SpringBoot集成哨兵集群
pom文件引入redis-starter:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
</dependency>
修改application.yml中redis配置:
redis:
database: 10
# host: 127.0.0.1
# port: 6379
password: redis密码
ssl: false
timeout: 3000
connect-timeout: 10000
sentinel:
master: mymaster
nodes: 10.10.10.10:6379,10.10.10.11:6379,10.10.10.12:6379
3)Redis Cluster(推荐,服务端分片)
3.0版本开始使用,将slot(槽,16384个)哈希分布在集群的每个结点,请求发送到任意结点之后,该结点发起转向指令,将查询请求发送到正确的结点。
所有节点互为主从,互相备份。
同一分片,多个节点之间数据不保证强一致性。
扩容:需要将旧结点数据迁移一部分到新结点。哈希函数:对16384取模,平均分配到每个结点。
使用:
每个redis开发两个端口号,比如一个是6379,另一个就是16379(加1w);其中6379用于客户端访问,16379用来进行节点间通信(cluster bus,用二进制协议gossip),用来进行故障检测、故障转移、故障切换、配置更新。
优点:
- 无中心,支持动态扩容。
- 具备sentinel监控和自动Failover(故障转移)能力
- 客户端连接一个可用结点即可
- 高性能 直接连接redis即可,不需要连接代理。
缺点:
- 运维复杂,扩容需要人工干预(槽的导出与导入)。
- 只能使用0号数据库。(redis中默认有11个数据库)
- 不支持批量操作。(pipeline操作,批量操作可能导致高耗时)
- 分布式逻辑和存储模块耦合。
4)redis sharding(客户端分片)
Redis Cluster出来之前用的比较多,由客户端进行分片,服务端的每个结点互相独立。
缺点:
- 扩容非常复杂,不支持动态增删结点。
- 结点之间不能共享,资源浪费。
5,分布式锁
1)概念
分布式锁在分布式系统、集群里实现多个进程互斥且可见,实现单一结点单线程执行。
2)setNX(redis)
redis单线程,天然可做分布式锁。
//expire 设置超时时间
//value设置线程标识 是同一线程才允许del
set(key,value,expire)
//锁续期:执行时间太长,利用redisson进行续期,也可以每次自己续期。
SETNX命令:
向Redis中添加一个key,
key不存在时才添加并返回1,存在则不添加返回0。
这个命令是原子性的。
通过删除命令释放锁。
常见问题处理:
-
死锁问题
一个线程长期持有锁导致其它线程都阻塞。==》设置超时时间 -
锁误删问题
锁设置线程标识,是当前线程才允许删除锁。==》必须保证判断和删除的逻辑是原子的,不然还会误删。
1)利用Lua脚本: 会将key存储在KEYS数组中,会将value存储在ARVG数组中。下标从1开始
if (redis.call('get',KEYS[1])==ARGV[1]) then
return redis.call('DEL',KEYS[1])
end
return 0
2)JAVA:通过execute()方法执行Lua本,DefaultRedisScript加载脚本
private static final DefaultRedisScript<Long> DEFAULT_REDIS_SCRIPT;
static {
DEFAULT_REDIS_SCRIPT=new DefaultRedisScript<Long>();
DEFAULT_REDIS_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
DEFAULT_REDIS_SCRIPT.setResultType(Long.class);
}
public void unLock() {
stringRedisTemplate.execute(DEFAULT_REDIS_SCRIPT, Collections.singletonList(RedisConstants.KEY_PREFIX + name),ID_PREFIX+ Thread.currentThread().getId());
}
- 重入问题
重入问题
获得锁的线程可以再次进入到相同的锁的代码块中,可重入锁的意义在于防止死锁,比如HashTable这样的代码中,他的方法都是使用synchronized修饰的,假如他在一个方法内,调用另一个方法,那么此时如果是不可重入的,不就死锁了吗?所以可重入锁他的主要意义是防止死锁,我们的synchronized和Lock锁都是可重入的。
- 不可重试
不可重试
是指目前的分布式只能尝试一次,合理的情况是:当线程在获得锁失败后,应该能再次尝试获得锁。
- 主从一致性
如果Redis提供了主从集群,当向集群写数据时,主机需要异步的将数据同步给从机,如果在同步过去之前,主机宕机了,就会出现死锁问题。
使用redLock避免此类问题:redLock获取锁时从每个节点都获取锁,如果半数锁都获得成功才能获得锁。(主从切换可能会导致主节点数据丢失)。redisson中有redLock的实现。
6,对比Memcached
- 常见的缓存服务器;
- 只支持简单的k-v存储;
- 不支持持久化;
- 数据分片:手动分片。
redis分片是哈希槽分片,支持数据的自动分片和负载均衡。 - 其它功能都少很多
7,Stream
redis5.0版本新加的数据结构,主要用于处理有序的、可追溯的消息流。
- 有序:每个消息由唯一id,按消息发布时间排序。
- 消费:Stream支持消息添加、读取、删除、订阅;支持消费组,可以让多个消费者并发地处理消息流。
支持竞争式消费、共享式消费两种模式。 - 持久化:消息可以持久化、主备复制、记录每个客户端访问位置(客户端+偏移量)。
5.0之前不支持。 - 场景:消息队列、日志收集、实时数据处理和聊天室应用等。
1)延迟消息
开启过期监听配置,监听key过期事件通知。==》key不一定失效就立刻删除,延迟可能超过预期。
可靠的方法:Redission。
RDelayedQueue:基于zset结构实现的延迟队列,允许指定延迟时长将元素放到目标队列中。