一、Redis常用命令

《「Redis」 - 基本命令》中对Redis基础数据结构命令进行了简单总结,下图把最常用命令进行梳理,可点开大图查看。

二、Redis应用场景

A、string类型使用场景

1、商品库存数

从业务上,商品库存数据是热点数据,交易行为会直接影响库存。而Redis自身string类型提供了原生命令。

incr key

decr key

incrby key increment

decrby key decrementset goods_id 10,设置id为good_id的商品的库存初始值为10

decr goods_id,当商品被购买时候,库存数据减1

依次类推的场景:商品的浏览次数,问题或者回复的点赞次数等,这种计数的场景都可以考虑利用Redis来实现。

2、时效信息存储

Redis的数据存储具有自动失效能力,存储的key-value可以设置过期时间。

例如,用户登录某个App需要获取登录验证码,验证码在30秒内有效。就可以使用string类型存储验证码,同时设置30秒的失效时间。

keys = redisCli.get(key);
if(keys != null)
{
return false;
}
else
{
sendMsg();
redisCli.set(keys, value, expireTime);
}

B、list类型使用场景

list是按照插入顺序排序的字符串链表,可以在头部和尾部插入新的元素(双向链表实现,两端添加元素的时间复杂度为O(1))。

1、消息队列实现

有很多专业的消息队列组件Kafka、RocketMQ、RabbitMQ等。这里仅仅是使用list的特征来实现消息队列的要求。在实际技术选型的过程中,对于MQ使用需要慎重权衡考虑。

list存储就是一个队列的存储形式:lpush key value,在key对应list的头部添加字符串元素

rpop key,移除列表的最后一个元素,返回值为移除的元素

2、最新上架商品

在电商网站首页经常会有新上架产品推荐的模块,这个模块存储了最新上架前100名。使用Redis的list数据结构,进行TOP品的存储。

Redis的ltrim命令可以对一个列表进行修剪,list就会只包含指定范围的指定元素。

ltrim key start stop

start和stop都是由0开始计数,0是列表里的第一个元素(表头)。

// 把新上架商品添加到链表里

ret = r.lpush("new:goods", goodsId)

// 保持链表100位

ret = r.ltrim("new:goods", 0, 99)

// 获得前100个最新上架的商品id列表

newest_goods_list = r.lrange("new:goods", 0, 99)

C、set类型使用场景

set存储了一个无序集合,具备去重功能。

当需要存储一个列表信息,同时要求列表内的元素不能有重复,用set比较合适。与此同时,set还提供求交集、并集、差集。

例如,在电商网站,存储用户感兴趣的商品信息,在进行相似用户分析时,可以通过计算两个不同用户之间感兴趣商品的数量来提供一些依据。

// userid为用户ID,goodID为感兴趣的商品信息
// sadd "user:userId" goodID
sadd "user:101" 1
sadd "user:101" 2
sadd "user:102" 1
sadd "user:102" 3
sinter "user:101" "user:102"
// 解果为1

获取到两个用户相似的产品,然后确定相似产品的类目就可以进行用户分析。类似的应用场景还有,社交场景下共同关注好友,相似兴趣tag等。

D、hash类型使用场景

Redis在存储对象(例如,用户信息)的时候需要对对象进行序列化转换然后存储。

对于对象类型,还有一种比较方便的存储方式,按照Redis的hash类型进行存储。

hset key field value

例如,存储一些网站用户的基本信息。

hset user101 name "isisiwish"
hset user101 phone "1008611"
hset user101 sex "男"

以上就存储了一个用户基本信息。

{
"name": "isisiwish",
"phone": "1008611",
"sex": "男"
}

这种类似场景还很多,例如订单数据,产品数据,商家基本信息等。

E、sorted set类型使用场景

Redis sorted set的使用场景与set类似,区别是set无序的,而sorted set可以通过提供一个score参数来为存储数据排序,并且是自动排序,插入既有序。业务中如果需要一个有序且不重复的集合列表,就可以选择sorted set数据结构。

比如,商品的购买热度可以将购买总量num当做商品列表的score,这样获取最热门的商品时就是可以自动按售卖总量排好序。sorted set适合有排序需求的集合存储场景。

