redis笔记整合

一、安装、配置和启动

1.windows版

(1)安装

https://github.com/MicrosoftArchive/redis/releases/open in new window

(2)启动

redis-server.exe redis.windows.conf
 redis-server.exe  #简写。有些版本的 Windows 系统无法简写,原因不明。

(3)客户端连接到Redis Server

进入 redis 的解压目录,打开一个命令行终端,执行如下命令:
 redis-cli.exe
 redis-cli.exe -h # 完整格式:<指定ip> -p <指定端口> -a <指定密码>

(4)关闭

进入 redis 的解压目录,打开一个命令行终端,执行如下命令:
 ./redis-cli.exe -h 127.0.0.1 -p 6379 shutdown 
 ./redis-cli.exe shutdown  # 简写

(补充)借助 WinSW 启停 Redis

  • 第 1 步:下载 Windows Service Wrapper 工具
    WinSW github( .NET 4.6.1 版 )open in new window将下载好的 WinSW 放到 REDIS_HOME 目录下,并重命名。名字任意,例如:systemctl.exe 。
  • 第 2 步:为 WinSW 创建配置文件
    在 REDIS_HOME 下为 WinSW 创建配置文件( 配置文件与 WinSW 程序平级 )。配置文件为 .xml 文件,且名字必须与 WinSW 程序相同。例如:systemctl.xml ,与上面的 systemctl.exe 相呼应。
    systemctl.xml 配置文件内容如下:
<service>
   <id>redis-server</id>
   <name>Redis Server</name>
   <description>Redis Server</description>
 
   <executable>%BASE%\redis-server.exe</executable>
   <startargument>redis.windows.conf</startargument>
 
   <stopexecutable>%BASE%\redis-cli.exe</stopexecutable>
   <stopargument>shutdown</stopargument>
 
   <logpath>%BASE%\logs</logpath>
   <logmode>roll</logmode>
 </service>

在上述的配置文件中,我们「告知」了 WinSW 以什么命令启停 Redis Server 。未来,我们不再亲自启停 Redis Server ,而是通过 WinSW 间接启停 Redis Server 。

  • 第 3 步:安装 Redis Server 服务
    在 REDIS_HOME 目录下打开 cmd 命令行执行如下命令:
# 安装服务。开机启动,当前未启动,重启后生效。
 systemctl install
 
 # 如果对「开机启动」有异议,可通过 Windows 的 sc 命令调整
 sc config redis-server start= demand           # 手动启动
 # sc config <服务名称> start= auto      # 开机启动,当前未启动,重启后生效
 # sc config <服务名称> start= disabled  # 禁用

安装成功后,你可以在 Windows 系统的服务中看到 Redis Server 。

2.linux版

二、简介

  1. 数据模型
Redis 数据模型不仅仅与关系数据库管理系统(RDBMS)不同,也不同于任何简单的 NoSQL 键-值数据存储系统。
 
 Redis 数据类型类似于编程语言的基础数据类型,因此开发人员感觉很自然,每个数据类型都支持适『用于其类型的操作』,以最大限度发挥每种数据类型的特性。
 
 受支持的数据类型包括:
 - string( 字符串 )
 - hash( 哈希 )
 - list( 列表 )
 - set( 集合 )
 - zset( sorted set:有序集合 )
  1. 关键优势
Redis 的优势包括它的速度、对富数据类型的支持、操作的原子性,以及通用性:
 
 性能极高,它每秒可执行约 10k 个 SET 以及约 100k 个 GET 操作;
 
 丰富的数据类型,Redis 对大多数开发人员已知的多数数据类型提供了原生支持,这使得各种问题得以轻松解决;
 
 原子性,因为所有 Redis 操作都是原子性的,所以多个客户端会并发地访问一个 Redis 服务器器,获取相同的更新值;
 
 丰富的特性,Redis 是一个多效用工具,有非常多的应用场景,包括缓存、消息队列( Redis 原生支持发布/订阅 )、短期应用程序数据据( 比如 Web 会话 )等。
  1. Redis 的『快』的原因
