NoSQL 简述
NoSQL(Not Only SQL),是对不同于传统的关系型数据库的数据库管理系统的统称。与传统的关系型数据库不同,NoSQL数据库不保证关系型数据库的ACID特性。然而,它们具有灵活的架构,专门用于特定的数据模型,具有易扩展、高可用、大数据量、灵活的数据模型等特点。
NoSQL 数据库根据存储的数据类型分为以下几种:
- **键值存储数据库:**所有的数据都是以键值方式存储的,类似于HashMap,使用起来非常简单方便,性能也非常高。
- **列存储数据库:**这部分数据库通常是用来应对分布式存储的海量数据。键仍然存在,但是它们的特点是指向了多个列。
- **文档型数据库:**它是以一种特定的文档格式存储数据,比如JSON格式,在处理网页等复杂数据时,文档型数据库比传统键值数据库的查询效率更高。
- **图形数据库:**利用类似于图的数据结构存储数据,结合图相关算法实现高速访问。
Redis数据库即是一个开源的键值存储数据库,所有的数据全部存放在内存中,它的性能大大高于磁盘IO,并且它也可以支持数据持久化,它还支持横向扩展、主从复制等。
基本操作
Redis下,数据库是由一个整数索引标识,而不是由一个数据库名称。 默认情况下,用户连接Redis数据库之后,会使用0号数据库,用户可以通过Redis配置文件中的参数来修改数据库总数,默认为16个。
SELECT 数据库编号
数据操作
增删改查:
-- 向数据库中添加数据(存入数据默认以字符串形式保存,多次设定同一个值时会覆盖)
set <key> <value>
-- 一次性添加多个数据
mset [<key> <value>] ...
-- 根据键获取存入的值
get <key>
-- 查看数据库中所有键
keys *
-- 查询某个键是否存在
exists <key>...
-- 删除一个或多个数据
del <key> ...
-- 键值具有一定的命名规范,以便快速定位数据属于哪个部分,比如用户数据:
set user:info:20272110:sex male
get user:info:20272110:sex
其它操作:
-- 设定数据过期时间(过期则自动删除)
set <key> <value> EX 秒
set <key> <value> PX 毫秒
-- 单独为其他的键值对设置过期时间
expire <key> 秒
-- 查看键值对过期时间还剩多久
ttl <key>
-- 毫秒显示
pttl <key>
-- 转换为永久
persist <key>
-- 将一个数据库中的内容移动到另一个数据库
move <key> 数据库序号
-- 重命名键(对应键已存在时值会覆盖)
rename <key> <新的名称>
-- 下面这个会检查新的名称是否已经存在
renamex <key> <新的名称>
-- 查看值的数据类型
type <key>
-- 如果存放的数据为一个数字(存放类型仍为字符串),则可以执行加减操作
-- 等价于a = a + 1
incr <key>
-- 等价于a = a + b
incrby <key> b
-- 等价于a = a - 1
decr <key>
-- 等价于a = a - b
decrby <key> b
-- 查询某个键是否存在
exists <key>...
-- 随机获取一个键
randomkey
数据类型
Hash
本质上就相当于 HashMap 中嵌套了一个 HashMap,类似于:
#Redis默认存String类似于这样:
Map<String, String> hash = new HashMap<>();
#Redis存Hash类型的数据类似于这样:
Map<String, Map<String, String>> hash = new HashMap<>();
数据操作:
-- 添加一个 Hash 类型的数据(同一字段不同值时会覆盖)
hset <key> [<字段> <值>]...
-- 根据字段获取对应的值
hget <key> <字段>
-- 如果想要一次性获取所有的字段和值
hgetall <key>
-- 删除 Hash 中的某个字段
hdel <key> <字段>
-- 删除 Hash
del <key>
-- 查询Hash 中存放了多少个键值对,注意key为最外层的key
hlen <key>
-- 一次性获取所有字段
hkeys <key>
-- 一次性获取所有字段的值
hvals <key>
-- 判断某个字段是否存在
hexists <key> <字段>
List
Redis中的list数据类型是一种有序的集合,它可以存储任意类型的数据。在Redis中,list是一个双向链表,可以在两端进行插入和删除操作。
添加数据:
-- 向列表头部添加元素
lpush <key> <element>...
-- 向列表尾部添加元素
rpush <key> <element>...
-- 在指定元素前面/后面插入元素
linsert <key> before/after <指定元素> <element>
数据操作:
-- 根据下标获取元素
lindex <key> <下标>
-- 获取并移除头部元素
lpop <key>
-- 获取并移除尾部元素
rpop <key>
-- 获取指定范围内的
lrange <key> start stop
-- 注意下标可以使用负数来表示从后到前数的数字
-- 获取列表a中的全部元素
lrange a 0 -1
-- push和pop操作也可以混合使用
-- 从前一个数组的最后取一个数出来放到另一个数组的头部,并返回元素
rpoplpush 当前数组 目标数组
-- 列表同时还支持阻塞操作,类似于生产者和消费者,比如我们想要等待列表中有了数据后再进行pop操作:
-- 如果列表中没有元素,那么就等待,如果指定时间(秒)内被添加了数据,那么就执行pop操作,
-- 如果超时就作废,支持同时等待多个列表,只要其中一个列表有元素了,那么就能执行
blpop <key>... timeout
-- 删除列表
del <key>
Set和SortedSet
Set集合类似于Java中的HashSet(HashSet本质上就是利用了一个HashMap,但是Value都是固定对象,仅仅是Key不同),它不允许出现重复元素,不支持随机访问,但是能够利用Hash表提供极高的查找效率。
集合操作:
-- 向set中添加一个或多个值
sadd <key> <value>...
-- 查看集合中有多少个值
scard <key>
-- 是否包含指定值
sismember <key> <value>
-- 列出所有值
smembers <key>
-- 随机移除一个值
spop <key>
-- 移除指定值
srem <key> <value>...
-- 移动指定值到另一个集合中
smove <key> 目标 value
集合运算:
-- 集合之间的差集
sdiff <key1> <key2>
-- 集合之间的交集
sinter <key1> <key2>
-- 求并集
sunion <key1> <key2>
-- 将集合之间的差集存到目标集合中
sdiffstore 目标 <key1> <key2>
-- 同上
sinterstore 目标 <key1> <key2>
-- 同上
sunionstore 目标 <key1> <key2>
如果需要集合中的值按照指定顺序排列,则可以使用SortedSet,集合中的每个值根据设定的分值大小排序(默认升序排序):
-- 添加一个或多个带分数的值
zadd <key> [<value> <score>]...
-- 查询有多少个值
zcard <key>
-- 移除
zrem <key> <value>...
-- 获取区间内的所有(升序)
zrange <key> start stop
-- 获取区间内的所有(降序)
zrevrange <key> start stop
或者根据分数段来获取值
-- 通过分数段查看
zrangebyscore <key> start stop [withscores] [limit]
-- 统计分数段内的数量
zcount <key> start stop
-- 根据分数获取指定值的排名
zrank <key> <value>
持久化
将内存的数据写入到磁盘中,防止服务器宕机内存数据丢失。
RDB
直接保存当前已经存储的数据,相当于复制内存中的数据到硬盘上,需要恢复数据时直接读取即可。
-- 直接保存,会占用一定的时间
save
-- 单独开一个子进程后台执行保存
bgsave
在配置文件中设置自动保存(bgsave后台执行)
save 300 10 # 300秒(5分钟)内有10个写入
save 60 10000 # 60秒(1分钟)内有10000个写入
AOF
保存存放数据的所有过程,需要恢复数据时,只需要将整个过程完整地重演一遍就能保证与之前数据库中的内容一致。
对应保存策略有三种:
- always:每次执行写操作都会保存一次
- everysec:每秒保存一次(默认配置),这样就算丢失数据也只会丢一秒以内的数据
- no:看系统心情保存
在配置文件中设置保存策略:
# 注意得改成也是
appendonly yes
# appendfsync always
appendfsync everysec
# appendfsync no
手动执行重写操作:
bgrewriteaof
-- 配置自动重写策略
# 百分比计算
auto-aof-rewrite-percentage 100
# 当达到这个大小时,触发自动重写
auto-aof-rewrite-min-size 64mb
事务和锁
事务
当需要保证多条命令一次性完整执行而中途不受到其他命令干扰时,就可以使用事务机制。
-- 使用命令来直接开启事务
multi
-- 使用命令来立即执行事务
exec
-- 中途取消事务
discard
锁
虽然Redis中也有锁机制,但是它是一种乐观锁。Redis中可以使用watch来监视一个目标,如果执行事务之前被监视目标发生了修改,则取消本次事务:
-- 开启监视
watch
-- 取消监视
unwatch
Redis交互
使用Java与Redis 交互
首先引入依赖:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>5.0.0</version>
</dependency>
基本操作:
public static void main(String[] args) {
try(Jedis jedis = new Jedis("localhost", 6379);){
jedis.set("testKey", "testVal");
System.out.println(jedis.get("testKey"));
jedis.hset("testMap", "ab", "32");
jedis.hset("testMap", "ac", "38");
jedis.hgetAll("testMap").forEach((k, v) -> System.out.println(k + ": " + v));
jedis.lpush("testList", "22", "555", "6666");
jedis.lrange("testList", 0, -1).forEach(System.out::println);
}
}
SpringBoot 整合 Redis
引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
对Redis 进行相关配置(tarter默认会去连接本地的Redis服务器,并使用0号数据库):
spring:
data:
redis:
host: 127.0.0.1
port: 6379
database: 0
starter提供了两个默认的模板类:
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
public RedisAutoConfiguration() {
}
@Bean
@ConditionalOnMissingBean(
name = {"redisTemplate"}
)
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
return new StringRedisTemplate(redisConnectionFactory);
}
}
直接注入对应模板即可使用(所有的值的操作都被封装到了ValueOperations
对象中,而普通的键操作直接通过模板对象就可以使用了,大致使用方式其实和Jedis一致):
@Resource
StringRedisTemplate template;
@Test
void contextLoads() {
ValueOperations<String, String> operations = template.opsForValue();
operations.set("test1", "value1");
System.out.println(operations.get("test1"));
template.delete("test1");
System.out.println(template.hasKey("test1"));
由于Spring没有专门的Redis事务管理器,所以只能借用JDBC提供的:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
@Service
public class RedisService {
@Resource
StringRedisTemplate template;
@PostConstruct
public void init(){
template.setEnableTransactionSupport(true); //需要开启事务
}
@Transactional //需要添加此注解
public void test(){
template.multi();
template.opsForValue().set("d", "xxxxx");
template.exec();
}
}
使用Redis 做缓存
Mbatis二级缓存
Mybatis的二级缓存是Mapper级别的缓存,能够作用于所有会话。但是由于Mybatis的默认二级缓存只能是单机的,如果存在多台服务器访问同一个数据库,实际上二级缓存只会在各自的服务器上生效,但是我们希望的是多台服务器都能使用同一个二级缓存,这样就不会造成过多的资源浪费。
我们可以将Redis作为Mybatis的二级缓存,这样就能实现多台服务器使用同一个二级缓存,因为它们只需要连接同一个Redis服务器即可,所有的缓存数据全部存储在Redis服务器上。我们需要手动实现Mybatis提供的Cache接口,首先编写缓存类:
//实现Mybatis的Cache接口
public class RedisMybatisCache implements Cache {
private final String id;
private static RedisTemplate<Object, Object> template;
//注意构造方法必须带一个String类型的参数接收id
public RedisMybatisCache(String id){
this.id = id;
}
//初始化时通过配置类将RedisTemplate给过来
public static void setTemplate(RedisTemplate<Object, Object> template) {
RedisMybatisCache.template = template;
}
@Override
public String getId() {
return id;
}
@Override
public void putObject(Object o, Object o1) {
//这里直接向Redis数据库中丢数据即可,o就是Key,o1就是Value,60秒为过期时间
template.opsForValue().set(o, o1, 60, TimeUnit.SECONDS);
}
@Override
public Object getObject(Object o) {
//这里根据Key直接从Redis数据库中获取值即可
return template.opsForValue().get(o);
}
@Override
public Object removeObject(Object o) {
//根据Key删除
return template.delete(o);
}
@Override
public void clear() {
//由于template中没封装清除操作,只能通过connection来执行
template.execute((RedisCallback<Void>) connection -> {
//通过connection对象执行清空操作
connection.flushDb();
return null;
});
}
@Override
public int getSize() {
//这里也是使用connection对象来获取当前的Key数量
return template.execute(RedisServerCommands::dbSize).intValue();
}
}
接着编写配置类:
@Configuration
public class MainConfiguration {
@Resource
RedisTemplate<Object, Object> template;
@PostConstruct
public void init(){
//把RedisTemplate给到RedisMybatisCache
RedisMybatisCache.setTemplate(template);
}
}
最后在Mapper上启用此缓存即可:
//只需要修改缓存实现类implementation为我们的RedisMybatisCache即可
@CacheNamespace(implementation = RedisMybatisCache.class)
@Mapper
public interface MainMapper {
@Select("select name from student where sid = 1")
String getSid();
}
查看当前的二级缓存是否生效:
@SpringBootTest
class SpringBootTestApplicationTests {
@Resource
MainMapper mapper;
@Test
void contextLoads() {
System.out.println(mapper.getSid());
System.out.println(mapper.getSid());
System.out.println(mapper.getSid());
}
}
Token持久化存储
SpringSecurity remember-me 功能的 Token是支持持久化存储的,之前是存储在数据库中,现在来尝试将Token信息存储在缓存中:
//实现PersistentTokenRepository接口
@Component
public class RedisTokenRepository implements PersistentTokenRepository {
//Key名称前缀,用于区分
private final static String REMEMBER_ME_KEY = "spring:security:rememberMe:";
@Resource
RedisTemplate<Object, Object> template;
@Override
public void createNewToken(PersistentRememberMeToken token) {
//这里要放两个,一个存seriesId->Token,一个存username->seriesId,因为删除时是通过username删除
template.opsForValue().set(REMEMBER_ME_KEY+"username:"+token.getUsername(), token.getSeries());
template.expire(REMEMBER_ME_KEY+"username:"+token.getUsername(), 1, TimeUnit.DAYS);
this.setToken(token);
}
//先获取,然后修改创建一个新的,再放入
@Override
public void updateToken(String series, String tokenValue, Date lastUsed) {
PersistentRememberMeToken token = this.getToken(series);
if(token != null)
this.setToken(new PersistentRememberMeToken(token.getUsername(), series, tokenValue, lastUsed));
}
@Override
public PersistentRememberMeToken getTokenForSeries(String seriesId) {
return this.getToken(seriesId);
}
//通过username找seriesId直接删除这两个
@Override
public void removeUserTokens(String username) {
String series = (String) template.opsForValue().get(REMEMBER_ME_KEY+"username:"+username);
template.delete(REMEMBER_ME_KEY+series);
template.delete(REMEMBER_ME_KEY+"username:"+username);
}
private void setToken(PersistentRememberMeToken token){
Map<String, String> map = new HashMap<>();
map.put("username", token.getUsername());
map.put("series", token.getSeries());
map.put("tokenValue", token.getTokenValue());
map.put("date", ""+token.getDate().getTime());
template.opsForHash().putAll(REMEMBER_ME_KEY+token.getSeries(), map);
template.expire(REMEMBER_ME_KEY+token.getSeries(), 1, TimeUnit.DAYS);
}
//由于PersistentRememberMeToken没实现序列化接口,这里只能用Hash来存储了,所以单独编写一个set和get操作
private PersistentRememberMeToken getToken(String series){
Map<Object, Object> map = template.opsForHash().entries(REMEMBER_ME_KEY+series);
if(map.isEmpty()) return null;
return new PersistentRememberMeToken(
(String) map.get("username"),
(String) map.get("series"),
(String) map.get("tokenValue"),
new Date(Long.parseLong((String) map.get("date"))));
}
}
接着实现验证Service:
@Service
public class AuthorizeService implements UserDetailsService {
@Resource
UserMapper mapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDto account = mapper.getUserByName(username);
if(account == null)
throw new UsernameNotFoundException("用户名或密码错误");
return User
.withUsername(username)
.password(account.getPassword())
.roles(account.getRole())
.build();
}
}
实现实体类和对应Mapper文件:
@Data
@Accessors(chain = true)
public class UserDto implements Serializable {
private Integer id;
private String username;
private String password;
private String role;
}
@Mapper
public interface UserMapper {
@Select("select * from user where username = #{username} ")
UserDto getUserByName(String username);
}
最后更新配置文件:
spring:
datasource:
url: jdbc:mysql://localhost:3306/test
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
mvc:
static-path-pattern: /static/**
thymeleaf:
# 关闭缓存,能让改动的页面及时生效,实现类似热部署效果
cache: false
data:
redis:
host: localhost
port: 6379
database: 0
@Configuration
public class SecurityConfig {
@Resource
RedisTokenRepository tokenRepository;
@Bean
public PasswordEncoder passwordEncoder(){
System.out.println(new BCryptPasswordEncoder().encode("123"));
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
//以下是验证请求拦截和放行配置
.authorizeHttpRequests(auth -> {
auth.requestMatchers("/static/**").permitAll(); //将所有的静态资源放行,一定要添加在全部请求拦截之前
auth.anyRequest().authenticated(); //将所有请求全部拦截,一律需要验证
})
//以下是表单登录相关配置
.formLogin(conf -> {
conf.loginPage("/login"); //将登录页设置为我们自己的登录页面
conf.loginProcessingUrl("/doLogin"); //登录表单提交的地址,可以自定义
conf.defaultSuccessUrl("/index"); //登录成功后跳转的页面
conf.failureForwardUrl("/"); // 登录失败
conf.permitAll(); //将登录相关的地址放行,否则未登录的用户连登录界面都进不去
})
//以下是退出登录相关配置
.logout(conf -> {
conf.logoutUrl("/doLogout"); //退出登录地址,跟上面一样可自定义
conf.logoutSuccessUrl("/login"); //退出登录成功后跳转的地址,这里设置为登录界面
conf.permitAll();
})
//以下是记住我功能相关配置
.rememberMe(conf -> {
conf.rememberMeParameter("remember-me");
conf.tokenRepository(tokenRepository); //设置刚刚的记住我持久化存储库
conf.tokenValiditySeconds(3600 * 7); //设置记住我有效时间为7天
})
//以下是csrf相关配置
.csrf(conf -> {
conf.disable(); //此方法可以直接关闭全部的csrf校验,一步到位
conf.ignoringRequestMatchers("/xxx/**"); //此方法可以根据情况忽略某些地址的csrf校验
})
.build();
}
}
测试发现记住我token 已经存入Redis!