目录

客户端 Jedis、Redisson、Lettuce 对比

RedisTemplate 常用方法

其它常用操作与方法

生产环境禁用Keys而推荐用Scan & 模糊删除

RedisTemplate 序列化方式

分布式锁需求分析 与 主流实现方式

基于 Redis 实现分布式锁

工具类封装

实现商品秒杀


客户端 Jedis、Redisson、Lettuce 对比

1、三个都提供了基于 Redis 操作的 Java API,只是封装程度,具体实现稍有不同。

Jedis

Java 实现的客户端。支持基本的数据类型如:String、Hash、List、Set、Sorted Set。

使用阻塞的 I/O,方法调用同步,程序流需要等到 socket 处理完 I/O 才能执行,不支持异步操作。Jedis 客户端实例不是线程安全的,需要通过连接池来使用 Jedis。

Redisson分布式锁,分布式集合,可通过 Redis 支持延迟队列。
Lettuce

用于线程安全同步,异步和响应使用,支持集群,Sentinel,管道和编码器。

基于 Netty 框架的事件驱动的通信层,其方法调用是异步的。Lettuce 的 API 是线程安全的,所以可以操作单个 Lettuce 连接来完成各种操作。

RedisTemplate 常用方法

org.springframework.data.redis.core.RedisTemplate 常用方法(本文环境 Spring Boot 2.1.3):

方法描述
Boolean expire(K key, final long timeout, final TimeUnit unit)

为指定的 key 指定缓存失效时间。时间一到 key 会被移除。key 不存在时,不影响。

注意:如果 key 后续被重新设置值,比如 set key value,则 key 过期时间失效,需要重新设置。

Boolean expireAt(K key, final Date date)设置 key 失效日期。注意:如果 key 后续被重新设置值,比如 set key value,则 key 过期时间失效,需要重新设置。
Long getExpire(K key)获取 key 的剩余过期时间。 -1 表示永久有效。-2 表示 key 不存在。
Long getExpire(K key, final TimeUnit timeUnit)获取 key 的剩余过期时间,并换算成指定的时间单位 
Boolean hasKey(K key)判断 key 是否存在
Boolean delete(K key)删除指定的 key
Long delete(Collection keys)删除多个 key
RedisSerializer<?> getDefaultSerializer()获取默认的序列化方式。RedisTemplate 是 JdkSerializationRedisSerializer;StringRedisTemplate 是 StringRedisSerializer
Set keys(K pattern)获取整个库下符合指定正则的所有 key,如 keys(*) 获取所有 key
Boolean move(K key, final int dbIndex)将 key 从当前库移动目标库 dbIndex
ClusterOperations<K, V> opsForCluster()获取 ClusterOperations 用于操作集群
GeoOperations<K, V> opsForGeo()获取 GeoOperations 用于操作地图
<HK, HV> HashOperations<K, HK, HV> opsForHash()获取 HashOperations 用于操作 hsha 数据类型
ListOperations<K, V> opsForList()获取 ListOperations 用于操作 list 类型
SetOperations<K, V> opsForSet()获取 SetOperations 用于操作无序集合
ValueOperations<K, V> opsForValue()获取 ValueOperations 用于操作 String类型
ZSetOperations<K, V> opsForZSet()获取 ValueOperations 用于操作有序集合
rename(K oldKey, K newKey)为 oldKey 进行重命名
DataType type(K key)查询缓存 key 的类型,DataType 是一个枚举,code(name) 可选值如下:none, string, list, set, zset hash
none 表示 key 不存在,或者类型不确定
T execute(RedisScript<T> script, List<K> keys, Object... args)执行给定的 lau 脚本。
1、多个操作使用 lau 脚本统一执行是事务安全的,具有原子性。
2、脚本中 KEYS[x] 是对 keys 进去取值,ARGV[x] 是对 args 进行取值,索引从1开始。
3、返回脚本执行的结果,类型与 RedisScript 的类型一致。
T RedisTemplate.execute(RedisCallback<T> action)在Redis连接中执行给定的操作。只要有可能,动作对象抛出的应用程序异常就会传播给调用者(只能取消选中)。Redis异常被转换为适当的DAO异常。允许返回结果对象,即域对象或域对象的集合。对给定对象与适用于Redis存储的二进制数据之间执行自动序列化/反序列化。注意:回调代码本身不应该处理事务!使用适当的事务管理器。通常,回调代码不得触及任何连接生命周期方法,如close,以让模板完成其工作。
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.types.RedisClientInfo;
import org.springframework.test.context.junit4.SpringRunner;

