我的个人博客Alexios,欢迎大家来吐槽交流。
一、NoSQL
NoSQL = Not Only SQL,不仅仅是SQL,泛指非关系数据库
使用场景:数据变化不是很频繁,访问量相对较大
二、Redis入门
2.1、概述
-
Redis指Remote Dictionary Server,即远程字典服务
-
一个开源的由ANSI C语言编写,支持网络、可基于内存也可持久化的日志型Key-value数据库,支持多种语言
-
redis会周期性将更新的数据写入磁盘或者把修改操作写入追加的记录文件
2.2、特性
- 开源
- 支持多种语言
- 支持持久化、集群和事务
2.3、基础知识
- redis有16个数据库,默认使用第1个数据库(下标为0)
- 使用select可以切换数据库,使用DBSIZE可以查看当前数据库的数据数量
- 清除当前数据库:flushdb
- 清楚所有数据库:flushall
- redis是单线程的,redis的瓶颈是根据机器的内存和网络带宽。
三、五大数据类型
3.1、Redis-key
EXISTS 键名,如果有这个键,返回1,否则返回0
exists 键名
3.2、String(字符串)
1、append 键名 要追加的内容:
往已有的键值对的值中拼接新内容,如果没有该键,那么就新建一个键值对(相当于set)
set name hello
get name
输出hello
append name world
输出10-->返回拼接后值的长度
get name
输出helloworld
- 先设置一个键值对,即name–>hello,然后往该键值对的值中拼接字符world,最后使用get name查看结果
2、strlen 键名 :
获取该键值对中值的长度
strlen name
输出10
3、incr 键名:
针对数值使用,让数值的值+1
- 初始化一个键值对(views–0),然后使用incr 命令让其+1
set views 0
输出0
incr views
get views
输出1
4、decr 键名:
同针对数值使用,让该数值的值-1
- 将上面的views从3减到2
decr views
5、incrby/decrby:
类似incr/decr,多了个步长
- 将views的值从2直接加到12(步长设置为10),然后把views的值从12降到7(步长为5)
incrby views 10
decrby views 5
6、GETRANGE 键名 起始坐标 终止坐标(类似java中String类的substring)
截取(终止坐标 - 起始坐标) + 1个字符,闭区间[起始,终止]
- 设置一个键值对,然后截取该值的一部分
使用GETRANGE 键名 0 -1获取值对应的整个字符串,相当于get 键名
- 获取该键值对值的全部内容
7、SETRANGE 键名 偏移量n 要替换的字符串
替换指定字符开始的字符串
- 先设置一个键值对:key2–>abcdefg,然后偏移一个单位,将xx替换到目标串中
8、setex 键名 过期时间 值
为指定的key设置值和过期时间,如果key已经存在,SETEX命令会替换旧的值
- 先设置一个键值对,然后使用setex覆盖
9、setnx(SET IF NOT EXISTS) 键名 值
当指定的key不存在时,为key设置指定的值,如果存在会覆盖失败,返回0,成功返回1,这个命令经常在分布式锁中用到
- 先设置一个键值对,然后尝试使用setnx覆盖,观察结果,发现返回0,且key的值还是wuhu111
10、mset 键1 值1 键2 值2 …
使用这个命令可以批量添加键值对
- 添加三个键值对
11、mget 键1 键2 …
批量取得值
- 取得上面添加的值
12、msetnx 键1 值1 …
同时插入多个键值对,如果有一个键已经存在,就不执行插入操作,所有键都不存在时才插入
13、设置对象
set user : 1 {username : zhuo,age : 18}
- 使用mset和mget存储和获取对象
127.0.0.1:6379> mset user:1:name hzx user:1:age 18
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "hzx"
2) "18"
14、getset 先取后赋值
由于是先get后set,所以注意结果是上次的值,先返回当前值,后设置新值
- 设置一个键值对k1-v1,然后使用getset将值变为value1,观察
15、String应用场景:
- 由于redis中没有数值类型,所以数字也是用string存储
- 可以用作计数器
- 统计数量
- 对象缓存存储
3.3、List
- 在redis中,我们可以通过设置规则来使list成为一个栈或队列
- 所有list命令大部分以l开头
1、LPUSH 集合名 值
将一个或多个值从左边插进列表
127.0.0.1:6379> LPUSH list one # 在列表左边插入值
(integer) 1
127.0.0.1:6379> LPUSH list two three # 在列表中插入多个值
(integer) 3
127.0.0.1:6379> LRANGE list 0 -1 # 获取列表中所有的值
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> Lrange list 0 1 #获取列表中下标在[0,1]的值
1) "three"
2) "two"
2、RPUSH 集合名 值
将一个或多个值从右边插进列表
127.0.0.1:6379> Rpush list right
(integer) 4
127.0.0.1:6379> rpush list right1 right2
(integer) 6
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "right"
5) "right1"
6) "right2"
3、LPOP 列表名
从左边弹出(移出)一个元素
127.0.0.1:6379> LPOP list
"three"
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
3) "right"
4) "right1"
5) "right2"
4、RPOP 列表名
从右边弹出一个元素
127.0.0.1:6379> RPOP list
"right2"
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
3) "right"
4) "right1"
5、LINDEX 列表名 下标
通过下标获取值
127.0.0.1:6379> LINDEX list 0
"two"
127.0.0.1:6379> LINDEX list 3
"right1"
6、Llen 列表名
获取列表长度
127.0.0.1:6379> LRANGE list 0 -1
1) "two"
2) "one"
3) "right"
4) "right1"
127.0.0.1:6379> Llen list
(integer) 4
7、Lrem 列表名 移除个数 精确值
移除列表中的一个或多个值,精确匹配
127.0.0.1:6379> lrange list 0 -1
1) "one"
2) "one"
3) "one"
4) "one"
5) "two"
6) "one"
7) "right"
8) "right1"
127.0.0.1:6379> lrem list 1 two
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "one"
2) "one"
3) "one"
4) "one"
5) "one"
6) "right"
7) "right1"
127.0.0.1:6379> lrem list 5 one
(integer) 5
127.0.0.1:6379> lrange list 0 -1
1) "right"
2) "right1"
8、ltrim 列表名 起始坐标 结束坐标
截断列表,列表元素变为[start ,end]
127.0.0.1:6379> lrange list 0 -1
1) "hello1"
2) "hello2"
3) "hello3"
4) "hello4"
127.0.0.1:6379> ltrim list 1 2
OK
127.0.0.1:6379> lrange list 0 -1
1) "hello2"
2) "hello3"
9、exists 列表名
判断列表中有几个值
10、LSET 列表名 下标 值
替换列表指定下标的值,如果当前列表中不存在指定下标所对应的值,就报错
127.0.0.1:6379> lrange list 0 -1
1) "value3"
2) "value2"
3) "value1"
4) "hello2"
127.0.0.1:6379> lset list 0 item
OK
127.0.0.1:6379> lrange list 0 -1
1) "item"
2) "value2"
3) "value1"
4) "hello2"
11、LINSERT 列表名 前|后 要插入的位置 要插入的值
在列表的某个值前|后插入一个值
127.0.0.1:6379> rpush list hello
(integer) 1
127.0.0.1:6379> rpush list world
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "hello"
2) "world"
# 在world单词前插入一个值:other
127.0.0.1:6379> linsert list before "world" other
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "hello"
2) "other"
3) "world"
- 往上面list的other元素后插入一个值wuhu
127.0.0.1:6379> linsert list after "other" wuhu
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "hello"
2) "other"
3) "wuhu"
4) "world"
12、小结:
-
list实际上是一个链表,所以可以在节点前后插入节点,也可以在最左端、最右端插入元素
-
如果key不存在,创建新的链表
-
如果key存在,新增内容
-
如果移除了key,那么链表被移除
-
在两边插入或改动值效率最高!操作中间值效率会降低
-
可以用来模拟栈(LPUSH、LPOP)、消息队列(LPUSHR、RPOP)
四、事务
4.1、介绍
Redis事务是一个单独的隔离过程:事务中的所有命令都会被序列化、按顺序地执行。事务在执行过程中不会被其他客户端发送来的命令请求所打断,redis事务的主要作用就是串联多个命令防止别的命令插队。
4.2、Redis事务的特性
单独的隔离操作:
- 事务中的所有命令都会被序列化、按顺序地执行。事务在执行过程中不会被其他客户端发送来的命令请求所打断
没有隔离级别的概念
- 队列中的命令没有提交之前都不会实际的被执行。
不保证原子性
- Redis同一个事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
4.3、使用
- 开启事务(multi)
- 命令入队
- 执行事务(exec)
正常执行事务
- 使用redis事务
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set k1 v1 # QUEUED代表命令入队列
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> get k1
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> set k4 1
QUEUED
127.0.0.1:6379> incr k4
QUEUED
127.0.0.1:6379> exec # exec代表执行事务
1) OK
2) OK
3) "v1"
4) OK
5) OK
6) (integer) 2
放弃事务(discard)
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 1
QUEUED
127.0.0.1:6379> incrby k3 50
QUEUED
127.0.0.1:6379> decrby k3 26
QUEUED
127.0.0.1:6379> DISCARD
OK
127.0.0.1:6379> get k3
(nil) #放弃事务后发现k3值为null
(报告错误)编译型异常(代码/命令有问题),事务中所有命令都不会被执行
(执行错误)运行时异常(例如:1/0或空指针),如果事务队列中存在运行时异常,那么这条发生异常的命令不会被执行,其他命令继续执行(redis事务没有一致性),错误命令抛出异常
原子性:指事务的不可分割性,一个事务的所有操作要么不间断地全部被执行,要么一个也没有执行。
4.4、注意点
Redis单条命令是保证原子性的,但是Redis的事务是不保证原子性的!
4.5、监控
1、悲观锁
- 无论做什么都加锁(会影响性能)
2、乐观锁
- 认为什么时候都不会出问题,所以不会上锁!更新数据时去判断以下期间有没有人修改过此数据。
- mysql中:获取version,比较version
3、使用watch来监控
在执行multi之前,先执行watch key1 [key2],可以监视一个或多个key,如果在事务执行之前这个(或这些)key被其他命令所改动,那么事务将被打断
- 正常执行成功
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> watch money #使用watch监视money对象
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby money 20
QUEUED
127.0.0.1:6379> incrby out 20
QUEUED
127.0.0.1:6379> exec
1) (integer) 80
2) (integer) 20
五、Jedis
JRedis是Redis官方推荐的java连接开发工具,使用java操作redis的中间件,类似JDBCDriver
5.1、常用API
1、导入依赖
<!-- 导入Jedis的包 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
2、编码测试
使用Jedis连接Redis
public class TestPing {
public static void main(String[] args) {
//1 new 一个Jedis对象,构造参数填入地址和端口号
Jedis jedis = new Jedis("127.0.0.1", 6379);
//jedis 的函数就是我们之前学习的所有命令
System.out.println(jedis.ping());
}
}
结果
3、操作命令
Jedis的方法和上面的命令几乎一样,这里列出Redis-key的
Jedis jedis = new Jedis("127.0.0.1",6379);
System.out.println("清空指定数据库中的数据:" + jedis.flushDB());
System.out.println("清空所有数据:" + jedis.flushAll());
System.out.println("新增<'username','wuhu'>的键值对:" + jedis.set("username","wuhu"));
System.out.println("新增<'password','qifei'>的键值对:" + jedis.set("password","qifei"));
System.out.print("系统中所有的键如下:");
Set<String> keys = jedis.keys("*");
System.out.println(keys);
System.out.println("删除键password:" + jedis.del("password"));
System.out.println("判断键password是否存在:" + jedis.exists("password"));
System.out.println("查看键username所存储的值的类型" + jedis.type("username"));
System.out.println("随机返回key空间的一个" + jedis.randomKey());
System.out.println("重命名key:" + jedis.rename("username","userName"));
System.out.println("取出改后的userName:" + jedis.get("userName"));
System.out.println("按索引查询:" + jedis.select(0));
System.out.println("返回当前数据库中key的数量:" + jedis.dbSize());
System.out.println("删除当前选择数据库中所有的key:" + jedis.flushDB());
System.out.println("返回当前数据库中key的数量:" + jedis.dbSize());
System.out.println("删除所有数据库中那个所有key:" + jedis.flushAll());
结果
断开连接
jedis.close();
5.2、使用Jedis模拟Redis中的事务
1、模拟事务成功执行
Jedis jedis = new Jedis("127.0.0.1", 6379);
JSONObject jsonObject = new JSONObject();
jsonObject.put("name","wuhu");
jsonObject.put("age",18);
String jsonStr = jsonObject.toJSONString();
//开启事务
Transaction multi = jedis.multi();
try {
//将方法放入执行队列
multi.set("user1",jsonStr);
multi.set("user2",jsonStr);
multi.set("user3",jsonStr);
multi.set("user4",jsonStr);
//执行事务
multi.exec();
} catch (Exception e) {
//如果出现异常,就放弃事务
multi.discard();
e.printStackTrace();
} finally {
//如果没有异常,那么可以输出值
System.out.println(jedis.get("user1"));
System.out.println(jedis.get("user2"));
System.out.println(jedis.get("user3"));
System.out.println(jedis.get("user4"));
jedis.close();//关闭连接
}
结果
2、模拟事务出现异常的情况
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.flushAll();//先清除所有键值对
JSONObject jsonObject = new JSONObject();
jsonObject.put("name","wuhu");
jsonObject.put("age",18);
String jsonStr = jsonObject.toJSONString();
//开启事务
Transaction multi = jedis.multi();
try {
//将方法放入执行队列
multi.set("user1",jsonStr);
multi.set("user2",jsonStr);
multi.set("user3",jsonStr);
multi.set("user4",jsonStr);
//模拟异常
int i = 1 / 0;
//执行事务
multi.exec();
} catch (Exception e) {
//如果出现异常,就放弃事务
multi.discard();
e.printStackTrace();
} finally {
//如果没有异常,那么可以输出值
System.out.println(jedis.get("user1"));
System.out.println(jedis.get("user2"));
System.out.println(jedis.get("user3"));
System.out.println(jedis.get("user4"));
jedis.close();//关闭连接
}
结果
六、Spring Boot整合Redis
Spring Boot Data redis提供了RedisTemplate和StringTemplate,其中StringTemplate是RedisTemplate的子类,RedisTemplate中两个泛型都是Object,意味着存储的key和value都可以是一个对象,而StringTemplate两个泛型都是String
注:对象的保存需要序列化
6.1、引入依赖
<!-- 操作redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 在springboot2.x后,原来的jedis被替换为了lecture
- jedis:采用直连,多线程操作不安全,需要使用连接池,BIO
- lecture:底层使用netty,不存在性能不安全的情况,NIO
6.2、配置
spring:
redis:
host: 127.0.0.1
port: 6379
6.3、测试
分析源码可知,Spring Boot已经为我们注册了一个key和value均为Object的RedisTemplate,我们可以自己注册一个RedisTemplate来替换Spring Boot为我们自动注入的RedisTemplate
@Test
void contextLoads() {
//redisTemplate
//opsForValue 操作字符串,类似String
//opsForList 操作list,类似List
//opsForSet 操作Set,类似Set
//opsForZset 操作Zset,类似Zset
//opsForHash 操作Hash,类似Hash
//基本操作已经封装到redisTemplate中
// RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
// connection.flushAll();
// connection.flushDb();
redisTemplate.opsForValue().set("key","value");
System.out.println(redisTemplate.opsForValue().get("key"));
}
Spring Boot 默认使用JDK序列化,JDK序列化会使字符转义,我们可以自定义配置类,使用JSON进行序列化。
6.4、自定义RedisTemplate
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
public class RedisConfig {
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
在自定义的RedisTemplate中,将键从Object换为String,同时使用JSON序列化对象。
@Test
void contextLoads() {
User user = new User(1,"芜湖","起飞");
String userJson = JSON.toJSONString(user);
redisTemplate.opsForValue().set("user2",userJson);
}
1、使用JDK序列化对象
2、使用JSON序列化对象
删除Redis中的键值对,引入我们自定义的RedisTemplate,重新执行上面的方法。
6.5、RedisUtils
package com.hzx.springboot_redis.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Component
public final class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// =============================common============================
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
* @param key 键
* @param delta 要增加几(大于0)
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
* @param key 键
* @param delta 要减少几(小于0)
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* HashGet
* @param key 键 不能为null
* @param item 项 不能为null
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
* @param key 键
* @param map 对应多个键值
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
* @param key 键
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0)
expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
七、Redis.conf配置文件
7.1、网络
bind 127.0.0.1 # 绑定的ip
protected-mode yes # 保护模式,默认开启
port 6379 # 端口设置
7.2、快照
1、持久化
在规定时间内执行了多少操作,就会持久到文件(.rdb、.aof)
由于redis是内存数据库,如果没有持久化,那么数据断电即失
# 如果900s内至少有一个key进行了修改,我们就进行持久化操作
save 900 1
# 如果300s内至少有10个key进行了修改,我们就进行持久化操作
save 300 10
# 如果60s内至少有一万个key进行了修改,我们就进行持久化操作
save 60 10000
# 之后我们可以设置自己的
2、限制Clients
maxclients 10000 # 最多有10000个客户端能连接上此redis
maxmemory <bytes> # redis设置最大内存容量
3、内存达到上限的处理策略
maxmemory-policy noeviction # 内存到达上限后的处理策略,默认不删除数据,直接报错
有六种策略:
1 volatile-lru:从已设置过期时间的内存数据集中挑选最近最少使用的数据 淘汰;
2 volatile-ttl: 从已设置过期时间的内存数据集中挑选即将过期的数据 淘汰;
3 volatile-random:从已设置过期时间的内存数据集中任意挑选数据 淘汰;
4 allkeys-lru:从内存数据集中挑选最近最少使用的数据 淘汰;
5 allkeys-random:从数据集中任意挑选数据 淘汰;
6 noenviction(驱逐):禁止驱逐数据。
八、持久化
Redis是一个内存数据库,如果不对Redis数据进行持久化操作,那么一旦服务器进程退出,服务器中的数据库状态也会消失。
Redis的持久化可分为RDB(Redis DataBase)和AOF(Append Only File)
8.1、RDB
1、什么是RDB
在指定时间间隔将内存集的数据集快照写入磁盘,恢复时将快照文件直接读入内存。在进行写入的过程中,Redis会单独创建一个子进程,会先将数据写入一个临时文件中,待持久化过程结束后,再将这个临时文件替换上次持久化好的文件。整个过程中,主线程不进行任何IO操作,确保了极高的性能。
如果要进行大规模的数据恢复,且对于数据恢复完整性并不是特别敏感,则RDB方式要比AOF方式更加高效,Rdb的缺点是最后一次持久化后的数据可能丢失,Redis默认使用RDB进行持久化操作。
2、RDB保存的文件是dump.rdb
3、优点:
- 适合大规模数据恢复
- 对数据完整性要求不高
4、缺点:
- 需要一定的时间间隔进程操作,如果redis意外宕机,最后一次修改数据会消失
- fork进程时会占用一定空间
5、触发机制
- save的规则满足的条件下,会自动触发RDB规则
- 执行flashall命令,会触发RDB规则
- 退出redis,也会产生rdb文件
- 备份生成一个dump.rdb文件
6、如何恢复rdb文件
只需要将rdb文件放在redis启动目录下即可,redis启动时会检查并恢复dump.rdb中的数据
8.2、AOF
1、什么是AOF?
以日志的形式来记录每个写操作,将Redis执行过的所有指令**(读操作不记录)记录下来,只许追加文件但不可以改写文件,redis启动之初会读取文件重新构建数据,换言之,redis重启的话就要根据日志文件的内容将写指令从头到尾执行一次以完成数据的恢复工作**
2、使用
appendonly yes
九、Redis实现手机验证码
9.1、实现原理
- 使用工具类生成4位或6位的数字验证码
- 校验手机号为合法后通过短信微服务发送验证码
- 将手机号作为key,验证码作为value存入redis中,并设置一个过期时间
- 用户进行登录/注册时通过key(手机号)到redis中取出验证码
- 进行验证码比对,若匹配则登录/注册通过,随机删除redis中的key-value
9.2、实现
1、校验手机号是否合法的工具类
public class FormUtils {
private static Pattern NUMBER_PATTERN = Pattern.compile("^[1][3,4,5,7,8,9][0-9]{9}$");
/**
* 手机号验证
*/
public static boolean isMobile(String str) {
//验证手机号的正则表达式
return NUMBER_PATTERN.matcher(str).matches();
}
}
2、生成验证码的工具类
public class RandomUtils {
private static final Random random = new Random();
private static final DecimalFormat fourdf = new DecimalFormat("0000");
private static final DecimalFormat sixdf = new DecimalFormat("000000");
public static String getFourBitRandom() {
return fourdf.format(random.nextInt(10000));
}
public static String getSixBitRandom() {
return sixdf.format(random.nextInt(1000000));
}
/**
* 给定数组,抽取n个数据
* @param list
* @param n
* @return
*/
public static ArrayList getRandom(List list, int n) {
Random random = new Random();
HashMap<Object, Object> hashMap = new HashMap<Object, Object>();
// 生成随机数字并存入HashMap
for (int i = 0; i < list.size(); i++) {
int number = random.nextInt(100) + 1;
hashMap.put(number, i);
}
// 从HashMap导入数组
Object[] robjs = hashMap.values().toArray();
ArrayList r = new ArrayList();
// 遍历数组并打印数据
for (int i = 0; i < n; i++) {
r.add(list.get((int) robjs[i]));
System.out.print(list.get((int) robjs[i]) + "\t");
}
System.out.print("\n");
return r;
}
}
3、短信微服务Controller
@Slf4j
@CrossOrigin
@RestController
@Api(tags = "短信管理控制器")
@RequestMapping("/api/sms")
public class ApiSmsController {
@Autowired
private SmsService smsService;
@Autowired
private RedisTemplate redisTemplate;
/***
* 根据传入的手机号来生成并发送验证码
* 并将验证码存入redis缓存
* @param mobile 手机号
* @return
*/
@GetMapping("send/{mobile}")
@ApiOperation("一个用于生成验证码,并将验证码存入redis中的接口")
public R getCode(@PathVariable("mobile") String mobile) throws ClientException {
//0 校验手机号是否合法
if(StringUtils.isBlank(mobile) || !FormUtils.isMobile(mobile)) {
//如果手机号为空或手机号不合法
log.error("手机号不合法!");
// new GrainException(ResultCodeEnum.LOGIN_MOBILE_ERROR);
return R.error().message("手机号不正确!").code(28001);
}
//1 使用工具类生成生成验证码
String checkCode = RandomUtils.getSixBitRandom();
//2 发送验证码
smsService.send(mobile,checkCode);
//3 存储验证码到redis
//使用redisTemplate,使用手机号作为键,保存时长为5分钟
redisTemplate.opsForValue().set(mobile,checkCode,5, TimeUnit.MINUTES);
return R.ok().message("短信发送成功!");
}
}
4、短信微服务Service层
@Slf4j
@Service
public class SmsServiceImpl implements SmsService {
@Autowired
private SmsProperties smsProperties;
@Override
public void send(String mobile, String checkCode) throws ClientException, ClientException {
//调用短信发送SDK,创建client对象
DefaultProfile profile = DefaultProfile.getProfile(
smsProperties.getRegionId(),
smsProperties.getKeyId(),
smsProperties.getKeySecret());
IAcsClient client = new DefaultAcsClient(profile);
//组装请求参数
CommonRequest request = new CommonRequest();
request.setSysMethod(MethodType.POST);
request.setSysDomain("dysmsapi.aliyuncs.com");
request.setSysVersion("2017-05-25");
request.setSysAction("SendSms");
request.putQueryParameter("RegionId", smsProperties.getRegionId());
request.putQueryParameter("PhoneNumbers", mobile);
request.putQueryParameter("SignName", smsProperties.getSignName());
request.putQueryParameter("TemplateCode", smsProperties.getTemplateCode());
Map<String, Object> param = new HashMap<>();
param.put("code", checkCode);
//将包含验证码的集合转换为json字符串
Gson gson = new Gson();
request.putQueryParameter("TemplateParam", gson.toJson(param));
//发送短信
CommonResponse response = client.getCommonResponse(request);
//得到json字符串格式的响应结果
String data = response.getData();
//解析json字符串格式的响应结果
HashMap<String, String> map = gson.fromJson(data, HashMap.class);
String code = map.get("Code");
String message = map.get("Message");
//配置参考:短信服务->系统设置->国内消息设置
//错误码参考:
//https://help.aliyun.com/document_detail/101346.html?spm=a2c4g.11186623.6.613.3f6e2246sDg6Ry
//控制所有短信流向限制(同一手机号:一分钟一条、一个小时五条、一天十条)
if ("isv.BUSINESS_LIMIT_CONTROL".equals(code)) {
log.error("短信发送过于频繁 " + "【code】" + code + ", 【message】" + message);
throw new GrainException(ResultCodeEnum.SMS_SEND_ERROR_BUSINESS_LIMIT_CONTROL);
}
if (!"OK".equals(code)) {
log.error("短信发送失败 " + " - code: " + code + ", message: " + message);
throw new GrainException(ResultCodeEnum.SMS_SEND_ERROR);
}
}
}
5、用户注册微服务Controller
@Slf4j
@CrossOrigin
@RestController
@Api(tags = "会员管理控制器")
@RequestMapping("/api/ucenter/member")
public class ApiMemberController {
@Autowired
private MemberService memberService;
@ApiOperation("会员注册")
@PostMapping("register")
public R register(@RequestBody RegisterVo registerVo) {
memberService.register(registerVo);
return R.ok().message("注册成功!");
}
}
6、用户注册微服务Service
@Service
public class MemberServiceImpl extends ServiceImpl<MemberMapper, Member> implements MemberService {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private MemberMapper memberMapper;
@Override
public void register(RegisterVo registerVo) {
String nickname = registerVo.getNickname();
String mobile = registerVo.getMobile();
String code = registerVo.getCode();
String password = registerVo.getPassword();
if(StringUtils.isBlank(mobile) || !FormUtils.isMobile(mobile)) {
throw new GrainException(ResultCodeEnum.LOGIN_MOBILE_ERROR);
}
if(StringUtils.isBlank(nickname) || StringUtils.isBlank(password) || StringUtils.isBlank(code)) {
throw new GrainException(ResultCodeEnum.PARAM_ERROR);
}
//校验验证码
String checkCode = (String)redisTemplate.opsForValue().get(mobile);
if(!StringUtils.equals(code,checkCode)) {
//如果用户输入验证码和redis中验证码不相等
//校验失败
throw new GrainException(ResultCodeEnum.CODE_ERROR);
}
//判断用户手机号是否注册
QueryWrapper<Member> memberQueryWrapper = new QueryWrapper<>();
memberQueryWrapper.eq("mobile",mobile);
Integer result = memberMapper.selectCount(memberQueryWrapper);
if(result > 0) {
throw new GrainException(ResultCodeEnum.REGISTER_MOBLE_ERROR);
}
//注册
Member member = new Member();
member.setNickname(nickname);
member.setMobile(mobile);
//密码需要加密
member.setPassword(MD5.encrypt(password));
member.setAvatar("http://tiebapic.baidu.com/forum/w%3D580/sign=21e19fd45010b912bfc1f6f6f3fcfcb5/c0d5d04b20a44623e5077d0d8f22720e0ef3d78e.jpg");
member.setDisabled(false);
memberMapper.insert(member);
//将redis缓存中的验证码删除
redisTemplate.delete(mobile);
}
}
9.3、测试
启动短信微服务和用户注册微服务,使用Swagger进行测试
1、使用短信微服务向指定手机号发送验证码
2、查看Redis缓存和短信
短信
3、打开用户中心微服务进行测试
4、结果
swagger提示注册成功
redis缓存中该键值对被删除
数据库