xiaoWang3486

博客园 首页 新随笔 联系 订阅 管理

Redis

1.安装

​ 1.官网下载redis-7.0.10.tar.gz

​ 2.传到Linux的/usr/local目录中

cd /usr/local
#解压
tar -zxvf redis-7.0.10.tar.gz 
#redis-7.0.10
cd redis-7.0.10
#使用make命令
make&&make install
#进入文件
cd redis.conf
#修改配置

daemonize yes
protected-mode no
#bind 127.0.0.1
自己添加requirepass *****

#完成后重启
#启动redis
redis-server redis.conf
#启动redis客户端
redis-cli -a ***** -p 6379
#查看
ps -ef |grep redis
#关闭
redis-cli -a 111111 shutdown

2.key的基本命令

查看所有key: keys *

查看key类型:type key

查看是否存在:exists key | exists key1,key2,key3

切换Redis库:select (index)

设定过期时间:expire key seconds

查看剩余过期:ttl key (永久返回-1,已经过期返回-2)

查看数据库的数量:dbsize

清空当前数据库:flushdb

清空所有数据库:fludb

删除key:del key

3.基本数据类型

string

获取长度:strlen k1

追加字符:append k1 [value]

添加单个:set k1 [value]

获取单个:get k1

秒过期时间:set k1 [value] ex [过期时间]

毫秒过期时间:set k1 v1 px [过期时间]

秒单位时间戳:set k1 v1 exat 【过了当前时间戳就为过期】

毫秒单位时间戳:set k1 v1 pxat【时间戳】

不存在时添加:set k1 v1 nx

存在的时候:set k1 v1 xx

保存过期时间:set k1 v1 keepttl

先get后set:getset k1 【value】

不存在时添加:setnx k1 v1

批处理

添加多个:mset ka va kb vb

获取多个:mget ka kb

不存在时添加多个:msetnx ka va kc vc 必须全部成功

获取指定位置:getrange k1 【sta】【 end

设置指定位置:setrange k1 【index】【 value

加减

对数字类型

自增:incr k1

自减:decr k1

增量20:incrby k1 【增量】

减量20:decrby k1 【减量】

list

左边加入:lpush list1 【elements.......】

右边加入:rpush list1 【elements.......】

左边遍历:lrange list1 【sta】【end】

左边出栈:lpop list1 【出栈个数】

右边出栈:rpop list1 【出栈个数】

索引取值:lindex list1 【index】

获取长度:llen list1

删除指定个数指定元素:lrem list1 【个数】【元素】

截取指定长度赋值给list:ltrim list1 【sta】【end】

原子操作尾出头插:rpoplpush list1 list2

指定下标左边加入:lset list1 【index】【element】

左边指定元素前后加入:linsert list1 【after/before】【element】【java】

hash

添加对象:hset 【user:001】 name zhangsan age lisi

获取对象:hget 【user:001】 name

获取全部参数:hgetall 【user:001】

获取多个:hmget 【user:001】 name age

属性值增加:hincrby 【user:001】 age 【增量】

属性值增加: hincrbyfloat 【user:001】 age 【增量】

添加对象属性如果不存在:hsetnx user:001 name zhangsan

删除对象元素:hdel user:001 name

set

添加 sadd s1 【elements......】 //只有abcd四个元素

获取 smembers s1

判断是否存在 sismember s1 【元素】 //1

获取长度 scard s1

删除元素 srem s1 【elements】

随机产生元素 srandmember s1 【产生个数】

随机弹出一个元素 spop s1 【数字】

移动集合a到b smove s1 s2 【元素】

集合运算

s1 【1 2 3 4 5 6】 s2 【4 5 6 7 8 9】

交集 sinter s1 s2 //4 5 6

并集 sunion s1 s2 //123456

差集 sdiff s1 s2 //以一为主 为123 以二为主 为 789

zset

添加元素:zadd z1 【score】【value】【score】【value】

查看数量:zcard z1

返回指定score范围的数据数量:zcount z1 【min】【max】

返回指定范围数据 zrange z1 【起始】【结尾】

返回指定范围反转的数据:zrevrange z1 【起始】【结尾】

