Spring Boot - 集成 Redis

前言

很久之前,有寫過一篇博文介紹下 Redis 相關(guān)內(nèi)容及操作:Redis 學(xué)習(xí)筆記

本篇博文主要介紹下如何在 Spring Boot 中集成 Redis。

依賴導(dǎo)入

Spring Boot 中集成 Redis,第一步就是導(dǎo)入相關(guān)依賴,如下所示:

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

在 IDEA 中,點擊spring-boot-starter-data-redis可以進入該依賴詳情配置,可以看到如下內(nèi)容:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
        <version>2.5.6</version>
        <scope>compile</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-redis</artifactId>
        <version>2.5.6</version>
        <scope>compile</scope>
    </dependency>

    <dependency>
        <groupId>io.lettuce</groupId>
        <artifactId>lettuce-core</artifactId>
        <version>6.1.5.RELEASE</version>
        <scope>compile</scope>
    </dependency>
</dependencies>

所以其實spring-boot-starter-data-redis起步依賴實際上就是導(dǎo)入了三個依賴:spring-boot-starter、spring-data-redislettuce-core。這幾個依賴主要就是加載了 Redis 以及對 Redis 的自動裝配進行了使能,具體分析請參考后文。

自動裝配原理

導(dǎo)入spring-boot-starter-data-redis后,其實就可以在 Spring Boot 項目中使用 Redis 了,因為 Spring Boot 默認就對 Redis 進行了自動裝配,可以查看自動配置文件spring-boot-autoconfigure.jar/META-INF/spring.factories,其內(nèi)與 Redis 相關(guān)的自動配置類有:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\

我們主要關(guān)注的自動配置類為RedisAutoConfiguration,其源碼如下所示:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
}

