Azhang nga gipatik nga mga artikulo

SpringBoot 整合 Redis

说明:在 SpringBoot2.x 之后,原来使用的 Jedis 被替换为了 lettuce

jedis:采用的是直连,多个线程操作的话,是不安全的,如果要想避免不安全,则使用 jedis pool 连接池

lettuce:采用 netty,实例可以在多个线程中进行共享,不存在线程不安全的情况,可以减少线程数量

@Bean
@ConditionalOnMissingBean(
    name = {"redisTemplate"}
)    // 我们可以自定义一个 RedisTemplate 来替换这个默认的
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
    // 默认的 RedisTemplate 没有过多的设置,redis 对象都是需要序列化的
    // 两个泛型都是 object,我们后面使用需要强制转换 <String, Object>
    RedisTemplate<Object, Object> template = new RedisTemplate();
    template.setConnectionFactory(redisConnectionFactory);
    return template;
}

@Bean
@ConditionalOnMissingBean    // 由于 String 是 Redis 中最常使用的类型,所以单独提出来一个 bean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
    return new StringRedisTemplate(redisConnectionFactory);
}
整合测试

1、导入依赖

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

2、配置连接

spring.redis.host=localhost
spring.redis.port=6379

3、测试

@SpringBootTest
class Redis02SpringbootApplicationTests {

    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    void contextLoads() {

        // redisTemplate 操作不同的数据类型,api 和我们的指令是一样的
        // opsForValue  操作字符串   类似 String
        // opsForList   操作 list   类似 list
        // opsForSet
        // opsForHash
        // opsForZSet
        // opsForGeo
        // opsForHyperLogLog

        // 除了基本的操作,我们常用的方法可以直接通过 redisTemplate 来操作,如 事务 和 基本的 CRUD

        // 获取 redis 的连接对象
        RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
        connection.flushDb();
        connection.flushAll();

    }

}

序列化配置:

@Nullable
private RedisSerializer keySerializer = null;
@Nullable
private RedisSerializer valueSerializer = null;
@Nullable
private RedisSerializer hashKeySerializer = null;
@Nullable
private RedisSerializer hashValueSerializer = null;

默认的序列化方式是 JDK 序列化image-20220720211540968

关于对象的保存,所有的对象都需要序列化

image-20220721110718816

自定义的 RedisTemplate

package com.config;

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.context.annotation.Configuration;
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;

@Configuration
public class RedisConfig {

    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // 为了开发方便,一般使用 <String, Object> 类型
        RedisTemplate<String, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        // JSON 序列化配置
        Jackson2JsonRedisSerializer objectJackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
//        om.activateDefaultTyping()
        // String 的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // key 采用 String 的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash 的 key 采用 String 的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value 的序列化方式采用 Jackson
        template.setValueSerializer(objectJackson2JsonRedisSerializer);
        // hash 的 value 序列化方式采用 Jackson
        template.setHashValueSerializer(objectJackson2JsonRedisSerializer);
        template.afterPropertiesSet();

        return template;
    }

/*    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        return new StringRedisTemplate(redisConnectionFactory);
    }*/

}

所有的 Redis 操作,其实对于 java 开发人员来说,十分简单,更重要是要去理解 redis 的思想和每一种数据结构的用处和作用场景。

Jedis

Jedis 是 Redis 官方推荐的 java 连接开发工具,使用 java 操作 Redis 的中间件。

测试

1、导入对应的依赖

<!-- 导入 Jedis 的包 -->
<dependencies>
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>3.8.0</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>2.0.9</version>
    </dependency>
</dependencies>

2、编码测试

  • 连接数据库
  • 操作命令
  • 断开连接
package com;

import redis.clients.jedis.Jedis;

public class TestPing {

    public static void main(String[] args) {

        Jedis jedis = new Jedis("localhost", 6379);
        // Jedis 的所有命令就是 redis 里面的所有指令。
        System.out.println(jedis.ping());    // 输出 PONG

    }

}

常用的 API

String

List

Set

Hash

Zset

所有的 API 命令,就是之前学的 Redis 指令

事务

Redis 事务本质:一组命令的集合。一个事务中所有的命令都会被序列化,在事务执行过程中,会按照顺序执行。

一次性、顺序性、排他性。

要么同时成功,要么同时失败,原子性。

==Redis 事务没有隔离级别的概念==

所有的命令在事务中,并没有直接被执行,只有发起执行命令的时候才会执行。Exec

==Redis 单条命令是保证原子性的,但是事务不保证原子性==

redis 的事务:

  • 开启事务(multi)
  • 命令入队(......)
  • 执行事务(exec)
正常执行事务
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> get k2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> exec        # 执行事务
1) OK
2) OK
3) "v2"
4) OK
放弃事务
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 k4 v4
QUEUED
127.0.0.1:6379> DISCARD        # 取消事务  放弃事务
OK
127.0.0.1:6379> get k4        # 事务队列中的命令都不会被执行
(nil)
编译型异常(命令有错),事务中所有的命令都不会被执行
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v3
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> getset k3 3
QUEUED
127.0.0.1:6379> getset k3    # 错误的命令
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> set k5 v5
QUEUED
127.0.0.1:6379> exec        # 执行事务报错
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k1        # 所有的命令都不会被执行
(nil)
127.0.0.1:6379> get k5
(nil)
运行时异常( 1/0 ),如果事务队列中存在语法性错误,那么执行命令时其他命令是可以正常执行的,错误命令会抛出异常
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr k1        # 会执行的时候失败
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> get k3
QUEUED
127.0.0.1:6379> exec
1) (error) ERR value is not an integer or out of range    # 虽然第一条命令报错了,但是整个事务依旧正常执行成功
2) OK
3) OK
4) "v3"
127.0.0.1:6379> get k2
"v2"
127.0.0.1:6379> get k3
"v3"
监控 Watch

悲观锁:

  • 很悲观,认为什么时候都会出问题,无论做什么都会加锁

乐观锁:

  • 很乐观,认为什么时候都不会出问题,所以不会上锁,更新数据的时候去判断一下,在此期间,是否有人修改过这个数据
  • 获取 version
  • 更新的时候比较 version
Redis 的监视测试

正常执行成功:

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        # 监视 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

测试多线程修改值,使用 watch 可以当做 redis 的乐观锁

127.0.0.1:6379> watch money    # 监视 money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby money 10
QUEUED
127.0.0.1:6379> incrby out 10
QUEUED
127.0.0.1:6379> exec    # 执行之前,另外一个线程,修改了我们的值,这个时候,就会导致事务执行失败.
(nil)

# 如果发现事务执行失败 就先解锁
127.0.0.1:6379> UNWATCH
OK
127.0.0.1:6379> watch money    # 获取最新的值 再次监视
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby money 10
QUEUED
127.0.0.1:6379> incrby out 10
QUEUED
127.0.0.1:6379> exec        # 比对监视的值是否发生了变化如果没有变化,那么可以执行成功,如果变化就执行失败
1) (integer) 990
2) (integer) 30

三种特殊数据类型

geospatial 地理位置

朋友的定位,附近的人,打车距离计算等

Redis 的 Geo,这个功能可以推算地理位置信息,两地之间的距离,方圆几里的人。

只有六个命令:

geoadd 添加地理位置

规则:两级无法直接添加,我们一般会下载城市数据,直接通过 java 程序一次性导入

参数 key 值(经度、纬度、名称)

127.0.0.1:6379> geoadd china:city 116.40 39.90 beijing
(integer) 1
127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai
(integer) 1
127.0.0.1:6379> geoadd china:city 106.50 29.53 chongqing 114.08 22.54 shenzhen
(integer) 2
127.0.0.1:6379> geoadd china:city 120.16 30.24 hangzhou 108.96 34.26 xian
(integer) 2
geopos 获取当前定位 一定是一个坐标值

获取指定城市的经度和纬度

127.0.0.1:6379> geopos china:city beijing
1) 1) "116.39999896287918091"
   2) "39.90000009167092543"
127.0.0.1:6379> geopos china:city beijing chongqing
1) 1) "116.39999896287918091"
   2) "39.90000009167092543"
