SpringBoot-Redis 入门

SpringBoot-Redis 入门

Redis 的数据类型

String 字符串

  • string 是 redis 最基本的类型,一个 key 对应一个 value。
  • string 类型是二进制安全的。意思是 redis 的 string 可以包含任何数据。比如 jpg 图片或者序列化的对象 。
  • string 类型是 Redis 最基本的数据类型,一个键最大能存储 512MB。
  • String 类型的操作参考

链表

  • redis 列表是简单的字符串列表,排序为插入的顺序。列表的最大长度为 2^32-1。
  • redis 的列表是使用链表实现的,这意味着,即使列表中有上百万个元素,增加一个元素到列表的头部或尾部的操作都是在常量的时间完成。
  • 可以用列表获取最新的内容(像帖子,微博等),用 ltrim 很容易就会获取最新的内容,并移除旧的内容。
  • 用列表可以实现生产者消费者模式,生产者调用 lpush 添加项到列表中,消费者调用 rpop 从列表中提取,如果没有元素,则轮询去获取,或者使用 brpop 等待生产者添加项到列表中。
  • List 类型的操作参考

集合

  • redis 集合是无序的字符串集合,集合中的值是唯一的,无序的??梢远约现葱泻芏嗖僮?,例如,测试元素是否存在,对多个集合执行交集、并集和差集等等。
  • 我们通??梢杂眉洗娲⒁恍┪薰厮承虻?,表达对象间关系的数据,例如用户的角色,可以用 sismember 很容易就判断用户是否拥有某个角色。
  • 在一些用到随机值的场合是非常适合的,可以用 srandmember/spop 获取/弹出一个随机元素。
    同时,使用@EnableCaching 开启声明式缓存支持,这样就可以使用基于注解的缓存技术。注解缓存是一个对缓存使用的抽象,通过在代码中添加下面的一些注解,达到缓存的效果。
  • Set 类型的操作参考

ZSet 有序集合

  • 有序集合由唯一的,不重复的字符串元素组成。有序集合中的每个元素都关联了一个浮点值,称为分数。可以把有序看成 hash 和集合的混合体,分数即为 hash 的 key。
  • 有序集合中的元素是按序存储的,不是请求时才排序的。
  • ZSet 类型的操作类型

Hash-哈希

  • redis 的哈希值是字符串字段和字符串之间的映射,是表示对象的完美数据类型。
  • 哈希中的字段数量没有限制,所以可以在你的应用程序以不同的方式来使用哈希。
  • Hash 类型的操作参考

关于 key 的设计

key 的存活时间:

无论什么时候,只要有可能就利用 key 超时的优势。一个很好的例子就是储存一些诸如临时认证 key 之类的东西。当你去查找一个授权 key 时——以 OAUTH 为例——通常会得到一个超时时间。
这样在设置 key 的时候,设成同样的超时时间,Redis 就会自动为你清除。

关系型数据库的 redis

  1. 把表名转换为 key 前缀 如, tag:
  2. 第 2 段放置用于区分区 key 的字段--对应 mysql 中的主键的列名,如 userid
  3. 第 3 段放置主键值,如 2,3,4...., a , b ,c
  4. 第 4 段,写要存储的列名

例:user:userid:9:username

RedisTemplate 常用操作集合

方法 Redis 类型 备注
opsForValue() String 对 redis 字符串类型数据操作
opsForList() List 对链表类型的数据操作
opsForHash() Hash 对 hash 类型的数据操作
opsForSet() Set 对无序集合类型的数据操作
opsForZSet() ZSet 对有序集合类型的数据操作

Serializer

目前已经支持的序列化策略:

  • JdkSerializationRedisSerializer:POJO 对象的存取场景,使用 JDK 本身序列化机制,将 pojo 类通过 ObjectInputStream/ObjectOutputStream 进行序列化操作,最终 redis-server 中将存储字节序列。是目前最常用的序列化策略
  • StringRedisSerializer :Key 或者 value 为字符串的场景,根据指定的 charset 对数据的字节序列编码成 string,是 “new String(bytes, charset)” 和 “string.getBytes(charset)” 的直接封装。是最轻量级和高效的策略。
  • JacksonJsonRedisSerializer:jackson-json 工具提供了 javabean 与 json 之间的转换能力,可以将 pojo 实例序列化成 json 格式存储在 redis 中,也可以将 json 格式的数据转换成 pojo 实例。因为 jackson 工具在序列化和反序列化时,需要明确指定 Class 类型,因此此策略封装起来稍微复杂。【需要 jackson-mapper-asl 工具支持】
  • OxmSerializer :提供了将 javabean 与 xml 之间的转换能力,目前可用的三方支持包括 jaxb,apache-xmlbeans;redis 存储的数据将是 xml 工具。不过使用此策略,编程将会有些难度,而且效率最低;不建议使用。【需要 spring-oxm ??榈闹С帧?/li>