import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisStuWebApplicationTests {

    //注入 RedisTemplate 或者 StringRedisTemplate 其中一个即可,前者是后者的父类。它们默认已经全部在容器种了.
    //org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration 中已经自动将 RedisTemplate 添加到了容器中,直接获取使用即可.
    @Resource
    private RedisTemplate redisTemplate;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Test
    public void test1() throws InterruptedException {
        ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
        opsForValue.set("wmx_name", "Zhang San");//设置字符串值
        System.out.println(opsForValue.get("wmx_name"));//Zhang San
        stringRedisTemplate.expire("wmx_name", 60, TimeUnit.SECONDS);//设置 key 失效时间
//        for (int i = 0; i < 15; i++) {
//            System.out.println(stringRedisTemplate.getExpire("wmx_name"));
//            System.out.println(stringRedisTemplate.getExpire("wmx_name", TimeUnit.SECONDS));
//            Thread.sleep(1000);
//        }

        //判断是否含有指定的 key
        System.out.println(stringRedisTemplate.hasKey("wmx_name") + ", " + stringRedisTemplate.hasKey("wmx_name_2"));//true, false

        opsForValue.set("age", "33");
        opsForValue.set("address", "长沙");
        stringRedisTemplate.delete("age");//删除 key

        Date stopDate = new Date();
        stopDate.setTime(System.currentTimeMillis() + (60 * 1000));
        stringRedisTemplate.expireAt("address", stopDate);//设置 key 1 分钟后失效

        List<RedisClientInfo> clientList = stringRedisTemplate.getClientList();
        System.out.println(clientList);
        System.out.println(redisTemplate.getDefaultSerializer());//获取默认序列化方式


        Set<String> keys = stringRedisTemplate.keys("*");//获取当前库下所有的 key
        System.out.println("keys=" + keys);

        Boolean move = stringRedisTemplate.move("address", 2);//将 address 移动 2 号数据库
        System.out.println("move=" + move);

        opsForValue.set("info", "描述");
        System.out.println(opsForValue.get("info"));
        stringRedisTemplate.rename("info", "summary");//对 key 进行重命名
    }
}

演示源码:src/test/java/com/wmx/wmxredis/WmxRedisApplicationTests.java · 汪少棠/wmx-redis - Gitee.com

其它常用操作与方法

redisTemplate.getConnectionFactory().getConnection().flushAll(); //清空 redis 所有数据库(all databases)中的所有数据(all keys)
redisTemplate.getConnectionFactory().getConnection().flushDb(); //清空 redis 当前连接的数据库(selected database)中的所有数据(all keys)

生产环境禁用Keys而推荐用Scan & 模糊删除

https://gitee.com/wangmx1993/material/blob/blob/master/doc/csdn/md/生产环境禁用Keys而推荐用Scan.md

模糊删除key:wmx-redis/blob/master/src/main/java/com/wmx/wmxredis/controller/RedisCacheUtil.java

RedisTemplate 序列化方式

1、StringRedisTemplate 继承 RedisTemplate,主要区别就是前者默认使用 StringRedisSerializer 序列化 String,后者默认使用 JdkSerializationRedisSerializer 序列化对象。

2、RedisTemplate<K, V> 可以用来存储对象,如 Map、List 、Set、POJO 等,但对象需要实现 Serializable 序列化接口。此种方式序列化时,以二进制数组方式存储,内容没有可读性。

Map<String, Object> dataMap = new HashMap<>();
dataMap.put("id", 1001);
dataMap.put("name", "张三");
HashOperations opsForHash = redisTemplate.opsForHash();
opsForHash.putAll("map_1",dataMap);
redisTemplate.expire("map_1",60,TimeUnit.SECONDS);
Map map_11 = opsForHash.entries("map_1");
System.out.println(map_11);//{name=张三, id=1001}

3、StringRedisTemplate 专门用来存储字符串,StringRedisTemplate extends RedisTemplate<String, String>。序列化接口 org.springframework.data.redis.serializer.RedisSerializer 的实现类如下:

序列化方式描述
FastJsonRedisSerializer采用 com.alibaba.fastjson 进行序列化与反序列化 ,提供了 (Class type) 参数的构造器,需要传入对象类型。
GenericFastJsonRedisSerializer提供了基本的 GenericFastJsonRedisSerializer() 构造器。
GenericJackson2JsonRedisSerializer与 Jackson2JsonRedisSerializer 功能差不多,构造函数可以指定 Class,默认为 String.
GenericToStringSerializer需要调用者传一个对象到字符串互转的 Converter(相当于转换为字符串的操作交给转换器去做)
Jackson2JsonRedisSerializer采用 com.fasterxml.jackson 进行序列化与反序列化。优点是速度快,序列化后的字符串短小精悍,不需要实现Serializable接口。构造函数必须传入 Class 类型。
JdkSerializationRedisSerializerRedisTemplate 默认的序列化方式。要求存储的对象必须实现java.io.Serializable接口。序列化后的结果非常庞大。存储的为二进制数据,对开发者不友好。
OxmSerializer以 xml 格式存储,解析起来比较复杂,效率也比较低
StringRedisSerializerStringRedisTemplate 默认的字符串序列化方式。key 和 value 都会采用此方式进行序列化,是被推荐使用的,对开发者友好,轻量级,效率也比较高。此时只能是 String,不能是其它对象,否则报错。