2) 1) "106.49999767541885376"
   2) "29.52999957900659211"
geodist

两人之间的距离

单位:

  • m 米
  • km 千米
  • mi 英里
  • ft 英尺
127.0.0.1:6379> geodist china:city beijing shanghai
"1067378.7564"
127.0.0.1:6379> geodist china:city beijing shanghai km
"1067.3788"
127.0.0.1:6379> geodist china:city beijing chongqing km
"1464.0708"
georadius 以给定的经纬度为中心,找出某一半径内的元素

附近的人(获得所有附近的人的住址,定位)通过半径来查询

获得指定数量的人

所有数据应该都录入:china:city 才会让结果更加清晰

127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km    #以 100 30 这个经纬度为中心 寻找方圆1000km内的城市
1) "chongqing"
2) "xian"
3) "shenzhen"
4) "hangzhou"
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km
1) "chongqing"
2) "xian"
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km withdist        # 显示到中心距离的位置
1) 1) "chongqing"
   2) "341.9374"
2) 1) "xian"
   2) "483.8340"
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km withcoord    # 显示出他人的定位信息
1) 1) "chongqing"
   2) 1) "106.49999767541885376"
      2) "29.52999957900659211"
2) 1) "xian"
   2) 1) "108.96000176668167114"
      2) "34.25999964418929977"
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km withdist withcoord count 1    # 筛选出指定的结果
1) 1) "chongqing"
   2) "341.9374"
   3) 1) "106.49999767541885376"
      2) "29.52999957900659211"
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km withdist withcoord count 2
1) 1) "chongqing"
   2) "341.9374"
   3) 1) "106.49999767541885376"
      2) "29.52999957900659211"
2) 1) "xian"
   2) "483.8340"
   3) 1) "108.96000176668167114"
      2) "34.25999964418929977"
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km withdist withcoord count 3
1) 1) "chongqing"
   2) "341.9374"
   3) 1) "106.49999767541885376"
      2) "29.52999957900659211"
2) 1) "xian"
   2) "483.8340"
   3) 1) "108.96000176668167114"
      2) "34.25999964418929977"
127.0.0.1:6379>
georadiusbymember

找出位于指定元素周围的其他元素

127.0.0.1:6379> georadiusbymember china:city beijing 1000 km
1) "beijing"
2) "xian"
127.0.0.1:6379> georadiusbymember china:city shanghai 400 km
1) "hangzhou"
2) "shanghai"
geohash

该命令将返回 11 个字符的 geohash 字符串

将二维的经纬度转换为一维的字符串,如果两个字符串越接近,则距离越近。

127.0.0.1:6379> GEOHASH china:city beijing chongqing
1) "wx4fbxxfke0"
2) "wm5xzrybty0"
GEO 底层实现原理其实就是 Zset,可以使用 Zet 命令来操作 GEO
127.0.0.1:6379> zrange china:city 0 -1    # 查看地图中全部元素
1) "chongqing"
2) "xian"
3) "shenzhen"
4) "hangzhou"
5) "shanghai"
6) "beijing"
127.0.0.1:6379> zrem china:city beijing        # 移除指定元素
(integer) 1
127.0.0.1:6379> zrange china:city 0 -1
1) "chongqing"
2) "xian"
3) "shenzhen"
4) "hangzhou"
5) "shanghai"

hyperloglog

基数:不重复的元素,可以接受误差

hyperloglog 简介

是一种数据结构,用来做基数统计的算法

优点:占用内存是固定的

127.0.0.1:6379> PFADD mykey a b c d e f g h i j        # 创建第一组元素
(integer) 1
127.0.0.1:6379> PFCOUNT mykey        # 统计 mykey 中元素的基数数量
(integer) 10
127.0.0.1:6379> pfadd mykey2 i j z x c v b n m        # 创建第二组元素
(integer) 1
127.0.0.1:6379> PFCOUNT mykey2
(integer) 9
127.0.0.1:6379> PFMERGE mykey3 mykey mykey2        # 合并两组 mykey mykey2 => mykey3  并集
OK
127.0.0.1:6379> PFCOUNT mykey3    # 查看并集的数量
(integer) 15

如果允许容错,那么一定可以使用 Hyperloglog

bitmap

位存储

统计用户信息,活跃/不活跃,登录/未登录,打卡/未打卡等这种两个状态的,都可以使用 bitmap

bitmap 叫位图,也是一种数据结构,都是操作二进制位来进行记录,就只有 0 和 1 两个状态。

使用 bitmaps 来记录周一到周日的打卡:

127.0.0.1:6379> setbit sign 0 1
(integer) 0
127.0.0.1:6379> setbit sign 1 0
(integer) 0
127.0.0.1:6379> setbit sign 2 0
(integer) 0
127.0.0.1:6379> setbit sign 3 1
(integer) 0
127.0.0.1:6379> setbit sign 4 1
(integer) 0
127.0.0.1:6379> setbit sign 5 0
(integer) 0
127.0.0.1:6379> setbit sign 6 0
(integer) 0

查看某一天是否有打卡:

127.0.0.1:6379> getbit sign 3
(integer) 1
127.0.0.1:6379> getbit sign 6
(integer) 0

统计操作,统计打卡的天数:

127.0.0.1:6379> BITCOUNT sign    # 统计这周的打卡记录,就可以看到是否有全勤
(integer) 3

五大数据类型

Redis-Key

127.0.0.1:6379> keys *    # 查看所有的 key
1) "age"
127.0.0.1:6379> set name azhang        # set key
OK
127.0.0.1:6379> keys *
1) "name"
2) "age"
127.0.0.1:6379> exists name        # 判断当前 key 是否存在
(integer) 1
127.0.0.1:6379> exists name1
(integer) 0
127.0.0.1:6379> move name 1        # 将 key 移动到指定 DB
(integer) 0
127.0.0.1:6379>  keys *
1) "name"
2) "age"
127.0.0.1:6379> move name 2
(integer) 1
127.0.0.1:6379>  keys *
1) "age"
127.0.0.1:6379> set name azhang
OK
127.0.0.1:6379>  keys *
1) "name"
2) "age"
127.0.0.1:6379> get name
"azhang"
127.0.0.1:6379> expire name 10        # 设置过期时间 单位是 s
(integer) 1
127.0.0.1:6379> ttl name    # 查看当前 key 的剩余时间
(integer) 7
127.0.0.1:6379> ttl name
(integer) 3
127.0.0.1:6379> ttl name
(integer) -2
127.0.0.1:6379> keys *
1) "age"
127.0.0.1:6379> set name azhang
OK
127.0.0.1:6379> del name    # 删除指定 key
(integer) 1
127.0.0.1:6379> keys *
1) "age"
127.0.0.1:6379> set name azhang
OK
127.0.0.1:6379> keys *
1) "name"
2) "age"
127.0.0.1:6379> type name    # 查看当前 key 的类型
string
127.0.0.1:6379> type age
string
127.0.0.1:6379> clear    # 清空控制台

String

#####################################################
127.0.0.1:6379> set key1 v1        # 设置值
OK
127.0.0.1:6379> get key1        # 获得值
"v1"
127.0.0.1:6379> keys *            # 获得所有的 key
1) "key1"
127.0.0.1:6379> exists key1        # 判断当前 key 是否存在
(integer) 1
127.0.0.1:6379> append key1 hello    # 往某个 key 中追加 字符串 如果当前 key 不存在 就相当于 set key
(integer) 7
127.0.0.1:6379> keys *
1) "key1"
127.0.0.1:6379> get key1
"v1hello"
127.0.0.1:6379> STRLEN key1        # 获取字符串长度
(integer) 7
127.0.0.1:6379> append key1 ,azhang
(integer) 14
#####################################################
# i++
# 步长 i+=
127.0.0.1:6379> set views 0        # 初始浏览量为 0
OK
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> INCR views        # 自增 1    浏览量变为 1
(integer) 1
127.0.0.1:6379> INCR views
(integer) 2
127.0.0.1:6379> get views
"2"
127.0.0.1:6379> DECR views
(integer) 1
127.0.0.1:6379> DECR views        # 自减 1    浏览量变为 0
(integer) 0
127.0.0.1:6379> INCRBY views 10    # 可以设置步长来指定增量
(integer) 10
127.0.0.1:6379> INCRBY views 10
(integer) 20
127.0.0.1:6379> DECRBY views 10
(integer) 10
127.0.0.1:6379> DECRBY views 10
(integer) 0
#####################################################
# 字符串范围 range
127.0.0.1:6379> set key1 hello,azhang    # 设置 key1 的值
OK
127.0.0.1:6379> get key1
"hello,azhang"
127.0.0.1:6379> GETRANGE key1 0 3        # 截取字符串 [0,3]
"hell"
127.0.0.1:6379> GETRANGE key1 6 -1
"azhang"
127.0.0.1:6379> GETRANGE key1 0 -1        # 获取全部的字符串和 get key 是一样的
"hello,azhang"