1.是对内存的较为简单的数据进行读写操作。
 2.Redis 是 C 语言实现的,它的底层实现原理是基于 IO 多路复用技术。这个实现方案本身就是先进/高级/高效的。
 3.Redis 的工作是基于单线程的,从而节省了多线程形式下线程切换的开销。
  一次只运行一条命令,每条命令天生就是一个独立的事务。
  拒绝使用长/慢命令,Redis 对外提供的每条命令都很高效快速。
  其实并不是单线程。有些操作使用独立线程,其它的线程是去干其它的事情,和执行命令的工作线程无关
 
 最后的事实也证明了 Redis 的作者的最初分析思路的正确性:Redis 的单机性能的瓶颈是网络速度和网卡性能,而非 CPU 。
  1. Redis 的通用命令简介
一个 Redis 实例可以包括多个数据库。不过,一个 redis 实例最多可提供 16 个数据库,而且固定了以下标从 0 到 15 作为数据库名。客户端默认连接第 0 号数据库。
 
 可以通过 select 命令来当前数据库,如果选择一个不存在数据库则会报错。
 select N



命令

说明

ping

PING 命令来测试客户端与 Redis 的连接是否正常。 连接正常时会收到回复 PONG

set / get

使用 set 和 get 可以向 redis 设置数据、获取数据。

del

删除指定 key 的内容。

Keys *

查看当前库中所有的 key 值

  1. Redis 单机多实例
如果需要启动两个 Redis 实例,你可以这么干:
 
 1.在 Redis 解压目录下创建两个文件夹,例如:6379 和 16379 。以各自所占端口号命名。
 2.将 redis.windows.conf 文件复制进这两个文件夹,并将 16379 中的配置文件中的 port 6379 改为 port 16379 。
 3.在这两个文件夹中,分别启动两个命令行,执行 ../redis-server.exe redis.windows.conf 命令。
 4.未来,在使用 redis-cli.exe 连接 Redis 服务端的时候,就需要明确指定连接端口号。
  1. 持久化

Redis 的高性能是由于其将所有数据都存储在了内存中,为了使 Redis 在重启之后仍能保证数据不丢失,需要将数据从内存中同步到硬盘中,这一过程就是持久化。

Redis支持两种方式的持久化,一种是 RDB 方式,一种是 AOF 方式。

#RDB 方式

RDB 方式是 Redis 的默认持久化方式。

它是通过快照( snapshotting )完成的,当符合一定条件时 Redis 会自动将内存中的数据进行快照并持久化到硬盘。简单来说,就是直接将内存中的数据直接保存到硬盘上。

redis.windows.conf

save 900 1
 save 300 10
 save 60 10000

save 开头的一行就是持久化配置,可以配置多个条件( 每行配置一个条件 ),每个条件之间是『或』的关系,save 900 1 表示 900 秒钟( 15 分钟 )内至少 1 个键被更改则进行快照,save 300 10

Redis 启动后会读取 RDB 快照文件,将数据从硬盘载入到内存。根据数据量大小与结构和服务器性能不同,这个时间也不同。通常将记录一千万个字符串类型键、大小为 1GB 的快照文件载入到内存中需要花费 20~30 秒钟。

但是 RDB 方式实现持久化有个问题啊:一旦 Redis 异常( 突然 )退出,就会丢失最后一次快照以后更改的所有数据。因此在使用 RDB 方式时,需要根据实际情况,调整配置中的参数,以便将数据的遗失控制在可接受范围内。

#AOF 方式

默认情况下 Redis 没有开启 AOF( append only file )方式的持久化,可以通过配置文件中的 appendonly

appendonly yes

开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入硬盘中的 AOF 文件。AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof,可以通过 appendfilename 参数修改:appendfilename appendonly.aof

三、常用命令

1. 通用命令



#

命令

说明

1

keys *

查看所有的 key

2

dbsize

统计键值对的数量

3

exists key

判断指定键值对是否存在,存在则返回 1,不存在返回 0

4

del key

删除指定键值对

5

