文章目录
一.概念
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);
redisTemplate.expire(lockName,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 流
5.12 Redis+Caffeine两级缓存
二级缓存
平时我们会将数据存储到磁盘上,如:数据库。如果每次都从数据库里去读取,会因为磁盘本身的IO影响读取速度,所以就有了像redis这种的内存缓存。可以将数据读取出来放到内存里,这样当需要获取数据时,就能够直接从内存中拿到数据返回,能够很大程度的提高速度。 但是一般redis是单独部署成集群,所以会有网络IO上的消耗,虽然与redis集群的链接已经有连接池这种工具,但是数据传输上也还是会有一定消耗。所以就有了进程内缓存,如:caffeine。当应用内缓存有符合条件的数据时,就可以直接使用,而不用通过网络到redis中去获取,这样就形成了两级缓存。应用内缓存叫做一级缓存,远程缓存(如redis)叫做二级缓存。
系统是否需要缓存
- CPU占用:如果你有某些应用需要消耗大量的cpu去计算获得结果。
- 数据库IO占用:如果你发现你的数据库连接池比较空闲,那么不应该用缓存。但是如果数据库连接池比较繁忙,甚至经常报出连接不够的报警,那么是时候应该考虑缓存了。
分布式二级缓存的优势
- Redis如果不可用,这个时候我们只能访问数据库,很容易造成雪崩,但一般不会出现这种情况。
- 访问Redis会有一定的网络I/O以及序列化反序列化开销,虽然性能很高但是其终究没有本地方法快,可以将最热的数据存放在本地,以便进一步加快访问速度。这个思路并不是我们做互联网架构独有的,在计算机系统中使用L1,L2,L3多级缓存,用来减少对内存的直接访问,从而加快访问速度。
另外,如果是分布式环境下,一级缓存之间也会存在一致性问题,当一个节点下的本地缓存修改后,需要通知其他节点也刷新本地缓存中的数据,否则会出现读取到过期数据的情况,这一问题可以通过类似于Redis中的发布/订阅功能解决。
此外,缓存的过期时间、过期策略以及多线程访问的问题也都需要考虑进去,不过我们今天暂时先不考虑这些问题,先看一下如何简单高效的在代码中实现两级缓存的管理。
配置
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.8.1</version>
</dependency>
spring:
redis:
host: 127.0.0.1
port: 6379
database: 0
timeout: 10000ms
lettuce:
pool:
max-active: 8
max-wait: -1ms
max-idle: 8
min-idle: 0
版本V1:基础版
@Configuration
public class CaffeineConfig {
@Bean
public Cache<String,Object> caffeineCache(){
return Caffeine.newBuilder()
.initialCapacity(128)//初始缓存空大小
.maximumSize(1024)//缓存的最大数量,设置这个值可以避免出现内存溢出
.expireAfterWrite(60, TimeUnit.SECONDS)//指定缓存的过期时间,是最后一次写操作后的一个时间 缓存的过期策略也可以通过expireAfterAccess或refreshAfterWrite指定。
.build();
}
}
@PostMapping("/caffeine")
public void testCaffeine() throws InterruptedException {
RedisFollow obj= redisCaffeine.test1("1");
System.out.println("result===="+obj);
}
public class RedisCaffeine {
@Autowired
public UserMapper userMapper;
Cache<String, Object> cache = caffeineCache();
public RedisFollow test1(String id) {
Object cscc = cache.getIfPresent("order:redis:key:"+id);
if (ObjectUtil.isNotNull(cscc)){
System.out.println("getDataFromCache====="+cscc);
return (RedisFollow) cscc;
} else {
//redis没有 则查数据库
RedisFollow one = userMapper.findOne(id);
System.out.println("get data from DB===="+one);
cache.put("order:redis:key:"+id,one);
return one;
}
}
}
V2.0版本:使用注解
- @Cacheable
源码
public @interface Cacheable {
/**
* 设定要使用的cache的名字,必须提前定义好缓存
*/
@AliasFor("cacheNames")
String[] value() default {};
/**
* 同value(),决定要使用那个/些缓存
*/
@AliasFor("value")
String[] cacheNames() default {};
/**
* 使用SpEL表达式来设定缓存的key,如果不设置默认方法上所有参数都会作为key的一部分
*/
String key() default "";
/**
* 用来生成key,与key()不可以共用
*/
String keyGenerator() default "";
/**
* 设定要使用的cacheManager,必须先设置好cacheManager的bean,这是使用该bean的名字
*/
String cacheManager() default "";
/**
* 使用cacheResolver来设定使用的缓存,用法同cacheManager,但是与cacheManager不可以同时使用
*/
String cacheResolver() default "";
/**
* 使用SpEL表达式设定出发缓存的条件,在方法执行前生效
*/
String condition() default "";
/**
* 使用SpEL设置出发缓存的条件,这里是方法执行完生效,所以条件中可以有方法执行后的value
* 因为是方法执行后生效,可以决定是否放入缓存,返回true的放缓存
*/
String unless() default "";
/**
* 用于同步的,在缓存失效(过期不存在等各种原因)的时候,如果多个线程同时访问被标注的方法
* 则只允许一个线程通过去执行方法
*/
boolean sync() default false;
}
@Cacheable(value = "order:redis:key",key = "#id")
public RedisFollow test1(String id) {
//redis没有 则查数据库
RedisFollow one = userMapper.findOne(id);
System.out.println("get data from DB===="+one);
return one;
}
- @CachePut
这是个一般用于修改方法上的注解,它的代码跟Cacheable基本相同,这里不做介绍。
现在说下CachePut和Cacheable的主要区别。
@Cacheable:它的注解的方法是否被执行取决于Cacheable中的条件,方法很多时候都可能不被执行。
@CachePut:这个注解不会影响方法的执行,也就是说无论它配置的条件是什么,方法都会被执行,更多的时候是被用到修改上。
@CachePut(value = "order:redis:key",key = "#id")
public RedisFollow test3(String id) {
//redis没有 则查数据库
RedisFollow one = userMapper.findOne(id);
userMapper.updateEntity("1101110110",one.getId());
System.out.println("get data from DB and update cache ===="+one);
return one;
}
- @CacheEvict
它跟上边的两个注解相比,源码中多了两个属性
public @interface CacheEvict {
/**
* 是否删除缓存中的所有数据,默认为false,只会删除被注解方法中传入的key的缓存
*/
boolean allEntries() default false;
/**
* 设置缓存的删除在方法执行前执行还是执行后执行。如果设置true,则无论该方法是否正常结束,缓存中的值都会被删除。
*/
boolean beforeInvocation() default false;
}
@CacheEvict(value = "order:redis:key",key = "#id")
public void test4(String id) {
//redis没有 则查数据库
RedisFollow one = userMapper.findOne(id);
System.out.println("get data from DB and deleted cache ===="+one);
userMapper.deletedEntity(id);
}
- @Caching
public @interface Caching {
Cacheable[] cacheable() default {};
CachePut[] put() default {};
CacheEvict[] evict() default {};
}
它只是给出了三种注解的组合,并没有给出限制条件,所以其使用也很简单,如下
@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") })
public Book importBooks(String deposit, Date date)
V3.0版本
六.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全量文件重放
八.主从复制和哨兵模式
九.布隆过滤器
布隆过滤器是一种概率型数据结构,它的特点是高效地插入和查询,能确定某个字符串一定不存在或者可能存在。(注意:能判断一定不存在,但是不能判断一定存在,也就是只能知道100%不在里面,且有一定概率在里面)
布隆过滤器的原理是,当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。
它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
1.缓存穿透
缓存穿透就是服务调用方每次都是查询不在缓存中的数据,这样每次服务调用都会到数据库中进行查询,如果这类请求比较多的话,就会导致数据库压力增大,这样缓存就失去了意义。
2.黑白名单
黑名单:发现存在黑名单中的,就执行特定操作。比如:识别垃圾邮件,只要是邮箱在黑名单中的邮件,就识别为垃圾邮件。假设黑名单的数量是数以亿计的,存放起来就是非常耗费存储空间的,布隆过滤器则是一个较好的解决方案。
把所有黑名单都放在布隆过滤器中,在收到邮件时,判断邮件地址是否在布隆过滤器中即可。
3.实战
Redis集成布隆过滤器的主要指令如下:
- bf.add 添加一个元素
- bf.exists 判断一个元素是否存在
- bf.madd 添加多个元素
- bf.mexists 判断多个元素是否存在
Java集成Redis使用布隆过滤器
- 引入pom依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.16.0</version>
</dependency>
- 测试代码
@Log4j2
@Component
public class BloomInit {
@Autowired
public UserMapper userMapper;
@Autowired
RedisTemplate<String,Object> redisTemplate;
/**
* 初始化布隆过滤器
*/
public BloomFilter bloomInit() {
// 初始化布隆过滤器,设置数据类型,数组长度和误差值
BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), 1000000L, 0.01);
// 获取要装入过滤器的数据
List<RedisFollow> list = userMapper.list();
// 循环装填
for (RedisFollow obj : list) {
//将存放的id放入布隆过滤器
bloomFilter.put("BLOOM_FILTER" +"_"+ obj.getId());
log.info("BLOOM_FILTER" +"_"+ obj.getId());
}
log.info("布隆过滤器装载完成");
return bloomFilter;
}
public RedisFollow getRedisFollow(String id) {
//BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), 1000000L, 0.01);
BloomFilter bloomFilter = bloomInit();
boolean contains = bloomFilter.mightContain("BLOOM_FILTER" +"_"+ id);
if (!contains) {
return null;
}
// 尝试从redis中获取
String skillActivityResp = (String) redisTemplate.opsForValue().get("BLOOM_FILTER" +"_"+ id);
if (skillActivityResp == null) {
RedisFollow goods = userMapper.findOne(id);
// 存入redis
redisTemplate.opsForValue().set("BLOOM_FILTER" +"_"+ id, JSONObject.toJSONString(goods),24, TimeUnit.HOURS);
return goods;
}
RedisFollow redisFollow = JSONObject.parseObject(skillActivityResp, RedisFollow.class);
System.out.println("布隆过滤器存在,直接从缓存读取"+redisFollow.toString());
return JSONObject.parseObject(skillActivityResp, RedisFollow.class);
}
}
十.双写一致性
1.异常场景
-
先更新数据库,再更新缓存场景-不推荐
当有两个线程A、B,同时对一条数据进行操作,一开始数据库和redis的数据都为1,当线程A去修改数据库,将1改为2,然后线程A在修改缓存中的数据,可能因为网络原因出现延迟,这个时候线程B,将数据修改成了3、然后将数据库中的1也改成了3,然后线程A恢复正常,将redis中的缓存改成了2,此时就出现了缓存数据和数据库数据不一致情况。
-
先更新缓存,再更新数据库场景-不推荐
当有两个线程A、B,同时对一条数据进行操作,线程A先将redis中的数据修改为了2,然后CPU切换到了线程B,将redis中的数据修改为了3,然后将数据库中的信息也修改了3,然后线程A获得CPU执行,将数据库中的信息改为了2,此时出现缓存和数据库数据不一致情况。不推荐
-
先删除缓存,再更新数据库的场景-不推荐
当有两个线程A、B,同时对一条数据进行操作,当线程A进行修改缓存操作时,先删除掉缓存中的数据,然后去修改数据库,因为网络问题出现延迟,这时线程B查新redis没有值,因此去数据库中查询数据为1,然后将数据1更新到缓存中,线程A网络恢复,又将数据库数据修改为了2,此时出现数据不一致。不推荐
-
先更新数据库,在删除缓存场景-可以接受
当有两个线程A、B,线程A先去将数据库的值修改为2,然后需要去删除redis中的缓存,当线程B去读取缓存时,线程A已经完成delete操作时,缓存不命中,需要去查询数据库,然后在更新缓存,数据一致性;
如果线程A没有完成delete操作(图中案例),线程B直接命中,返回的数据与数据库中的数据不一致,可能会短暂出现数据不一致情况,但最终都会一致。推荐
2.延时双删策略
既然先更新数据库,在删除缓存的场景可能会短暂出现数据不一致情况,那在高并发下如何解决呢?答案是采用延时双删策略。
先删除缓存,再写数据库,异步等待一段时间后,再次淘汰缓存(这里设置时间主要是保证读请求结束,写请求可以删除读请求造成的缓存脏数据,需要自行评估确定)。该方案解决了高并发情况下,同时有读请求和写请求时导致的不一致问题,读取速度快,但是可能会导致短时间出现脏数据。为了保证第二次删除不会失败,可以加入重试机制(缓存删除失败,将需要删除的key发送给消息队列,自己在消费信息,获得要删除的key,重新删除)保证删除一定成功。
3.最佳实现解决方案
使用canal:参考文章
十一.Lua脚本
总结lua脚本调用redis的优势
- 多个命令合并到脚本统一处理,减少多个命令的网络开销
- 因为redis中的原子性和redis的单线程特性,lua脚本能确保操作的原子性,不会受到其他客户端命令的影响
- 代码复用,redis将永久存放客户端发送的脚本
1.基本命令
- EVAL命令
命令格式:EVAL script numkeys key [key …] arg [arg …]
script参数是一段 Lua5.1脚本程序。脚本不必(也不应该)定义为一个 Lua 函数
numkeys指定后续参数有几个key,即:key [key …]中key的个数。如没有key,则为
key [key …] 从 EVAL 的第三个参数开始算起,表示在脚本中所用到的那些 Redis 键(key)。在Lua脚本中通过KEYS[1], KEYS[2]获取。
arg [arg …] 附加参数。在Lua脚本中通过ARGV[1],ARGV[2]获取。
eg:
// 例1:numkeys=1,keys数组只有1个元素key1,arg数组无元素
127.0.0.1:6379> EVAL "return KEYS[1]" 1 key
"key"
// 例2:numkeys=0,keys数组无元素,arg数组元素中有1个元素value1
127.0.0.1:6379> EVAL "return ARGV[1]" 0 value
"value"
// 例3:numkeys=2,keys数组有两个元素key1和key2,arg数组元素中有两个元素first和second
// 其实{KEYS[1],KEYS[2],ARGV[1],ARGV[2]}表示的是Lua语法中“使用默认索引”的table表[数组],
// 相当于java中的map中存放四条数据。Key分别为:1、2、3、4,而对应的value才是:KEYS[1]、KEYS[2]、ARGV[1]、ARGV[2]
// 举此例子仅为说明eval命令中参数的如何使用。项目中编写Lua脚本最好遵从key、arg的规范。
127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"
// 例4:使用了redis为lua内置的redis.call函数
// 脚本内容为:先执行SET命令,在执行EXPIRE命令(相当于执行SETEX key1 60 10)
// numkeys=1,keys数组有一个元素userAge(代表redis的key)
// arg数组元素中有两个元素:10(代表userAge对应的value)和60(代表redis的存活时间)
127.0.0.1:6379> EVAL "redis.call('SET', KEYS[1], ARGV[1]);redis.call('EXPIRE', KEYS[1], ARGV[2]); return 1;" 1 userAge 10 60
(integer) 1
127.0.0.1:6379> get userAge
"10"
127.0.0.1:6379> ttl userAge
(integer) 50
redis.call() 和 redis.pcall()
这两个函数的唯一区别在于它们使用不同的方式处理执行命令所产生的错误,差别如下:
错误处理
当 redis.call() 在执行命令的过程中发生错误时,脚本会停止执行,并返回一个脚本错误,错误的输出信息会说明错误造成的原因:
127.0.0.1:6379> lpush foo a
(integer) 1
127.0.0.1:6379> eval "return redis.call('get', 'foo')" 0
(error) ERR Error running script (call to f_282297a0228f48cd3fc6a55de6316f31422f5d17): ERR Operation against a key holding the wrong kind of value
和 redis.call() 不同, redis.pcall() 出错时并不引发(raise)错误,而是返回一个带 err 域的 Lua 表(table),用于表示错误:
127.0.0.1:6379> EVAL "return redis.pcall('get', 'foo')" 0
(error) ERR Operation against a key holding the wrong kind of value
- SCRIPT LOAD命令 和 EVALSHA命令
SCRIPT LOAD命令格式:SCRIPT LOAD script
EVALSHA命令格式:EVALSHA sha1 numkeys key [key …] arg [arg …]
这两个命令放在一起讲的原因是:EVALSHA 命令中的sha1参数,就是SCRIPT LOAD 命令执行的结果。
SCRIPT LOAD 将脚本 script 添加到Redis服务器的脚本缓存中,并不立即执行这个脚本,而是会立即对输入的脚本进行求值。并返回给定脚本的 SHA1 校验和。如果给定的脚本已经在缓存里面了,那么不执行任何操作。
在脚本被加入到缓存之后,在任何客户端通过EVALSHA命令,可以使用脚本的 SHA1 校验和来调用这个脚本。脚本可以在缓存中保留无限长的时间,直到执行SCRIPT FLUSH为止。
eg:
## SCRIPT LOAD加载脚本,并得到sha1值
127.0.0.1:6379> SCRIPT LOAD "redis.call('SET', KEYS[1], ARGV[1]);redis.call('EXPIRE', KEYS[1], ARGV[2]); return 1;"
"6aeea4b3e96171ef835a78178fceadf1a5dbe345"
## EVALSHA使用sha1值,并拼装和EVAL类似的numkeys和key数组、arg数组,调用脚本。
127.0.0.1:6379> EVALSHA 6aeea4b3e96171ef835a78178fceadf1a5dbe345 1 userAge 10 60
(integer) 1
127.0.0.1:6379> get userAge
"10"
127.0.0.1:6379> ttl userAge
(integer) 50
- SCRIPT EXISTS 命令
命令格式:SCRIPT EXISTS sha1 [sha1 …]
作用:给定一个或多个脚本的 SHA1 校验和,返回一个包含 0 和 1 的列表,表示校验和所指定的脚本是否已经被保存在缓存当中
127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe345
1) (integer) 1
127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe346
1) (integer) 0
127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe345 6aeea4b3e96171ef835a78178fceadf1a5dbe366
1) (integer) 1
2) (integer) 0
- SCRIPT FLUSH 命令
命令格式:SCRIPT FLUSH
作用:清除Redis服务端所有 Lua 脚本缓存
127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe345
1) (integer) 1
127.0.0.1:6379> SCRIPT FLUSH
OK
127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe345
1) (integer) 0
- SCRIPT KILL 命令
命令格式:SCRIPT FLUSH
作用:杀死当前正在运行的 Lua 脚本,当且仅当这个脚本没有执行过任何写操作时,这个命令才生效。 这个命令主要用于终止运行时间过长的脚本,比如一个因为 BUG 而发生无限 loop 的脚本,诸如此类。
假如当前正在运行的脚本已经执行过写操作,那么即使执行SCRIPT KILL,也无法将它杀死,因为这是违反 Lua 脚本的原子性执行原则的。在这种情况下,唯一可行的办法是使用SHUTDOWN NOSAVE命令,通过停止整个 Redis 进程来停止脚本的运行,并防止不完整(half-written)的信息被写入数据库中。
2.简单的Lua脚本
local sum = 0;
for i,k in pairs(redis.call('keys','*')) do
redis.call('del', k);
sum=sum+1;
end;
return 'clear '..sum..' key'
- “–eval"而不是命令模式中的"eval”,一定要有前面的两个-
- 脚本路径后紧跟key [key …],相比命令行模式,少了numkeys这个key数量值
- key [key …] 和 arg [arg …] 之间的“ , ”,英文逗号前后必须有空格,否则死活都报错
3.IP限制
local visitNum = redis.call('incr', KEYS[1])
if visitNum == 1 then
redis.call('expire', KEYS[1], ARGV[1])
end
if visitNum > tonumber(ARGV[2]) then
return 0
end
return 1;
4.秒杀
需求:公司2周年准备上一个秒杀,针对商品A,价值1W,准备在上午10点整,进行一次秒杀,一共10个库存。
预测当日会有1000用户进行抢购
- 初始化商品的库存
- lua脚本
local isExist = redis.call('exists', KEYS[1]);
if (tonumber(isExist) > 0) then
local goodsNumber = redis.call('get', KEYS[1]);
if (tonumber(goodsNumber) > 0) then
local userExists = redis.call('sismember',KEYS[2],ARGV[1]);
if tonumber(userExists, 10) == 1 then
return -2;
else
redis.call('decr',KEYS[1]);
redis.call('sadd',KEYS[2],ARGV[1]);
return 1;
end;
else
redis.call('del', KEYS[1]);
return 0;
end;
else
return -1;
end;
- 秒杀接口
@PostMapping("/lua/lock")
public void luaLock(@RequestParam(value = "key",required = false) String key) throws InterruptedException {
List<Thread> allThread = new ArrayList<>();
for(int i = 1; i<= 1000; i++){
Thread thread = new Thread(()-> {
Long b = redisLua.reduceStock(key);
if(b==1){
System.out.println("恭喜您,抢到了库存!!!" + Thread.currentThread().getName());
//MQ.sendSuccessMessage();
//最终抢到库存的用户,可以发送一条消息到队列中,进行异步下单。
}else if (b==-2){
System.out.println("对不起,你已秒杀购买过该商品,请勿重复抢购"+ Thread.currentThread().getName());
} else {
System.out.println("对不起,库存已卖光啦!!!" + Thread.currentThread().getName());
}
},"线程" + i);
allThread.add(thread);
}
for(Thread thread : allThread){
thread.start();
}
System.out.println("秒杀结束,以下20位用户抢得商品:"+redisTemplate.opsForSet().members(key+"_MEMBER"));
}
@Component
public class RedisLua {
private static final String STOCK_LUA;
@Resource
private RedisTemplate redisTemplate;
static {
StringBuilder s = new StringBuilder();
s.append("local isExist = redis.call('exists', KEYS[1]); ");
s.append("if (tonumber(isExist) > 0) then ");
s.append(" local goodsNumber = redis.call('get', KEYS[1]); ");
s.append(" if (tonumber(goodsNumber) > 0) then ");
s.append(" local userExists = redis.call('sismember',KEYS[2],ARGV[1]); ");
s.append(" if tonumber(userExists, 10) == 1 then ");
s.append(" return -2; ");
s.append(" else ");
s.append(" redis.call('decr',KEYS[1]); ");
s.append(" redis.call('sadd',KEYS[2],ARGV[1]); ");
s.append(" return 1; ");
s.append(" end; ");
s.append(" else "
+" redis.call('del', KEYS[1]); ");
s.append(" return 0; ");
s.append(" end; ");
s.append("else ");
s.append(" return -1; ");
s.append(" end;");
STOCK_LUA = s.toString();
}
/**
* 减库存
* @param key
* @return
*/
public Long reduceStock(String key){
List<String> keys = new ArrayList<>();
//商品key
keys.add(key);
//商品和用户key 防止重复秒杀
keys.add(key+"_MEMBER");
List<String> args = new ArrayList<>();
//用户id
args.add(randomUserId());
DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<>(STOCK_LUA, Long.class);
Long result = (Long) redisTemplate.execute(defaultRedisScript, keys, args);
return result;
}
//获取随机字符串
private String randomUserId(){
int length = 10;
Random random = new Random();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append(random.nextInt(10));
}
String randomString = sb.toString();
return randomString;
}
}
十二.自定义注解+AOP+redis实现接口幂等性
1.注解
/**
* 接口幂等性防重复提交
*/
@Target(ElementType.METHOD) // 注解只能用于方法
@Retention(RetentionPolicy.RUNTIME) // 修饰注解的生命周期
@Documented
public @interface Idempotence {
/**
* 防重复操作过期时间,默认1s
*/
long expireTime() default 1;
/**
* key
*/
String key() default "idempotenceKey";
}
2.切面
@Slf4j
@Component
@Aspect
public class IdempotenceAspect {
@Autowired
private RedisTemplate redisTemplate;
//定义切点
@Pointcut("@annotation(com.sise.idempotence.Idempotence)")
public void idempotence(){}
@Around("idempotence()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
//获取注解
Idempotence annotation = method.getAnnotation(Idempotence.class);
//获取token
String token = request.getHeader("Authorization");
String tokenTest = "token test!!!";
if (StringUtils.isEmpty(tokenTest)){
throw new Exception("token不存在,请重新登录");
}
/**
* 通过前缀 + url + token 来生成redis上的 key
* 可以在加上用户id,小编这里没办法获取,大家可以在项目中加上
*/
String url = request.getRequestURI();
String redisKey = annotation.key().concat(url).concat("123456");
//String redisKey = annotation.key().concat(url).concat(MyUtil.randomUserId());
if (!redisTemplate.hasKey(redisKey)){
redisTemplate.opsForValue().set(redisKey,redisKey,annotation.expireTime(), TimeUnit.SECONDS);
try {
//正常执行方法
return joinPoint.proceed();
} catch (Exception e){
throw new Exception();
}
} else {
throw new Exception("请勿重复提交");
}
}
}
3.测试
@Idempotence(expireTime = 10,key = "testKey")
@PostMapping("/test")
public void testIdempotence() {
System.out.println("成功进入接口");
}
缓存里面存了该key
成功验证