三、Redis常见问题

缓存使用过程当中,常遇到的一些问题:

A、缓存穿透

一般访问缓存,如果缓存中存在查询的商品数据,那么直接返回;如果缓存中不存在商品数据,一般需要访问数据库获取数据。

由于不恰当的业务实现,或者外部恶意攻击不断请求非缓存数据,导致所有的请求都会在短时间访问到数据库,对数据库可能带来一定的压力,甚至引起崩溃。

解决方案:针对缓存穿透的情况,最简单的对策就是将不存在的数据访问结果,也存储到缓存中,避免缓存访问的穿透;并将不存在商品数据的访问结果也缓存下来,有效的避免缓存穿透的风险。

B、缓存雪崩

当缓存重启或者大量的缓存在某一时间段失效,会导致大批流量直接访问数据库,对DB造成压力,从而引起DB故障,引起系统崩溃。

举个栗子,对于抢购类的促销运营活动,活动期间将带来大量的商品信息、库存等相关信息的查询。为了避免商品数据库的压力,将商品数据放入缓存中存储。不巧的是,抢购活动期间,大量的热门商品缓存同时失效过期了,导致大量查询流量落到数据库之上,对于数据库来说造成很大的压力。

解决方案:将商品根据品类热度分类,购买比较多的类目商品缓存周期长一些,购买相对冷门的类目商品,缓存周期短一些;

在设置商品具体的缓存生效时间的时候,加上一个随机的区间因子,比如说5-10分钟之间来随意选择失效时间;

提前预估DB能力,如果缓存挂掉,数据库仍可以在一定程度上抗住流量的压力。

C、缓存预热

缓存预热是指系统上线后,将相关的缓存数据直接加载到缓存系统。可以避免在用户请求时,先查询数据库,然后再将数据缓存的问题。用户直接查询事先被预热的缓存数据。

如果不进行预热,那么Redis初识状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库中,对数据库造成流量的压力。

解决方案:数据量不大的时候,工程启动的时候进行加载缓存动作;

数据量大的时候,设置一个定时任务脚本,进行缓存的刷新;

数据量太大的时候,优先保证热点数据进行提前加载到缓存。

D、缓存降级

缓存降级是指缓存失效或者缓存服务挂掉的情况下,也不访问数据库,直接访问内存数据或者直接返回默认数据。

举个栗子,对于应用的首页,一般访问量会非常大,首页里面往往包含了部分推荐商品的展示信息,这些推荐商品都会放到缓存中进行存储,同时为了避免缓存的异常情况,对热点商品数据也存储到了内存中,同时内存中还保留了一些默认的商品信息。

四、分布式应用场景

A、分布式锁

当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的操作或者访问。

分布式锁实现方式有很多种:数据库乐观锁方式

基于Redis的分布式锁

基于ZooKeeper的分布式锁

分布式锁实现要保证几个基本点:互斥性:任意时刻,只有一个资源能够获取到锁。

容灾性:能够在未成功释放锁的的情况下,一定时限内能够恢复锁的正常功能。

统一性:加锁和解锁保证同一资源来进行操作。

加锁代码:

public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String traceId, int expireTime)
{
SetParams setParams = new SetParams();
setParams.ex(expireTime);
setParams.nx();
String result = jedis.set(lockKey, traceId, setParams);
if (LOCK_SUCCESS.equals(result))
{
return true;
}
return false;
}
解锁代码:
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String traceId)
{
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(traceId));
if (RELEASE_SUCCESS.equals(result))
{
return true;
}
return false;
}

B、分布式自增ID

随着用户以及订单量的增加,系统优化时会针对用户数据、商品数据、以及订单数据进行分库分表操作。

由于进行了分库分表,所以MySQL自增ID无法作为主键使用,因此需要分布式ID生成器,来提供唯一ID的信息。

通常对于分布式自增ID的实现方式有下面几种:利用数据库自增ID的属性

通过UUID来实现唯一ID生成

Twitter的SnowFlake算法

利用Redis生成唯一ID

Redis的incr命令可以用来实现唯一ID。

Redis是单进程单线程架构,不会因为多个incr命令导致取号重复。因此Redis实现序列号的生成基本能满足全局唯一与单调递增的特性。