expire key seconds ttl key persist key

设置过期自动删除 查询 key 的剩余过期时间。 (返回 -1 表示没有过期设置;返回 -2 表示过期删除。) 去掉 key 的过期设置

6

type

查看键值对数据模型类型

2. 字符串命令

增、改:set <key> <value>
 增、改:set <key> <val> NX (key 不存在,才设置)
 增、改:set <key> <val> XX (key 存在,才设置)
 
 删:del <key>
 查:get <key>

3. 哈希命令

增、改:hset <key> <field> <value> (例:hset user name xiaoming)
 批量增:Hmset <key> <field1> <value1> [field2 value2] ... 
 删:Hdel <key> <field1> [ field2 ... ]
 查:hget <key> <field> (例:hget user name)
 查询某个key是否存在:Hexists <key> <field>

4. 链表命令

增:Lpush <key> <value> <value> <value> ... (从key的左边插入数据,如果不存在则创建)
 增:Rpush <key> <value> <value> <value> ... (从key的右边插入数据,如果不存在则创建)
 增:Linsert <key> [before | after] <pivot> <value> (在key的value之前或者之后插入元素)
 
 删:Lpop <key> (删除key下面的最左边的元素)
 删:Rpop <key> (删除key下面的最右边的元素)
 删:Lrem <key> <count> <value>
 Lrem 根据参数 count 的值,移除某个列表中与参数 value 相等的元素。
 count 的值可以是以下几种:
 count > 0 : 从表头开始向表尾搜索,移除与 VALUE 相等的元素,数量为 count 。
 count < 0 : 从表尾开始向表头搜索,移除与 VALUE 相等的元素,数量为 count 的绝对值。
 count = 0 : 移除表中所有与 VALUE 相等的值。
 
 查批量:Lrange <key> 开始索引 结束索引
 查单个:Lindex <key> <index>
 查长度:Llen <key>

5. 集合(Set)命令

相当于对value去重的哈希

增:Sadd <key> <value> <value> <value> ...
 删:Spop key (随机移除一个元素)
 删:Srem <key> <member> <member> <member> ... (移除指定个元素)
 改:SMOVE <source> <target> <member> (把source中的member移动到target中)
 查长度:Scard <key>v
 查是否包含:Sismember <key> <value>
 查所有:Smembers <key>
 随机查一个或指定个:Srandmember <key> [ count ]

6.有序集合命令

和『哈希』有点类似,有序集合中的键值对的值中,也是有两个部分:scorevalue

score 的值决定了与之对应的 value 的顺序

  • zadd
  • zrem
  • zscore
  • zincrby
  • zcard
  • zrange
  • zrangebyscore
  • zcount
  • zremrangebyrank
  • zremrangebyscore

(1)Zadd 命令

Zadd 命令用于将一个或多个成员元素及其分数值加入到某个有序集当中。

如果某个成员已经是有序集的成员,那么更新这个成员的分数值。有序集合内部会重新调整成员元素的位置,来保证这个集合的有序性。

分数值可以是整数值或双精度浮点数。通常使用整数值。

如果有序集合不存在,则创建一个空的有序集并执行 Zadd 操作。

当 key 所对应的并非有序集类型时,返回一个错误。

语法:

Zadd <key> <score> <value> [scoren value ... ]

Zadd 命令将被成功添加的新成员的数量,不包括那些被更新的、已经存在的成员。

> ZADD set1 1 "hello"
 (integer) 1
 > ZADD set1 1 "foo"
 (integer) 1
 > ZADD set1 2 "world" 3 "bar"
 (integer) 2
     
 > ZRANGE set1 0 -1 WITHSCORES
 1) "hello"
 2) "1"
 3) "foo"
 4) "1"
 5) "world"
 6) "2"
 7) "bar"
 8) "3"

(2)Zcard 命令

Zcard 命令用于计算某个集合中元素的数量。

语法:

Zcard <key>

当集合存在时,Zcard 返回有序集的基数;当集合不存在时,返回 0 。