RedisAutoConfiguration自動配置類主要做了如下幾件事:

  1. @ConditionalOnClass(RedisOperations.class):當(dāng)項目中存在RedisOperations類時,使能自動配置類RedisAutoConfiguration。

    其中,RedisOperations類存在于依賴org.springframework.data:spring-data-redis,也就是上文導(dǎo)入依賴spring-boot-starter-data-redis時,就會自動使能RedisAutoConfiguration自動配置類。

  2. @EnableConfigurationProperties(RedisProperties.class):裝載 Redis 配置文件。

    其中,RedisProperties源碼如下所示:

    @ConfigurationProperties(prefix = "spring.redis")
    public class RedisProperties {
    
        /**
         * Database index used by the connection factory.
         */
        private int database = 0;
    
        /**
         * Connection URL. Overrides host, port, and password. User is ignored. Example:
         * redis://user:password@example.com:6379
         */
        private String url;
    
        /**
         * Redis server host.
         */
        private String host = "localhost";
    
        /**
         * Login username of the redis server.
         */
        private String username;
    
        /**
         * Login password of the redis server.
         */
        private String password;
    
        /**
         * Redis server port.
         */
        private int port = 6379;
    
        /**
         * Whether to enable SSL support.
         */
        private boolean ssl;
    
        /**
         * Read timeout.
         */
        private Duration timeout;
    
        /**
         * Connection timeout.
         */
        private Duration connectTimeout;
    
        /**
         * Client name to be set on connections with CLIENT SETNAME.
         */
        private String clientName;
    
        /**
         * Type of client to use. By default, auto-detected according to the classpath.
         */
        private ClientType clientType;
    
        private Sentinel sentinel;
    
        private Cluster cluster;
    
        private final Jedis jedis = new Jedis();
    
        private final Lettuce lettuce = new Lettuce();
        ...
        /**
         * Type of Redis client to use.
         */
        public enum ClientType {
    
            /**
             * Use the Lettuce redis client.
             */
            LETTUCE,
    
            /**
             * Use the Jedis redis client.
             */
            JEDIS
    
        }
    
        /**
         * Pool properties.
         */
        public static class Pool {
            ...
        }
    
        /**
         * Cluster properties.
         */
        public static class Cluster {
            ...
        }
    
        /**
         * Redis sentinel properties.
         */
        public static class Sentinel {
            ...
        }
    
        /**
         * Jedis client properties.
         */
        public static class Jedis {
            ...
        }
    
        /**
         * Lettuce client properties.
         */
        public static class Lettuce {
            ...
        }
    }
    

    RedisProperties會自動加載前綴為spring.redis的配置選項,Redis 所有可配置項查看RedisProperties對應(yīng)屬性即可,某些選項未設(shè)置則使用缺省值:

    private int database = 0;
    private String host = "localhost";
    private int port = 6379;
    
  3. @Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class }):自動導(dǎo)入兩個配置類LettuceConnectionConfigurationJedisConnectionConfiguration。這兩個配置類會自動各自加載LettuceJedis相關(guān)配置。

    其中,LettuceJedis都是 Redis 的客戶端,它們都可以連接到 Redis 服務(wù)器,并封裝了對 Redis 的相關(guān)操作。

    在多線程環(huán)境下,使用單一Jedis實例,可能會存在線程安全問題,原因是Jedis#connect()方法中,最終會調(diào)用到Connection#connect()方法,而該方法內(nèi)是通過Socket去連接 Redis Server,源碼如下所示:

    public class Connection implements Closeable {
       ...
      // socket 非線程安全
      private Socket socket;
      ...
      public void connect() throws JedisConnectionException {
        ...
        socket = socketFactory.createSocket();
        ...
      }
    }
    

    Jedis源碼導(dǎo)入方法請查看:附錄 - 導(dǎo)入 Jedis

    socketFactory實際運行時對象為DefaultJedisSocketFactory,因此最終會調(diào)用到DefaultJedisSocketFactory#createSocket()

    public class DefaultJedisSocketFactory implements JedisSocketFactory {
        ...
        @Override
        public Socket createSocket() throws JedisConnectionException {
            Socket socket = null;
            socket = new Socket();
            ...
        }
    

    Jedis在執(zhí)行每個命令之前,都會先進行連接(即調(diào)用Jedis#connect()),多線程環(huán)境下,此時Socket創(chuàng)建就可能存在并發(fā)安全問題。

    :還有其他情況也可能導(dǎo)致Jedis并發(fā)安全問題,關(guān)于Jedis并發(fā)安全更詳細分析,可參考文章:jedis和lettuce的對比

    解決Jedis并發(fā)安全問題的一個方法就是使用連接池(Jedis Pool),為每條線程都單獨創(chuàng)建一個對應(yīng)的Jedis實例。缺點就是連接數(shù)增加,開銷變大。

    其實,在 Spring Boot 2.0 之后,默認使用的都是Lettuce,所以可以看到,上文起步依賴spring-boot-starter-data-redis,其內(nèi)部導(dǎo)入的客戶端連接依賴是lettuce-core

    相比較于Jedis,Lettuce底層采用的是 Netty,在多線程環(huán)境下,也可以保證只創(chuàng)建一個連接,所有線程共享該Lettuce連接,無須使用連接池,并且該方式具備線程安全??梢哉f,Lettuce既輕量又安全。

    Lettuce的自動配置類如下所示:

    @Configuration(proxyBeanMethods = false)
    // 存在類 RedisClient 時,自動裝配
    @ConditionalOnClass(RedisClient.class)
    @ConditionalOnProperty(name = "spring.redis.client-type", havingValue = "lettuce", matchIfMissing = true)
    class LettuceConnectionConfiguration extends RedisConnectionConfiguration {
    
        @Bean(destroyMethod = "shutdown")
        // 不存在自定義 DefaultClientResources 時,自動裝配
        @ConditionalOnMissingBean(ClientResources.class)
        DefaultClientResources lettuceClientResources() {
            return DefaultClientResources.create();
        }
    
        @Bean
        // 不存在自定義 LettuceConnectionFactory 時,自動裝配
        @ConditionalOnMissingBean(RedisConnectionFactory.class)
        LettuceConnectionFactory redisConnectionFactory(
                ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers,
                ClientResources clientResources) {
            LettuceClientConfiguration clientConfig = getLettuceClientConfiguration(builderCustomizers, clientResources,
                    getProperties().getLettuce().getPool());
            return createLettuceConnectionFactory(clientConfig);
        }
        ...
    }
    

    主要就是自動裝配了兩個BeanDefaultClientResourcesLettuceConnectionFactory。

  4. RedisAutoConfiguration:主要就是自動裝配了兩個BeanRedisTemplateStringRedisTemplate。

    其中,RedisTemplate的類型是RedisTemplate<Object,Object>,它能操作所有類型數(shù)據(jù)。

    StringRedisTemplate繼承RedisTemplate

    public class StringRedisTemplate extends RedisTemplate<String, String> {...}
    

    StringRedisTemplate就是一個特例,用于操作鍵為String,值也為String類型的數(shù)據(jù),這也是 Redis 操作使用最多的場景。

基本使用

下面介紹下在 Spring Boot 項目中使用 Redis,操作步驟如下:

  1. 首先啟動一個 Redis Server,此處采用 Docker 開啟一個 Redis Server:

    # 啟動 Redis Server
    $ docker run --name redis -d -p 6379:6379 redis
    
    # 進入 Redis 容器
    $ docker exec -it redis bash
    
    # 啟動 redis-cli
    $ root@c0f7159a3081:/data# redis-cli
    # ping 一下 Redis Serve
    $ 127.0.0.1:6379> ping
    PONG # 返回響應(yīng)
    

    上面我們首先啟動了一個 Redis 容器,此時 Redis Server 也會自動啟動,我們在容器內(nèi)通過redis-cli可以啟動一個 Redis 命令行客戶端,并通過命令ping一下 Redis Serve,得到了響應(yīng)PONG,說明 Redis Server 啟動成功。

  2. 配置 Redis 相關(guān)信息:

    # application.properties
    # Redis Server 地址
    spring.redis.host=localhost
    # Redis Server 端口
    spring.redis.port=6379
    # Redis 數(shù)據(jù)庫索引
    spring.redis.database=0
    # 鏈接超時時間 單位 ms(毫秒)
    spring.redis.timeout=1000
    ################ Redis 線程池設(shè)置 ##############
    # 連接池最大連接數(shù)(使用負值表示沒有限制)
    spring.redis.pool.max-active=200  
    # 連接池最大阻塞等待時間(使用負值表示沒有限制)
    spring.redis.pool.max-wait=-1  
    # 連接池中的最大空閑連接
    spring.redis.pool.max-idle=10 
    # 連接池中的最小空閑連接
    spring.redis.pool.min-idle=0  
    

    主要配置項是hostport

  3. 注入RedisTemplate,操作 Redis:

    @SpringBootTest
    class RedisDemoApplicationTests {
    
        // 注入 RedisTemplate
        @Autowired
        private RedisTemplate redisTemplate;
    
        @Test
        public void testRedis() {
            redisTemplate.opsForValue().set("username", "Whyn");
            String username = (String) redisTemplate.opsForValue().get("username");
            Assertions.assertEquals("Whyn",username);
        }
    }
    

    運行以上程序,可以觀察到測試運行結(jié)果正確。

以上,就可以在 Spring Boot 中使用 Redis 了,可以看到,非常方便。

自定義配置

前面章節(jié)我們成功往 Redis 設(shè)置了username:Whyn的鍵值數(shù)據(jù),但此時我們在終端查看下存儲內(nèi)容:

$ 127.0.0.1:6379> keys *
1) "\xac\xed\x00\x05t\x00\busername"

$ 127.0.0.1:6379> get username
(nil)

此處可以看到,數(shù)據(jù)確實存入了,但是編碼方式似乎不對,實際存儲的數(shù)據(jù)我們無法直接觀測到具體內(nèi)容(但是代碼獲取是可以獲取到實際數(shù)據(jù)的),原因是數(shù)據(jù)存入 Redis 時,進行了序列化,且RedisTemplate默認使用的序列化方式為JdkSerializationRedisSerializer,其會將存儲數(shù)據(jù)的keyvalue都序列化為字節(jié)數(shù)組,因此終端看到的就是數(shù)據(jù)的字節(jié)序列。相關(guān)源碼如下所示:

public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAware {
    ...
    private @Nullable RedisSerializer<?> defaultSerializer;
    ...
    @SuppressWarnings("rawtypes") private @Nullable RedisSerializer keySerializer = null;
    @SuppressWarnings("rawtypes") private @Nullable RedisSerializer valueSerializer = null;
    ...
    // 初始化 Bean(即 RedisTemplate)的時候執(zhí)行,用于對 RedisTemplate 進行配置
    @Override
    public void afterPropertiesSet() {
        ...
        if (defaultSerializer == null) {
            // 默認序列化工具:JdkSerializationRedisSerializer
            defaultSerializer = new JdkSerializationRedisSerializer(
                    classLoader != null ? classLoader : this.getClass().getClassLoader());
        }

        if (enableDefaultSerializer) {

            if (keySerializer == null) {
                // 默認的 key 序列化
                keySerializer = defaultSerializer;
                defaultUsed = true;
            }
            if (valueSerializer == null) {
                // 默認的 value 序列化
                valueSerializer = defaultSerializer;
                defaultUsed = true;
            }
            if (hashKeySerializer == null) {
                hashKeySerializer = defaultSerializer;
                defaultUsed = true;
            }
            if (hashValueSerializer == null) {
                hashValueSerializer = defaultSerializer;
                defaultUsed = true;
            }
        }
        ...
    }
    ...
}

public class JdkSerializationRedisSerializer implements RedisSerializer<Object> {

    // 序列化:將 Object 序列化為 byte[]
    private final Converter<Object, byte[]> serializer;
    // 反序列化:將 byte[] 反序列化為 Object
    private final Converter<byte[], Object> deserializer;
    ...
    public JdkSerializationRedisSerializer(@Nullable ClassLoader classLoader) {
        // 實際序列化工具為:SerializingConverter
        // 實際反序列化工具為:DeserializingConverter
        this(new SerializingConverter(), new DeserializingConverter(classLoader));
    }

    public JdkSerializationRedisSerializer(Converter<Object, byte[]> serializer, Converter<byte[], Object> deserializer) {
        ...
        this.serializer = serializer;
        this.deserializer = deserializer;
    }

    ...
}

// 序列化
public class SerializingConverter implements Converter<Object, byte[]> {
    private final Serializer<Object> serializer;

    public SerializingConverter() {
        // 實際序列化工具為:DefaultSerializer
        this.serializer = new DefaultSerializer();
    }
    ...
    public byte[] convert(Object source) {
        ...
        return this.serializer.serializeToByteArray(source);
    }
}

public class DefaultSerializer implements Serializer<Object> {
    ...
    public void serialize(Object object, OutputStream outputStream) throws IOException {
        ...
        // 最終通過 ObjectOutputStream 進行序列化
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
        objectOutputStream.writeObject(object);
        objectOutputStream.flush();
    }
}

@FunctionalInterface
public interface Serializer<T> {
    void serialize(T object, OutputStream outputStream) throws IOException;

    // 最終調(diào)用的序列化方法
    default byte[] serializeToByteArray(T object) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream(1024);
        // 最終回調(diào)到 DefaultDeserializer.serialize 
        this.serialize(object, out);
        return out.toByteArray();
    }
}

