文章目录
一.概念
1.1 什么是redis
redis是一个开源的、使用C语言编写的、支持网络交互的、可基于内存也可持久化的Key-Value数据库。
1.2 redis优点
- 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s,可以用于高速缓存 。
- 丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
- 原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
- 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。
- 可持久化,内存中是断电即失、所以说持久化很重要(rdb、aof)
- 发布订阅系统,地图信息分析,计时器、计数器(浏览量!)等应用
二.redis的存储结构
2.1 结构
redis单机服务端有16个数据库,每个数据库都有一个字典结构,这个字典里存着两个hash表(为了之后的扩缩容),而这个hash表里有一个dictEntry 组成的数组,里面存放的就是所有的键值对。这个dictEntry还有指向下一个节点的指针,就是为了在hash冲突的情况,采用拉链法扩展出一个链表。
/*
* 字典
*/
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
// 目前正在运行的安全迭代器的数量
int iterators; /* number of iterators currently running */
} dict;
/*
* 哈希表
* 每个字典都使用两个哈希表,从而实现渐进式 rehash 。
*/
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
} dictht;
/*
* 哈希表节点
*/
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
2.2 渐进式rehash
当我们哈希表中存的数据越来越多,哈希冲突的概率就会越来越大。这样所有的键值对冲突后会形成一个链表,查询的效率就由原先的O(1)变成了O(n),所以我们要有一个评估的标准,用来判断是否需要扩缩容。
负载因子=used / size
- 为什么需要渐进式rehash
然而redis并不像我画的那样,只有一两个key。一个生产使用的redis可以达到几百上千万个。而redis的核心计算是单线程的,一次性重新散列这么多的key会造成长时间的服务不可用,因此需要采用渐进式的rehash。
三.Redis数据类型及应用场景
3.1 字符串(string)
String是redis中最基本的数据类型,一个key对应一个value。
String类型是二进制安全的,意思是 redis 的 string 可以包含任何数据。如数字,字符串,jpg图片或者序列化的对象。
- 命令
- 实战场景
缓存: 经典使用场景,把常用信息,字符串,图片或者视频等信息放到redis中,redis作为缓存层,mysql做持久化层,降低mysql的读写压力。
计数器:redis是单线程模型,一个命令执行完才会执行下一个,同时数据可以一步落地到其他的数据源。
session:常见方案spring session + redis实现session共享,
3.2 字符串列表(list)
Redis中的List其实就是链表(Redis用双端链表实现List)。
使用List结构,我们可以轻松地实现最新消息排队功能(比如新浪微博的TimeLine)。List的另一个应用就是消息队列,可以利用List的 PUSH 操作,将任务存放在List中,然后工作线程再用 POP 操作将任务取出进行执行。
-
命令
-
使用列表的技巧
lpush+lpop=Stack(栈)
lpush+rpop=Queue(队列)
lpush+ltrim=Capped Collection(有限集合)
lpush+brpop=Message Queue(消息队列) -
实战场景
微博TimeLine: 有人发布微博,用lpush加入时间轴,展示新的列表信息。
消息队列
3.3 字符串集合(set)
Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
-
命令
-
实战场景
标签(tag),给用户添加标签,或者用户给消息添加标签,这样有同一标签或者类似标签的可以给推荐关注的事或者关注的人。
点赞,或点踩,收藏等,可以放到set中实现
3.4 有序字符串集合(sorted set)
Redis 有序集合和集合一样也是 string 类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的, 但分数(score)却可以重复。有序集合是通过两种数据结构实现:
压缩列表(ziplist): ziplist是为了提高存储效率而设计的一种特殊编码的双向链表。它可以存储字符串或者整数,存储整数时是采用整数的二进制而不是字符串形式存储。它能在O(1)的时间复杂度下完成list两端的push和pop操作。但是因为每次操作都需要重新分配ziplist的内存,所以实际复杂度和ziplist的内存使用量相关
跳跃表(zSkiplist): 跳跃表的性能可以保证在查找,删除,添加等操作的时候在对数期望时间内完成,这个性能是可以和平衡树来相比较的,而且在实现方面比平衡树要优雅,这是采用跳跃表的主要原因。跳跃表的复杂度是O(log(n))。
- 实战场景
排行榜:有序集合经典使用场景。例如小说视频等网站需要对用户上传的小说视频做排行榜,榜单可以按照用户关注数,更新时间,字数等打分,做排行。
3.5 哈希(hash)
hash 是一个 string 类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象。
- 命令
- 实战场景
缓存: 能直观,相比string更节省空间,的维护缓存信息,如用户信息,视频信息等。
3.6 位图(bitmap)
都是操作二进制位来进行记录,就只有0 和 1 两个状态
-
命令
setbit(添加)、getset(获取)、bitcount(统计)操作 -
实战场景
签到,打卡等功能。
3.7 基数(Hyperloglog)
基数:比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。
在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。
HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
- 命令
3.8 地理位置(Geospatial)
①geoadd(添加)、geopos(查看)、geodist(计算距离)操作
- 实战场景
可以用来查询附近的人、计算两人之间的距离等
四.SpringBoot整合Redis
4.1 POM依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
4.2 yml配置
# SpringBoot所有的配置类,默认都会有一个XXXAutoConfiguration# 自动配置类会绑定一个配置文件 XXXProperties
# 配置redis
spring:
redis:
host: 127.0.0.1
password: 123456
port: 6379
4.3 添加redis序列化及配置
对于RedisTemplate,如果不指定默认的序列化方式,默认为JdkSerializationRedisSerializer
/**
* @author zhouhengzhe
* @Description: redis序列化配置工具
* @date 2021/7/24上午1:38
*/
@Configuration
public class RedisConfiguration {
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
//json序列化配置
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer=new Jackson2JsonRedisSerializer(Object.class);
//转义
ObjectMapper objectMapper=new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
//替换默认序列化
//String的序列化
StringRedisSerializer stringRedisSerializer=new StringRedisSerializer();
//key采用String的序列化方式
redisTemplate.setKeySerializer(stringRedisSerializer);
//hash的key也采用string的序列化方式
redisTemplate.setHashKeySerializer(stringRedisSerializer);
//value序列化方式采用jackson
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
//设置hash的value序列化方式采用jackson
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
4.4 实体类
@Data
public class RedisUser implements Serializable {
private Integer id;
private String name;
}
4.5 redis工具类
@Component
public class RedisUtil {
@Autowired
private static RedisTemplate<String,Object> redisTemplate;
//+++++++++++++++++common+++++++++++++++++++++
/**
* 添加缓存并设置失效时间
* @param key 键
* @param time 时间(秒)
*/
public static 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 键
* @return 时间(秒) 返回0代表永久有效
*/
public static long getExpire(String key){
return redisTemplate.getExpire(key);
}
/**
* 判断key是否存在
* @param key 键
* @return true存在 false不存在
*/
public static boolean hasKey(String key) {
try {
redisTemplate.hasKey(key);
return true;
} catch (Exception e){
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
* @param key 键,可以有多个
*/
public static void del(String... key){
if (key != null && key.length>0){
if (key.length==1){
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
//+++++++++++++++++string+++++++++++++++++++++
/**
* 获取缓存
* @param key 键
*/
public static Object get(String key){
return key==null? null : redisTemplate.opsForValue().get(key);
}
/**
* 存入缓存
* @param key 键 val 值
*/
public static boolean set(String key,Object obj){
try {
redisTemplate.opsForValue().set(key, obj);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 存入缓存
* @param key 键
* @param obj 值
* @param time 过期时间
*/
public static boolean set(String key,Object obj,long time){
try {
if (time>0){
redisTemplate.opsForValue().set(key, obj,time,TimeUnit.SECONDS);
} else {
set(key,obj);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
* @param key 键
* @param delta 递增值
*/
public static long incr(String key,long delta) {
if (delta<0){
throw new RuntimeException("递增值必须大于0");
}
return redisTemplate.opsForValue().increment(key,delta);
}
/**
* 递减
* @param key 键
* @param delta 递减值
*/
public static long decr(String key,long delta) {
if (delta<0){
throw new RuntimeException("递减值必须大于0");
}
return redisTemplate.opsForValue().decrement(key,delta);
}
//+++++++++++++++++HASH+++++++++++++++++++++
/**
* HashGet
* @param key 键
* @param item 项
*/
public static Object hget(String key,String item){
return redisTemplate.opsForHash().get(key,item);
}
/**
* 获取hashKey对应的所有键值
* @param key 键
*/
public static Map<Object , Object> hmget(String key){
return redisTemplate.opsForHash().entries(key);
}
/**
* hmset
* @param key 键
* @param map 对应多个键值
*/
public static boolean hmset(String key,Map<String,Object> map){
try {
redisTemplate.opsForHash().putAll(key,map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* hmset 设置时间
* @param key 键
* @param map 对应多个键值
* @param map 对应多个键值
*/
public static 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;
}
}
/**
* hmset 向hash表存放数据 不存在则创建
* @param key 键
* @param item 项
* @param value 值
*/
public static boolean hset(String key,String item,Object value){
try {
redisTemplate.opsForHash().put(key,item,value);
return true;
} catch (Exception e){
e.printStackTrace();
return false;
}
}
/**
* hmset 向hash表存放数据 不存在则创建 并可设置过期时间
* @param key 键
* @param item 项
* @param value 值
* @param time 值
*/
public static 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 键
* @param item 项 可以有多个,不能为null
*/
public static void hdel(String key,Object... item){
redisTemplate.opsForHash().delete(key,item);
}
/**
* 判断hash表中是否有该项的值
* @param key 键
* @param item 项 不能为null
*/
public static boolean hHasKey(String key,String item){
return redisTemplate.opsForHash().hasKey(key,item);
}
/**
* hash递增 不存在则创建
* @param key 键
* @param item 项 不能为null
* @param by 递增值
*/
public static double hincr(String key,String item,double by){
return redisTemplate.opsForHash().increment(key,item,by);
}
/**
* hash递减 不存在则创建
* @param key 键
* @param item 项 不能为null
* @param by 递减值
*/
public static double hdecr(String key,String item,double by){
return redisTemplate.opsForHash().increment(key,item,-by);
}
//+++++++++++++++++SET+++++++++++++++++++++
/**
* SET 根据key获取set的所有值
* @param key 键
*/
public static Set<Object> sGet(String key){
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e){
e.printStackTrace();
return null;
}
}
/**
* SET 判断set中是否存在值
* @param key 键
* @param obj 值
*/
public static boolean sHasKey(String key,Object obj){
try {
return redisTemplate.opsForSet().isMember(key,obj);
} catch (Exception e){
e.printStackTrace();
return false;
}
}
/**
* SET 将数据存入set
* @param key 键
* @param obj 值,可以有多个
*/
public static long sSet(String key,Object... obj){
try {
return redisTemplate.opsForSet().add(key,obj);
} catch (Exception e){
e.printStackTrace();
return 0;
}
}
/**
* SET 将数据存入set,并设置过期时间
* @param key 键
* @param obj 值,可以有多个
* @param time 过期时间
*/
public static long sSet(String key,long time,Object... obj){
try {
if (time>0){
expire(key,time);
}
return redisTemplate.opsForSet().add(key,obj);
} catch (Exception e){
e.printStackTrace();
return 0;
}
}
/**
* SET 获取set长度
* @param key 键
*/
public static long sSetSize(String key){
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e){
e.printStackTrace();
return 0;
}
}
/**
* SET 移除set中的value值
* @param key 键
* @param values 值,可以有多个
*/
public static long sSetRemove(String key,Object... values){
try {
return redisTemplate.opsForSet().remove(key,values);
} catch (Exception e){
e.printStackTrace();
return 0;
}
}
//+++++++++++++++++LIST+++++++++++++++++++++
/**
* list 获取list中的value值
* @param key 键
* @param start 值,开始
* @param end 值,结束 0到-1代表所有值
*/
public static 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 通过索引获取list的值
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
*/
public static Object lGetIndex(String key , long index){
try {
return redisTemplate.opsForList().index(key,index);
} catch (Exception e){
e.printStackTrace();
return null;
}
}
/**
* list 将list放入缓存
* @param key 键
* @param value 值
*/
public static boolean lSet(String key,Object value){
try {
redisTemplate.opsForList().rightPush(key,value);
return true;
} catch (Exception e){
e.printStackTrace();
return false;
}
}
/**
* list 将list放入缓存
* @param key 键
* @param value 值
* @param time 过期时间
*/
public static 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 将list放入缓存
* @param key 键
* @param value 值
*/
public static boolean lSet(String key,List<Object> value){
try {
redisTemplate.opsForList().rightPushAll(key,value);
return true;
} catch (Exception e){
e.printStackTrace();
return false;
}
}
/**
* list 将list放入缓存
* @param key 键
* @param value 值
* @param time 过期时间
*/
public static 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 根据索引修改list中的某条数据
* @param key 键
* @param index 索引
* @param value 值
*/
public static boolean lUpdateIndex(String key,long index ,Object value){
try {
redisTemplate.opsForList().set(key,index,value);
return true;
} catch (Exception e){
e.printStackTrace();
return false;
}
}
/**
* list 移除N个值
* @param key 键
* @param count 移除个数
* @param value 值
*/
public static 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;
}
}
}
五.应用场景实战
5.1 redis实现消息队列
生产者
@Slf4j
public class RedisQueueProducer implements Runnable{
//队列key
public static final String QUEUE_NAME="queue-name";
private RedisTemplate<String,Object> redisTemplate;
public RedisQueueProducer(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public void run() {
Random random = new Random();
while (true){
try {
Thread.sleep(random.nextInt(600)+600);
//1.模拟生成一个任务
UUID queueProducerId = UUID.randomUUID();
//2.放入消息队列
redisTemplate.opsForList().leftPush(QUEUE_NAME,queueProducerId);
log.error("放入消息队列>>{}",queueProducerId);
} catch (Exception e){
e.printStackTrace();
}
}
}
}
消费者
@Slf4j
public class RedisQueueConsumer implements Runnable{
//队列key
public static final String QUEUE_NAME="queue-name";
public static final String DEALWITH_QUEUE="dealwith-queue_name";
private RedisTemplate<String,Object> redisTemplate;
public RedisQueueConsumer(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public void run() {
Random random = new Random();
//从任务队列"task-queue"中获取一个任务,并将该任务放入暂存队列"tmp-queue"
Object taskId = redisTemplate.opsForList().rightPopAndLeftPush(QUEUE_NAME, DEALWITH_QUEUE);
while (true){
try {
//模拟处理业务
Thread.sleep(1000);
} catch (Exception e){
e.printStackTrace();
}
//模拟处理成功和失败现象
if (random.nextInt(13) % 7 == 0){
//处理失败重新弹回原队列
redisTemplate.opsForList().rightPopAndLeftPush(DEALWITH_QUEUE,QUEUE_NAME);
log.error("数据处理失败:{}",taskId);
} else {
redisTemplate.opsForList().rightPop(DEALWITH_QUEUE);
log.error("数据处理成功:{}",taskId);
}
}
}
}
测试
@PostMapping("/queue")
public void testQueue() throws InterruptedException {
// 启动一个生产者线程,模拟任务的产生
new Thread(new RedisQueueProducer(redisTemplate)).start();
Thread.sleep(15000);
//启动一个线程者线程,模拟任务的处理
new Thread(new RedisQueueConsumer(redisTemplate)).start();
//主线程休眠
Thread.sleep(Long.MAX_VALUE);
}
5.2 Redis实现发布订阅功能
Redis发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收信息。
监听绑定
@Configuration
public class RedisMessageListener {
/**
* 创建连接工厂
* @param connectionFactory
* @param listenerAdapter
* @return
*/
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter, MessageListenerAdapter listenerAdapterTest2){
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
//接受消息的key
container.addMessageListener(listenerAdapter,new PatternTopic("phoneTest1"));
container.addMessageListener(listenerAdapterTest2,new PatternTopic("phoneTest2"));
return container;
}
/**
* 绑定消息监听者和接收监听的方法
* @param receiver
* @return
*/
@Bean
public MessageListenerAdapter listenerAdapter(ReceiverRedisMessage receiver){
return new MessageListenerAdapter(receiver,"receiveMessage");
}
/**
* 绑定消息监听者和接收监听的方法
* @param receiver
* @return
*/
@Bean
public MessageListenerAdapter listenerAdapterTest2(ReceiverRedisMessage receiver){
return new MessageListenerAdapter(receiver,"receiveMessage2");
}
/**
* 注册订阅者
* @param latch
* @return
*/
@Bean
ReceiverRedisMessage receiver(CountDownLatch latch) {
return new ReceiverRedisMessage(latch);
}
/**
* 计数器,用来控制线程
* @return
*/
@Bean
public CountDownLatch latch(){
return new CountDownLatch(1);//指定了计数的次数 1
}
}
消费
public class ReceiverRedisMessage {
private static final Logger log = LoggerFactory.getLogger(ReceiverRedisMessage.class);
private CountDownLatch latch;
@Autowired
public ReceiverRedisMessage(CountDownLatch latch) {
this.latch = latch;
}
/**
* 队列消息接收方法
*
* @param jsonMsg
*/
public void receiveMessage(String jsonMsg) {
log.info("[开始消费REDIS消息队列phoneTest1数据...]");
try {
System.out.println(jsonMsg);
log.info("[消费REDIS消息队列phoneTest1数据成功.]");
} catch (Exception e) {
log.error("[消费REDIS消息队列phoneTest1数据失败,失败信息:{}]", e.getMessage());
}
latch.countDown();
}
/**
* 队列消息接收方法
*
* @param jsonMsg
*/
public void receiveMessage2(String jsonMsg) {
log.info("[开始消费REDIS消息队列phoneTest2数据...]");
try {
System.out.println(jsonMsg);
/**
* 此处执行自己代码逻辑 例如 插入 删除操作数据库等
*/
log.info("[消费REDIS消息队列phoneTest2数据成功.]");
} catch (Exception e) {
log.error("[消费REDIS消息队列phoneTest2数据失败,失败信息:{}]", e.getMessage());
}
latch.countDown();
}
}
测试接口
@GetMapping(value = "/pub")
public String pubMsg(){
redisTemplate.convertAndSend("phoneTest1","你好呀 phoneTest1");
redisTemplate.convertAndSend("phoneTest2","你好呀 phoneTest2");
log.info("开始发送数据 Publisher sendes Topic... ");
return "success";
}
5.3 通过btimap实现签到功能
Redis提供了一种特殊的数据类型BitMap(位图),每个bit位对应0和1两个状态。虽然内部还是采用String类型存储,但Redis提供了一些指令用于直接操作BitMap,可以把它看作一个bit数组,数组的下标就是偏移量。它的优点是内存开销小,效率高且操作简单,很适合用于签到这类场景。缺点在于位计算和位表示数值的局限。如果要用位来做业务数据记录,就不要在意value的值。
因为 Bit 的值为 0 或 1,用户是否打卡也可以用 0 或 1 来表示,我们把签到的天数对应到每个字节上,打卡了就是 1,没打卡就是 0;setbit 的作用说的直白点就是:在你想要的位置操作字节值,比如说用户 1 在 6 月 7 号 签到了,那么 setbit (20220607, 1 ,1) 就可以实现签到功能了,这里的 offset 就是 1
bitmap命令
签到工具类
public class UserSignDemo {
private static Jedis jedis = null;
static {
jedis = new Jedis("192.168.111.5",6379);
jedis.auth("root");
}
private static String formatDate(LocalDate date) {
return formatDate(date, "yyyyMM");
}
private static String formatDate(LocalDate date, String pattern) {
return date.format(DateTimeFormatter.ofPattern(pattern));
}
private static String buildSignKey(int uid, LocalDate date) {
return String.format("userId:sign:%d:%s", uid, formatDate(date));
}
/** 用户签到
* @param userId 用户ID
* @param date 日期
* @return 签到状态
*/
public boolean doSign(int userId, LocalDate date){
int offset = date.getDayOfMonth() - 1;
return jedis.setbit(buildSignKey(userId,date),offset,true);
}
/** 检查用户某一天是否签到
* @param userId 用户ID
* @param date 日期
* @return 是否签到
*/
public boolean checkSign(int userId,LocalDate date){
int offset = date.getDayOfMonth()-1;
return jedis.getbit(buildSignKey(userId,date),offset);
}
/** 获取用户本月签到次数
* @param userId 用户ID
* @param date 日期
* @return 本月签到次数
*/
public long getSignCount(int userId,LocalDate date){
return jedis.bitcount(buildSignKey(userId,date));
}
/** 获取本月连续签到次数
* @param userId 用户ID
* @param date 日期
* @return 本月签到次数
*/
public long getContinuousSignCount(int userId,LocalDate date){
int signCount = 0;
String type = String.format("u%d", date.getDayOfMonth());
List<Long> list = jedis.bitfield(buildSignKey(userId, date), "GET", type, "0");
if (list != null && list.size()>0){
// 取低位连续不为0的个数即为连续签到次数,需考虑当天尚未签到的情况
long v = list.get(0) == null ? 0 : list.get(0);
for (int i = 0; i < date.getDayOfMonth(); i++) {
// i表示位移操作的次数,右移再左移如果等于自己说明最低位是0,表示为签到
if (v>> 1 << 1 == v){
// 低位为0且非当天说明连续签到中断了
if (i>0) break;
} else {
signCount += 1;
}
v >>= 1;
}
}
return signCount;
}
/** 获取本月连续签到次数
* @param userId 用户ID
* @param date 日期
* @return 首次签到日期
*/
public LocalDate getFirstSign(int userId,LocalDate date){
Long bitpos = jedis.bitpos(buildSignKey(userId, date), true);
return bitpos < 0 ? null : date.withDayOfMonth((int) (bitpos+1));
}
/** 获取本月签到情况
* @param userId 用户ID
* @param date 日期
* @return 本月签到情况
*/
public Map<String,Boolean> getSignInfo(int userId,LocalDate date){
Map<String,Boolean> signMap = new HashMap<>(date.getDayOfMonth());
String type = String.format("u%d", date.lengthOfMonth());
List<Long> list = jedis.bitfield(buildSignKey(userId, date), "GET", type, "0");
if (list!=null && list.size()>0){
//
long v = list.get(0) == null ? 0 : list.get(0);
for (int i = date.lengthOfMonth(); i > 0; i--) {
LocalDate d = date.withDayOfMonth(i);
signMap.put(formatDate(d,"yyyy-MM-dd"),v>> 1 << 1 != v);
v >>= 1;
}
}
return signMap;
}
}
测试类
public static void main(String[] args) {
System.out.println(jedis.ping());
UserSignDemo demo = new UserSignDemo();
LocalDate today = LocalDate.now();
int userId=2007;
System.out.println("用户:"+userId);
{ // doSign
boolean signed = demo.doSign(userId, today);
if (signed) {
System.out.println("您已签到:" + formatDate(today, "yyyy-MM-dd"));
} else {
System.out.println("签到完成:" + formatDate(today, "yyyy-MM-dd"));
}
}
{ // checkSign
boolean signed = demo.checkSign(userId, today);
if (signed) {
System.out.println("您已签到:" + formatDate(today, "yyyy-MM-dd"));
} else {
System.out.println("尚未签到:" + formatDate(today, "yyyy-MM-dd"));
}
}
{ // getSignCount
long count = demo.getSignCount(userId, today);
System.out.println("本月签到次数:" + count);
}
{ // getContinuousSignCount
long count = demo.getContinuousSignCount(userId, today);
System.out.println("连续签到次数:" + count);
}
{ // getFirstSignDate
LocalDate date = demo.getFirstSign(userId, today);
System.out.println("本月首次签到:" + formatDate(date, "yyyy-MM-dd"));
}
{ // getSignInfo
System.out.println("当月签到情况:");
Map<String, Boolean> signInfo = new TreeMap<>(demo.getSignInfo(userId, today));
for (Map.Entry<String, Boolean> entry : signInfo.entrySet()) {
System.out.println(entry.getKey() + ": " + (entry.getValue() ? "√" : "-"));
}
}
}
5.4 redis分布式锁的使用
什么是分布式锁
当在分布式模型下,数据只有一份(或有限制),此时需要利用锁的技术控制某一时刻修改数据的进程数。
用一个状态值表示锁,对锁的占用和释放通过状态值来标识。
redis实现
@GetMapping(value = "/lock")
public void lock() throws InterruptedException {
String clientId = UUID.randomUUID().toString();
String lockKey = "REDIS_TEST_KEY"+"id";
try {
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
if (!aBoolean){
log.error("加锁失败");
return;
}
//执行业务
System.out.println("开始执行业务逻辑");
Thread.sleep(5000);
} finally {
//释放锁
if (clientId.equals(redisTemplate.opsForValue().get(lockKey))){
redisTemplate.delete(lockKey);
System.out.println("业务执行完成");
}
}
}
应用:在导入时加锁,防止重复导入数据
public Result importMember(MultipartFile file, String type) {
//加锁
addLock(SAVE_IMPORT_MEMBER_TIME);
try {
//执行导入操作
return new Result("0000", saveNum + "条数据导入成功");
} catch (Exception e) {
log.error(e.getMessage());
throw new ScException("导入失败");
} finally {
//删除锁
delectLock(SAVE_IMPORT_MEMBER_TIME);
}
}
private void addLock(String lockName){
//检查锁是否存在,是否超时,不存在或超时则进入,否则返回异常,导入未完成不可导入! TODO
LockInfo lockInfo = new LockInfo<>(LockType.Reentrant, SAVE_IMPORT_MEMBER, 5, 3);
//获取锁,是否存在,存在抛出异常不可导入,锁时长三分钟,结束时释放
try {
redisLockClient.lock(() -> {
//查询锁是否存在
Long expire = redisTemplate.opsForValue().getOperations().getExpire(lockName);
if (expire > 0){
//未过期
throw new ScException(SuifyBaseRespCode.RETRY_MEMBER_IMPORT);
}else {
redisTemplate.opsForValue().set(lockName,1);
BoundGeoOperations boundGeoOperations = redisTemplate.boundGeoOps(lockName);
boundGeoOperations.expire(5, TimeUnit.MINUTES);
}
//存在 抛出异常
//不存在 新增锁,开始导入
return null;
},lockInfo);
} catch (Throwable e) {
log.error(e.getMessage());
throw new ScException(e.getMessage());
}
}
private void delectLock(String lockName){
redisTemplate.delete(lockName);
}
5.5 关注,取关和共同关注
创建表
CREATE TABLE `tb_follow` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_id` bigint(20) unsigned NOT NULL COMMENT '用户id',
`follow_user_id` bigint(20) unsigned NOT NULL COMMENT '关联的用户id',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT;
核心方法
//关注/取消关注
@PutMapping("/{id}/{isFollow}")
public void follow(@PathVariable("id") Long followUserId, @PathVariable("isFollow") Boolean isFollow) {
followMethod(followUserId, isFollow);
}
//判断当前用户是否关注
@GetMapping("/isFollowa/{id}")
public Result isFollowa(@PathVariable("id") Long followUserId) {
return isFollow(followUserId);
}
private void followMethod(Long followUserId,Boolean isFollow){
Long userId = 111L;
String key = "follows:"+userId;
//判断关注还是取关
//关注
if (isFollow){
RedisFollow follow = new RedisFollow();
follow.setUserId(userId);
follow.setFollowUserId(followUserId);
boolean isSuccess = saveFollow(follow);
if (isSuccess){
redisTemplate.opsForSet().add(key,followUserId.toString());
}
}
//取关
else {
boolean isSuccess = remove(follow);
if (isSuccess){
redisTemplate.opsForSet().remove(key,followUserId.toString());
}
}
}
//判断当前用户是否关注
public Result isFollow(Long followUserId) {
// 1.获取登录用户
Long userId = 111L;
// 2.查询是否关注 select count(*) from tb_follow where user_id = ? and follow_user_id = ?
Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count();
// 3.判断
return Result.ok(count > 0);
}
sinter key[key...] --交集
sunion key[key...] --并集
sdiff key[key...] --差集
共同关注
使用Redis的Set集合实现,我们把两人关注的人分别放入到一个Set集合中,然后再通过API去查看两个Set集合中的交集数据
public Result followCommons(Long id) {
// 1.获取当前用户
Long userId = 111L;
String key = "follows:" + userId;
// 2.求交集
String key2 = "follows:" + id;
Set<String> intersect = redisTemplate.opsForSet().intersect(key, key2);
if (intersect == null || intersect.isEmpty()) {
// 无交集
return Result.ok(Collections.emptyList());
}
// 3.解析id集合
List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
// 4.查询用户
List<UserDTO> users = userService.listByIds(ids)
.stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
return Result.ok(users);
}
5.6 红包业务
随机生成红包金额:二倍均值法
公式:(0, M/N * 2),M为剩余红包金额,N为剩余人数,这个公式,保证了每次随机金额的平均值是相等的,不会因为抢红包的先后顺序而造成不公平。
/**
* 二倍均值法的代码实战
*/
public class RedPacketUtil {
/**
* 发红包算法,金额参数以分为单位
*
* @param totalAmount
* @param totalPeopleNum
* @return
*/
public static List<Integer> divideRedPackage(Integer totalAmount, Integer totalPeopleNum) {
//用于存储每次产生的小红包随机金额列表,金额单位为分
List<Integer> amountList = new ArrayList<Integer>();
//判断总金额和总人数参数的合法性
if (totalAmount > 0 && totalPeopleNum > 0) {
//记录剩余的总金额,初始化时即为红包的总金额
Integer restAmount = totalAmount;
//记录剩余的总人数,初始化时即为指定的总人数
Integer restPeopleNum = totalPeopleNum;
//定义产生随机数的实例对象
Random random = new Random();
//不断循环遍历、迭代更新地产生随机金额,知道N-1<=0
for (int i = 0; i < totalPeopleNum - 1; i++) {
// 随机范围:[1,剩余人均金额的两倍),左闭右开
//随机金额R、单位为分
int amount = random.nextInt(restAmount / restPeopleNum * 2 - 1) + 1;
//更新剩余的总金额M=M-R
restAmount -= amount;
//更新剩余的总人数N=N-1
restPeopleNum--;
//将产生的随机金额添加进列表中
amountList.add(amount);
}
//循环完毕,剩余的金额为最后一个随机金额,也需要将其添加进列表
amountList.add(restAmount);
}
//最终产生的随机金额返回
return amountList;
}
}
发红包
核心的处理逻辑为在接收前端发红包着设定的红包总金额M和总个数M,后端根据这2个参数,采用二倍均值法生成N个随机金额的红包,最后将红包个数N与随机金额列表存到缓存中,同时将相关数据异步记录到数据库中。
此外,后端接口在收到前端用户发红包的请求时候,将采用当前的时间戳(纳秒级)作为红包全局唯一标识串,并返回给前端,后续用户发起抢红包的请求时候,将会带上这一参数,目的是为了给发出的红包作为标记,并根据这一标记去缓存中查询红包个数和随机金额列表等数据。
处理发红包的请求时,后端接口需要接收红包总金额和总个数等参数,故而将其封装成实体对象RedPacketDto
@Data
@ToString
public class RedPacketDto {
private Integer userId;
//指定多少人抢
@NotNull
private Integer total;
//指定总金额-单位为分
@NotNull
private Integer amount;
}
public class RedPacketController {
//定义请求前缀
private static final String prefix="red/packet";
@Autowired
private IRedPacketService redPacketService;
/**
* 发红包
*/
@PostMapping(value = prefix+"/hand/out")
public BaseResponse handOut(@Validated @RequestBody RedPacketDto dto, BindingResult result){
//参数校验
if (result.hasErrors()){
return new BaseResponse(StatusCode.InvalidParams);
}
BaseResponse response=new BaseResponse(StatusCode.Success);
try {
//核心业务逻辑处理服务,最终返回红包全局唯一标识符
String redId=redPacketService.handOut(dto);
//将红包全局唯一标识符返回前端
response.setData(redId);
}catch (Exception e){
//如果报异常则输出日志并且返回相应的错误信息
log.error("发红包发生异常:dto={} ",dto,e.fillInStackTrace());
response=new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
}
return response;
}
@Service
@Slf4j
public class RedPacketService implements IRedPacketService {
private final SnowFlake snowFlake=new SnowFlake(2,3);
//存储至缓存系统Redis时定义的key
private static final String keyPrefix="redis:red:packet:";
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private IRedService redService;
/**
* 发红包
* @throws Exception
*/
@Override
public String handOut(RedPacketDto dto) throws Exception {
if (dto.getTotal()>0 && dto.getAmount()>0){
//生成随机金额
List<Integer> list=RedPacketUtil.divideRedPackage(dto.getAmount(),dto.getTotal());
//生成红包全局唯一标识,并将随机金额、个数入缓存
String timestamp=String.valueOf(System.nanoTime());
//根据缓存key的前缀与其他信息拼成一个新的用于存储随机金额列表的key
String redId = new StringBuffer(keyPrefix).append(dto.getUserId()).append(":").append(timestamp).toString();
//将随机金额列表存入缓存列表中
redisTemplate.opsForList().leftPushAll(redId,list);
//根据缓存key的前缀与其他信息拼成一个新的用于存储红包总数的key
String redTotalKey = redId+":total";
//将红包总数存入缓存中
redisTemplate.opsForValue().set(redTotalKey,dto.getTotal());
//异步记录红包发出的记录-包括个数与随机金额
redService.recordRedPacket(dto,redId,list);
return redId;
}else{
throw new Exception("系统异常-分发红包-参数不合法!");
}
}
抢红包
- 从业务的角度分析,抢红包业务模块需要实现两大业务逻辑,包括点红包业务逻辑和拆红包业务逻辑,通俗地讲,他包含两大动作。
- 从技术的角度分析,抢红包业务模块对应的后端接口需要频繁地访问缓存系统Redis,用于获取红包剩余个数和剩余金额列表,进而用于判断用户点击红包,拆红包是否成功,除此之外,在每次用户成功抢到红包之后,后端接口需要及时更新缓存系统中的红包的剩余个数,记录相应的信息入数据库等。
/**
* 不加分布式锁的情况
* 抢红包-分“点”与“抢”处理逻辑
* @param userId
* @param redId
* @return
* @throws Exception
*/
public BigDecimal rob(Integer userId, String redId) throws Exception {
//用户是否抢过该红包
Object obj=redisTemplate.opsForValue().get(redId+userId+":rob");
if (obj!=null){
return new BigDecimal(obj.toString());
}
//"点红包"
Boolean res=click(redId);
if (res){
//"抢红包"-且红包有钱
Object value=redisTemplate.opsForList().rightPop(redId);
if (value!=null){
//红包个数减一
String redTotalKey = redId+":total";
Integer currTotal=redisTemplate.opsForValue().get(redTotalKey)!=null? (Integer) redisTemplate.opsForValue().get(redTotalKey) : 0;
redisTemplate.opsForValue().set(redTotalKey,currTotal-1);
//将红包金额返回给用户的同时,将抢红包记录入数据库与缓存
BigDecimal result = new BigDecimal(value.toString()).divide(new BigDecimal(100));
redService.recordRobRedPacket(userId,redId,new BigDecimal(value.toString()));
redisTemplate.opsForValue().set(redId+userId+":rob",result,24L,TimeUnit.HOURS);
log.info("当前用户抢到红包了:userId={} key={} 金额={} ",userId,redId,result);
return result;
}
}
return null;
}
/**
* 点红包-返回true,则代表红包还有,个数>0
* @throws Exception
*/
private Boolean click(String redId) throws Exception{
String redTotalKey = redId+":total";
Object total=redisTemplate.opsForValue().get(redTotalKey);
if (total!=null && Integer.valueOf(total.toString())>0){
return true;
}
return false;
}
注意: 在高并发的情况下,比如说魔偶一时刻同一用户在界面疯狂的点击红包图样时,如果前端不加以控制,同一时刻同一用户将发起多个请求,后端接收后很可能同时进行缓存系统中是否有红包的判断并成功通过,然后执行后面弹出红包随机金额的业务逻辑,导致一个用户抢到多个红包。
**优化:**加分布式锁
/**
* 加分布式锁的情况
* 抢红包-分“点”与“抢”处理逻辑
* @throws Exception
*/
public BigDecimal rob(Integer userId,String redId) throws Exception {
//用户是否抢过该红包
Object obj=redisTemplate.opsForValue().get(redId+userId+":rob");
if (obj!=null){
return new BigDecimal(obj.toString());
}
//"点红包"
Boolean res=click(redId);
if (res){
//上锁:一个红包每个人只能抢一次随机金额;一个人每次只能抢到红包的一次随机金额 即要永远保证 1对1 的关系
final String lockKey=redId+userId+"-lock";
Boolean lock=redisTemplate.opsForValue().setIfAbsent(lockKey,redId);
redisTemplate.expire(lockKey,24L,TimeUnit.HOURS);
try {
if (lock) {
//"抢红包"-且红包有钱
Object value=redisTemplate.opsForList().rightPop(redId);
if (value!=null){
//红包个数减一
String redTotalKey = redId+":total";
Integer currTotal=redisTemplate.opsForValue().get(redTotalKey)!=null? (Integer) redisTemplate.opsForValue().get(redTotalKey) : 0;
redisTemplate.opsForValue().set(redTotalKey,currTotal-1);
//将红包金额返回给用户的同时,将抢红包记录入数据库与缓存
BigDecimal result = new BigDecimal(value.toString()).divide(new BigDecimal(100));
redService.recordRobRedPacket(userId,redId,new BigDecimal(value.toString()));
redisTemplate.opsForValue().set(redId+userId+":rob",result,24L,TimeUnit.HOURS);
log.info("当前用户抢到红包了:userId={} key={} 金额={} ",userId,redId,result);
return result;
}
}
}catch (Exception e){
throw new Exception("系统异常-抢红包-加分布式锁失败!");
}
}
return null;
}
/**
* 点红包-返回true,则代表红包还有,个数>0
* @throws Exception
*/
private Boolean click(String redId) throws Exception{
String redTotalKey = redId+":total";
Object total=redisTemplate.opsForValue().get(redTotalKey);
if (total!=null && Integer.valueOf(total.toString())>0){
return true;
}
return false;
}
5.7 点赞
@Override
public Result likeBlog(Long id){
// 1.获取登录用户
Long userId = UserHolder.getUser().getId();
// 2.判断当前登录用户是否已经点赞
String key = BLOG_LIKED_KEY + id;
Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
if(BooleanUtil.isFalse(isMember)){
//3.如果未点赞,可以点赞
//3.1 数据库点赞数+1
boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();
//3.2 保存用户到Redis的set集合
if(isSuccess){
stringRedisTemplate.opsForSet().add(key,userId.toString());
}
}else{
//4.如果已点赞,取消点赞
//4.1 数据库点赞数-1
boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();
//4.2 把用户从Redis的set集合移除
if(isSuccess){
stringRedisTemplate.opsForSet().remove(key,userId.toString());
}
}
5.8 热搜
@Autowired
private RedisTemplate redisTemplate;
public void save(String queryWord) {
// 缓存搜索词 凌晨零点失效
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_YEAR, 1);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.MILLISECOND, 0);
// 晚上十二点与当前时间的毫秒差
Long timeOut = (calendar.getTimeInMillis() - System.currentTimeMillis()) / 1000;
redisTemplate.expire(SearchFieldEnum.HOT_SEARCH.getName(), timeOut, TimeUnit.SECONDS);
if (redisTemplate.opsForZSet().range(SearchFieldEnum.HOT_SEARCH.getName(), 0, -1).toString().contains(queryWord)) {
// 缓存已存在 当前分数加 1
Double score = redisTemplate.opsForZSet().score(SearchFieldEnum.HOT_SEARCH.getName(), queryWord);
redisTemplate.opsForZSet().add(SearchFieldEnum.HOT_SEARCH.getName(), queryWord, score + 1.0);
} else {
redisTemplate.opsForZSet().add(SearchFieldEnum.HOT_SEARCH.getName(), queryWord, 1);
}
public Set<String> get() {
return redisTemplate.opsForZSet().reverseRange(SearchFieldEnum.HOT_SEARCH.getName(), 0, -1);
}
5.9 粉丝和关注列表
@Autowired
private RedisTemplate redisTemplate;
// 关注列表
public static final String FOLLOWEE_SET_KEY = "followee:user:";
// 粉丝列表
public static final String FOLLOWER_SET_KEY = "follower:user:";
/**
* 关羽 = 2 关注刘备 = 1
* 张飞 = 3 关注刘备 = 1
* userId = 2 == followId = 1
*
* @param userId = 关羽
* @param followId = 张飞
*/
public void follow(Integer userId, Integer followId) {
// 获取redis的集合对象
SetOperations<String, Integer> setOperations = redisTemplate.opsForSet();
// 在刘备的粉丝集合列表中,把关羽和张飞加入到你的集合中
setOperations.add(FOLLOWER_SET_KEY + followId, userId);
// 关羽的和张飞关注集合列表中,增加刘备的信息
setOperations.add(FOLLOWEE_SET_KEY + userId, followId);
}
/**
* 查询我的关注
*
* @param userId = 关羽
*/
public List<User> listMyFollowee(Integer userId) throws IllegalAccessException {
// 获取redis的集合对象
SetOperations<String, Integer> setOperations = redisTemplate.opsForSet();
// 通过members方法,将关注列表的信息查询出来
Set<Integer> members = setOperations.members(FOLLOWEE_SET_KEY + userId);
return getUserInfos(members);
}
/**
* 我的粉丝列表
*
* @param userId =关羽
*/
public List<User> listMyFollower(Integer userId) throws IllegalAccessException {
// 获取redis的集合对象
SetOperations<String, Integer> setOperations = redisTemplate.opsForSet();
// 通过members方法,将关注列表的信息查询出来
Set<Integer> members = setOperations.members(FOLLOWER_SET_KEY + userId);
return getUserInfos(members);
}
/**
* 把集合和前面的hash集合起来
*
* @param userInfos
* @return
*/
private List<User> getUserInfos(Set<Integer> userInfos) throws IllegalAccessException {
// 创建用户集合
List<User> userList = new ArrayList<>();
// 从hash中获取属性
List<String> hasKeys = new ArrayList<>();
hasKeys.add("id");//list.get(0)
hasKeys.add("nickname");//list.get(1)
hasKeys.add("password");//list.get(2)
hasKeys.add("sex");//list.get(3)
// 定义一个hash数据操作对象
HashOperations opsForHash = this.redisTemplate.opsForHash();
// 循环关注列表的用户ID信息
for (Integer userId : userInfos) {
// 获取用户在hash中的注册完整信息对应的key
String hKey = "reg:user:hash:" + userId;
// 把"reg:user:hash:1" 的信息从hash数据结构中获取获取,获取id,nickname,password,sex
List<Object> list = opsForHash.multiGet(hKey, hasKeys);
// 如果在缓存中没有找到对应的用户信息
if (list.get(0) == null && list.get(1) == null) {
// 从数据库中根据用户id去查询
User user = this.getUserDbCache(userId);
userList.add(user);
} else {
User user = new User();
user.setId(Integer.valueOf(list.get(0).toString()));
user.setNickname(list.get(1).toString());
user.setPassword(list.get(2).toString());
user.setSex(Integer.parseInt(list.get(3).toString()));
userList.add(user);
}
}
return userList;
}
/**
* 从数据库去获取用户信息,并且把获取的用户新放入缓存HASH数据结构中
*
* @param userId
* @return
* @throws IllegalAccessException
*/
public User getUserDbCache(Integer userId) throws IllegalAccessException {
// 查询最新的用户信息放入到redis的hash中
// User user = this.getOne(userId);
// Map<String, Object> map = ObjectUtils.objectToMap(user);
// // 准备用存入的key,将用户信息存入到redis的hash中
// String key = "reg:user:" + user.getId();
// redisTemplate.opsForHash().putAll(key, map);
// // 设置key的失效时间一个月
// redisTemplate.expire(key, 30, TimeUnit.DAYS);
// return user;
return null;
}
5.10 Redis队列实现秒杀系统
5.11 Redis 实现 Feed 流
六.Redis事务
Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。
watch key1 key2 ... : 监视一或多个key,如果在事务执行之前,
被监视的key被其他命令改动,则事务被打断 ( 类似乐观锁 )
multi : 标记一个事务块的开始( queued )
exec : 执行所有事务块的命令 ( 一旦执行exec后,之前加的监控锁都会被取消掉 )
discard : 取消事务,放弃事务块中的所有命令
unwatch : 取消watch对所有key的监控
正常执行
放弃事务
若在事务队列中存在命令性错误(类似于java编译性错误),则执行EXEC命令时,所有命令都不会执行
若在事务队列中存在语法性错误(类似于java的1/0的运行时异常),则执行EXEC命令时,其他正确命令会被执行,错误命令抛出异常。
watch监控:相当于加了一把乐观锁
场景:模拟信用卡余额和欠款
①信用卡余额和欠款正常场景
②无加塞篡改,先监控再开启multi,保证两笔金额变动在同一个事务内
③有加塞篡改:此处监控的是balance,也可以同时监控多个,一旦被监控的键在事务(MULTI命令之后的命令队列)执行之前有set指令修改该键的值,则会取消事务中的所有指令
七.Redis持久化
7.1 RDB
在默认情况下, Redis 将内存数据库快照保存在名字为 dump.rdb 的二进制文件中。
你可以对 Redis 进行设置, 让它在“ N 秒内数据集至少有 M 个改动”这一条件被满足时, 自动保存一次 数据集。
比如说, 以下设置会让 Redis 在满足“ 60 秒内有至少有 1000 个键被改动”这一条件时, 自动保存一次 数据集:
#save 60 1000 //关闭RDB只需要将所有的save保存策略注释掉即可
还可以手动执行命令生成RDB快照,进入redis客户端执行命令save或bgsave可以生成dump.rdb文件,
每次命令执行都会将所有redis内存快照到一个新的rdb文件里,并覆盖原有rdb快照文件。
bgsave的写时复制(COW)机制(全量快照(把内存中的所有数据都记录到磁盘中))
Redis 借助操作系统提供的写时复制技术(Copy-On-Write, COW),在生成快照的同时,依然可以正常
处理写命令。简单来说,bgsave 子进程是由主线程 fork 生成的,可以共享主线程的所有内存数据。
bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。此时,如果主线程对这些
数据也都是读操作,那么,主线程和 bgsave 子进程相互不影响。但是,如果主线程要修改一块数据,那
么,这块数据就会被复制一份,生成该数据的副本。然后,bgsave 子进程会把这个副本数据写入 RDB 文
件,而在这个过程中,主线程仍然可以直接修改原来的数据。
save与bgsave对比
配置自动生成rdb文件后台使用的是bgsave方式。
7.2 AOF
打开AOF
appendonly yes
从现在开始,每当 Redis 执行一个改变数据集的命令时(比如 SET), 这个命令就会被追加到AOF 文件的末尾。 这样的话, 当 Redis 重新启动时, 程序就可以通过重新执行AOF文件中的命令来达到重建数据集的目的。
appendfsync配置
可配置 Redis 多久才将数据 fsync 到磁盘一次。
1.appendfsync always:每次有新命令追加到 AOF 文件时就执行一次 fsync ,非常慢,也非常安全。
2.appendfsync everysec:每秒 fsync 一次,足够快,并且在故障时只会丢失 1 秒钟的数据。
3.appendfsync no:从不 fsync ,将数据交给操作系统来处理。更快,也更不安全的选择。
推荐(并且也是默认)的措施为每秒 fsync 一次, 这种 fsync 策略可以兼顾速度和安全性。
AOF重写
定期重写:AOF文件里可能有太多没用指令,所以AOF会定期根据内存的最新数据生成aof文件
如下两个配置可以控制AOF自动重写频率
auto‐aof‐rewrite‐min‐size 64mb //aof文件至少要达到64M才会自动重写,文件太小恢复速度本来就很快,重写的意义不大
auto‐aof‐rewrite‐percentage 100 //aof文件自上一次重写后文件大小增长了100%则再次触发重写
7.3 RDB和AOF
生产环境可以都启用,redis启动时如果既有rdb文件又有aof文件则优先选择aof文件恢复数据,因为aof一般来说数据更全一点。
通过如下配置可以开启混合持久化(必须先开启aof):aof‐use‐rdb‐preamble yes
**如果开启了混合持久化,AOF在重写时,不再是单纯将内存数据转换为RESP命令写入AOF文件,而是将 重写这一刻之前的内存做RDB快照处理,并且将RDB快照内容和增量的AOF修改内存数据的命令存在一起,都写入新的AOF文件,新的文件一开始不叫appendonly.aof,等到重写完新的AOF文件才会进行改名,覆盖原有的AOF文件,完成新旧两个AOF文件的替换。
加载顺序
Redis重启的时候,可以先加载 RDB 的内容,然后再重放增量 AOF日志就可以完全替代之前的AOF全量文件重放