返回指定score范围的数据:zrangebyscore z1 【min】【max】加 ( 表示不包括

返回元素分数:zscore z1 【元素】

删除元素:zrem z1 【元素】

增加元素分数:zincrby 【增数】 【元素】

返回元素的排名位置:zrank z1 【元素】

返回反转的排名位置:zrevrank z1 【元素】

bitmap:签到

给指定key的offset赋值0|1:setbit 【element1】 【位置】【 状态01】

HyperLogLog:UA统计

添加元素:PFADD key element [element …]

返回储存在给定键的 HyperLogLog 的近似基数:PFCOUNT key

合并多个HyperLogLog于一个HyperLogLog:

​ PFMERGE destkey sourcekey [sourcekey …]

GEO

黑马点评文件

链接https://pan.baidu.com/s/1189u6u4icQYHg_9_7ovWmA?pwd=eh11
提取码:eh11

4.Jedis

使用

1.引入依赖

<dependencies>
    <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>4.2.3</version>
    </dependency>
</dependencies>

2.引入日志

 <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-log4j12 -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.7.21</version>
        <scope>test</scope>
    </dependency>

3.引入junit5

    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.9.1</version>
        <scope>test</scope>
    </dependency>

4.建立连接,执行方法,关闭连接

//        创建Jedis对象传入参数ip地址,redis端口号
        Jedis jedis = new Jedis("8.130.103.195", 6379);
//        配置密码
        jedis.auth("aaaaaa");
        jedis.set("k1","v1");
//		  关闭Redis
        jedis.close();

连接池

public class JedisConnectionFactory{
    private static JedisPool jedisPool;
    static{
    JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
    jedisPoolConfig.setMaxTotal(8);
    jedisPoolConfig.setMaxIdle(8);
    jedisPoolConfig.setMinIdle(2);
    new jedisPool = JedisPool(jedisPoolConfig,
                              "8.130.103.195",6379,1000,"417403");
    }
    public static Jedis getJedis(){
		return jedosPool.getResource();
    }
}

5.SpringDataRedis

1.使用:

引入依赖

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

配置信息

spring:
  redis:
    host: 8.130.114.137
    port: 6379
    password: aaaaaa
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0

自动注入

@Autowared
private RedisTemplate redisTemplate;

6.SpringRedisTemplate

1.使用

引入依赖

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

配置application.peoperties

spring.redis.host=8.130.114.137
spring.redis.database=1
spring.redis.port= 6379
spring.redis.password=417403

spring.redis.lettuce.pool.max-active= 10
spring.redis.lettuce.pool.max-idle=10
spring.redis.lettuce.pool.max-wait=1
spring.redis.lettuce.pool.time-between-eviction-runs=0

spring.redis.lettuce.shutdown-timeout=5000
spring.jackson.default-property-inclusion=non_null

注入SpringRedisTemplate

2.区别

StringRedisTemplate与RedisTemplate
两者的关系是StringRedisTemplate继承RedisTemplate。

两者的数据是不共通的;也就是说StringRedisTemplate只能管理StringRedisTemplate里面的数据,RedisTemplate只能管理RedisTemplate中的数据。

SDR默认采用的序列化策略有两种,一种是String的序列化策略,一种是JDK的序列化策略。

StringRedisTemplate默认采用的是String的序列化策略,保存的key和value都是采用此策略序列化保存的。

RedisTemplate默认采用的是JDK的序列化策略,保存的key和value都是采用此策略序列化保存的。

7.Redis缓存策略

0.最佳方案

1.更新策略

低一致性业务,建议配置最大内存占用和淘汰策略。

高一致性业务,可以结合超时剔除和主动更新。

算法剔除:不用管理,当内存不足时,redis淘汰机制会淘汰部分数据。

超时剔除:给缓存设置过期时间,过期后自动删除。

主动更新:需要管理,根据业务逻辑进行更新

2.主动更新策略

1.Cache Aside Pattern:

​ 由缓存的调用者,在更新数据库时同时更新。

2.Read/wirte Through Pattern:

​ 将缓存和数据库整合为一个业务,由服务器来维护,无需关心一致性。

3.Write Behind Caching Pattern:

​ 将写入业务保留在缓存中,再由其他线程异步将数据持久化到数据库中。

3.Cache Aside Pattern

删除缓存操作

​ 更新缓存:每次更新数据库后更新缓存,没有读业务,更新浪费

​ 删除缓存:当数据库更新时删除缓存,需要使用再次从数据库读取**

保证缓存与数据库业务操作一致性

​ 放在同一个事物中

CacheAsidePattern

​ 先更新数据库,再删除缓存

4.缓存穿透

什么是缓存穿透:

​ 每次从缓存中都查不到数据,而需要查询数据库,同时数据库中也没有查到该数据,也没法放入缓存。也就是说,每次这个用户请求过来的时候,都要查询一次数据库。给数据库造成压力

解决方案:

​ 使用缓存空对象:

​ 优点:实现方便,维护方便

​ 缺点:可能造成额外内存浪费,造成短期不一致

​ 布隆过滤:

​ 优点:内存占用少,没有多余key

​ 缺点:实现复杂,底层依靠哈希,最后转换为二进制存在误判。

5.缓存雪崩

什么是缓存雪崩:

缓存雪崩是指当大量的缓存key同时过期时或者Redsi宕机时,大量的请求到数据库,给数据库造成压力。

如何解决:

给缓存添加随机的TTL过期时间,集群部署,给业务降级,添加多级缓存。

6.缓存击穿

缓存击是什么

指当热点数据过期时,大量的请求到服务器,给服务器造成压力。

如何解决

添加互斥锁

设置逻辑过期

8.优惠券秒杀

1.订单的id生成

    private static final long BEGIN_TIMESTAMP = 1640995200L;
    private static final int COUNT_BITS = 32;

    public RedisIdGenerator(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private final StringRedisTemplate stringRedisTemplate;

    /**
     *
     * @param redisCachePrefix 业务前缀,区分业务
     * @return
     */
    public Long idGenerate(String redisCachePrefix){
//        生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long beginTimestamp = BEGIN_TIMESTAMP;
        long timestamp = nowSecond - BEGIN_TIMESTAMP;
        String data = now.format(DateTimeFormatter.ofPattern("yyyy:mm:dd"));
//        生成序列号  redis自增id
        Long count = stringRedisTemplate.opsForValue().increment("icr:"+redisCachePrefix+":"+data);
//        拼接
        return  timestamp << COUNT_BITS | count;
    }

2.秒杀下单

防止超卖,使用乐观锁,悲观锁。

乐观锁:给stock字段作为乐观锁版本,每次只要在操作时,再次判断当前的值与刚才读取的值是否相同

防止黄牛:

使用悲观锁,将每个用户的id上锁,再去查询库中相同对象,相同订单是否存在

Long userId = UserHolder.getUser().getId();
// 如果字符常量池中已经包含一个等于此String对象的字符串,则返回常量池中字符串的引用
// 对于任意两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true
synchronized (userId.toString().intern()) {
    IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
    return proxy.createVoucherOrder(voucherId);
}
Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0){
    return Result.fail("用户已经购买过一次了!");
}

9.分布式锁

1.分布式锁解决集群下锁的问题

使用redis的setnx,对用户或者线程进行控制

/**
 * 获取锁
 * @param timeOutExpire
 * @return
 */
@Override
public boolean tryLock(Long timeOutExpire) {
    Boolean aBoolean = stringRedisTemplate
        .opsForValue()
            .setIfAbsent(LOCK_PREFIX+name, String.valueOf(Thread.currentThread().getId()), timeOutExpire, TimeUnit.MINUTES);
    return Boolean.TRUE.equals(aBoolean);
}

2.解决分布式误删问题

在释放锁的时候,添加一层判断,当前线程对象和锁对象是否为同一个

3.解决redis分布式事务原子性

使用lua脚本,通过redis调用lua脚本,使释放锁的判断和删除操作都一次执行,保证原子性。

4.可重入锁

同一个线程的锁可以多次复用,Redis实现可重入锁,是通过使用hset结构,key为锁名称,value为当前线程标识,和锁的个数

转行程序员

10.单机Redis问题

10.Redis持久化

1.RDB

1.什么是RDB

​ RDB(Redis DataBase),是redis默认的存储方式,RDB方式是通过快照( snapshotting )完成的。它保存的是某一时刻的数据并不关注过程。RDB保存redis某一时刻的数据的快照

2.触发方式

Redis服务器主动关闭时,当操作符合快照规则时,执行主从复制时。

3.配置

4.bgsave执行流程

image-20230504170529720

5.优缺点

优点:

1.RDB是基于二进制存储的,占用空间小,便于传给slaver

2.主进程fork一个紫禁城,可以最大化Redis性能

3.使用RDB恢复文件较快

缺点:

1.不保证数据完整性,最后一次快照后面的数据会丢失

2.父进程在fork子进程时是阻塞的,如果主进程较大,可能会阻塞。

6.禁用RDB

#在Redis.cof中添加
save ""

2.AOF

1.什么是AOF

​ Redis默认情况下是不开启的。开启AOF持久化后Redis 将所有对数据库进行过写入的命令(及其参数)(RESP)记录到 AOF 文件, 以此达到记录数据库状态的目的,这样当Redis重启后只要按顺序回放这些命令就会恢复到原始状态了。AOF会记录过程,RDB只管结果

2.配置conf

3.AOF执行流程

4.AOF重写

当AOF体积超过配置中的阈值时,会自动在后台子进程对AOF文件重写。

​ 也可以使用bgrewriteaof命令

​ 运行时打开aof

redis-cli> CONFIG SET appendonly yes

5.AOF优缺点

优点

  • 充分保证数据的持久化,正确的配置一般最多丢失1秒的数据
  • aof 文件内容是以Redis协议格式保存, 易读

缺点

  • aof 文件通常大于 rdb 文件
  • 速度会慢于rdb, 具体得看具体fsyn策略
  • 重新启动redis时会极低的概率会导致无法将数据集恢复成保存时的原样(概率极低, 但确实出现过)

3.RDB和AOF区别

Redis

1.安装

​ 1.官网下载redis-7.0.10.tar.gz

​ 2.传到Linux的/usr/local目录中

cd /usr/local
#解压
tar -zxvf redis-7.0.10.tar.gz 
#redis-7.0.10
cd redis-7.0.10
#使用make命令
make&&make install
#进入文件
cd redis.conf
#修改配置

daemonize yes
protected-mode no
#bind 127.0.0.1
自己添加requirepass *****

#完成后重启
#启动redis
redis-server redis.conf
#启动redis客户端
redis-cli -a ***** -p 6379
#查看
ps -ef |grep redis
#关闭
redis-cli -a 111111 shutdown

2.key的基本命令

查看所有key: keys *

查看key类型:type key

查看是否存在:exists key | exists key1,key2,key3

切换Redis库:select (index)

设定过期时间:expire key seconds

查看剩余过期:ttl key (永久返回-1,已经过期返回-2)

查看数据库的数量:dbsize

清空当前数据库:flushdb

清空所有数据库:fludb

删除key:del key

3.基本数据类型

string

获取长度:strlen k1

追加字符:append k1 [value]

添加单个:set k1 [value]

获取单个:get k1

秒过期时间:set k1 [value] ex [过期时间]

毫秒过期时间:set k1 v1 px [过期时间]

秒单位时间戳:set k1 v1 exat 【过了当前时间戳就为过期】

毫秒单位时间戳:set k1 v1 pxat【时间戳】

不存在时添加:set k1 v1 nx

存在的时候:set k1 v1 xx

保存过期时间:set k1 v1 keepttl

先get后set:getset k1 【value】

不存在时添加:setnx k1 v1

批处理

添加多个:mset ka va kb vb

获取多个:mget ka kb

不存在时添加多个:msetnx ka va kc vc 必须全部成功

获取指定位置:getrange k1 【sta】【 end

设置指定位置:setrange k1 【index】【 value

加减

对数字类型

自增:incr k1

自减:decr k1

增量20:incrby k1 【增量】

减量20:decrby k1 【减量】

list

左边加入:lpush list1 【elements.......】

右边加入:rpush list1 【elements.......】

左边遍历:lrange list1 【sta】【end】

左边出栈:lpop list1 【出栈个数】

右边出栈:rpop list1 【出栈个数】

索引取值:lindex list1 【index】

获取长度:llen list1

删除指定个数指定元素:lrem list1 【个数】【元素】

截取指定长度赋值给list:ltrim list1 【sta】【end】

原子操作尾出头插:rpoplpush list1 list2

指定下标左边加入:lset list1 【index】【element】

左边指定元素前后加入:linsert list1 【after/before】【element】【java】

hash

添加对象:hset 【user:001】 name zhangsan age lisi

获取对象:hget 【user:001】 name

获取全部参数:hgetall 【user:001】

获取多个:hmget 【user:001】 name age

属性值增加:hincrby 【user:001】 age 【增量】

属性值增加: hincrbyfloat 【user:001】 age 【增量】

添加对象属性如果不存在:hsetnx user:001 name zhangsan

删除对象元素:hdel user:001 name

set

添加 sadd s1 【elements......】 //只有abcd四个元素

获取 smembers s1

判断是否存在 sismember s1 【元素】 //1

获取长度 scard s1

删除元素 srem s1 【elements】

随机产生元素 srandmember s1 【产生个数】

随机弹出一个元素 spop s1 【数字】

移动集合a到b smove s1 s2 【元素】

集合运算

s1 【1 2 3 4 5 6】 s2 【4 5 6 7 8 9】

交集 sinter s1 s2 //4 5 6

并集 sunion s1 s2 //123456

差集 sdiff s1 s2 //以一为主 为123 以二为主 为 789

zset

添加元素:zadd z1 【score】【value】【score】【value】

查看数量:zcard z1

返回指定score范围的数据数量:zcount z1 【min】【max】

返回指定范围数据 zrange z1 【起始】【结尾】

返回指定范围反转的数据:zrevrange z1 【起始】【结尾】

返回指定score范围的数据:zrangebyscore z1 【min】【max】加 ( 表示不包括

返回元素分数:zscore z1 【元素】

删除元素:zrem z1 【元素】

增加元素分数:zincrby 【增数】 【元素】

返回元素的排名位置:zrank z1 【元素】

返回反转的排名位置:zrevrank z1 【元素】

bitmap:签到

给指定key的offset赋值0|1:setbit 【element1】 【位置】【 状态01】

HyperLogLog:UA统计

添加元素:PFADD key element [element …]

返回储存在给定键的 HyperLogLog 的近似基数:PFCOUNT key

合并多个HyperLogLog于一个HyperLogLog:

​ PFMERGE destkey sourcekey [sourcekey …]

GEO

黑马点评文件

链接https://pan.baidu.com/s/1189u6u4icQYHg_9_7ovWmA?pwd=eh11
提取码:eh11

4.Jedis

使用

1.引入依赖

<dependencies>
    <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>4.2.3</version>
    </dependency>
</dependencies>

2.引入日志

 <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-log4j12 -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.7.21</version>
        <scope>test</scope>
    </dependency>

3.引入junit5

    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.9.1</version>
        <scope>test</scope>
    </dependency>

4.建立连接,执行方法,关闭连接

//        创建Jedis对象传入参数ip地址,redis端口号
        Jedis jedis = new Jedis("8.130.103.195", 6379);
//        配置密码
        jedis.auth("aaaaaa");
        jedis.set("k1","v1");
//		  关闭Redis
        jedis.close();

连接池

public class JedisConnectionFactory{
    private static JedisPool jedisPool;
    static{
    JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
    jedisPoolConfig.setMaxTotal(8);
    jedisPoolConfig.setMaxIdle(8);
    jedisPoolConfig.setMinIdle(2);
    new jedisPool = JedisPool(jedisPoolConfig,
                              "8.130.103.195",6379,1000,"417403");
    }
    public static Jedis getJedis(){
		return jedosPool.getResource();
    }
}

5.SpringDataRedis

1.使用:

引入依赖

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

配置信息

spring:
  redis:
    host: 8.130.114.137
    port: 6379
    password: aaaaaa
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0

自动注入

@Autowared
private RedisTemplate redisTemplate;

6.SpringRedisTemplate

1.使用

引入依赖

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

配置application.peoperties

spring.redis.host=8.130.114.137
spring.redis.database=1
spring.redis.port= 6379
spring.redis.password=417403

spring.redis.lettuce.pool.max-active= 10
spring.redis.lettuce.pool.max-idle=10
spring.redis.lettuce.pool.max-wait=1
spring.redis.lettuce.pool.time-between-eviction-runs=0

spring.redis.lettuce.shutdown-timeout=5000
spring.jackson.default-property-inclusion=non_null

注入SpringRedisTemplate

2.区别

StringRedisTemplate与RedisTemplate
两者的关系是StringRedisTemplate继承RedisTemplate。

两者的数据是不共通的;也就是说StringRedisTemplate只能管理StringRedisTemplate里面的数据,RedisTemplate只能管理RedisTemplate中的数据。

SDR默认采用的序列化策略有两种,一种是String的序列化策略,一种是JDK的序列化策略。

StringRedisTemplate默认采用的是String的序列化策略,保存的key和value都是采用此策略序列化保存的。

RedisTemplate默认采用的是JDK的序列化策略,保存的key和value都是采用此策略序列化保存的。

7.Redis缓存策略

0.最佳方案

1.更新策略

低一致性业务,建议配置最大内存占用和淘汰策略。

高一致性业务,可以结合超时剔除和主动更新。

算法剔除:不用管理,当内存不足时,redis淘汰机制会淘汰部分数据。

超时剔除:给缓存设置过期时间,过期后自动删除。

主动更新:需要管理,根据业务逻辑进行更新

2.主动更新策略

1.Cache Aside Pattern:

​ 由缓存的调用者,在更新数据库时同时更新。

2.Read/wirte Through Pattern:

​ 将缓存和数据库整合为一个业务,由服务器来维护,无需关心一致性。

3.Write Behind Caching Pattern:

​ 将写入业务保留在缓存中,再由其他线程异步将数据持久化到数据库中。

3.Cache Aside Pattern

删除缓存操作

​ 更新缓存:每次更新数据库后更新缓存,没有读业务,更新浪费

​ 删除缓存:当数据库更新时删除缓存,需要使用再次从数据库读取**

保证缓存与数据库业务操作一致性

​ 放在同一个事物中

CacheAsidePattern

​ 先更新数据库,再删除缓存

4.缓存穿透

什么是缓存穿透:

​ 每次从缓存中都查不到数据,而需要查询数据库,同时数据库中也没有查到该数据,也没法放入缓存。也就是说,每次这个用户请求过来的时候,都要查询一次数据库。给数据库造成压力

解决方案:

​ 使用缓存空对象:

​ 优点:实现方便,维护方便

​ 缺点:可能造成额外内存浪费,造成短期不一致

​ 布隆过滤:

​ 优点:内存占用少,没有多余key

​ 缺点:实现复杂,底层依靠哈希,最后转换为二进制存在误判。

5.缓存雪崩

什么是缓存雪崩:

缓存雪崩是指当大量的缓存key同时过期时或者Redsi宕机时,大量的请求到数据库,给数据库造成压力。

如何解决:

给缓存添加随机的TTL过期时间,集群部署,给业务降级,添加多级缓存。

6.缓存击穿

缓存击是什么

指当热点数据过期时,大量的请求到服务器,给服务器造成压力。

如何解决

添加互斥锁

设置逻辑过期

8.优惠券秒杀

1.订单的id生成

    private static final long BEGIN_TIMESTAMP = 1640995200L;
    private static final int COUNT_BITS = 32;

    public RedisIdGenerator(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private final StringRedisTemplate stringRedisTemplate;

    /**
     *
     * @param redisCachePrefix 业务前缀,区分业务
     * @return
     */
    public Long idGenerate(String redisCachePrefix){
//        生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long beginTimestamp = BEGIN_TIMESTAMP;
        long timestamp = nowSecond - BEGIN_TIMESTAMP;
        String data = now.format(DateTimeFormatter.ofPattern("yyyy:mm:dd"));
//        生成序列号  redis自增id
        Long count = stringRedisTemplate.opsForValue().increment("icr:"+redisCachePrefix+":"+data);
//        拼接
        return  timestamp << COUNT_BITS | count;
    }

2.秒杀下单

防止超卖,使用乐观锁,悲观锁。

乐观锁:给stock字段作为乐观锁版本,每次只要在操作时,再次判断当前的值与刚才读取的值是否相同

防止黄牛:

使用悲观锁,将每个用户的id上锁,再去查询库中相同对象,相同订单是否存在

Long userId = UserHolder.getUser().getId();
// 如果字符常量池中已经包含一个等于此String对象的字符串,则返回常量池中字符串的引用
// 对于任意两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true
synchronized (userId.toString().intern()) {
    IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
    return proxy.createVoucherOrder(voucherId);
}
Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0){
    return Result.fail("用户已经购买过一次了!");
}

9.分布式锁

1.分布式锁解决集群下锁的问题

使用redis的setnx,对用户或者线程进行控制

/**
 * 获取锁
 * @param timeOutExpire
 * @return
 */
@Override
public boolean tryLock(Long timeOutExpire) {
    Boolean aBoolean = stringRedisTemplate
        .opsForValue()
            .setIfAbsent(LOCK_PREFIX+name, String.valueOf(Thread.currentThread().getId()), timeOutExpire, TimeUnit.MINUTES);
    return Boolean.TRUE.equals(aBoolean);
}

2.解决分布式误删问题

在释放锁的时候,添加一层判断,当前线程对象和锁对象是否为同一个

3.解决redis分布式事务原子性

使用lua脚本,通过redis调用lua脚本,使释放锁的判断和删除操作都一次执行,保证原子性。

4.可重入锁

同一个线程的锁可以多次复用,Redis实现可重入锁,是通过使用hset结构,key为锁名称,value为当前线程标识,和锁的个数

转行程序员

10.单机Redis问题

10.Redis持久化

1.RDB

1.什么是RDB

​ RDB(Redis DataBase),是redis默认的存储方式,RDB方式是通过快照( snapshotting )完成的。它保存的是某一时刻的数据并不关注过程。RDB保存redis某一时刻的数据的快照

2.触发方式

Redis服务器主动关闭时,当操作符合快照规则时,执行主从复制时。

3.配置

4.bgsave执行流程

5.优缺点

优点:

1.RDB是基于二进制存储的,占用空间小,便于传给slaver

2.主进程fork一个紫禁城,可以最大化Redis性能

3.使用RDB恢复文件较快

缺点:

1.不保证数据完整性,最后一次快照后面的数据会丢失

2.父进程在fork子进程时是阻塞的,如果主进程较大,可能会阻塞。

6.禁用RDB

#在Redis.cof中添加
save ""

2.AOF

1.什么是AOF

​ Redis默认情况下是不开启的。开启AOF持久化后Redis 将所有对数据库进行过写入的命令(及其参数)(RESP)记录到 AOF 文件, 以此达到记录数据库状态的目的,这样当Redis重启后只要按顺序回放这些命令就会恢复到原始状态了。AOF会记录过程,RDB只管结果

2.配置conf

3.AOF执行流程

4.AOF重写

当AOF体积超过配置中的阈值时,会自动在后台子进程对AOF文件重写。

​ 也可以使用bgrewriteaof命令

​ 运行时打开aof

redis-cli> CONFIG SET appendonly yes

5.AOF优缺点

优点

  • 充分保证数据的持久化,正确的配置一般最多丢失1秒的数据
  • aof 文件内容是以Redis协议格式保存, 易读

缺点

  • aof 文件通常大于 rdb 文件
  • 速度会慢于rdb, 具体得看具体fsyn策略
  • 重新启动redis时会极低的概率会导致无法将数据集恢复成保存时的原样(概率极低, 但确实出现过)

3.RDB和AOF区别

1.缓存更新策略

1.算法剔除:不用管理,当redis内存不足时,会根据redis淘汰机制回收部分内存

2.超时剔除:设置超时时间,过期自动清除

3.主动更新:需要管理,数据库更新时,需要对缓存进行操作。

1.缓存与数据库双写一致

实现方式:需要对数据进行操作时,先修改数据库,再删除缓存。

注意:要保持事务原子性@Transactional

    @Transactional
    public Result update(Shop shop) {
        Long shopId = shop.getId();
//        对店铺进行判断,不能为空
        if (shopId == null) {
            return Result.fail("店铺id不能为空");
        }
//        对店铺进行更新
        updateById(shop);
//        删除缓存
        stringRedisTemplate.delete(CACHE_SHOP_KEY+shopId);
        return Result.ok();
    }

2.缓存穿透

什么是缓存穿透:

当缓存和数据库中都没数据信息时,请求就会直接对数据库进行访问,对数据库造成压力

如何解决:

缓存空对象,布隆过滤,增强id复杂度,避免被猜测,数据格式校验,作权限限制。

缓存空对象:

@Override
public Result getShopInfo(Long id) {
    //在redis中查询(判空)
    String jsonShop = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
    //存在,返回
    if (StrUtil.isNotBlank(jsonShop)) {
        //***如果jsonShop不为 空 或者不为 ""
        Shop shop = JSONUtil.toBean(jsonShop, Shop.class);
        return Result.ok(shop);
    }
    	//***说明为 空 或者为 ""
    if (jsonShop != null){//如果为 ""
        return Result.fail("商铺信息不存在!!");
    }
    //不存在,数据库中查询
    Shop shop = getById(id);
    //不存在,file("商铺不存在!!")
    if (shop == null) {
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,"",CACHE_NULL_TTL, TimeUnit.MINUTES);

        return Result.fail("暂无此商铺信息!!");
    }
    //存在,写入redis
    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
    //返回数据
    return Result.ok(shop);
}

3.缓存雪崩

什么是缓存雪崩

是指再某一时刻大量的缓存key同时失效或者Redsi服务宕机,导致大量请求到数据库,造成巨大压力

解决方案

给不同的key的过期时间设置TTL随机值

利用Redis集群提高服务可用性

给缓存业务添加降级限流策略

给业务添加多级缓存(nginx->jvm->redis)

4.缓存击穿

缓存击是什么

指当热点数据过期时,大量的请求到服务器,给服务器造成压力。

如何解决

添加互斥锁,设置逻辑过期

5.分布式锁

6.优化

1.异步秒杀

​ 项目使用的是所有的读写都由数据库完成,给数据库带来巨大压力,性能降低。为了解决这种问题,我将购买判断业务放在了redis缓存中,当业务请求发起的时候,会先通过哟lua脚本,在redis中判断库存是否充足,用户是否已经购买过。一系列完成之后,再创建订单,将订单保存再阻塞队列中,异步开启一个新线程,线程内部不断从阻塞队列获取订单任务,并调用函数执行,再去请求数据库,扣减库存,最后将订单保存在数据库。

创建秒杀券并同步到Redis

@Transactional
public void addSeckillVoucher(Voucher voucher) {
    // 保存优惠券
    save(voucher);
    // 保存秒杀信息
    SeckillVoucher seckillVoucher = new SeckillVoucher();
    seckillVoucher.setVoucherId(voucher.getId());
    seckillVoucher.setStock(voucher.getStock());
    seckillVoucher.setBeginTime(voucher.getBeginTime());
    seckillVoucher.setEndTime(voucher.getEndTime());
    seckillVoucherService.save(seckillVoucher);
    //将秒杀券添加到redis缓存
    stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY+voucher.getId(),String.valueOf(voucher.getStock()));
}