> ZADD myset 1 "hello"
 (integer) 1
 
 > ZADD myset 1 "foo"
 (integer) 1
 
 > ZADD myset 2 "world" 3 "bar"
 (integer) 2
 
 > ZCARD myzset
 (integer) 4

(3)Zcount 命令

Zcount 命令用于计算某有序集合中指定分数区间的成员数量。

语法:

Zcount <key> <min> <max>

Zcount 返回分数值在 minmax

> ZADD myzset 1 "hello"
 (integer) 1
 
 > ZADD myzset 1 "foo"
 (integer) 1
 
 > ZADD myzset 2 "world" 3 "bar"
 (integer) 2
 
 > ZCOUNT myzset 1 3
 (integer) 4

(4)Zincrby 命令

Zincrby 命令对某有序集合中指定成员的分数加上增量 increment

可以通过传递一个负数值 increment

当有序集合不存在,或有序集合中不存在指定分数时, Zincrby 等同于 Zadd 。

当 key 对应的不是有序集时,返回一个错误。

分数值可以是整数值或双精度浮点数。

语法:

Zincrby <key> <increment> <member>

Zincrby 命令返回参数 member

> ZADD myzset 1 "hello"
(integer) 1

> ZADD myzset 1 "foo"
(integer) 1

> ZINCRBY myzset 2 "hello"
(integer) 3

> ZRANGE myzset 0 -1 WITHSCORES
1) "foo"
2) "2"
3) "hello"
4) "3"

(5)Zrem 命令

Zrem 命令用于移除某个有序集中的一个或多个成员,不存在的成员将被忽略。

如果 key 对应的并非是有序集类型,则返回一个错误。

语法:

Zrem <key> <member>
# 基本示例
 > ZRANGE page_rank 0 -1 WITHSCORES
 1) "bing.com"
 2) "8"
 3) "baidu.com"
 4) "9"
 5) "google.com"
 6) "10"
 
 # 移除单个元素
 > ZREM page_rank google.com
 (integer) 1
     
 > ZRANGE page_rank 0 -1 WITHSCORES
 1) "bing.com"
 2) "8"
 3) "baidu.com"
 4) "9"
 
 # 移除多个元素
 > ZREM page_rank baidu.com bing.com
 (integer) 2
 
 > ZRANGE page_rank 0 -1 WITHSCORES
 (empty list or set)
 
 
 # 移除不存在元素
 > ZREM page_rank non-exists-element
 (integer) 0

(6)Zrange 命令

Zrange 返回某有序集中,指定区间内的成员。

如果需要逆序显示,请使用 Zrevrange 命令。

语法:

Zrange <key> <start> <stop> [ WITHSCORES ]

Zrange 命令将指定区间内,带有分数值(可选)的有序集成员的列表。

# 显示整个有序集成员
 > ZRANGE salary 0 -1 WITHSCORES
 1) "jack"
 2) "3500"
 3) "tom"
 4) "5000"
 5) "boss"
 6) "10086"
 
 # 显示有序集下标区间 1 至 2 的成员
 > ZRANGE salary 1 2 WITHSCORES
 1) "tom"
 2) "5000"
 3) "boss"
 4) "10086"
 
 # 测试 end 下标超出最大下标时的情况
 > ZRANGE salary 0 200000 WITHSCORES
 1) "jack"
 2) "3500"
 3) "tom"
 4) "5000"
 5) "boss"
 6) "10086"
 
 # 测试当给定区间不存在于有序集时的情况
 > ZRANGE salary 200000 3000000 WITHSCORES
 (empty list or set)

(7)Zrank 命令

Zrank 返回有序集中指定成员的排名。其中有序集成员按分数值递增(从小到大)顺序排列。

语法:

ZRANK <key> <member>

如果成员是有序集的成员,ZRANK 返回 member 的排名;如果成员不是有序集的成员,返回 nil 。

# 显示所有成员及其 score 值
 > ZRANGE salary 0 -1 WITHSCORES
 1) "peter"
 2) "3500"
 3) "tom"
 4) "4000"
 5) "jack"
 6) "5000"
 
 # 显示 tom 的薪水排名,第二
 > ZRANK salary tom
 (integer) 1