# 替换
127.0.0.1:6379> set key2 abcdefg
OK
127.0.0.1:6379> get key2
"abcdefg"
127.0.0.1:6379> SETRANGE key2 1 xx    # 替换指定位置开始的字符串
(integer) 7
127.0.0.1:6379> get key2
"axxdefg"
#####################################################
# setex (set with expire) 设置过期时间
# setnx (set if not exist) 不存在再设置 (在分布式锁中常常使用)
127.0.0.1:6379> setex key3 30 hello        # 设置 key3 的值为 hello 30s后过期
OK
127.0.0.1:6379> ttl key3
(integer) 25
127.0.0.1:6379> get key3
"hello"
127.0.0.1:6379> ttl key3
(integer) 18
127.0.0.1:6379> setnx mykey redis    # 如果 mykey 不存在 创建 mykey
(integer) 1
127.0.0.1:6379> keys *
1) "key2"
2) "mykey"
3) "key1"
127.0.0.1:6379> ttl key3
(integer) -2
127.0.0.1:6379> setnx mykey mongoDB    # 如果 mykey 存在 创建失败
(integer) 0
127.0.0.1:6379> get mykey
"redis"
#####################################################
# mset
# mget
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3    # 同时设置多个值
OK
127.0.0.1:6379> keys *
1) "k3"
2) "k2"
3) "k1"
127.0.0.1:6379> mget k1 k2 k3    # 同时获取多个值
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> msetnx k1 v1 k4 v4    # msetnx 是一个原子性操作 要么一起成功 要么一起失败
(integer) 0
127.0.0.1:6379> get k4
(nil)

# 对象
set user:1 {name:azhang, age:21}    # 设置一个 user:1 对象 值为 json 字符来保存一个对象

# 这里的 key 设计巧妙: user:{id}:{filed} 如此设计在 redis 中是可以的
127.0.0.1:6379> mset user:1:name azhang user:1:age 21
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "azhang"
2) "21"

##################################################### getset 先 get 然后再 set
127.0.0.1:6379> GETSET db redis     # 如果不存在值 则返回 null
(nil)
127.0.0.1:6379> get db
"redis"
127.0.0.1:6379> GETSET db mongodb    # 如果存在值 获取原来的值 并设置新的值
"redis"
127.0.0.1:6379> get db
"mongodb"
####################################################

String 的应用场景:value 除了是我们的字符串还可以是我们的数字

  • 计数器
  • 统计多单位的数量 uid:{95256449}:follow 0
  • 粉丝数
  • 对象缓存存储

List

在 redis 中,list 可以实现 栈、队列、阻塞队列。

所有的 list 命令都是用 l 开头的,不区分大小写命令

####################################################
# lpush
# lrange
# rpush

127.0.0.1:6379> LPUSH list one two    # 将一个值或者多个值插入列表的头部(左)
(integer) 2
127.0.0.1:6379> lrange list 0 -1    # 获取 list 中的值
1) "two"
2) "one"
127.0.0.1:6379> LPUSH list three
(integer) 3
127.0.0.1:6379> LRANGE list 0 1        # 通过区间获取具体的值
1) "three"
2) "two"
127.0.0.1:6379> RPUSH list zero        # 将一个值或者多个值插入列表的尾部(右)
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "zero"

####################################################
# lpop
# rpop

127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "zero"
127.0.0.1:6379> lpop list    # 移除 list 的第一个元素
"three"
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
3) "zero"
127.0.0.1:6379> rpop list    # 移除 list 的最后一个元素
"zero"
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"

####################################################
# lindex

127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
127.0.0.1:6379> LINDEX list 1    #通过下标获得 list 中的某一个值
"one"
127.0.0.1:6379> LINDEX list 0
"two"

####################################################
# llen

127.0.0.1:6379> lpush list one two three
(integer) 3
127.0.0.1:6379> llen list    # 返回 list 的长度
(integer) 3
127.0.0.1:6379>

####################################################
# 移除指定的值
# lrem

127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "three"
3) "two"
4) "one"
127.0.0.1:6379> lrem list 1 one    # 移除 list 集合中指定个数的 value 精确匹配
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "three"
3) "two"
127.0.0.1:6379> lrem list 1 three
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
127.0.0.1:6379> lpush list three
(integer) 3
127.0.0.1:6379> lrem list 2 three
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "two"
127.0.0.1:6379>

####################################################
# ltrim 截断

127.0.0.1:6379> rpush mylist hello
(integer) 1
127.0.0.1:6379> rpush mylist hello1
(integer) 2
127.0.0.1:6379> rpush mylist hello2
(integer) 3
127.0.0.1:6379> rpush mylist hello3
(integer) 4
127.0.0.1:6379> ltrim mylist 1 2    # 通过下标截取指定的长度
OK
127.0.0.1:6379> lrange mylist 0 -1
1) "hello1"
2) "hello2"

####################################################
# rpoplpush 移除列表的最后一个元素 并将它移动到新的列表中

127.0.0.1:6379> rpush mylist hello hello1 hello2
(integer) 3
127.0.0.1:6379> RPOPLPUSH mylist myotherlist    # 移除列表的最后一个元素 并将它移动到新的列表中
"hello2"
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "hello1"
127.0.0.1:6379> lrange myotherlist 0 -1
1) "hello2"
127.0.0.1:6379>

####################################################
# lset 将列表中指定下标的值替换为另外一个值,更新操作

127.0.0.1:6379> EXISTS list        # 判断列表是否存在
(integer) 0
127.0.0.1:6379> lset list 0 v1    # 如果不存在列表 更新就会报错
(error) ERR no such key
127.0.0.1:6379> lpush list v1
(integer) 1
127.0.0.1:6379> lrange list 0 0
1) "v1"
127.0.0.1:6379> lset list 0 item    #如果存在 更新当前下标的值
OK
127.0.0.1:6379> lrange list 0 0
1) "item"
127.0.0.1:6379> lset list 1 other    # 如果下标不存在 会报错
(error) ERR index out of range

####################################################
# linsert 将某个具体的 value 插入到列表中某个元素的前面或者后面

127.0.0.1:6379> rpush mylist hello world
(integer) 2
127.0.0.1:6379> linsert mylist before world other
(integer) 3
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "other"
3) "world"
127.0.0.1:6379> linsert mylist after world new
(integer) 4
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "other"
3) "world"
4) "new"

####################################################
  • list 实际上是一个链表,before Node after,left right 都可以插入值
  • 如果 key 不存在,可以创建新的链表
  • 如果 key 存在,可以新增内容
  • 如果移除了所有值,空链表,也代表不存在
  • 在两边插入或者改动值效率最高,中间元素相对效率会低一点

Set

set 中的值是不能重复的

####################################################
127.0.0.1:6379> sadd myset hello    # set集合中添加元素
(integer) 1
127.0.0.1:6379> sadd myset azhang
(integer) 1
127.0.0.1:6379> sadd myset love
(integer) 1
127.0.0.1:6379> smembers myset        # 查看指定 set 的所有值
1) "azhang"
2) "love"
3) "hello"
127.0.0.1:6379> sismember myset hello    # 判断某一个值是否在 set集合中
(integer) 1
127.0.0.1:6379> sismember myset world
(integer) 0

####################################################
127.0.0.1:6379> scard myset        # 获取 set 集合中的内容元素个数
(integer) 3