其中 JdkSerializationRedisSerializerStringRedisSerializer 是最基础的序列化策略,其中 “JacksonJsonRedisSerializer” 与 “OxmSerializer” 都是基于 String 存储,因此它们是较为“高级”的序列化 (最终还是使用 string 解析以及构建 java 对象)。

如果你的数据需要被第三方工具解析,那么数据应该使用 StringRedisSerializer 而不是 JdkSerializationRedisSerializer。

Redis Pipline

通过 RedisTemplete 实现 pipline 可以参考如下代码:

public List<Object> queryAll() {
    return redisTemplate.executePipelined((RedisConnection redisConnection) -> {
        RedisSerializer<String> stringSerializer = redisTemplate.getStringSerializer();
        Set<String> keys = redisTemplate.keys("*");
        if (Objects.nonNull(keys)) {
            for (String key : keys) {
                redisConnection.get(stringSerializer.serialize(key));
            }
        }
        return null;
    });
}

需要注意的是 redisTemplate.executePipelined() 里面的方法返回值必须为 null.

原因是该方法的源码如下:

public List<Object> executePipelined(final RedisCallback<?> action) {
    return executePipelined(action, valueSerializer);
}

public List<Object> executePipelined(final RedisCallback<?> action, final RedisSerializer<?> resultSerializer) {
    return execute(new RedisCallback<List<Object>>() {
        public List<Object> doInRedis(RedisConnection connection) throws DataAccessException {
            connection.openPipeline();
            boolean pipelinedClosed = false;
            try {
                Object result = action.doInRedis(connection);
                if (result != null) {
                    throw new InvalidDataAccessApiUsageException(
                            "Callback cannot return a non-null value as it gets overwritten by the pipeline");
                }
                List<Object> closePipeline = connection.closePipeline();
                pipelinedClosed = true;
                return deserializeMixedResults(closePipeline, resultSerializer, resultSerializer, resultSerializer);
            } finally {
                if (!pipelinedClosed) {
                    connection.closePipeline();
                }
            }
        }
    });
}

在代码段中有如下的判断:

Object result = action.doInRedis(connection);
if (result != null) {
    throw new InvalidDataAccessApiUsageException(
        "Callback cannot return a non-null value as it gets overwritten by the pipeline");
}

因此如果所传入的方法如果不为空,则会抛出异常,导致程序运行失败。

注意:

  • doInRedis 中的 redis 操作不会立刻执行
  • 所有 redis 操作会在 connection.closePipeline() 之后一并提交到 redis 并执行,这是 pipeline 方式的优势
  • 所有操作的执行结果为 executePipelined() 的返回值

RedisTemplete 执行 lua 脚本

Redis 命令行运行 Lua 脚本

假定我们有如下 lua 脚本:

--获取KEY
local key1 = KEYS[1]
local key2 = KEYS[2]
 
-- 获取ARGV[1],这里对应到应用端是一个List<Map>.
--  注意,这里接收到是的字符串,所以需要用csjon库解码成table类型
local receive_arg_json =  cjson.decode(ARGV[1])
 
--返回的变量
local result = {}
 
--打印日志到reids
--注意,这里的打印日志级别,需要和redis.conf配置文件中的日志设置级别一致才行
redis.log(redis.LOG_DEBUG,key1)
redis.log(redis.LOG_DEBUG,key2)
redis.log(redis.LOG_DEBUG, ARGV[1],#ARGV[1])
 
--获取ARGV内的参数并打印
local expire = receive_arg_json.expire
local times = receive_arg_json.times
redis.log(redis.LOG_DEBUG,tostring(times))
redis.log(redis.LOG_DEBUG,tostring(expire))
 
--往redis设置值
redis.call("set",key1,times)
redis.call("incr",key2)
redis.call("expire",key2,expire)
 
--用一个临时变量来存放json,json是要放入要返回的数组中的
local jsonRedisTemp={}
jsonRedisTemp[key1] = redis.call("get",key1)
jsonRedisTemp[key2] = redis.call("get", key2)
jsonRedisTemp["ttl"] = redis.call("ttl",key2)
redis.log(redis.LOG_DEBUG, cjson.encode(jsonRedisTemp))
 
 
result[1] = cjson.encode(jsonRedisTemp) --springboot redistemplate接收的是List,如果返回的数组内容是json对象,需要将json对象转成字符串,客户端才能接收
result[2] = ARGV[1] --将源参数内容一起返回
redis.log(redis.LOG_DEBUG,cjson.encode(result)) --打印返回的数组结果,这里返回需要以字符返回
 
return result

我们可以使用如下命令行查看执行结果:

其基本命令结构如下:

redis-cli [--ldb] --eval script [numkeys] key [key ...] , arg [arg ...]
  • --eval:告诉redis客户端去加载Lua脚本,后面跟着的就是 lua 脚本的路径
  • --ldb :进行命令调试的必要参数
  • numkeys:指定后续参数有几个key??墒÷?/li>
  • key [key ...]:是要操作的键,可以指定多个,在lua脚本中通过KEYS[1], KEYS[2]获取
  • arg [arg ...],参数,在lua脚本中通过ARGV[1], ARGV[2]获取。

注意: KEYS和ARGV中间的 ',' 两边的空格,不能省略

针对本例中的 Lua 脚本其对应的命令行如下:

bin/redis-cli -h localhost -p 7379 -a zcvbnm --ldb --eval script/LimitLoadTimes.lua count rate.limiting:127.0.0.1 , "{\"expire\":\"10000\",\"times\":\"10\"}"

其他的一些参数

  • -h 修改后的ip -a 修改后的密码 -p 修改后的端口号

结果输出为:

[root@VM_0_12_centos redis-4.0.8]# bin/redis-cli -h localhost -p 7379 -a zcvbnm --ldb --eval script/LimitLoadTimes.lua count rate.limiting:127.0.0.1 , "{\"expire\":\"10000\",\"times\":\"10\"}"
Lua debugging session started, please use:
quit    -- End the session.
restart -- Restart the script in debug mode again.
help    -- Show Lua script debugging commands.

* Stopped at 1, stop reason = step over
-> 1   local key1 = KEYS[1]
lua debugger> continue

1) "{\"rate.limiting:127.0.0.1\":\"1\",\"count\":\"10\",\"ttl\":10000}"
2) "{\"expire\":\"10000\",\"times\":\"10\"}"