(8)Zrevrank 命令

Zrevrank 命令返回有序集中成员的排名。其中有序集成员按分数值降序排序。

使用 Zrank 命令可以获得成员按分数值升序排序。

语法:

Zrevrank <key> <member>

如果成员是有序集的成员,Zrevrank 返回成员的排名;如果成员不是有序集的成员,返回 nil 。

> ZRANGE salary 0 -1 WITHSCORES     # 测试数据
 1) "jack"
 2) "2000"
 3) "peter"
 4) "3500"
 5) "tom"
 6) "5000"
     
 > ZREVRANK salary peter     # peter 的工资排第二
 (integer) 1
 
 > ZREVRANK salary tom       # tom 的工资最高
 (integer) 0

(9)Zscore 命令

Zscore 命令返回有序集中,成员的分数值。

如果成员元素不是有序集的成员,或有序集合不存在,返回 nil 。

语法:

Zscore <key> <member>

Zscore 返回成员的分数值(以字符串形式表示)。

> ZRANGE salary 0 -1 WITHSCORES    # 测试数据
 1) "tom"
 2) "2000"
 3) "peter"
 4) "3500"
 5) "jack"
 6) "5000"
     
 > ZSCORE salary peter              # 注意返回值是字符串
 "3500"

四、应用场景

1.存储登录用户权限集

步骤1:添加依赖

<!--springboot redis-->
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-redis</artifactId>
 </dependency>

步骤2:登录成功后,把用户权限集存储到redis中

MyAuthenticationSuccessHandler.java

@Component
 public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
     @Resource
     private RedisTemplate<String,String> redisTemplate;
     @Override
     public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
         try {
             //获取当前登录认证成功的用户名
             String username = request.getParameter("username");
             String strToken = JwtTokenUtil.createSign(username);
             //将authorities存入redis数据库
             Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
             StringBuilder sb = new StringBuilder("");
             for (GrantedAuthority authority : authorities) {
                 sb = sb.append(authority).append(",");
             }
             String auth = sb.substring(0, sb.length() - 1);
             ValueOperations<String, String> stringObjectValueOperations = redisTemplate.opsForValue();
             stringObjectValueOperations.set(username, auth);
 
             System.out.println(stringObjectValueOperations.get("username"));
             ///通过响应的json返回客户端
             ResponseResult<String> success = new ResponseResult<>(strToken,"ok",200);
             response.setContentType("application/json;charset=utf-8");
             response.getWriter().write(new ObjectMapper().writeValueAsString(success));
         } catch (Exception e) {
             e.printStackTrace();
         }
     }
 }

步骤3:发送下一次非认证请求时,从redis中读取用户权限集

JwtTokenAuthenticationFilter.java

/**
  * 将用户请求中携带的 JWT 转化为 Authentication Token
  * 存在 Spring Security 上下文( Context )
  */
 @Component
 public class JwtTokenAuthenticationFilter extends OncePerRequestFilter {
     //OncePerRequestFilter-每次请求只执行该过滤器一次
     @Autowired
     private UserMapper userMapper;
     @Autowired
     private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
     @Resource
     private RedisTemplate<String,String> redisTemplate;
 
     @Override
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
         String uri = request.getRequestURI();
         //判断是否属于认证请求,如果属于,放行
         if(uri.endsWith("/login")){
             filterChain.doFilter(request, response);
             return;
         }
 
         //获取请求头中的token串,判断token是否为空
         String strToken = request.getHeader("strToken");
         if(StringUtil.isEmpty(strToken)){
             //如果为空,抛自定义异常-token为空
             myAuthenticationFailureHandler.onAuthenticationFailure(request, response,new TokenIsNullException("token为空!"));
         }
 
         //token不是空,且不是认证请求
         if(JwtTokenUtil.checkSign(strToken)){
             String username = JwtTokenUtil.getUserId(strToken);
             //查询用户的权限集
 //            List<String> permList = userMapper.getPerCodesByPerm(username);
             //从redis缓存中读取用户权限集
             ValueOperations<String, String> stringObjectValueOperations = redisTemplate.opsForValue();
             String authorities = stringObjectValueOperations.get(username).replace("\"", "");
             String[] strings = authorities.split(",");
             //封装用户的权限集
             List<GrantedAuthority> auth = new ArrayList<>();
             for (String s : strings) {
                 auth.add(new SimpleGrantedAuthority(s));
             }
 //            //封装用户的权限集
 //            List<GrantedAuthority> authorities = new ArrayList<>();
 //            strings.forEach(perm->{
 //                authorities.add(new SimpleGrantedAuthority(perm));
 //            });
             //封装数据库存储的用户信息
             UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken
                     = new UsernamePasswordAuthenticationToken(username, null,auth);
             //存入securityContext
             SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
             filterChain.doFilter(request, response);
         }else{
             myAuthenticationFailureHandler.onAuthenticationFailure(request, response,new TokenIsInvalidException("token无效!"));
         }
     }
 
 }