4、本文环境 Spring Boot 2.1.3 + Java JDK 1.8,下面以指定 Jackson2JsonRedisSerializer 序列化方式为例:

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 {

    //自定义 RedisTemplate 序列化方式
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();//创建 RedisTemplate,key 和 value 都采用了 Object 类型
        redisTemplate.setConnectionFactory(redisConnectionFactory);//绑定 RedisConnectionFactory

        //创建 Jackson2JsonRedisSerializer 序列方式,对象类型使用 Object 类型,
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);//设置一下 jackJson 的 ObjectMapper 对象参数

        // 设置 RedisTemplate 序列化规则。因为 key 通常是普通的字符串,所以使用 StringRedisSerializer 即可。
        // 而 value 是对象时,才需要使用序列化与反序列化
        redisTemplate.setKeySerializer(new StringRedisSerializer());// key 序列化规则
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);// value 序列化规则
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());// hash key 序列化规则
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);// hash value 序列化规则
        redisTemplate.afterPropertiesSet();//属性设置后操作
        return redisTemplate;//返回设置好的 RedisTemplate
    }
}

在线演示源码:src/main/java/com/wmx/wmxredis/config/RedisConfig.java · 汪少棠/wmx-redis - Gitee.com

5、RedisTemplate 使用就很简单了,保存数据与读取数据时,直接操作对象即可,会自动进行序列化与反序列化:

import com.wmx.wmxredis.beans.Person;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * RedisTemplate 操作 redis
 *
 * @author wangMaoXiong
 */
@RestController
public class RedisController {
    /**
     * 从容器中获取 RedisTemplate 实例
     */
    @Resource
    private RedisTemplate redisTemplate;

    /**
     * 保存数据,设置缓存:http://localhost:8080/redis/save?id=1000&name=张三
     *
     * @param person
     * @return
     */
    @GetMapping("redis/save")
    public String redisCache(Person person) {
        ValueOperations opsForValue = redisTemplate.opsForValue();
        ListOperations opsForList = redisTemplate.opsForList();
        HashOperations opsForHash = redisTemplate.opsForHash();

        person.setBirthday(new Date());

        //设置缓存。演示三种数据类型:字符串、列表、hash
        opsForValue.set(RedisController.class.getName() + "_string" + person.getId(), person);
        opsForList.rightPushAll(RedisController.class.getName() + "_list" + person.getId(), person, person);
        opsForHash.put(RedisController.class.getName() + "_map", "person" + person.getId(), person);

        //设置 key 失效时间
        redisTemplate.expire(RedisController.class.getName() + "_string" + person.getId(), 60, TimeUnit.SECONDS);
        redisTemplate.expire(RedisController.class.getName() + "_list" + person.getId(), 60, TimeUnit.SECONDS);
        redisTemplate.expire(RedisController.class.getName() + "_map", 60, TimeUnit.SECONDS);
        return "缓存成功.";
    }

    /**
     * 查询缓存:http://localhost:8080/redis/get?personId=1000
     *
     * @param personId
     * @return
     */
    @GetMapping("redis/get")
    public List<Person> getRedisCache(@RequestParam Integer personId) {
        //1、演示三种数据类型:字符串、列表、hash
        ValueOperations opsForValue = redisTemplate.opsForValue();
        ListOperations opsForList = redisTemplate.opsForList();
        HashOperations opsForHash = redisTemplate.opsForHash();

        //2、读取缓存,如果 key 不存在,则返回为 null.
        Person person = (Person) opsForValue.get(RedisController.class.getName() + "_string" + personId);
        List<Person> personList = opsForList.range(RedisController.class.getName() + "_list" + personId, 0, -1);
        Person person1 = (Person) opsForHash.get(RedisController.class.getName() + "_map", "person" + personId);
        System.out.println("person=" + person);
        System.out.println("personList=" + personList);
        System.out.println("person1=" + person1);
        return personList;
    }
}

在线演示源码:

src/main/java/com/wmx/wmxredis/controller/RedisController.java · 汪少棠/wmx-redis - Gitee.com

src/main/java/com/wmx/wmxredis/beans/Person.java · 汪少棠/wmx-redis - Gitee.com

温馨提示!