####################################################
127.0.0.1:6379> srem myset hello    # 移除 set 中的指定元素
(integer) 1
127.0.0.1:6379> scard myset
(integer) 2
127.0.0.1:6379> SMEMBERS myset
1) "azhang"
2) "love"

####################################################
# set 无序不重复集合,抽随机

127.0.0.1:6379> SMEMBERS myset
1) "azhang"
2) "love"
127.0.0.1:6379> SRANDMEMBER myset    # 随机抽选出一个元素
"love"
127.0.0.1:6379> SRANDMEMBER myset
"azhang"
127.0.0.1:6379> SRANDMEMBER myset
"love"
127.0.0.1:6379> SRANDMEMBER myset
"azhang"
127.0.0.1:6379> SRANDMEMBER myset 2    # 随机抽选出指定个数的元素
1) "azhang"
2) "love"
127.0.0.1:6379> SRANDMEMBER myset 2
1) "azhang"
2) "love"

####################################################
# 删除指定的 key
# 随机删除 key

127.0.0.1:6379> SMEMBERS myset
1) "azhang"
2) "love"
127.0.0.1:6379> spop myset    # 随机删除一些 set 集合中的元素
"love"
127.0.0.1:6379> spop myset
"azhang"
127.0.0.1:6379> SMEMBERS myset
(empty list or set)

####################################################
# 将一个指定的值,移动到另外一个 set 集合中

127.0.0.1:6379> sadd myset hello world azhang
(integer) 3
127.0.0.1:6379> sadd myset2 set2
(integer) 1
127.0.0.1:6379> smove myset myset2 azhang    # 将一个指定的值,移动到另外一个 set 集合中
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "hello"
2) "world"
127.0.0.1:6379> SMEMBERS myset2
1) "azhang"
2) "set2"

####################################################
数字集合类:
  - 差集
  - 交集
  - 并集

127.0.0.1:6379> sadd key1 a b c
(integer) 3
127.0.0.1:6379> sadd key2 c d e
(integer) 3
127.0.0.1:6379> sdiff key1 key2        # 差集
1) "b"
2) "a"
127.0.0.1:6379> sinter key1 key2    # 交集 (共同好友)
1) "c"
127.0.0.1:6379> sunion key1 key2    # 并集
1) "e"
2) "c"
3) "a"
4) "b"
5) "d"
127.0.0.1:6379>

####################################################

Hash

Map 集合,key - map,这时候这个值是一个 map 集合,本质和 String 类型没有太大区别,还是一个简单的 key - value

####################################################
127.0.0.1:6379> hset myhash f1 azhang    # set 一个具体的 key-value
(integer) 1
127.0.0.1:6379> hget myhash f1        # 获取一个字段值
"azhang"
127.0.0.1:6379> hmset myhash f1 hello f2 world    # set 多个 key-value
OK
127.0.0.1:6379> hmget myhash f1 f2    # 获取多个字段值
1) "hello"
2) "world"
127.0.0.1:6379> hgetall myhash    # 获取全部的数据
1) "f1"
2) "hello"
3) "f2"
4) "world"
127.0.0.1:6379> hdel myhash f1    # 删除 hash 指定的 key 字段,对应的 value 值也就消失了
(integer) 1
127.0.0.1:6379> hgetall myhash
1) "f2"
2) "world"
127.0.0.1:6379> hgetall myhash
####################################################
# hlen

127.0.0.1:6379> hmset myhash f1 hello f2 world
OK
127.0.0.1:6379> hgetall myhash
1) "f2"
2) "world"
3) "f1"
4) "hello"
127.0.0.1:6379> hlen myhash    # 获取 hash 表的字段数量
(integer) 2

####################################################
127.0.0.1:6379> HEXISTS myhash f1    # 判断 hash 中指定字段是否存在
(integer) 1
127.0.0.1:6379> HEXISTS myhash f3
(integer) 0

####################################################
# 只获得所有 filed
# 只获得所有 value

127.0.0.1:6379> hkeys myhash
1) "f2"
2) "f1"
127.0.0.1:6379> hvals myhash
1) "world"
2) "hello"

####################################################
# incr decr

127.0.0.1:6379> hset myhash f3 5    # 指定增量
(integer) 1
127.0.0.1:6379> hincrby myhash f3 1
(integer) 6
127.0.0.1:6379> hincrby myhash f3 -1
(integer) 5
127.0.0.1:6379> hsetnx myhash f4 hello    # 如果不存在则可以设置
(integer) 1
127.0.0.1:6379> hsetnx myhash f4 world    # 如果存在则不能设置
(integer) 0
127.0.0.1:6379> hgetall myhash
1) "f2"
2) "world"
3) "f1"
4) "hello"
5) "f3"
6) "5"
7) "f4"
8) "hello"

####################################################

hash 变更的数据 user name age,尤其是用户信息之类的,经常变动的信息。

hash 更适合对象的存储,String 更加适合字符串存储。

Zset

在 set 的基础上,增加了一个值,set k1 v1,Zset k1 score1 v1

####################################################
127.0.0.1:6379> zadd myset 1 one    # 添加一个值
(integer) 1
127.0.0.1:6379> zadd myset 2 two
(integer) 1
127.0.0.1:6379> zadd myset 2 two
(integer) 0
127.0.0.1:6379> zadd myset 2 two 3 three    # 添加多个值
(integer) 1
127.0.0.1:6379> zrange myset 0 -1
1) "one"
2) "two"
3) "three"
127.0.0.1:6379>

####################################################
# 实现排序

127.0.0.1:6379> zadd salary 2500 xiaohong    # 添加三个用户
(integer) 1
127.0.0.1:6379> zadd salary 5000 zhangsan
(integer) 1
127.0.0.1:6379> zadd salary 500 azhang
(integer) 1
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf # 显示全部用户,从小到大排序
1) "azhang"
2) "xiaohong"
3) "zhangsan"
127.0.0.1:6379> ZREVRANGE salary 0 -1    # 从大到小进行排序
1) "zhangsan"
2) "xiaohong"
3) "azhang"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf withscores    # 显示全部用户并且附带 score
1) "azhang"
2) "500"
3) "xiaohong"
4) "2500"
5) "zhangsan"
6) "5000"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf 2500 withscores    # 显示工资小于 2500 的员工的升序排列
1) "azhang"
2) "500"
3) "xiaohong"
4) "2500"
127.0.0.1:6379>

####################################################
# 移除 zrem

127.0.0.1:6379> ZRANGE salary 0 -1
1) "azhang"
2) "xiaohong"
3) "zhangsan"
127.0.0.1:6379> ZREM salary xiaohong    # 移除有序集合中的指定元素
(integer) 1
127.0.0.1:6379> ZRANGE salary 0 -1
1) "azhang"
2) "zhangsan"

####################################################
# zcard

127.0.0.1:6379> zcard salary    # 获取有序集合中的个数
(integer) 2

####################################################
127.0.0.1:6379> zadd myset 1 hello 2 world 3 azhang
(integer) 3
127.0.0.1:6379> zcount myset 1 3
(integer) 3
127.0.0.1:6379> zcount myset 1 2    # 获取指定区间的成员数量
(integer) 2
127.0.0.1:6379> zcount myset (1 3
(integer) 2

####################################################

其余的一些 API,可以去查看官方文档。

案例思路:set 排序 存储班级成绩,工资表排序......

普通消息1,重要消息2,带权重进行判断

排行榜应用实现,取 Top N 测试

Redis

概述

Redis 是什么

Redis( ==Re==mote ==Di==ctionary ==S==erver ),远程字典服务

是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。

Redis 的用途
  1. 内存存储、持久化,内存中是断电即失,所以说持久化很重要 ( rdb、aof)
  2. 效率高,可以用于高速缓存
  3. 发布订阅系统
  4. 地图信息分析
  5. 计时器、计数器 ( 浏览量 )
  6. ......
特性
  1. 多样的数据类型
  2. 持久化
  3. 集群
  4. 事务
  5. ......

==Redis 推荐在 Linux 服务器上搭建==

redis 的基础知识

  1. redis 默认有 16 个数据库。默认使用的是第 0 个,可以使用 select 进行切换。

    127.0.0.1:6379> select 3    # 切换数据库
    OK
    127.0.0.1:6379[3]> DBSIZE    # 查看 DB 大小
    (integer) 0
  2. 查看当前所有的 key

    127.0.0.1:6379> keys *
    1) "azhang"
  3. 清空数据库

    # 清空全部数据库
    127.0.0.1:6379> flushall
    OK
    127.0.0.1:6379> keys *
    (empty list or set)
    
    # 清空当前数据库
    127.0.0.1:6379> flushdb
    OK
  4. redis 是单线程的