步骤4:注销成功后,清除存储在redis中的用户权限集

MyLogoutSuccessHandler.java

@Component
 public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
     @Resource
     private RedisTemplate<String,String> redisTemplate;
     @Override
     public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
         String strToken = request.getHeader("strToken");
         if(!StringUtil.isEmpty(strToken)){
             SecurityContextHolder.clearContext();
             //清除用户存在redis缓存中的权限集
             String username = JwtTokenUtil.getUserId(strToken);
             redisTemplate.delete(username);
             
             ResponseResult<String> result = new ResponseResult<>("","注销成功",200);
             response.setContentType("application/json;charset=UTF-8");
             response.getWriter().write(new ObjectMapper().writeValueAsString(result));
         }else{
             ResponseResult<String> result = ResponseResult.error(ResultCode.TOKEN_IS_NULL);
             response.setContentType("application/json;charset=utf-8");
             response.getWriter().write(new ObjectMapper().writeValueAsString(result));
         }
     }
 }

2.缓存关系型数据库查询结果

进行重复的数据库查询时,如果每次都要连接MySQL数据库查询,会造成资源的浪费,因此可以把第一次查询的结果存储到redis数据库中,下次执行相同的查询操作时,就从redis中读取数据。
 
 Spring Data Redis 从 1.7 开始提供 Redis Repositories ,可以无缝的转换并存储 domain objects,使用的数据类型为哈希( hash )。

步骤1:启用 Repository 功能

@Configuration
 @EnableRedisRepositories(basePackages = "com.yebuxiu.dao.redis")
 public class RedisConfig {
 }

步骤2:注解需要缓存的实体

添加关键的两个注解 @RedisHash@Id

@Data
 @NoArgsConstructor
 @AllArgsConstructor
 @RedisHash("department")
 public class Department implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
     @Id
     @TableId(value = "id", type = IdType.AUTO)
     private Long id;
     private String name;
     private String location;
 }

步骤3:创建一个 Repository 接口

自定的 Repository 接口必须继承 CrudRepository ,才能「天生」具有存取数据的能力。

@Repository
public interface DepartmentRedisDao extends CrudRepository<Department,Long> {

}

步骤4:测试查询

@Slf4j
@Repository
@RequiredArgsConstructor
public class DepartmentRepository {
    private final DepartmentRedisDao departmentRedisDao;
    private final DepartmentMysqlDao departmentMysqlDao;

    public Department getById(Long id) {
        Optional<Department> departmentBox = departmentRedisDao.findById(id);
        Department department = null;
        try {
            department = departmentBox.get();
            log.info("从Redis中读取的数据");

        } catch (Exception e) {
            e.printStackTrace();
            log.info("Redis中没有,需要去查数据库");
            department = departmentMysqlDao.selectById(id);
        }

        return department;
    }

    public void update(Long id, String newName,String newLocation) {
        Department department = new Department(id, newName, newLocation);
        //修改数据库
        departmentMysqlDao.updateById(department);
        //州除redis
        departmentRedisDao.deleteById(id);
    }

}