关于 RedisTemplate 的序列化,实际生产中遇到一次需要将数据库查出来的一个 List 对象放到 Redis 中缓存,当时数据量达到 20 万行,结果 put 到缓存的时间长达 6、7 秒,然后当使用 alibaba 的 fastjson 手动先将 List 对象序列化为字符串,然后作为普通的字符串使用 ValueOperations set 到缓存,此时 set 的时间降到了 100 毫秒内,而 fastjson 序列化对象同样非常之快。取值时,同样将取出的字符串使用 fastjosn 手动反序列化成 List 即可。

分布式锁需求分析 与 主流实现方式

分布式锁需求分析 与 主流实现方式 

基于 Redis 实现分布式锁

1、分布式索最常见的一种方案就是使用 Redis 做分布式锁,使用 Redis 做分布式锁的思路是:在 redis 中设置一个值表示加了锁,然后释放锁的时候就把这个 key 删除。

// 获取锁
// NX是指如果key不存在就成功,key存在返回false,PX可以指定过期时间
SET anyLock unique_value NX PX 30000

// 释放锁:通过执行一段lua脚本
// 释放锁涉及到两条指令,这两条指令不是原子性的
// 需要用到redis的lua脚本支持特性,redis执行lua脚本是原子性的
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end

2、一定要用 "SET key value NX PX milliseconds" 命令:否则先设置了值,再设置过期时间,这个不是原子性操作,有可能在设置过期时间之
前宕机,会造成死锁(key永久存在)

3、value 要具有唯一性:在解锁的时候,需要验证 value 是和加锁的一致才删除 key。
这是避免了一种情况:假设A获取了锁,过期时间30s,此时35s之后,锁已经自动释放了,A
去释放锁,但是此时可能B获取了锁。A客户端就不能删除B的锁了。

4、下面通过 RedisTemplate 进行实现:

    /**
     * http://localhost:8080/redis/execute?key=wwww&value=1rui
     * <p>
     * Boolean setIfAbsent(K key, V value, long timeout, TimeUnit unit)
     * Boolean setIfAbsent(K key, V value, Duration timeout)
     * * 1、key 不存在时进行设值,返回 true; 否则 key 存在时,不进行设值,返回 false.
     * * 2、此方法相当于先设置 key,然后设置 key 的过期时间,它的操作是原子性的,是事务安全的。
     * * 3、相当于:SET anyLock unique_value NX PX 30000,NX是指如果key不存在就成功,key存在返回false,PX可以指定过期时间
     * T execute(RedisScript<T> script, List<K> keys, Object... args):执行给定的脚本。
     * * 1、多个操作使用 lau 脚本统一执行是事务安全的,具有原子性
     * * 2、脚本中 KEYS[x] 是对 keys 进去取值,ARGV[x] 是对 args 进行取值,索引从1开始.
     * * 3、返回脚本执行的结果,类型与 RedisScript 的类型一致。
     *
     * @param key
     * @param value
     * @return
     * @throws InterruptedException
     */
    @GetMapping("redis/execute")
    public Map<String, Object> execute(@RequestParam String key, @RequestParam String value) throws InterruptedException {
        Map<String, Object> returnMap = new HashMap<>();
        Boolean ifAbsent = false;
        try {
            ifAbsent = redisTemplate.opsForValue().setIfAbsent(key, value, Duration.ofSeconds(60));
            if (!ifAbsent) {
                returnMap.put("code", 500);
                returnMap.put("msg", "程序正在处理中,请稍后再试!");
                return returnMap;
            }
            TimeUnit.SECONDS.sleep(10);//休眠 10 秒,模拟执行业务代码
            System.out.println("执行业务代码.");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (ifAbsent){
//接口执行完毕后删除 key,key 不存在时 execute 方法返回 0
//此种脚本删除的方式在 redis 集群部署时会报错,实际上直接使用  redisTemplate.delete(cacheKey) 也是可以的
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                RedisScript<Long> redisScript = RedisScript.of(script, Long.class);
// 返回删除key的个数,未删除成功时,返回 0
                Object execute = redisTemplate.execute(redisScript, Arrays.asList(key), value);
                returnMap.put("data", execute);
            }
        }
        returnMap.put("code", 200);
        returnMap.put("msg", "seccess");
        return returnMap;
    }

在线演示源码:src/main/java/com/wmx/wmxredis/controller/RedisController.java · 汪少棠/wmx-redis - Gitee.com

并发压测:src/main/java/com/wmx/hb/controller/FlowConfigController.java · 汪少棠/hb - Gitee.com

工具类封装

/src/main/java/com/wmx/wmxredis/controller/RedisCacheUtil.java

实现商品秒杀

doc/csdn/md/Redis队列实现秒杀系统.md · 汪少棠/material - Gitee.com

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