MyBatis

  1. MyBatis 开发步骤:

    • 添加 MyBatis 的坐标

      <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.9</version>
      </dependency>
    • 创建 user 数据表
    • 创建 User 实体类
    • 编写映射文件 UserMapper.xml

      <?xml version="1.0" encoding="UTF-8"?>
      <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
      <mapper namespace="userMapper">
      
          <select id="findAll" resultType="com.domain.User">
              select * from user
          </select>
      
      </mapper>
    • 编写核心文件 SqlMapConfig.xml

      <?xml version="1.0" encoding="UTF-8"?>
      <!DOCTYPE configuration
              PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
              "http://mybatis.org/dtd/mybatis-3-config.dtd" >
      <configuration>
      
          <!-- 配置数据源环境 -->
          <environments default="development">
              <environment id="development">
                  <transactionManager type="JDBC"/>
                  <dataSource type="POOLED">
                      <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                      <property name="url" value="jdbc:mysql://localhost:3306/test"/>
                      <property name="username" value="root"/>
                      <property name="password" value="jie13727507037"/>
                  </dataSource>
              </environment>
          </environments>
      
          <!-- 加载映射文件 -->
          <mappers>
              <mapper resource="com.mapper\UserMapper.xml"/>
          </mappers>
      
      </configuration>
    • 编写测试类

      public void test1() throws IOException {
          // 加载核心配置文件
          InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
          // 获得 SQLSession 工厂对象
          SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
          // 获得 session 会话对象
          SqlSession sqlSession = sqlSessionFactory.openSession();
          // 执行操作     参数:namespace+语句id
          List<User> userList = sqlSession.selectList("userMapper.findAll");
          // 打印数据
          System.out.println(userList);
          // 释放资源
          sqlSession.close();
      }
  2. 映射文件概述

    image-20220630141304364

  3. MyBatis 的插入操作注意问题

    • 插入语句使用 insert 标签
    • 在映射文件中使用 parameterType 属性指定要插入的数据类型
    • Sql 语句中使用 #{实例属性名} 的方式引用实体中的属性值
    • 插入操作使用的 API 是 sqlSession.insert("namespace.id", 实体对象);
    • 插入操作涉及数据库数据变化,所以要使用 sqlSession对象显示的提交事务,即 sqlSession.commit()
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="userMapper">
    
        <!-- 查询操作 -->
        <select id="findAll" resultType="com.domain.User">
            select * from user
        </select>
    
        <!-- 插入操作 -->
        <insert id="save" parameterType="com.domain.User">
            insert into user values(#{id}, #{username}, #{password})
        </insert>
    
    </mapper>
    public void test2() throws IOException {
    
        // 模拟 user 对象
        User user = new User();
        user.setUsername("tom");
        user.setPassword("abc");
    
        // 加载核心配置文件
        InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
        // 获得 SQLSession 工厂对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        // 获得 session 会话对象
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 执行操作     参数:namespace+语句id
        sqlSession.insert("userMapper.save", user);
    
        // mybatis 执行更新操作  需要提交事务
        sqlSession.commit();
    
        // 释放资源
        sqlSession.close();
    }
  4. MyBatis 修改操作注意问题

    • 修改语句使用 update 标签
    • 修改操作使用的 API 是 sqlSession.update("namespace.id", 实体对象);
    <!-- 修改操作 -->
    <update id="update" parameterType="com.domain.User">
        update user set username=#{username}, password=#{password} where id=#{id}
    </update>
    public void test3() throws IOException {
    
        // 模拟 user 对象
        User user = new User();
        user.setId(7);
        user.setUsername("lucy");
        user.setPassword("123");
    
        // 加载核心配置文件
        InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
        // 获得 SQLSession 工厂对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        // 获得 session 会话对象
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 执行操作     参数:namespace+语句id
        sqlSession.update("userMapper.update", user);
    
        // mybatis 执行更新操作  需要提交事务
        sqlSession.commit();
    
        // 释放资源
        sqlSession.close();
    }
  5. MyBatis 删除操作注意问题

    • 删除语句使用 delete 标签
    • Sql 语句中使用 #{任意字符串} 方式引用传递的单个参数
    • 删除操作使用的 API 是 sqlSession.delete("namespace.id", Object);
    <!-- 删除操作 -->
    <delete id="delete" parameterType="java.lang.Integer">
        delete from user where id=#{id}
    </delete>
    public void test4() throws IOException {
    
        // 加载核心配置文件
        InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
        // 获得 SQLSession 工厂对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        // 获得 session 会话对象
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 执行操作     参数:namespace+语句id
        sqlSession.delete("userMapper.delete", 7);
    
        // mybatis 执行更新操作  需要提交事务
        sqlSession.commit();
    
        // 释放资源
        sqlSession.close();
    }
  6. MyBatis 核心配置文件常用配置

    • properties 标签:改标签可以加载外部的 properties 文件

      <!-- 加载外部 properties 文件 -->
      <properties resource="jdbc.properties"/>
    • typeAliases 标签:设置类型别名

      <!-- 定义别名 -->
      <typeAliases>
          <typeAlias type="com.domain.User" alias="user"/>
      </typeAliases>
    • mappers 标签:加载映射配置

      <!-- 加载映射文件 -->
      <mappers>
          <mapper resource="com.mapper/UserMapper.xml"/>
      </mappers>
    • environments 标签:数据源环境配置标签

      <!-- 配置数据源环境 -->
      <environments default="development">
          <environment id="development">
              <transactionManager type="JDBC"/>
              <dataSource type="POOLED">
                  <property name="driver" value="${jdbc.driver}"/>
                  <property name="url" value="${jdbc.url}"/>
                  <property name="username" value="${jdbc.username}"/>
                  <property name="password" value="${jdbc.password}"/>
              </dataSource>
          </environment>
      </environments>
  7. MyBatis 相应 API

    • SqlSession 工厂构造器 SqlSessionFactory build(InputStream inputstream)

      // 加载核心配置文件
      InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
      // 获得 SQLSession 工厂对象
      SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
    • SqlSession 工厂对象 SqlSessionFactory

      • openSession():会默认开启一个事务,但事务不会自动提交,也就是一位置需要手动提交该事务,更新操作数据才会持久化到数据库中
      • openSession(boolean autoCommit):参数为是否自动提交,如果设置为 true,那么不需要手动提交事务
      // 获得 session 会话对象
      SqlSession sqlSession = sqlSessionFactory.openSession();
    • SqlSession 会话对象

      • 执行语句的方法

        // 查询单个对象
        T selectOne(String statement, Object parameter);
        // 查询全部
        List<E> selectList(String statement, Object parameter);
        // 插入
        int insert(String statement, Object parameter);
        // 修改
        int update(String statement, Object parameter);
        // 删除
        int delete(String statement, Object parameter);
      • 操作事务的方法

        void.commit();     // 事务提交
        void rollback(); // 事务回滚
  8. MyBatis DAO 层实现

    • 代理开发方式

      UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
      • Mapper 接口开发需要遵循以下规范

        1. Mapper.xml 文件中的 namespace 与 mapper 接口的全限定名相同
        2. Mapper 接口方法名和 Mapper.xml 中定义的每个 statement 的 id 相同
        3. Mapper 接口方法的输入参数类型和 mapper.xml 中定义的每个 sql 的 parameterType 的类型相同
        4. Mapper 接口方法的输出参数类型和 mapper.xml 中定义的每个 sql 的 resultType 的类型相同
  9. MyBatis 映射文件深入

    • 动态 sql —— if

      <select id="findByCondition" resultType="user" parameterType="user">
          select * from user
          <where>
              <if test="id!=0">
                  id=#{id}
              </if>
              <if test="username!=null">
                  and username=#{username}
              </if>
              <if test="password!=null">
                  and password=#{password}
              </if>
          </where>
      </select>
    • 动态 sql —— foreach

      <select id="findByIds" parameterType="list" resultType="user">
          select * from user
          <where>
              <foreach collection="list" open="id in(" close=")" item="id" separator=",">
                  #{id}
              </foreach>
          </where>
      </select>
    • 动态 sql —— sql 语句的抽取

      <!-- sql 语句的抽取 -->
      <sql id="selectUser">select * from user</sql>
      
          <include refid="selectUser"></include>
  10. MyBatis 核心配置文件深入

    • typeHandlers 标签

      • 开发步骤

        1. 定义转换类继承类 BaseTypeHandler<T> 
        2. 覆盖 4 个未实现的方法,其中 setNonNullParameter 为 java 程序设置数据到数据库的回调方法,getNullableResult 为查询时 mysql 的字符串类型转换成 java 的 Type 类型的方法
        3. 在 MyBatis 核心配置文件中个进行注册
        4. 测试转换是否正确
    • plugins 标签

      • 开发步骤

        1. 导入通用 PageHelper 的坐标
        2. 在 mybatis 核心配置文件中配置 PageHelper 插件
        3. 测试分页数据获取
  11. MyBatis 多表操作

    1. 一对一

      • 第一种配置
     

     ```xml
     <resultMap id="orderMap" type="order">
         <!-- 手动指定字段与实体的映射关系
              column: 数据表的字段名称
              property: 实体的属性名称
          -->
         <id column="oid" property="id"/>
         <result column="ordertime" property="orderTime"/>
         <result column="total" property="total"/>
         <result column="uid" property="user.id"/>
         <result column="username" property="user.username"/>
         <result column="password" property="user.password"/>
         <result column="birthday" property="user.birthday"/>
     </resultMap>
     
     <select id="findAll" resultMap="orderMap">
         select *, o.id oid from orders o, user u where o.id=u.id
     </select>
     ```

   - 第二种配置

     ```xml
     <resultMap id="orderMap" type="order">
         <!-- 手动指定字段与实体的映射关系
              column: 数据表的字段名称
              property: 实体的属性名称
          -->
         <id column="oid" property="id"/>
         <result column="ordertime" property="orderTime"/>
         <result column="total" property="total"/>
         <!--<result column="uid" property="user.id"/>
         <result column="username" property="user.username"/>
         <result column="password" property="user.password"/>
         <result column="birthday" property="user.birthday"/>-->
     
         <!--
             property: 当前实体的属性名称
             javaType: 当前实体中的属性的类型
         -->
         <association property="user" javaType="user">
             <id column="uid" property="id"/>
             <result column="username" property="user.username"/>
             <result column="password" property="user.password"/>
             <result column="birthday" property="user.birthday"/>
         </association>
     </resultMap>
     
     <select id="findAll" resultMap="orderMap">
         select *, o.id oid from orders o, user u where o.id=u.id
     </select>
     ```

2. 一对多

   - ```xml
     <resultMap id="userMap" type="user">
         <id column="id" property="id"/>
         <result column="username" property="username"/>
         <result column="password" property="password"/>
         <result column="birthday" property="birthday"/>
     
         <!-- 配置集合信息
              property: 集合名称
              ofType: 当前集合中的数据类型
          -->
         <collection property="orderList" ofType="order">
             <id column="oid" property="id"/>
             <result column="ordertime" property="orderTime"/>
             <result column="total" property="total"/>
         </collection>
     </resultMap>
     
     <select id="findAll" resultMap="userMap">
         select *, o.id oid from user u, order o where u.id=o.uid
     </select>
     ```

3. 多对多

   - ```xml
     <resultMap id="userRoleMap" type="user">
         <id column="userId" property="id"/>
         <result column="username" property="username"/>
         <result column="password" property="password"/>
         <result column="birthday" property="birthday"/>
         
         <collection property="roleList" ofType="role">
             <id column="roleId" property="id"/>
             <result column="roleName" property="roleName"/>
             <result column="roleDesc" property="roleDesc"/>
         </collection>
     </resultMap>
     
     <select id="findUserAndRoleAll" resultMap="userRoleMap">
         select * from user u, sys_user_role ur, sys_role r where u.id=ur.userId and ur.roleId
     </select>
     ```
  1. MyBatis 注解开发

    • 常用注解

      • @Insert:实现新增
      • @Update:实现更新
      • @Delete:实现删除
      • @Select:实现查询
      • @Result:实现结果集封装
      • @Results:可以与 @Result 一起使用,封装多个结果集
      • @One:实现一对一结果封装
      • @Many:实现一对多结果集封装
    • 简单查询

      package com.mapper;
      
      import com.domain.User;
      import org.apache.ibatis.annotations.Delete;
      import org.apache.ibatis.annotations.Insert;
      import org.apache.ibatis.annotations.Select;
      import org.apache.ibatis.annotations.Update;
      
      import java.util.List;
      
      public interface UserMapper {
      
          @Insert("insert into user values(#{id}, #{username}, #{password}, #{birthday})")
          void save(User user);
      
          @Delete("delete from user where id=#{id}")
          void delete(int id);
      
          @Update("update user set username=#{username}, password=#{password} where id=#{id}")
          void update(User user);
      
          @Select("select * from user where id=#{id}")
          User findById(int id);
      
          @Select("select * from user")
          List<User> findAll();
      
      }
      <!-- 加载映射关系 -->
      <mappers>
          <!-- 指定一个包下的接口 -->
          <package name="com.mapper"/>
      </mappers>
    • 复杂查询 —— 一对一

      两种方式:

      • package com.mapper;
        
        import com.domain.Order;
        import org.apache.ibatis.annotations.Result;
        import org.apache.ibatis.annotations.Results;
        import org.apache.ibatis.annotations.Select;
        
        import java.util.List;
        
        public interface OrderMapper {
        
            @Select("select *, o.id oid from orders o, user u where o.id=u.id")
            @Results({
                    @Result(column = "oid", property = "id"),
                    @Result(column = "ordertime", property = "orderTime"),
                    @Result(column = "total", property = "total"),
                    @Result(column = "uid", property = "user.id"),
                    @Result(column = "username", property = "user.username"),
                    @Result(column = "password", property = "user.password")
            })
            List<Order> findAll();
        
        }
      • @Select("select * from orders")
        @Results({
                @Result(column = "id", property = "id"),
                @Result(column = "ordertime", property = "orderTime"),
                @Result(column = "total", property = "total"),
                @Result(
                        javaType = User.class,  // 要封装的实体类型
                        column = "uid",            // 根据哪个字段去查询 user 表的数据
                        property = "user",       // 要封装的属性名
                        // select 属性 代表查询哪个接口的方法获得数据
                        one = @One(select = "com.mapper.UserMapper.findById")
                )
        })
        List<Order> findAll();
    • 复杂查询——一对多

      @Select("select * from orders where uid=#{uid}")
      List<Order> findByUid(int uid);
      @Select("select * from user")
      @Results({
              @Result(id = true, column = "id", property = "id"),
              @Result(column = "username", property = "username"),
              @Result(column = "password", property = "password"),
              @Result(
                      javaType = List.class,  // 要封装的实体类型
                      column = "id",            // 根据哪个字段去查询 user 表的数据
                      property = "orderList",       // 要封装的属性名
                      // select 属性 代表查询哪个接口的方法获得数据
                      many = @Many(select = "com.mapper.OrderMapper.findByUid")
              )
      })
      List<User> findUserAndOrderAll();
    • 复杂查询 —— 多对多

Spring Boot 与 Web 开发

  1. SpringMVC 快速使用

    • 基于 restful http 接口的 CURD

      @RestController
      @RequestMapping("/user")
      public class UserController {
      
          @Autowired
          UserService userService;
      
          @GetMapping("/{id}")
          public Result getUser(@PathVariable Integer id) {
              User user = userService.getUserById(id);
              return new Result<>(200, "查询成功", user);
          }
      
          @PostMapping("/add")
          public Result getUser(User user) {
              userService.add(user);
              return new Result<>(200, "添加成功");
          }
      
          @PutMapping("/{id}")
          public Result editUser(User user) {
              userService.update(user);
              return new Result(200, "修改成功");
          }
      
          @DeleteMapping("/{id}")
          public Result deleteUser(@PathVariable Integer id) {
              userService.delete(id);
              return new Result<>(200, "删除成功");
          }
      }
    • 调用 rest http 接口

      • 通过 restTemplate 调用

        RestTemplate 类可用于在应用中调用 rest 服务,它简化了与 http 服务的通信方式,统一了 RESTful 的标准,封装了 http 链接,我们只需要传入 url 及返回值类型即可。

        适用于微服务架构下,服务之间的远程调用。 ps:以后使用微服务架构,使用 spring cloud feign 组件

        restTemplate 和 webClient 都可以调用远程服务,区别:webclient 依赖 webflux,webclient 请求远程服务是无阻塞的,响应的。RestTemplate 是阻塞的,需要等待请求响应后才能执行下一句代码。

        DELETEdelete
        GETgetForObject 按照指定 Class 返回对象
        getForEntity 返回对象为 ResponseEntity 对象,包含了响应中的一些重要信息,比如响应头、响应状态码、响应体等。
        HEADheadForHeaders
        OPTIONSoptionForAllow
        POSTpostForLocaltion
        postForObject
        PUTput
        any
        支持任何请求方法类型
        exchange
        execute
        • 基于 restTemplate 调用查询

          // 声明了 RestTemplate
          private final RestTemplate restTemplate;
          
          // 当 Bean 没有无参构造函数的时候, spring 将自动拿到有参的构造函数,参数进行自动注入
          public OrderController(RestTemplateBuilder restTemplateBuilder) {
              this.restTemplate = restTemplateBuilder.build();
          }
          
          @RequestMapping("/order")
          public String order() {
              Result forObject = restTemplate.getForObject("http://localhost:8080/user/{id}", Result.class, 1);
          
              return forObject.toString();
          }
        • 基于 restTemplate 调用查询

          @RequestMapping("/order")
          public String order() {
              //基于 restTemplate 调用查询
              User user = new User("AZhang", "wyu");
              // url: 请求的远程 rest url
              // object: post 请求的参数
              // Class<T>: 返回的类型
              // ...Object: 是 @PathVariable 占位符的参数
              ResponseEntity<Result> resultResponseEntity = restTemplate.postForEntity("http://localhost:8080/user/add", user, Result.class);
              System.out.println(resultResponseEntity);
              return resultResponseEntity.getBody().toString();
          }
        • 基于 restTemplate 调用修改

          @RequestMapping("/order")
          public String order() {
              //基于 restTemplate 调用修改
              User user = new User(1, "AZhang", "wyu");
              //restTemplate.put("http://localhost:8080/user/{id}", user, Result.class);
              HttpEntity<User> httpEntity = new HttpEntity<>(user);
              ResponseEntity<Result> resultResponseEntity = restTemplate.exchange("http://localhost:8080/user/{id}", HttpMethod.PUT, httpEntity, Result.class, 1);
              System.out.println(resultResponseEntity);
              return resultResponseEntity.getBody().toString();
          }
        • 基于 restTemplate 调用删除

          @RequestMapping("/order")
          public String order() {
              //基于 restTemplate 调用删除
              ResponseEntity<Result> resultResponseEntity = restTemplate.exchange("http://localhost:8080/user/{id}", HttpMethod.DELETE, null, Result.class, 1);
              System.out.println(resultResponseEntity);
              return resultResponseEntity.getBody().toString();
          }
     

 - 通过 postman 调用

 - 通过 MockMvc 测试

   MockMvc 是由 spring-test 包提供,实现了对 Http 请求的模拟,能够直接使用网络的形式,转换到 Controller 的调用,使得测试速度快、不依赖网络环境。同时提供了一套验证的工具,结果的验证十分方便。
  1. SpringMVC 自动配置原理分析

    ​ Spring Boot 为 Spring MVC 提供了自动配置,可与大多数应用程序完美配合。

    自动配置在 Spring 的默认值之上添加了以下功能:

    • 包含 ContentNegotiatingViewResolverBeanNameViewResolver

      • ViewResolver 都是 SpringMVC 内置的视图解析器

        • ContentNegotiatingViewResolver

          • 不会解析视图,而是委派给其他视图解析器进行解析。
          • 所有的视图解析器,都会根据返回的视图名称进行解析视图 resolveViewName

            public View resolveViewName(String viewName, Locale locale) throws Exception {
                RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
                Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
                List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
                if (requestedMediaTypes != null) {
                    // 获得所有匹配的视图
                    List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
                    // 获取最终的这个
                    View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
                    if (bestView != null) {
                        return bestView;
                    }
                }

            委派给其他视图解析器进行解析:

            @Override
            protected void initServletContext(ServletContext servletContext) {
                Collection<ViewResolver> matchingBeans =
                        BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values();
                if (this.viewResolvers == null) {
                    this.viewResolvers = new ArrayList<>(matchingBeans.size());
                    for (ViewResolver viewResolver : matchingBeans) {
                        if (this != viewResolver) {
                            this.viewResolvers.add(viewResolver);
                        }
                    }
                }
        • BeanNameViewResolver

          • 会根据 handler 方法返回的视图名称,去 IOC 容器中找到名字叫 azhang 的一个 Bean,并且这个 Bean要实现了 View接口。
          • 示例:

            @GetMapping("/test")
            public String getUser() {
                return "azhang";
            }

            可以配置一个名字叫 azhang 的视图(View)

            @Component
            public class Azhang implements View {
                @Override
                public String getContentType() {
                    return "text/html";
                }
            
                @Override
                public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
                    response.getWriter().print("Welcome to AzhangView");
                }
            }
       

   - 由以上代码可以得出结论,它是从 Spring IOC 容器获得 ViewResolve 类型 Bean,name我们可以自己定制一个 ViewResolver,ContentNegotiatingViewResolver 也会帮我们委派解析:

     ```java
     @Bean
     public ViewResolver AZhangViewResolve() {
         InternalResourceViewResolver resolver = new InternalResourceViewResolver();
         resolver.setPrefix("/");
         resolver.setSuffix(".html");
         return resolver;
     }
     ```

     ![image-20220506164908963](https://static-upyun.hackerjk.top/markdown/image-20220506164908963.png!)
  • 支持提供静态资源,包括对 WebJars 的支持。

    • 以前要访问 jpg / css / js 等这些静态资源文件,需要在 web.xml 配置,但现在在 SpringBoot 中不需要配置,只需要放在约定文件夹中就可以(约定大于配置)
    • 原理:

      • WebJars:就是将静态资源放在 jar 包中进行访问
      • WebJars 官网:https://www.webjars.org/
      • @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {
            if (!this.resourceProperties.isAddMappings()) {
                logger.debug("Default resource handling disabled");
                return;
            }
            addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
            addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
                registration.addResourceLocations(this.resourceProperties.getStaticLocations());
                if (this.servletContext != null) {
                    ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION);
                    registration.addResourceLocations(resource);
                }
            });
        }
        
        private void addResourceHandler(ResourceHandlerRegistry registry, String pattern, String... locations) {
            addResourceHandler(registry, pattern, (registration) -> registration.addResourceLocations(locations));
        }
        
        private void addResourceHandler(ResourceHandlerRegistry registry, String pattern,
                Consumer<ResourceHandlerRegistration> customizer) {
            if (registry.hasMappingForPattern(pattern)) {
                return;
            }
            ResourceHandlerRegistration registration = registry.addResourceHandler(pattern);
            customizer.accept(registration);
            registration.setCachePeriod(getSeconds(this.resourceProperties.getCache().getPeriod()));
            registration.setCacheControl(this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl());
            registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified());
            customizeResourceHandlerRegistration(registration);
        }
      • 当访问 /webjars/** 时,就会去 classpath:/META-INF/resources/webjars/ 对应进行映射

        当访问 http://localhost:8080/webjars/jquery/3.5.1/jquery.js 对应映射到 /META-INF/resources/webjars/jquery/3.5.1/jquery.js

        image-20220506171959211

        • 在 static 文件中访问的静态资源,默认的四个静态资源路径 (对应的映射地址) :
        { "classpath:/META-INF/resources/",
                "classpath:/resources/",
                "classpath:/static/",
    • 配置首页欢迎页:

      private Resource getWelcomePage() {
          for (String location : this.resourceProperties.getStaticLocations()) {    // 拿到上面静态资源地址
              Resource indexHtml = getIndexHtml(location);
              if (indexHtml != null) {
                  return indexHtml;
              }
          }
          ServletContext servletContext = getServletContext();
          // 去里面找一个 index.html 的首页文件
          if (servletContext != null) {
              return getIndexHtml(new ServletContextResource(servletContext, SERVLET_LOCATION));
          }
          return null;
      }
      
      private Resource getIndexHtml(String location) {
          return getIndexHtml(this.resourceLoader.getResource(location));
      }
      
      private Resource getIndexHtml(Resource location) {
          try {
              Resource resource = location.createRelative("index.html");
              if (resource.exists() && (resource.getURL() != null)) {
                  return resource;
              }
          }
          catch (Exception ex) {
          }
          return null;
      }
    • 也可以通过配置文件指定具体的静态资源地址:

  • 自动注册 ConverterGenericConverterFormatter Bean 类。

    • 使用方式
  • 支持 HttpMessageConverters

    • HttpMessageConverters 负责 http 请求和响应的报文处理。
    • image-20220507011511095
  • 自动注册 MessageCodesResolver

    • 修改 4xx 错误下格式转换出错、类型转换出错的错误代码
    • 以前的格式是 errorCode + "." + object name + "." + field

      例:typeMismatch.user.birthday

    • 可以通过 spring.mvc.message-codes-resolver-format=postfix_error_code 将格式修改为:object name + "." + field + "." + errorCode
  • 静态 index.html 支持。

    • 在 springboot 中可以直接返回 html 的视图,因为在 WebMvcAutoConfiguration 配置类配置了:

      @Bean
      @ConditionalOnMissingBean
      public InternalResourceViewResolver defaultViewResolver() {
          InternalResourceViewResolver resolver = new InternalResourceViewResolver();
          resolver.setPrefix(this.mvcProperties.getView().getPrefix());
          resolver.setSuffix(this.mvcProperties.getView().getSuffix());
          return resolver;
      }

      所以可以通过在配置文件中完成:

      spring.mvc.view.prefix=/pages/
      spring.mvc.view.suffix=.html
  • 自动使用 ConfigurableWebBindingInitializer bean。

    1. 定制 SpringMVC 的自动配置

      SpringMVC 的自动配置类:WebMvcAutoConfiguration

  • 在大多数情况下,SpringBoot 在自动配置类中标记了很多 @ConditionalOnMissingBean(xxxxxxxxxx.class) ;(意思就是如果容器中没有,当前的 Bean 才会生效)。只需要在自己的配置类中配置对应的一个 @Bean 就可以覆盖默认自动配置。
  • 通过 WebMvcConfigurer 进行拓展

    • 扩展视图控制器
    • 扩展拦截器
    • 扩展全局 CORS
    • @Configuration
      public class MyWebMvcConfigurer implements WebMvcConfigurer {
          /*@Override
          public void configurePathMatch(PathMatchConfigurer configurer) {
              configurer.setUseTrailingSlashMatch(false);
          }*/
      
          @Override
          public void addInterceptors(InterceptorRegistry registry) {
              registry.addInterceptor(new TimeInterceptor())      // 添加拦截器
                  .addPathPatterns("/**")      // 拦截映射规则  /**拦截所有的请求
                  .excludePathPatterns("/pages/**");        // 排除 pages 下面所有的请求
          }
      
          /**
           *  CORS 配置
           *  全局跨域请求配置
           */
          /*@Override
          public void addCorsMappings(CorsRegistry registry) {
              registry.addMapping("/user/*")       // 映射服务器中哪些 http 接口允许运行跨域访问
                      .allowedOrigins("http://localhost:8081")                // 配置哪些来源有权限跨域
                      .allowedMethods("GET", "POST", "PUT", "DELETE");        // 配置允许跨域访问的请求方法
          }*/
      
          @Bean
          public InternalResourceViewResolver AZhangViewResolver() {
              InternalResourceViewResolver resolver = new InternalResourceViewResolver();
              resolver.setPrefix("/pages");
              resolver.setSuffix(".html");
              return resolver;
          }
      }
    • WebMvcConfigurer 原理

      实现 WebMvcConfigurer 接口可以扩展 Mvc 实现,又既保留了 SpringBoot 的自动配置

      • WebMvcAutoConfiguration 也有一个实现了 WebMvcConfigurer 的配置类
      • WebMvcAutoConfigurationAdapter 它也是利用这种方式去进行扩展,所以我们通过查看这个类我们发现它帮我们实现了其他不常用的方法,帮助我们进行自动配置,我们只需要定制(拦截器、视图控制器、CORS 等在开发中需要额外定制的功能)

        @Configuration(proxyBeanMethods = false)
        @Import(EnableWebMvcConfiguration.class)
        @EnableConfigurationProperties({ WebMvcProperties.class, WebProperties.class })
        @Order(0)
        public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {
      • 导入了 EnableWebMvcConfiguration

        @Import(EnableWebMvcConfiguration.class)

        EnableWebMvcConfiguration 它的父类上 setConfigurers 使用了 @AutoWired 注解

        • 它会去容器中将所有实现了 WebMvcConfigurer 接口的 Bean 都自动注入进来,添加到 configurers 变量中
      public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
      
          private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
      
          @Autowired(required = false)
          public void setConfigurers(List<WebMvcConfigurer> configurers) {
              if (!CollectionUtils.isEmpty(configurers)) {
                  this.configurers.addWebMvcConfigurers(configurers);
              }
          }
      ```
    
    - 添加到 ``delegates`` 委派器中
    
      ```java
      public void addWebMvcConfigurers(List<WebMvcConfigurer> configurers) {
          if (!CollectionUtils.isEmpty(configurers)) {
              this.delegates.addAll(configurers);
          }
      }
      ```
    
    - 底层调用 WebMvcConfigurer 对应的方法时,就是去拿到之前注入到 delegates 的 WebMvcConfigurer,依次调用
    
      ```java
      @Override
      public void addInterceptors(InterceptorRegistry registry) {
          for (WebMvcConfigurer delegate : this.delegates) {
              delegate.addInterceptors(registry);
          }
      }
      ```
    
    • 当添加了 @EnableWebMvc 就不会使用 SpringMVC 自动配置类的默认配置了

      原理:

      • 在 EnableMvc 中 @Import(DelegatingWebMvcConfiguration.class)

        @Retention(RetentionPolicy.RUNTIME)
        @Target(ElementType.TYPE)
        @Documented
        @Import(DelegatingWebMvcConfiguration.class)
        public @interface EnableWebMvc {
        }
      • 在 DelegatingWebMvcConfiguration 中继承了 WebMvcConfigurationSupport

        @Configuration(proxyBeanMethods = false)
        public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
      • 在 WebMvcAutoConfiguration 中 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)

        ​ 当容器中不存在 WebMvcConfigurationSupport 这个 Bean 的时候自动配置类才会生效

        ​ 正因为通过 @EnableWebMvc 导入了 DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport 从而才使自动配置类失效

        @Configuration(proxyBeanMethods = false)
        @ConditionalOnWebApplication(type = Type.SERVLET)
        @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
        @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
        @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
        @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
                ValidationAutoConfiguration.class })
        public class WebMvcAutoConfiguration {
  • Json 开发

    • Spring Boot 提供了与三个 JSON 映射库的集成:

      • Gson

        • Jackson 性能最好
      • JSON-B

      Jackson 是我们使用的默认 json 库

    • jackson 的使用

      • @JsonIgnore

        • 进行排除 json 序列化,将它标注在属性上将不会进行 json 格式化
  • 国际化
  • 统一异常处理
  1. SpringBoot 的嵌入式 Servlet 容器