五、Redis其他机制

A、Redis持久化机制

Redis提供两种持久化方式。RDB持久化:将Reids在内存中的数据定时dump到磁盘上的rdb文件

AOF(Append Only File)持久化:将Redis的操作日志以追加的方式写入文件,通过操作还原数据

RDB持久化,在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。

AOF持久化,以日志的形式记录服务器所处理的每一个写、删操作,查询操作不会记录,以文本的方式记录。RDB优点RDB是紧凑的二进制文件,适合备份,全量复制等场景

RDB恢复数据远快于AOF

RDB缺点RDB无法实现实时或者秒级持久化

新老版本无法兼容RDB格式

AOF优点可以更好地保护数据不丢失

appen-only模式写入性能比较高

适合做灾难性的误删除紧急恢复

AOF缺点对于同一份文件,AOF文件要比RDB快照大

AOF开启后,写的QPS会有所影响,相对于RDB来说写QPS要下降

数据库恢复比较慢,不合适做冷备

对于缓存数据存储敏感的场景,一般通过两种方式配合进行备份。

B、Redis缓存失效策略

1、定时删除策略

在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除。优点:保证内存尽快释放

缺点:若key过多,删除这些key会占用很多CPU时间,而且每个key创建一个定时器,性能影响严重

2、惰性删除策略

key过期的时候不删除,每次从数据库获取key的时候去检查是否过期,若过期,则删除,返回null。优点:CPU时间占用比较少

缺点:若key很长时间没有被获取,将不会被删除,可能造成内存泄露

3、定期删除策略

每隔一段时间执行一次删除(在redis.conf配置文件设置,1s刷新的频率)过期key操作。优点:可以控制删除操作的时长和频率,来减少CPU时间占用,可以避免惰性删除时候内存泄漏的问题

缺点:对内存友好方面,不如定时策略;对CPU友好方面,不如惰性策略

Redis一般采用:惰性策略和定期策略两个相结合。

C、缓存命中率命中:可以直接通过缓存获取到需要的数据

未命中:无法直接通过缓存获取到想要的数据,需要再次查询数据库或者执行其它的操作原因可能是由于缓存中根本不存在,或者缓存已经过期

命中率越高表示使用缓存作用越好,性能越高(响应时间越短、吞吐量越高),并发能力也越好。利用预加载(预热)、扩容、优化缓存粒度、更新缓存等手段来提高命中率。

七、Lettuce

Spring Boot 2.0中Redis客户端驱动由Jedis变为了Lettuce,相较Jedis还有自己的优点的。

A、Lettuce优点基于netty,支持事件模型

支持同步、异步、响应式的方式

可以方便的连接Redis Sentinel

完全支持Redis Cluster

SSL连接

Streaming API

CDI和Spring的集成

兼容Java 8、9

B、Lettuce特性 - 多线程共享

Jedis是直连模式,在多个线程间共享一个Jedis实例时是线程不安全的,如果想要在多线程环境下使用Jedis,需要使用连接池,每个线程都去拿自己的Jedis实例,当连接数量增多时,物理连接成本就较高了。

Lettuce是基于netty的,连接实例可以在多个线程间共享,所以一个多线程的应用可以使用一个连接实例,而不用担心并发线程的数量。

C、Lettuce特性 - 多线程异步

异步的方式可以更好的利用系统资源,而不用浪费线程等待网络或磁盘I/O。

Lettuce是基于netty的,netty是一个多线程、事件驱动的I/O框架,所以Lettuce可以充分利用异步的优势。

D、基本使用

private static RedisClient client = RedisClient.create("redis://localhost:6379");

private static StatefulRedisConnection connection = client.connect();

public static RedisCommands commands = connection.sync();

Long redisTotalCount = RedisUtil.commands.exists("CI:TOTAL:COUNT");

RedisUtil.commands.set("CI:TOTAL:COUNT", String.valueOf(totalCount));

RedisUtil.commands.setex("CI:" + String.valueOf(i), 60*60*6, JSON.toJSONString(poem));

String json = RedisUtil.commands.get("CI:" + String.valueOf(i));

pom配置。

io.lettuce

lettuce-core

5.0.4.RELEASE