// 反序列化
public class DeserializingConverter implements Converter<byte[], Object> {
    private final Deserializer<Object> deserializer;

    public DeserializingConverter(ClassLoader classLoader) {
        // 實際反序列化工具為:DefaultDeserializer
        this.deserializer = new DefaultDeserializer(classLoader);
    }

    public Object convert(byte[] source) {
        ByteArrayInputStream byteStream = new ByteArrayInputStream(source);
        ...
        return this.deserializer.deserialize(byteStream);
    }
    ...
}

public class DefaultDeserializer implements Deserializer<Object> {
    ...
    // 最終調(diào)用的反序列化方法
    public Object deserialize(InputStream inputStream) throws IOException {
        ConfigurableObjectInputStream objectInputStream = new ConfigurableObjectInputStream(inputStream, this.classLoader);
        ...
        return objectInputStream.readObject();
    }
}

public class ConfigurableObjectInputStream extends ObjectInputStream {...}

主要就是在自動配置了RedisTemplate后,就會回調(diào)RedisTemplate#afterPropertiesSet()方法,對RedisTemplate實例進行初始化操作,其內(nèi)就設(shè)置了keyvalue默認采用的序列化工具為JdkSerializationRedisSerializer,其底層最終都是通過ObjectOutputStream/ObjectInputStream進行序列化/反序列化。

StringRedisTemplate采用的序列化工具為StringRedisSerializer,它其實就是直接將keyvalue轉(zhuǎn)換為對應(yīng)的字節(jié)數(shù)組。相關(guān)源碼如下所示:

public class StringRedisTemplate extends RedisTemplate<String, String> {