3.执行新增操作时确保不重复提交

步骤1:前台新增页面添加一个隐藏的输入框

定义一个onload方法,每次加载前台页面时都通过axios请求,获取一个唯一的uuid,用于标识这次新增操作

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="js/axios.min.js"></script>
    <script>
        window.addEventListener("load",function (){
            axios.post("department/getUuid").then((res)=>{
                console.log(res);
                document.getElementById("uuid").value=res.data.data;
            }).catch((e)=>{
                console.log(e);
            });
        })
    </script>
</head>
<body>
<form action="department/add">
    用户名:<input type="text" name="username" placeholder="请输入用户名"><br>
    密码:<input type="text" name="password" placeholder="请输入密码"><br>
    token:<input type="text" id="uuid" name="uuid" placeholder="这里是隐藏的uuid"><br>
    <button type="submit">提交</button>
</form>

</body>
</html>

步骤2:后台写获取uuid的方法

@RestController
@RequestMapping("/department")
public class DepartmentController {
    @Autowired
    private DepartmentService departmentService;
    @Resource
    private RedisTemplate<String,String> redisTemplate;
    @RequestMapping("getUuid")
    //获取uuid,传到前台并存入redis缓存
    public ResponseResult<String> getUuid(){
        String uuid = UUID.randomUUID().toString();
        redisTemplate.opsForValue().set(uuid, uuid);
        ResponseResult<String> responseResult = new ResponseResult<>(uuid,"ok",200);
        return responseResult;
    }
}

步骤3:后台从redis中删除前台表单提交uuid

那么就存在两种情况:
第一次提交:uuid存在,执行删除操作,删除成功,执行添加操作
重复提交:uuid已经被删除,执行删除操作,删除失败,不执行添加操作
@RestController
@RequestMapping("/department")
public class DepartmentController {
    @Autowired
    private DepartmentService departmentService;
    @Resource
    private RedisTemplate<String,String> redisTemplate;
    
    @RequestMapping("add")
    public String add(@RequestParam("username") String username,
                    @RequestParam("password") String password,
                    @RequestParam("uuid") String uuid
    ){

        Boolean delete = redisTemplate.delete(uuid);
        if(delete){
            System.out.println("执行添加操作");
            return "success";
        }else {
            System.out.println("重复提交表单,不执行添加操作");
            return "failure";
        }
    }
}

4.执行修改操作确保数据不重复修改

解决方案: 乐观锁方案

步骤1: 在数据库表中添加一个int类型字段version,并赋予默认值1或者0





redis 命令行auth redis 命令行连接 windows_java


步骤2:前端获取到version值,在打开修改界面时将version进行传值


redis 命令行auth redis 命令行连接 windows_数据库_02


步骤3:后端方法在更新前判断version值是否已经改变,如果改变则更新失败返回0

@Transactional
    public Integer updateEmployeeById(Employee employee) {
        QueryWrapper<Employee> wrapper = new QueryWrapper<>();
        //判断version值是否相等
        wrapper.eq("version", employee.getVersion());
        wrapper.eq("id", employee.getId());
        Integer update = employeeMapper.update(employee, wrapper);
        //如果更新失败则返回0
        if (update == 0){
            return 0;
        }
        //更新成功后再将version值加一
        //如果自己写动态sql则不需要后面
        employee.setVersion(employee.getVersion()+1);
        employeeMapper.updateById(employee);
        return update;
    }

步骤4:判断更新的结果后发送对应ResponseResult

@RequestMapping("/update")
    @PreAuthorize("hasAnyAuthority('all')")
    public ResponseResult<Void> updateUser(@RequestBody Employee employee){
        Integer integer = employeeService.updateEmployeeById(employee);
        ResponseResult<Void> result = new ResponseResult<>();
        if (integer == 0){
            result = new ResponseResult<>(404, "failure");
        }else {
            result = ResponseResult.ok();
        }
        return result;
    }

步骤5:前端通过获取对应的结果进行更新判断