Redis中lua脚本

体检初始化lua脚本到内存

private static final DefaultRedisScript SECKILL_SCRIPT;

static {
    SECKILL_SCRIPT = new DefaultRedisScript();
    SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
    SECKILL_SCRIPT.setResultType(Long.class);
}

lua脚本

--参数列表
--1.1优惠券id:查询库存是否充足
local voucherId = ARGV[1]
--1.2用户id:用户是否已经购买过
local userId = ARGV[2]
--key列表
--优惠券key
local stockKey = 'seckill:stock:' .. voucherId
--订单key
local orderKey = 'seckill:order:' .. voucherId .. ':' .. userId

--lua脚本命令
--判断库存是否充足
if(tonumber(redis.call('get',stockKey)) <= 0) then
    -- 不足返回1
 return 1
 end
    --充足则判断用户是否下单
 if(redis.call('sismember',orderKey,userId) == 1) then
    --存在则说明已经下单,返回2
 return 2
 end
 --扣库存
 redis.call('incrby',stockKey,-1)
 --下单
 redis.call('sadd',orderKey,userId)
 return 0

判断脚本执行结果的返回值

将订单添加到阻塞队列中

//        为零将订单存入阻塞队列
        // 6. 创建订单
        VoucherOrder voucherOrder = new VoucherOrder();

        // 6.1 订单id
        long orderId = redisIdGenerator.idGenerate("order");
        voucherOrder.setId(orderId);
        // 6.2 用户id
        voucherOrder.setUserId(userId);
        // 6.3代金券id
        voucherOrder.setVoucherId(voucherId);
        orderTasks.add(voucherOrder);
        proxy = (IVoucherOrderService) AopContext.currentProxy();
        return Result.ok(orderId);

7.Redis消息队列

1.基于List数据类型的消息队列

1.基于PubSub数据类型的消息队列

1.基于Stream数据类型的消息队列

8.最佳实践

1.key的设计

​ 1.遵循:业务名:数据名:id,可读性强

​ 2.长度不超过44,当key为纯数字时,会使用int存储,当长度大于44字节,会从embstr变为raw,raw是指,不是一段完整的内存空间,以指针的形式指向另一个空间,会造成内存碎片。

posted on 2023-09-25 14:34  xiaoWang3486  阅读(12)  评论(0)    收藏  举报