    public StringRedisTemplate() {
        // key 序列化
        setKeySerializer(RedisSerializer.string());
        // value 序列化
        setValueSerializer(RedisSerializer.string());
        setHashKeySerializer(RedisSerializer.string());
        setHashValueSerializer(RedisSerializer.string());
    }
    ...
}

public interface RedisSerializer<T> {

    @Nullable
    byte[] serialize(@Nullable T t) throws SerializationException;

    @Nullable
    T deserialize(@Nullable byte[] bytes) throws SerializationException;

    static RedisSerializer<String> string() {
        // 實際使用的是 StringRedisSerializer.UTF_8
        return StringRedisSerializer.UTF_8;
    }
    ...
}

public class StringRedisSerializer implements RedisSerializer<String> {

    private final Charset charset;

    // UTF_8 序列化其實就是直接將對應(yīng)的字符串轉(zhuǎn)換為字節(jié)數(shù)組
    public static final StringRedisSerializer UTF_8 = new StringRedisSerializer(StandardCharsets.UTF_8);

    // 反序列化
    @Override
    public String deserialize(@Nullable byte[] bytes) {
        return (bytes == null ? null : new String(bytes, charset));
    }

    // 序列化
    @Override
    public byte[] serialize(@Nullable String string) {
        return (string == null ? null : string.getBytes(charset));
    }
}

默認的序列化/反序列化方式可能不是我們所期望的,因此,通常我們都會自己創(chuàng)建一個配置類,注入一個RedisTemplate,并設(shè)置自定義配置:

@Configuration
public class RedisConfiguration {