使用 Java 运行 Lua 脚本

实现代码如下:

package cn.sjsdfg.redis.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Created by Joe on 2019/5/8.
 */
@Service
public class LuaScriptService {
    @Autowired
    @Qualifier("customRedisTemplate")
    private RedisTemplate<String, Object> redisTemplate;

    private DefaultRedisScript<List> getRedisScript;

    @PostConstruct
    public void init(){
        getRedisScript = new DefaultRedisScript<List>();
        getRedisScript.setResultType(List.class);
        getRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("luascript/LimitLoadTimes.lua")));
    }

    public void redisAddScriptExec(){
        /**
         * List设置lua的KEYS
         */
        List<String> keyList = new ArrayList<>();
        keyList.add("count");
        keyList.add("rate.limiting:127.0.0.1");

        /**
         * 用Mpa设置Lua的ARGV[1]
         */
        Map<String,Object> argvMap = new HashMap<String,Object>();
        argvMap.put("expire", 10000);
        argvMap.put("times", 10);

        /**
         * 调用脚本并执行
         */
        List result = redisTemplate.execute(getRedisScript, keyList, argvMap);
        System.out.println(result);
    }
}

测试代码在 cn.sjsdfg.redis.service.LuaScriptServiceTest#testRedisAddScriptExec,其输出为:

[{rate.limiting:127.0.0.1=3, count=10, ttl=10000}, {times=10, expire=10000}]

与前面直接执行 lua 脚本的输出结果一致。

注意

  1. Lua脚本可以在redis单机模式、主从模式、Sentinel集群模式下正常使用,但是无法在分片集群模式下使用。(脚本操作的key可能不在同一个分片)
  2. Lua脚本中尽量避免使用循环操作(可能引发死循环问题),尽量避免长时间运行。
  3. redis在执行lua脚本时,默认最长运行时间时5秒,当脚本运行时间超过这一限制后,Redis将开始接受其他命令但不会执行(以确保脚本的原子性,因为此时脚本并没有被终止),而是会返回“BUSY”错误。

spring-data-redis 和 jedis 版本对应收集总结

如果不使用对饮版本的 Jedis,在项目构建的时候必定会出现 java.lang.NoClassFoundException。

Jedis 代码重构变革很大

spring-data-redis 版本 jedis 版本 备注
1.5.2.RELEASE 2.7.3
1.6.0.RELEASE 2.7.2 2.7.3
1.6.2.RELEASE 2.8.0
1.8.1.RELEASE 2.9.0
1.8.4.RELEASE 2.9.0
2.1.x.RELEASE 2.9.0

参考资料

连接 Redis 工具

github

?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,100评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,308评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,718评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,275评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,376评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,454评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,464评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,248评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,686评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,974评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,150评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,817评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,484评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,140评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,374评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,012评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,041评论 2 351

推荐阅读更多精彩内容

  • Lua 5.1 参考手册 by Roberto Ierusalimschy, Luiz Henrique de F...
    苏黎九歌阅读 13,780评论 0 38
  • 原帖地址:http://08643.cn/p/2f14bc570563 redis概述 Redis...
    onlyHalfSoul阅读 2,160评论 0 28
  • Redis 简介 Redis 是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库。 Redi...
    奋斗的小鸟GO阅读 442评论 0 2
  • 吐槽一下今天小师姐没看书!
    大纲_6599阅读 108评论 1 0
  • 《南方姑娘》——赵雷 (点击链接,听着音乐看文字。) 有这么一个姑娘啊,她爱吃栗子。在图书馆自习时吃,去夫子庙每次...
    王咕噜阅读 319评论 1 3