updateUser() {
            this.$axios.post('/api/employee/update', this.objUser)
                .then(res => {
                    console.log(res)
                    if (res.data.status === 200) {
                        this.$message({
                            showClose: true,
                            message: '修改成功',
                            type: 'success',
                            duration: 2000, //弹窗停留时间
                            offset: 25,  //设置弹窗与顶部偏移距离
                        });
                        this.dialogFormVisible = false;
                        this.$emit('func')
                    } else if (res.data.status === 404) {
                        this.$message({
                            showClose: true,
                            message: '该用户信息已经改变',
                            type: 'error',
                            duration: 2000, //弹窗停留时间
                            offset: 25,  //设置弹窗与顶部偏移距离
                        });
                        // this.dialogFormVisible = true;
                    }
                })
                .catch(err => {
                    this.$message({
                        showClose: true,
                        message: '服务器跑路了,请联系管理员',
                        type: 'error',
                        duration: 2000, //弹窗停留时间
                        offset: 25,  //设置弹窗与顶部偏移距离
                    });
                    // console.error(err);
                    this.$emit('func')
                })
        },

5.spring security 匿名对象访问匿名可访问路径

步骤1:在webSecurityconfig配置类的config方法中增加可访问路径

方法一:

@Override
    protected void configure(HttpSecurity http) throws Exception {
        //http.httpBasic().and().authorizeRequests().anyRequest().authenticated(); //关闭httpBasic认证
        //需要放行的url在这里配置,必须要放行/login和/login.html,不然会报错
        http.authorizeRequests()
                //增加可访问的匿名对象
                .antMatchers("/login", "/login.html","/employee/list","/department/list").permitAll()
             //此方法会让匿名对象直接访问,但是有权限用户无法访问
             //.antMatchers("/employee/list","/department/list").anonymous()
                .anyRequest().authenticated().and().
                // 设置登陆页、登录表单form中action的地址,也就是处理认证请求的路径
                        formLogin().loginPage("/login.html").loginProcessingUrl("/login")
                //登录表单form中密码输入框input的name名,不修改的话默认是password
                .usernameParameter("username").passwordParameter("password")
                //登录认证成功后默认转跳的路径
                .defaultSuccessUrl("/home")
                .successHandler(myAuthenticationSuccessHandler)
                .failureHandler(myAuthenticationFailureHandler)
                .and().exceptionHandling().authenticationEntryPoint(myAuthenticationEntryPoint)
                .accessDeniedHandler(myAccessDeniedHandler)
                .and().logout().logoutSuccessHandler(myLogoutSuccessHandler);
                //出异常后执行的路径,此方法为重定向,源码将错误信息存储在session中,如果使用
                //failureForwardUrl则为请求,源码将错误信息存储在请求作用域中
//                .failureUrl("/error1").permitAll();
        //将自定义的JwtTokenAuthenticationFilter插入到过滤器链中的指定的过滤器前面
        http.addFilterBefore(jwtTokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        //关闭CSRF跨域
        http.csrf().disable();

        //关闭session最严格的策略 -jwt认证的情况下,不需要security会话参与
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

方法二:

在controller类中的方法上添加注解@PreAuthorize("permitAll()")

@RequestMapping("/list")
    @PreAuthorize("permitAll()")
    public ResponseResult<List<Employee>> getList(){
        List<Employee> list = employeeService.list();
        ResponseResult<List<Employee>> result = new ResponseResult<>(list,"ok",200);
        return result;
    }

步骤2:在新增的jwt过滤器中判断token为空时进行放行

if (StringUtils.isEmpty(strToken)){
            //Token验证为空
            //myAuthenticationFailureHandler.onAuthenticationFailure(request, response,new TokenIsNullException("Token为空"));
            filterChain.doFilter(request, response);
            return;
        }

步骤3:在登录页面添加游客访问按钮



redis 命令行auth redis 命令行连接 windows_缓存_03


<el-button @click="anonymous()">游客访问</el-button>
anonymous()
{
      this.$router.push("/home")
}