    @Bean("redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // 泛型改成 String Object,方便使用
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        // Json序列化配置
        // 使用 json解析對象
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        // 通過 ObjectMapper進行轉(zhuǎn)義
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(om.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        // String 的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key 采用 String的序列化方式
        redisTemplate.setKeySerializer(stringRedisSerializer);
        // hash 的 key 也采用 String的序列化方式
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        // value 的序列化方式采用 jackson
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        // hash 的 value 序列化也采用 jackson
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

        // 初始化 redisTemplate
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

此時我們在運行先前的測試程序,然后在終端查看,如下所示:

$ 127.0.0.1:6379> keys *
1) "username"
$ 127.0.0.1:6379> get username
"\"Whyn\""

可以看到實際字符內(nèi)容了,證明我們自定義配置的序列化器生效了。

工具封裝

RedisTemplate的操作相對繁瑣,通常我們都會抽取出一個工具類,封裝簡化RedisTemplate調(diào)用。

RedisTemplate具體操作 API,可參考文章:如何使用RedisTemplate訪問Redis數(shù)據(jù)結(jié)構(gòu)

以下是本人對RedisTemplate常用操作進行的封裝,簡化調(diào)用,使用時注入該服務(wù)即可:

@Service
public class RedisService {
    private KeysOps<String, Object> keysOps;
    private StringOps<String, Object> stringOps;
    private HashOps<String, String, Object> hashOps;
    private ListOps<String, Object> listOps;
    private SetOps<String, Object> setOps;
    private ZSetOps<String, Object> zsetOps;

    @Autowired
    public RedisService(RedisTemplate<String, Object> redisTemplate) {
        this.keysOps = new KeysOps<>(redisTemplate);
        this.stringOps = new StringOps<>(redisTemplate);
        this.hashOps = new HashOps<>(redisTemplate);
        this.listOps = new ListOps<>(redisTemplate);
        this.setOps = new SetOps<>(redisTemplate);
        this.zsetOps = new ZSetOps<>(redisTemplate);
    }


    /* ##### Key 操作 ##### */
    public KeysOps<String, Object> keys() {
        return this.keysOps;
    }


    /* ##### String 操作 ##### */
    public StringOps<String, Object> string() {
        return this.stringOps;
    }

    /* ##### List 操作 ##### */
    public ListOps<String, Object> list() {
        return this.listOps;
    }

    /* ##### Hash 操作 ##### */
    public HashOps<String, String, Object> hash() {
        return this.hashOps;
    }

    /* ##### Set 操作 ##### */
    public SetOps<String, Object> set() {
        return this.setOps;
    }

    /* ##### Zset 操作 ##### */
    public ZSetOps<String, Object> zset() {
        return this.zsetOps;
    }

    public static class KeysOps<K, V> {
        private final RedisTemplate<K, V> redis;

        private KeysOps(RedisTemplate<K, V> redis) {
            this.redis = redis;
        }

        /**
         * 刪除一個 key
         *
         * @param key
         * @return
         */
        public Boolean delete(K key) {
            return this.redis.delete(key);
        }

        /**
         * 批量刪除 key
         *
         * @param keys
         * @return
         */
        public Long delete(K... keys) {
            return this.redis.delete(Arrays.asList(keys));
        }

        /**
         * 批量刪除 key
         *
         * @param keys
         * @return
         */
        public Long delete(Collection<K> keys) {
            return this.redis.delete(keys);
        }

        /**
         * 設(shè)置 key 過期時間
         *
         * @param key     鍵值
         * @param timeout 過期時間
         * @return
         */
        public Boolean expire(K key, long timeout, TimeUnit timeunit) {
            return this.redis.expire(key, timeout, timeunit);
        }

        /**
         * 設(shè)置 key 過期時間
         *
         * @param key
         * @param date 指定過期時間
         * @return
         */
        public Boolean expireAt(K key, Date date) {
            return this.redis.expireAt(key, date);
        }

        /**
         * 獲取 key 過期時間
         *
         * @param key 鍵值
         * @return key 對應(yīng)的過期時間(單位:毫秒)
         */
        public Long getExpire(K key, TimeUnit timeunit) {
            return this.redis.getExpire(key, timeunit);
        }

        /**
         * 判斷 key 是否存在
         *
         * @param key
         * @return key 存在返回 TRUE
         */
        public Boolean hasKey(K key) {
            return this.redis.hasKey(key);
        }

        /**
         * 模糊匹配 key
         *
         * @param pattern 匹配模式(可使用通配符)
         * @return 返回匹配的所有鍵值
         */
        public Set<K> keys(K pattern) {
            return this.redis.keys(pattern);
        }

        /**
         * 返回數(shù)據(jù)庫所有鍵值
         *
         * @return
         */
        public Set<K> keys() {
            return this.keys((K) "*");
        }

        /**
         * 序列化 key
         *
         * @param key
         * @return 返回 key 序列化的字節(jié)數(shù)組
         */
        public byte[] dump(K key) {
            return this.redis.dump(key);
        }

        /**
         * 移除 key 過期時間,相當(dāng)于持久化 key
         *
         * @param key
         * @return
         */
        public Boolean persist(K key) {
            return this.redis.persist(key);
        }

        /**
         * 從當(dāng)前數(shù)據(jù)庫中隨機返回一個 key
         *
         * @return
         */
        public K random() {
            return this.redis.randomKey();
        }

        /**
         * 重命名 key
         *
         * @param oldKey
         * @param newKey
         */
        public void rename(K oldKey, K newKey) {
            this.redis.rename(oldKey, newKey);
        }

        /**
         * 僅當(dāng) newKey 不存在時,才將 oldKey 重命名為 newKey
         *
         * @param oldKey
         * @param newKey
         * @return
         */
        public Boolean renameIfAbsent(K oldKey, K newKey) {
            return this.redis.renameIfAbsent(oldKey, newKey);
        }

        /**
         * 返回 key 存儲值對應(yīng)的類型
         *
         * @param key
         * @return
         */
        public DataType type(K key) {
            return this.redis.type(key);
        }
    }

    public static class StringOps<K, V> {
        private final ValueOperations<K, V> valueOps;

        private StringOps(RedisTemplate<K, V> redis) {
            this.valueOps = redis.opsForValue();
        }

        /**
         * 設(shè)置 key 對應(yīng)的 value
         *
         * @param key
         * @param value
         */
        public void set(K key, V value) {
            this.valueOps.set(key, value);
        }

        /**
         * 設(shè)置鍵值,附帶過期時間
         *
         * @param key
         * @param value
         * @param timeout
         * @param unit
         */
        public void set(K key, V value, long timeout, TimeUnit unit) {
            this.valueOps.set(key, value, timeout, unit);
        }

        /**
         * 只有當(dāng) key 不存在時,才進行設(shè)置
         *
         * @param key
         * @param value
         * @return
         */
        public Boolean setIfAbsent(K key, V value) {
            return this.valueOps.setIfAbsent(key, value);
        }

        /**
         * 當(dāng) key 不存在時,進行設(shè)置,同時指定其過期時間
         * @param key
         * @param value
         * @param timeout
         * @param unit
         * @return
         */
        public Boolean setIfAbsent(K key, V value, long timeout, TimeUnit unit) {
            return this.valueOps.setIfAbsent(key, value, timeout, unit);
        }

        /**
         * 獲取 key 對應(yīng)的 value
         *
         * @param key
         * @return
         */
        public V get(K key) {
            return this.valueOps.get(key);
        }

        /**
         * 批量添加
         *
         * @param map
         */
        public void multiSet(Map<K, V> map) {
            this.valueOps.multiSet(map);
        }

        /**
         * 批量添加鍵值對(只有當(dāng) key 不存在時,才會進行添加)
         *
         * @param map
         * @return
         */
        public Boolean multiSetIfAbsent(Map<K, V> map) {
            return this.valueOps.multiSetIfAbsent(map);
        }

        /**
         * 批量獲取 key 對應(yīng)的 value
         *
         * @param keys
         * @return key 對應(yīng)的 value(按訪問順序排列)
         */
        public List<V> multiGet(K... keys) {
            return this.valueOps.multiGet(Arrays.asList(keys));
        }

        /**
         * 批量獲取 key 對應(yīng)的 value
         *
         * @param keys
         * @return
         */
        public List<V> multiGet(Collection<K> keys) {
            return this.valueOps.multiGet(keys);
        }

        /**
         * 將指定 key 的值設(shè)為 value,并返回 key 的舊值
         *
         * @param key
         * @param value
         * @return key 的舊值
         */
        public V getAndSet(K key, V value) {
            return this.valueOps.getAndSet(key, value);
        }

        /**
         * 將 key 對應(yīng)的 value 添加一個步進 delta(value 仍以字符串存儲)
         *
         * @param key
         * @param delta
         */
        public void increment(K key, long delta) {
            this.valueOps.increment(key, delta);
        }
    }

    public static class HashOps<K, HK, HV> {
        private final HashOperations<K, HK, HV> hashOps;

        private HashOps(RedisTemplate<K, ?> redis) {
            this.hashOps = redis.opsForHash();
        }

        /**
         * 獲取 key 對應(yīng)的哈希表
         *
         * @param key
         * @return
         */
        public Map<HK, HV> getMap(K key) {
            return this.hashOps.entries(key);
        }

        /**
         * 從 key 對應(yīng)的哈希表中查找 hashKey 的值
         *
         * @param key
         * @param hashKey
         * @return
         */
        public HV get(K key, HK hashKey) {
            return this.hashOps.get(key, hashKey);
        }

        /**
         * 從 key 對應(yīng)哈希表中批量獲取給定字段的值
         *
         * @param key
         * @param hashKeys
         * @return
         */
        public List<HV> multiGet(K key, HK... hashKeys) {
            return this.hashOps.multiGet(key, Arrays.asList(hashKeys));
        }

        /**
         * 從 key 對應(yīng)哈希表中批量獲取給定字段的值
         *
         * @param key
         * @param hashKeys
         * @return
         */
        public List<HV> multiGet(K key, Collection<HK> hashKeys) {
            return this.hashOps.multiGet(key, hashKeys);
        }

        /**
         * 插入 (hashKey,value) 到 key 對應(yīng)的哈希表中
         *
         * @param key
         * @param hashKey
         * @param value
         */
        public void put(K key, HK hashKey, HV value) {
            this.hashOps.put(key, hashKey, value);
        }

        /**
         * 只有當(dāng) key 對應(yīng)的哈希表不存在 hashKey 時,才進行插入
         *
         * @param key
         * @param hashKey
         * @param value
         * @return
         */
        public Boolean putIfAbsent(K key, HK hashKey, HV value) {
            return this.hashOps.putIfAbsent(key, hashKey, value);
        }

        /**
         * 批量插入到 key 對應(yīng)的哈希表中
         *
         * @param key
         * @param map
         */
        public void putAll(K key, Map<? extends HK, ? extends HV> map) {
            this.hashOps.putAll(key, map);
        }

        /**
         * 刪除一個或多個哈希字段
         *
         * @param key
         * @param hashKeys
         * @return
         */
        public Long delete(K key, HK... hashKeys) {
            return this.hashOps.delete(key, hashKeys);
        }

        /**
         * 哈希表是否存在指定字段
         *
         * @param key
         * @param hashKey
         * @return
         */
        public Boolean exists(K key, HK hashKey) {
            return this.hashOps.hasKey(key, hashKey);
        }

        /**
         * 獲取哈希表中的所有字段
         *
         * @param key
         * @return
         */
        public Set<HK> keys(K key) {
            return this.hashOps.keys(key);
        }

        /**
         * 獲取哈希表中的所有值
         *
         * @param key
         * @return
         */
        public List<HV> values(K key) {
            return this.hashOps.values(key);
        }


        /**
         * 查看 key 對應(yīng)哈希表大小
         *
         * @param key
         * @return 哈希表大小
         */
        public Long size(K key) {
            return this.hashOps.size(key);
        }

    }

    public static class ListOps<K, V> {
        private final ListOperations<K, V> listOps;

        private ListOps(RedisTemplate<K, V> redis) {
            this.listOps = redis.opsForList();
        }

        /**
         * 獲取列表索引對應(yīng)元素
         *
         * @param key
         * @param index
         * @return
         */
        public V get(K key, long index) {
            return this.listOps.index(key, index);
        }

        /**
         * 獲取列表指定范圍內(nèi)的元素
         *
         * @param key
         * @param start
         * @param end
         * @return
         */
        public List<V> range(K key, long start, long end) {
            return this.listOps.range(key, start, end);
        }

        /**
         * 獲取列表所有元素
         *
         * @param key
         * @return
         */
        public List<V> getList(K key) {
            return this.range(key, 0, -1);
        }

        /**
         * 插入數(shù)據(jù)到列表頭部
         *
         * @param key
         * @param value
         * @return
         */
        public Long leftPush(K key, V value) {
            return this.listOps.leftPush(key, value);
        }

        /**
         * value 插入到值 pivot 前面
         *
         * @param key
         * @param pivot
         * @param value
         * @return
         */
        public Long leftPush(K key, V pivot, V value) {
            return this.listOps.leftPush(key, pivot, value);
        }

        /**
         * 批量插入數(shù)據(jù)到列表頭部
         *
         * @param key
         * @param values
         * @return
         */
        public Long leftPushAll(K key, V... values) {
            return this.listOps.leftPushAll(key, values);
        }

        /**
         * 批量插入數(shù)據(jù)到列表頭部
         *
         * @param key
         * @param values
         * @return
         */
        public Long leftPushAll(K key, Collection<V> values) {
            return this.listOps.leftPushAll(key, values);
        }

        /**
         * 插入數(shù)據(jù)到列表尾部
         *
         * @param key
         * @param value
         * @return
         */
        public Long push(K key, V value) {
            return this.listOps.rightPush(key, value);
        }

        /**
         * value 插入到值 pivot 后面
         *
         * @param key
         * @param pivot
         * @param value
         * @return
         */
        public Long rightPush(K key, V pivot, V value) {
            return this.listOps.rightPush(key, pivot, value);
        }

        /**
         * 設(shè)置元素到指定索引位置
         *
         * @param key
         * @param index
         * @param value
         */
        public void set(K key, long index, V value) {
            this.listOps.set(key, index, value);
        }

        /**
         * 移除列表頭部元素
         *
         * @param key
         * @return 返回移除的頭部元素
         */
        public V leftPop(K key) {
            return this.listOps.leftPop(key);
        }

        /**
         * 移除列表尾部元素
         *
         * @param key
         * @return 返回移除的尾部元素
         */
        public V pop(K key) {
            return this.listOps.rightPop(key);
        }

        /**
         * 刪除值為 value 的 count 個元素
         *
         * @param key
         * @param count count = 0: 刪除列表所有值為 value 的元素
         *              count > 0: 從頭到尾,刪除 count 個值為 value 的元素
         *              count < 0: 從尾到頭,刪除 count 個值為 value 的元素
         * @param value
         * @return 實際刪除的元素個數(shù)
         */
        public Long remove(K key, long count, V value) {
            return this.listOps.remove(key, count, value);
        }

        /**
         * 刪除列表值為 value 的所有元素
         *
         * @param key
         * @param value
         * @return
         */
        public Long removeAll(K key, V value) {
            return this.remove(key, 0, value);
        }

        /**
         * 裁剪列表,只保留 [start, end] 區(qū)間的元素
         *
         * @param key
         * @param start
         * @param end
         */
        public void trim(K key, long start, long end) {
            this.listOps.trim(key, start, end);
        }

        /**
         * 獲取列表長度
         *
         * @param key
         * @return
         */
        public Long size(K key) {
            return this.listOps.size(key);
        }
    }

    public static class SetOps<K, V> {

        private final SetOperations<K, V> setOps;

        private SetOps(RedisTemplate<K, V> redis) {
            this.setOps = redis.opsForSet();
        }

        /**
         * 集合添加元素
         *
         * @param key
         * @param value
         * @return
         */
        public Long add(K key, V value) {
            return this.setOps.add(key, value);
        }

        /**
         * 彈出元素
         *
         * @param key
         * @return 返回彈出的元素
         */
        public V pop(K key) {
            return this.setOps.pop(key);
        }

        /**
         * 批量移除元素
         *
         * @param key
         * @param values
         * @return
         */
        public Long remove(K key, V... values) {
            return this.setOps.remove(key, values);
        }

        /**
         * 獲取集合所有元素
         *
         * @param key
         * @return
         */
        public Set<V> getSet(K key) {
            return this.setOps.members(key);
        }

        /**
         * 獲取集合大小
         *
         * @param key
         * @return
         */
        public Long size(K key) {
            return this.setOps.size(key);
        }

        /**
         * 判斷集合是否包含指定元素
         *
         * @param key
         * @param value
         * @return
         */
        public Boolean contains(K key, Object value) {
            return this.setOps.isMember(key, value);
        }

        /**
         * 獲取 key 集合和其他 key 指定的集合之間的交集
         *
         * @param key
         * @param otherKeys
         * @return
         */
        public Set<V> intersect(K key, Collection<K> otherKeys) {
            return this.setOps.intersect(key, otherKeys);
        }

        /**
         * 獲取多個集合的交集
         *
         * @param key
         * @param otherKeys
         * @return
         */
        public Set<V> intersect(K key, K... otherKeys) {
            return this.intersect(key, Stream.of(otherKeys).collect(Collectors.toSet()));
        }

        /**
         * 獲取 key 集合和其他 key 指定的集合之間的并集
         *
         * @param key
         * @param otherKeys
         * @return
         */
        public Set<V> union(K key, Collection<K> otherKeys) {
            return this.setOps.union(key, otherKeys);
        }

        /**
         * 獲取多個集合之間的并集
         *
         * @param key
         * @param otherKeys
         * @return
         */
        public Set<V> union(K key, K... otherKeys) {
            return this.union(key, Stream.of(otherKeys).collect(Collectors.toSet()));
        }

        /**
         * 獲取 key 集合和其他 key 指定的集合間的差集
         *
         * @param key
         * @param otherKeys
         * @return
         */
        public Set<V> difference(K key, Collection<K> otherKeys) {
            return this.setOps.difference(key, otherKeys);
        }

        /**
         * 獲取多個集合間的差集
         *
         * @param key
         * @param otherKeys
         * @return
         */
        public Set<V> difference(K key, K... otherKeys) {
            return this.difference(key, Stream.of(otherKeys).collect(Collectors.toSet()));
        }
    }

    public static class ZSetOps<K, V> {
        private final ZSetOperations<K, V> zsetOps;

        private ZSetOps(RedisTemplate<K, V> redis) {
            this.zsetOps = redis.opsForZSet();
        }

        /**
         * 添加元素(有序集合內(nèi)部按元素的 score 從小到達進行排序)
         *
         * @param key
         * @param value
         * @param score
         * @return
         */
        public Boolean add(K key, V value, double score) {
            return this.zsetOps.add(key, value, score);
        }

        /**
         * 批量刪除元素
         *
         * @param key
         * @param values
         * @return
         */
        public Long remove(K key, V... values) {
            return this.zsetOps.remove(key, values);
        }

        /**
         * 增加元素 value 的 score 值
         *
         * @param key
         * @param value
         * @param delta
         * @return 返回元素增加后的 score 值
         */
        public Double incrementScore(K key, V value, double delta) {
            return this.zsetOps.incrementScore(key, value, delta);
        }

        /**
         * 返回元素 value 在有序集合中的排名(按 score 從小到大排序)
         *
         * @param key
         * @param value
         * @return 0 表示排名第一,依次類推
         */
        public Long rank(K key, V value) {
            return this.zsetOps.rank(key, value);
        }

        /**
         * 返回元素 value 在有序集合中的排名(按 score 從大到小排序)
         *
         * @param key
         * @param value
         * @return
         */
        public Long reverseRank(K key, V value) {
            return this.zsetOps.reverseRank(key, value);
        }

        /**
         * 獲取有序集合指定范圍 [start, end] 之間的元素(默認按 score 由小到大排序)
         *
         * @param key
         * @param start
         * @param end
         * @return
         */
        public Set<V> range(K key, long start, long end) {
            return this.zsetOps.range(key, start, end);
        }

        /**
         * 獲取有序集合所有元素(默認按 score 由小到大排序)
         *
         * @param key
         * @return
         */
        public Set<V> getZSet(K key) {
            return this.range(key, 0, -1);
        }

        /**
         * 獲取有序集合指定區(qū)間 [start, end] 內(nèi)的所有元素,同時攜帶對應(yīng)的 score 值。
         *
         * @param key
         * @param start
         * @param end
         * @return
         */
        public Set<ZSetOperations.TypedTuple<V>> rangeWithScores(K key, long start, long end) {
            return this.zsetOps.rangeWithScores(key, start, end);
        }

        /**
         * 獲取 score 介于 [min, max] 之間的所有元素(按 score 由小到大排序)
         *
         * @param key
         * @param min
         * @param max
         * @return
         */
        public Set<V> rangeByScore(K key, double min, double max) {
            return this.zsetOps.rangeByScore(key, min, max);
        }

        /**
         * 獲取 score 介于 [min, max] 之間的所有元素,同時攜帶其 score 值
         *
         * @param key
         * @param min
         * @param max
         * @return
         */
        public Set<ZSetOperations.TypedTuple<V>> rangeByScoreWithScores(K key, double min, double max) {
            return this.zsetOps.rangeByScoreWithScores(key, min, max);
        }

        /**
         * 返回有序集合指定區(qū)間 [start, end] 內(nèi)的所有元素(按元素 score 值從大到小排列)
         *
         * @param key
         * @param start
         * @param end
         * @return
         */
        public Set<V> reverseRange(K key, long start, long end) {
            return this.zsetOps.reverseRange(key, start, end);
        }

        /**
         * 獲取有序集合指定區(qū)間 [start, end] 內(nèi)的所有元素,包含其 score 值,且按 score 值由大到小排列
         *
         * @param key
         * @param start
         * @param end
         * @return
         */
        public Set<ZSetOperations.TypedTuple<V>> reverseRangeWithScore(K key, long start, long end) {
            return this.zsetOps.reverseRangeWithScores(key, start, end);
        }

        /**
         * 獲取 score 介于 [min, max] 之間的所有元素(按 score 由大到小排序)
         *
         * @param key
         * @param min
         * @param max
         * @return
         */
        public Set<V> reverseRangeByScore(K key, double min, double max) {
            return this.zsetOps.reverseRangeByScore(key, min, max);
        }

        /**
         * 獲取 score 介于 [min, max] 之間的所有元素,同時攜帶其 score 值,元素按 score 值由大到小排序
         *
         * @param key
         * @param min
         * @param max
         * @return
         */
        public Set<ZSetOperations.TypedTuple<V>> reverseRangeByScoreWithScores(K key, double min, double max) {
            return this.zsetOps.reverseRangeByScoreWithScores(key, min, max);
        }


        /**
         * 獲取 score 值介于 [min, max] 之間的元素數(shù)量
         *
         * @param key
         * @param min
         * @param max
         * @return
         */
        public Long count(K key, double min, double max) {
            return this.zsetOps.count(key, min, max);
        }

        /**
         * 獲取有序集合大小
         *
         * @param key
         * @return
         */
        public Long size(K key) {
            return this.zsetOps.size(key);
        }

        /**
         * 獲取指定元素 value 的 score 值
         *
         * @param key
         * @param value
         * @return
         */
        public Double score(K key, V value) {
            return this.zsetOps.score(key, value);
        }

        /**
         * 移除指定區(qū)間 [start, end] 的元素
         *
         * @param key
         * @param start
         * @param end
         * @return
         */
        public Long removeRange(K key, long start, long end) {
            return this.zsetOps.removeRange(key, start, end);
        }

        /**
         * 移除 score 指定區(qū)間 [min, max] 內(nèi)的所有元素
         *
         * @param key
         * @param min
         * @param max
         * @return
         */
        public Long removeRangeByScore(K key, double min, double max) {
            return this.zsetOps.removeRangeByScore(key, min, max);
        }
    }
}

此工具類大致的使用方式如下:

@SpringBootTest
class RedisDemoApplicationTests {

    @Autowired
    private RedisService redisService;

    @Test
    public void testRedisService() {
        String keyString = "keyString";
        String expectString = "set String value";
        this.redisService.keys().delete(keyString);
        this.redisService.string().set(keyString, expectString);
        String actualString = (String) this.redisService.string().get("keyString");

        String keyHash = "keyHash";
        String hashKey = "hashKey";
        String expectHash = "set Hash value";
        this.redisService.keys().delete(keyHash);
        this.redisService.hash().put(keyHash, hashKey, expectHash);
        String actualHash = (String) this.redisService.hash().get(keyHash, hashKey);

        String keyList = "keyList";
        String expectList = "set List value";
        this.redisService.keys().delete(keyList);
        this.redisService.list().push(keyList, expectList);
        String actualList = (String) this.redisService.list().get(keyList, 0);

        String keySet = "keySet";
        String expectSet = "set Set value";
        this.redisService.keys().delete(keySet);
        this.redisService.set().add(keySet, expectSet);
        String actualSet = (String) this.redisService.set().pop(keySet);

        String keyZSet = "keyZSet";
        String expectZSet = "set Sorted Set value";
        this.redisService.keys().delete(keyZSet);
        this.redisService.zset().add(keyZSet, expectZSet, 1.0);
        Set<Object> actualZSet = this.redisService.zset().getZSet(keyZSet);

        assertAll("test RedisService",
                () -> Assertions.assertEquals(expectString, actualString),
                () -> Assertions.assertEquals(expectHash, actualHash),
                () -> Assertions.assertEquals(expectList, actualList),
                () -> Assertions.assertEquals(expectSet, actualSet),
                () -> assertThat(actualZSet.stream().findFirst().get(), equalTo(expectZSet))
        );
    }
}

附錄

  • 示例代碼:本文全部示例代碼可查看:redis-demo

  • 導(dǎo)入 Jedis:可以將 Redis 客戶端更改為Jedis,依賴導(dǎo)入如下所示:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
        <exclusions>
            <!-- 排除 Lettuce 包 -->
            <exclusion>
                <groupId>io.lettuce</groupId>
                <artifactId>lettuce-core</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    
    <!-- 添加 Jedis 客戶端 -->
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
    </dependency>
    
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內(nèi)容