分布式場景Redis客戶端最優(yōu)解決方案-lettuce + Redisson

Redis 官方推薦的 Java 客戶端有Jedis、lettuce 和 Redisson糠悯。三種客戶端各有優(yōu)缺點(diǎn)柒傻,在我們業(yè)務(wù)實(shí)現(xiàn)中選擇合適的客戶端晶乔,有助于提高Redis的使用性能。要選擇合適的客戶端我們應(yīng)該大概清楚Redis客戶端的通訊方式和各種客戶端的優(yōu)缺點(diǎn)。

Redis概述

Redis支持多種語言客戶端赞弥,支持的客戶端可以從官網(wǎng)查看毅整,這里列舉一系列常見的客戶端:

  • C語言
  • C++
  • C#
  • Java
  • Python
  • Node.js
  • PHP

Redis采用單線程方式處理多個(gè)客戶端連接,作為程序開發(fā)者應(yīng)該了解 Redis 服務(wù)端和客戶端的通信協(xié)議绽左,以及主流編程語言的 Redis 客戶端使用方法悼嫉,同時(shí)還需要了解客戶端管理的相應(yīng) API 以及開發(fā)運(yùn)維中可能遇到的問題。

Redis客戶端通訊協(xié)議

Redis制定RESP(Redis Serializable Protocol, RESP)協(xié)議拼窥,實(shí)現(xiàn)客戶端和服務(wù)端的正常交互戏蔑,這種協(xié)議簡單高效,既能夠被機(jī)器解析鲁纠,又容易被人類識(shí)別辛臊。
RESP可以序列號(hào)不同數(shù)據(jù)類型數(shù)據(jù)(整型、字符串房交、數(shù)組以及特殊類型Error)彻舰。需要執(zhí)行的Redis命令會(huì)封裝成類似字符串?dāng)?shù)組的請(qǐng)求通過客戶端發(fā)送到服務(wù)端。Redis會(huì)基于制定的命令類型選擇對(duì)應(yīng)的數(shù)據(jù)類型進(jìn)行回復(fù)候味。

RESP發(fā)送命令格式

在RESP中刃唤,發(fā)送數(shù)據(jù)的類型取決于數(shù)據(jù)報(bào)文的首個(gè)字節(jié):

  • 單行字符串:單行字符串的第一個(gè)字節(jié)為+ ,最后兩個(gè)字節(jié)是\r\n白群,其他字節(jié)是字符串內(nèi)容+OK\r\nError尚胞;
  • 錯(cuò)誤消息:錯(cuò)誤消息的第一個(gè)字節(jié)為-,最后兩個(gè)字節(jié)是\r\n帜慢,其他字節(jié)是異常消息的文本內(nèi)容-ERR\r\nInteger笼裳;
  • 整型數(shù)字:整型數(shù)字的第一個(gè)字節(jié)是:,最后兩個(gè)字節(jié)是\r\n粱玲,其他字節(jié)是數(shù)字的文本內(nèi)容:100\r\nBulk躬柬;
  • 定長字符串:定長字符串的String定長字符串第一個(gè)字節(jié)是美元符號(hào) ,緊接著的字節(jié)是內(nèi)容字符串長度\r\n抽减,最后兩個(gè)字節(jié)是\r\n允青,其他字節(jié)是字符串內(nèi)容$4\r\ndoge\r\n;
  • RESP數(shù)組:RESP數(shù)組的第一個(gè)字節(jié)是卵沉,緊接著的字節(jié)是元素個(gè)數(shù)\r\n颠锉,最后兩個(gè)字節(jié)是\r\n,其他字節(jié)是各個(gè)元素的內(nèi)容史汗,每個(gè)元素可以是任意一種數(shù)據(jù)類型2\r\n:100\r\n$4\r\ndoge\r\n琼掠。

發(fā)送的命令格式如下,CRLF代表"\r\n":

Copy*<參數(shù)數(shù)量> CRLF
$<參數(shù)1的字節(jié)數(shù)量> CRLF
<參數(shù)1> CRLF
...
$<參數(shù)N的字節(jié)數(shù)量> CRLF
<參數(shù)N> CRLF

以set hello world這個(gè)命令為例停撞,發(fā)送的內(nèi)容就是這樣的:

Copy*3
$3
SET
$5
hello
$5
world

第一行*3表示有3個(gè)參數(shù)瓷蛙,3表示接下來的一個(gè)參數(shù)有3個(gè)字節(jié),接下來是參數(shù),5表示下一個(gè)參數(shù)有5個(gè)字節(jié)速挑,接下來是參數(shù)谤牡,$5表示下一個(gè)參數(shù)有5個(gè)字節(jié),接下來是參數(shù)姥宝。
所以set hello world最終發(fā)送給redis服務(wù)器的命令是:

Copy*3\r\n$3\r\nSET\r\n$5\r\nhello\r\n$5\r\nworld\r\n

RESP響應(yīng)內(nèi)容

CopyRedis的返回結(jié)果類型分為以下五種:
正確回復(fù):在RESP中第一個(gè)字節(jié)為"+"
錯(cuò)誤回復(fù):在RESP中第一個(gè)字節(jié)為"-"
整數(shù)回復(fù):在RESP中第一個(gè)字節(jié)為":"
字符串回復(fù):在RESP中第一個(gè)字節(jié)為"$"
多條字符串回復(fù):在RESP中第一個(gè)字節(jié)為"*"

(+) 表示一個(gè)正確的狀態(tài)信息翅萤,具體信息是當(dāng)前行+后面的字符。
(-) 表示一個(gè)錯(cuò)誤信息腊满,具體信息是當(dāng)前行-后面的字符套么。
(* ) 表示消息體總共有多少行,不包括當(dāng)前行,*后面是具體的行數(shù)碳蛋。
(美元符合) 表示下一行數(shù)據(jù)長度胚泌,不包括換行符長度\r\n,$后面則是對(duì)應(yīng)的長度的數(shù)據(jù)肃弟。
(:) 表示返回一個(gè)數(shù)值玷室,:后面是相應(yīng)的數(shù)字節(jié)符。


Java客戶端

Jedis

傳統(tǒng)老牌Java客戶端笤受,一直在更新穷缤,對(duì)redis命令有比較全面的支持。

優(yōu)點(diǎn)

  • 支持全面的Redis命令箩兽,具有全面的API津肛。

缺點(diǎn)

  • 阻塞式I/O,其方法都是同步調(diào)用汗贫,程序流需要等到 sockets 處理完 I/O 才能執(zhí)行身坐,不支持異步;
  • Jedis 客戶端實(shí)例不是線程安全的落包,所以需要通過連接池來使用 Jedis部蛇。

lettuce

lettuce是可擴(kuò)展的線程安全的客戶端,支持異步模式妥色。如果避免阻塞和事務(wù)操作搪花,如BLPOP和MULTI/EXEC遏片,多個(gè)線程就可以共享一個(gè)連接嘹害。lettuce 底層基于 Netty,支持高級(jí)的 Redis 特性吮便,比如哨兵笔呀,集群,管道髓需,自動(dòng)重新連接和Redis數(shù)據(jù)模型许师。

優(yōu)點(diǎn):

  • 支持同步異步通信模式;
  • Lettuce 的 API 是線程安全的,如果不是執(zhí)行阻塞和事務(wù)操作微渠,如BLPOP和MULTI/EXEC搭幻,多個(gè)線程就可以共享一個(gè)連接。
  • SpringBoot 2.x默認(rèn)Redis客戶端逞盆,對(duì)Lettuce 提供完美支持

Redisson

Redisson 是一個(gè)在 Redis 的基礎(chǔ)上實(shí)現(xiàn)的 Java 駐內(nèi)存數(shù)據(jù)網(wǎng)格(In-Memory Data Grid)檀蹋。它不僅提供了一系列的分布式的 Java 常用對(duì)象,還提供了許多分布式服務(wù)云芦。其中包括( BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson 提供了使用Redis 的最簡單和最便捷的方法俯逾。Redisson 的宗旨是促進(jìn)使用者對(duì)Redis的關(guān)注分離(Separation of Concern),從而讓使用者能夠?qū)⒕Ω械胤旁谔幚順I(yè)務(wù)邏輯上舅逸。
優(yōu)點(diǎn):

  • 使用者對(duì) Redis 的關(guān)注分離桌肴,可以類比 Spring 框架,這些框架搭建了應(yīng)用程序的基礎(chǔ)框架和功能琉历,提升開發(fā)效率坠七,讓開發(fā)者有更多的時(shí)間來關(guān)注業(yè)務(wù)邏輯;
  • 提供很多分布式相關(guān)操作服務(wù)旗笔,例如灼捂,分布式鎖,分布式集合换团,可通過Redis支持延遲隊(duì)列等悉稠。
    缺點(diǎn):
  • Redisson 對(duì)字符串的操作支持比較差。

Redis Java客戶端技術(shù)選型

文章基于SpringBoot 2.x進(jìn)行選型

  • 僅使用Redis進(jìn)行字符串的數(shù)據(jù)進(jìn)行緩存處理艘包,可以使用默認(rèn)的lettuce的猛。
  • 需要使用Redis做分布式事務(wù),可以選擇lettuce + Redisson想虎。
  • 需要緩存分布式集合數(shù)據(jù)并對(duì)集合進(jìn)行頻繁讀寫操作卦尊,可以選擇lettuce + Redisson。
  • 使用Redis 做消息隊(duì)列舌厨,可以選擇lettuce + Redisson岂却。

SpringBoot 集成lettuce + Redisson

在大多數(shù)分布式互聯(lián)網(wǎng)應(yīng)用中,redis經(jīng)常用來處理復(fù)雜業(yè)務(wù)而不是單純的字符串存儲(chǔ)裙椭,比如使用Redis集合躏哩、分布式鎖、隊(duì)列等揉燃,因此集成lettuce + Redisson扫尺,按照不同業(yè)務(wù)而使用不同組件是比較優(yōu)秀的方案。

集成lettuce

  • 引入依賴
       <!--spring session-->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

SpringBoot 2.X默認(rèn)使用lettuce 炊汤,因此提供了比較好的支持正驻,RedisAutoConfiguration完成了對(duì)JedisConnectionFactory和LettuceConnectionFactory 的自動(dòng)配置弊攘。同時(shí)RedisProperties封裝了redis配置。

  • 添加配置
    application.yml
spring:
    redis:
        host: 192.168.128.20 #單機(jī)配置
        port: 6379 #單機(jī)配置
        #cluster:
          #nodes: xxxurl  集群配置
        # Redis默認(rèn)情況下有16個(gè)分片姑曙,配置具體使用的分片襟交,默認(rèn)為0
        database: 0
        lettuce:
          pool:
            # 連接池最大連接數(shù) 默認(rèn)8 ,負(fù)數(shù)表示沒有限制
            max-active: 8
            # 連接池最大阻塞等待時(shí)間(使用負(fù)值表示沒有限制) 默認(rèn)-1
            max-wait: -1
            # 連接池中的最大空閑連接 默認(rèn)8
            max-idle: 8
            # 連接池中的最小空閑連接 默認(rèn)0
            min-idle: 0

集成Redission

  • 引入依賴
<!--redisson客戶端-->
      <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson</artifactId>
        <version>${redisson.version}</version>
      </dependency>
  • 添加配置
    redisson.yml
    redission:
      pool:
            # 連接池最大連接數(shù) 默認(rèn)8 伤靠,負(fù)數(shù)表示沒有限制
            max-active: 8
            # 連接池最大阻塞等待時(shí)間(使用負(fù)值表示沒有限制) 默認(rèn)-1
            max-wait: -1
            # 連接池中的最大空閑連接 默認(rèn)8
            max-idle: 8
            # 連接池中的最小空閑連接 默認(rèn)0
            min-idle: 0
            connTimeout: 10000
            timeout: 3000
            soTimeout: 3000
            size: 32
       #集群配置
        cluster:
          #集群掃描間隔時(shí)間 單位毫秒
          scanInterval: 1000
          # 只在從服務(wù)節(jié)點(diǎn)里讀取
          readMode: "SLAVE"
           slaveSubscriptionConnectionPoolSize: 50
            # 從節(jié)點(diǎn)最小空閑連接數(shù)
            slaveConnectionMinimumIdleSize: 32
            # 從節(jié)點(diǎn)連接池大小
            slaveConnectionPoolSize: 64
            # 主節(jié)點(diǎn)最小空閑連接數(shù)
            masterConnectionMinimumIdleSize: 32
            # 主節(jié)點(diǎn)連接池大小
            masterConnectionPoolSize: 64
            # 命令失敗重試次數(shù)
            retryAttempts: 3
             # 命令重試發(fā)送時(shí)間間隔
              retryInterval: 1500
              # failedAttempts
              failedAttempts: 3

配置客戶端

@EnableConfigurationProperties({RedissionProperties.class, RedisProperties.class})
public class RedisConfig {

  @Value("${spring.profiles.active}")
  private String env;

  @Value("${global-conf.active-profiles.prod}")
  private String activeProd;

  @Autowired
  private RedisProperties redisProperties;

  @Autowired
  private RedissionProperties redissionProperties;

  /**
   * springboot2.x 使用LettuceConnectionFactory 代替 RedisConnectionFactory
   * 在application.yml配置基本信息后,springboot2.x  RedisAutoConfiguration能夠自動(dòng)裝配 LettuceConnectionFactory 和
   * RedisConnectionFactory 及其 RedisTemplate 單機(jī)模式和集群模式配置都自動(dòng)注入婿着,因?yàn)樵诙xLettuce客戶端時(shí)無需顯示指定模式
   *
   * @param redisConnectionFactory
   * @return
   */
  @Bean
  public RedisTemplate<String, Object> redisTemplate(
      LettuceConnectionFactory redisConnectionFactory) {

    RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
    redisTemplate.setConnectionFactory(redisConnectionFactory);
    redisTemplate.setKeySerializer(new StringRedisSerializer());
    redisTemplate.setValueSerializer(redisSerializer());
    redisTemplate.setHashKeySerializer(new StringRedisSerializer());
    redisTemplate.setHashValueSerializer(redisSerializer());
    return redisTemplate;
  }

  @Bean
  @ConditionalOnProperty(name = "spring.redis.model", havingValue = "single")
  public RedissonClient getSingleRedisson() {
    Config config = new Config();

    String node = redisProperties.getHost();
    //用"rediss://"來啟用SSL連接
    node = node.startsWith("redis://") ? node : "redis://" + node;
    SingleServerConfig serverConfig = config.useSingleServer();
    serverConfig.setAddress(node)
        .setConnectionPoolSize(redissionProperties.getPool().getSize())
        .setTimeout(redissionProperties.getPool().getTimeout())
        .setConnectionMinimumIdleSize(redissionProperties.getPool().getMinIdle());
    if (StringUtils.isNotBlank(redisProperties.getPassword())) {
      serverConfig.setPassword(redisProperties.getPassword());
    }

    return Redisson.create(config);
  }

  @Bean
  @ConditionalOnProperty(name = "spring.redis.model", havingValue = "cluster")
  public RedissonClient getClusterRedisson() {
    Config config = new Config();

    //用"rediss://"來啟用SSL連接
    List<String> newNodes = redisProperties.getCluster().getNodes()
        .stream()
        .map(node -> node.startsWith("redis://") ? node : "redis://" + node)
        .collect(
            Collectors.toList());

    ClusterServersConfig serverConfig = config.useClusterServers()
        .addNodeAddress(newNodes.toArray(new String[0]))
        .setScanInterval(
            redissionProperties.getCluster().getScanInterval())
        .setIdleConnectionTimeout(
            redissionProperties.getPool().getSoTimeout())
        .setConnectTimeout(
            redissionProperties.getPool().getConnTimeout())
        .setRetryAttempts(
            redissionProperties.getCluster().getRetryAttempts())
        .setRetryInterval(
            redissionProperties.getCluster().getRetryInterval())
        .setMasterConnectionPoolSize(redissionProperties.getCluster()
            .getMasterConnectionPoolSize())
        .setSlaveConnectionPoolSize(redissionProperties.getCluster()
            .getSlaveConnectionPoolSize())
        .setTimeout(redissionProperties.getPool().getTimeout());
    if (StringUtils.isNotBlank(redisProperties.getPassword())) {
      serverConfig.setPassword(redisProperties.getPassword());
    }
    return Redisson.create(config);
  }


  @Bean
  public RedisSerializer<Object> redisSerializer() {
    //創(chuàng)建JSON序列化器
    Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(
        Object.class);
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    //必須設(shè)置,否則無法將JSON轉(zhuǎn)化為對(duì)象醋界,會(huì)轉(zhuǎn)化成Map類型
    objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
        ObjectMapper.DefaultTyping.NON_FINAL);
    serializer.setObjectMapper(objectMapper);
    return serializer;
  }

  @Bean
  public RedisCacheManager redisCacheManager(LettuceConnectionFactory redisConnectionFactory) {
    RedisCacheWriter redisCacheWriter = RedisCacheWriter
        .nonLockingRedisCacheWriter(redisConnectionFactory);
    //設(shè)置Redis緩存有效期為1天
    RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
        .serializeValuesWith(
            RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer())).entryTtl(
            Duration.ofDays(1));
    return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
  }
}

自定義Redis工具類

通常情況key一般是String類型竟宋,而value可能不一樣,一般有兩種:String和Object形纺。
如果k-v都是String丘侠,我們直接使用StringRedisTemplate

  • SimpleStringRedisUtil
@Component
public class SimpleStringRedisUtil {

  @Autowired
  private StringRedisTemplate redisTemplate;

  /**
   * 刪除key
   *
   * @param key
   */
  public void delete(String key) {
    redisTemplate.delete(key);
  }

  /**
   * 序列化key
   *
   * @param key
   * @return
   */
  public byte[] dump(String key) {
    return redisTemplate.dump(key);
  }

  /**
   * 是否存在key
   *
   * @param key
   * @return
   */
  public Boolean exists(String key) {
    return redisTemplate.hasKey(key);
  }

  /**
   * 設(shè)置指定 key 的值
   *
   * @param key
   * @param value
   */
  public void set(String key, String value) {
    redisTemplate.opsForValue().set(key, value);
  }

  /**
   * 寫入redis緩存(設(shè)置expire存活時(shí)間)
   * @param key
   * @param value
   * @param expire
   * @return
   */
  public void set(final String key, String value, Long expire){
      ValueOperations operations = redisTemplate.opsForValue();
      operations.set(key, value);
      redisTemplate.expire(key, expire, TimeUnit.SECONDS);
  }

  /**
   * 獲取指定 key 的值
   *
   * @param key
   * @return
   */
  public String get(String key) {
    return redisTemplate.opsForValue().get(key);
  }
}

如果k-是String,v-是Object,比如Json對(duì)象逐样、可序列化對(duì)象以及固定不變的集合等蜗字,那就需要需要RedisTemplate接口

  • SimpleObjectRedisUtil
@Component
public class SimpleObjectRedisUtil {

  @Autowired
  private RedisTemplate redisTemplate;


  public Set<String> keys(String keys){
    try {
      return redisTemplate.keys(keys);
    }catch (Exception e){
      e.printStackTrace();
      return null;
    }
  }

  /**
   * 指定緩存失效時(shí)間
   * @param key 鍵
   * @param time 時(shí)間(秒)
   * @return
   */
  public boolean expire(String key, long time) {
    try {
      if (time > 0) {
        redisTemplate.expire(key, time, TimeUnit.SECONDS);
      }
      return true;
    } catch (Exception e) {
      e.printStackTrace();
      return false;
    }
  }

  /**
   * 指定緩存失效時(shí)間
   * @param key 鍵
   * @param time 時(shí)間
   * @param time timeUnit
   * @return
   */
  public boolean expire(String key, long time, TimeUnit timeUnit) {
    try {
      if (time > 0) {
        redisTemplate.expire(key, time, timeUnit);
      }
      return true;
    } catch (Exception e) {
      e.printStackTrace();
      return false;
    }
  }

  /**
   * 根據(jù)key 獲取過期時(shí)間
   * @param key 鍵 不能為null
   * @return 時(shí)間(秒) 返回0代表為永久有效
   */
  public long getExpire(String key) {
    return redisTemplate.getExpire(key, TimeUnit.SECONDS);
  }

  /**
   * 根據(jù)key 獲取過期時(shí)間
   * @param key 鍵 不能為null
   * @param timeUnit
   * @return 時(shí)間(秒) 返回0代表為永久有效
   */
  public long getExpire(String key, TimeUnit timeUnit) {
    return redisTemplate.getExpire(key, timeUnit);
  }

  /**
   * 判斷key是否存在
   * @param key 鍵
   * @return true 存在 false不存在
   */
  public boolean hasKey(String key) {
    try {
      return redisTemplate.hasKey(key);
    } catch (Exception e) {
      e.printStackTrace();
      return false;
    }
  }
  /**
   * 刪除緩存
   * @param key 可以傳一個(gè)值 或多個(gè)
   */
  @SuppressWarnings("unchecked")
  public void del(String... key) {
    if (key != null && key.length > 0) {
      if (key.length == 1) {
        redisTemplate.delete(key[0]);
      } else {
        redisTemplate.delete(CollectionUtils.arrayToList(key));
      }
    }
  }
  /**
   * 普通緩存獲取
   * @param key 鍵
   * @return 值
   */
  public Object get(String key) {
    return key == null ? null : redisTemplate.opsForValue().get(key);
  }
  /**
   * 普通緩存放入
   * @param key 鍵
   * @param value 值
   * @return true成功 false失敗
   */
  public boolean set(String key, Object value) {
    try {
      redisTemplate.opsForValue().set(key, value);
      return true;
    } catch (Exception e) {
      e.printStackTrace();
      return false;
    }
  }
  

  /**
   * 普通緩存放入并設(shè)置時(shí)間
   * @param key 鍵
   * @param value 值
   * @param time 時(shí)間(秒) time要大于0 如果time小于等于0 將設(shè)置無限期
   * @return true成功 false 失敗
   */
  public boolean set(String key, Object value, long time) {
    try {
      if (time > 0) {
        redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
      } else {
        set(key, value);
      }
      return true;
    } catch (Exception e) {
      e.printStackTrace();
      return false;
    }
  }

  /**
   * 普通緩存放入并設(shè)置時(shí)間
   * @param key 鍵
   * @param value 值
   * @param time 時(shí)間 time要大于0 如果time小于等于0 將設(shè)置無限期
   * @param timeUnit
   * @return true成功 false 失敗
   */
  public boolean set(String key, Object value, long time, TimeUnit timeUnit) {
    try {
      if (time > 0) {
        redisTemplate.opsForValue().set(key, value, time, timeUnit);
      } else {
        set(key, value);
      }
      return true;
    } catch (Exception e) {
      e.printStackTrace();
      return false;
    }
  }
  

  /**
   * 遞增
   * @param key 鍵
   * @param delta 要增加幾(大于0)
   * @return
   */
  public long incr(String key, long delta) {
    if (delta < 0) {
      throw new RuntimeException("遞增因子必須大于0");
    }
    return redisTemplate.opsForValue().increment(key, delta);
  }
  /**
   * 遞減
   * @param key 鍵
   * @param delta 要減少幾(小于0)
   * @return
   */
  public long decr(String key, long delta) {
    if (delta < 0) {
      throw new RuntimeException("遞減因子必須大于0");
    }
    return redisTemplate.opsForValue().increment(key, -delta);
  }
  /**
   * HashGet
   * @param key 鍵 不能為null
   * @param item 項(xiàng) 不能為null
   * @return 值
   */
  public Object hget(String key, String item) {
    return redisTemplate.opsForHash().get(key, item);
  }
  /**
   * 獲取hashKey對(duì)應(yīng)的所有鍵值
   * @param key 鍵
   * @return 對(duì)應(yīng)的多個(gè)鍵值
   */
  public Map<Object, Object> hmget(String key) {
    return redisTemplate.opsForHash().entries(key);
  }
  
  /**
   * HashSet
   * @param key 鍵
   * @param map 對(duì)應(yīng)多個(gè)鍵值
   * @return true 成功 false 失敗
   */
  public boolean hmset(String key, Map<String, ?> map) {
    try {
      redisTemplate.opsForHash().putAll(key, map);
      return true;
    } catch (Exception e) {
      e.printStackTrace();
      return false;
    }
  }
  /**
   * HashSet 并設(shè)置時(shí)間
   * @param key 鍵
   * @param map 對(duì)應(yīng)多個(gè)鍵值
   * @param time 時(shí)間(秒)
   * @return true成功 false失敗
   */
  public boolean hmset(String key, Map<String, ?> map, long time) {
    try {
      redisTemplate.opsForHash().putAll(key, map);
      if (time > 0) {
        expire(key, time);
      }
      return true;
    } catch (Exception e) {
      e.printStackTrace();
      return false;
    }
  }
  /**
   * 向一張hash表中放入數(shù)據(jù),如果不存在將創(chuàng)建
   * @param key 鍵
   * @param item 項(xiàng)
   * @param value 值
   * @return true 成功 false失敗
   */
  public boolean hset(String key, String item, Object value) {
    try {
      redisTemplate.opsForHash().put(key, item, value);
      return true;
    } catch (Exception e) {
      e.printStackTrace();
      return false;
    }
  }
  /**
   * 向一張hash表中放入數(shù)據(jù),如果不存在將創(chuàng)建
   * @param key 鍵
   * @param item 項(xiàng)
   * @param value 值
   * @param time 時(shí)間(秒) 注意:如果已存在的hash表有時(shí)間,這里將會(huì)替換原有的時(shí)間
   * @return true 成功 false失敗
   */
  public boolean hset(String key, String item, Object value, long time) {
    try {
      redisTemplate.opsForHash().put(key, item, value);
      if (time > 0) {
        expire(key, time);
      }
      return true;
    } catch (Exception e) {
      e.printStackTrace();
      return false;
    }
  }
  /**
   * 刪除hash表中的值
   * @param key 鍵 不能為null
   * @param item 項(xiàng) 可以使多個(gè) 不能為null
   */
  public void hdel(String key, Object... item) {
    redisTemplate.opsForHash().delete(key, item);
  }
  /**
   * 判斷hash表中是否有該項(xiàng)的值
   * @param key 鍵 不能為null
   * @param item 項(xiàng) 不能為null
   * @return true 存在 false不存在
   */
  public boolean hHasKey(String key, String item) {
    return redisTemplate.opsForHash().hasKey(key, item);
  }
  /**
   * hash遞增 如果不存在,就會(huì)創(chuàng)建一個(gè) 并把新增后的值返回
   * @param key 鍵
   * @param item 項(xiàng)
   * @param by 要增加幾(大于0)
   * @return
   */
  public double hincr(String key, String item, double by) {
    return redisTemplate.opsForHash().increment(key, item, by);
  }
  /**
   * hash遞減
   * @param key 鍵
   * @param item 項(xiàng)
   * @param by 要減少記(小于0)
   * @return
   */
  public double hdecr(String key, String item, double by) {
    return redisTemplate.opsForHash().increment(key, item, -by);
  }

  // ===============================Set=================================
  /**
   * 根據(jù)key獲取Set中的所有值
   * @param key 鍵
   * @return
   */
  public Set<Object> sGet(String key) {
    try {
      return redisTemplate.opsForSet().members(key);
    } catch (Exception e) {
      e.printStackTrace();
      return null;
    }
  }
  /**
   * 根據(jù)value從一個(gè)set中查詢,是否存在
   * @param key 鍵
   * @param value 值
   * @return true 存在 false不存在
   */
  public boolean sHasKey(String key, Object value) {
    try {
      return redisTemplate.opsForSet().isMember(key, value);
    } catch (Exception e) {
      e.printStackTrace();
      return false;
    }
  }
  /**
   * 將數(shù)據(jù)放入set緩存
   * @param key 鍵
   * @param values 值 可以是多個(gè)
   * @return 成功個(gè)數(shù)
   */
  public long sSet(String key, Object... values) {
    try {
      return redisTemplate.opsForSet().add(key, values);
    } catch (Exception e) {
      e.printStackTrace();
      return 0;
    }
  }
  /**
   * 將set數(shù)據(jù)放入緩存
   * @param key 鍵
   * @param time 時(shí)間(秒)
   * @param values 值 可以是多個(gè)
   * @return 成功個(gè)數(shù)
   */
  public long sSetAndTime(String key, long time, Object... values) {
    try {
      Long count = redisTemplate.opsForSet().add(key, values);
      if (time > 0)
        expire(key, time);
      return count;
    } catch (Exception e) {
      e.printStackTrace();
      return 0;
    }
  }
  /**
   * 獲取set緩存的長度
   * @param key 鍵
   * @return
   */
  public long sGetSetSize(String key) {
    try {
      return redisTemplate.opsForSet().size(key);
    } catch (Exception e) {
      e.printStackTrace();
      return 0;
    }
  }
  /**
   * 移除值為value的
   * @param key 鍵
   * @param values 值 可以是多個(gè)
   * @return 移除的個(gè)數(shù)
   */
  public long setRemove(String key, Object... values) {
    try {
      Long count = redisTemplate.opsForSet().remove(key, values);
      return count;
    } catch (Exception e) {
      e.printStackTrace();
      return 0;
    }
  }
  // ===============================list=================================
  /**
   * 獲取list緩存的內(nèi)容
   * @param key 鍵
   * @param start 開始
   * @param end 結(jié)束 0 到 -1代表所有值
   * @return
   */
  public List<Object> lGet(String key, long start, long end) {
    try {
      return redisTemplate.opsForList().range(key, start, end);
    } catch (Exception e) {
      e.printStackTrace();
      return null;
    }
  }
  /**
   * 獲取list緩存的長度
   * @param key 鍵
   * @return
   */
  public long lGetListSize(String key) {
    try {
      return redisTemplate.opsForList().size(key);
    } catch (Exception e) {
      e.printStackTrace();
      return 0;
    }
  }
  /**
   * 通過索引 獲取list中的值
   * @param key 鍵
   * @param index 索引 index>=0時(shí), 0 表頭脂新,1 第二個(gè)元素挪捕,依次類推;index<0時(shí)争便,-1级零,表尾,-2倒數(shù)第二個(gè)元素滞乙,依次類推
   * @return
   */
  public Object lGetIndex(String key, long index) {
    try {
      return redisTemplate.opsForList().index(key, index);
    } catch (Exception e) {
      e.printStackTrace();
      return null;
    }
  }
  /**
   * 將list放入緩存
   * @param key 鍵
   * @param value 值
   * @return
   */
  public <T> boolean lSet(String key, T value) {
    try {
      redisTemplate.opsForList().rightPush(key, value);
      return true;
    } catch (Exception e) {
      e.printStackTrace();
      return false;
    }
  }
  /**
   * 將list放入緩存
   * @param key 鍵
   * @param value 值
   * @param time 時(shí)間(秒)
   * @return
   */
  public <T> boolean lSet(String key, T value, long time) {
    try {
      redisTemplate.opsForList().rightPush(key, value);
      if (time > 0)
        expire(key, time);
      return true;
    } catch (Exception e) {
      e.printStackTrace();
      return false;
    }
  }
  /**
   * 將list放入緩存
   * @param key 鍵
   * @param value 值
   * @return
   */
  public <T> boolean lSet(String key, List<T> value) {
    try {
      redisTemplate.opsForList().rightPushAll(key, value);
      return true;
    } catch (Exception e) {
      e.printStackTrace();
      return false;
    }
  }
  /**
   * 將list放入緩存
   *
   * @param key 鍵
   * @param value 值
   * @param time 時(shí)間(秒)
   * @return
   */
  public <T> boolean lSet(String key, List<T> value, long time) {
    try {
      redisTemplate.opsForList().rightPushAll(key, value);
      if (time > 0)
        expire(key, time);
      return true;
    } catch (Exception e) {
      e.printStackTrace();
      return false;
    }
  }
  /**
   * 根據(jù)索引修改list中的某條數(shù)據(jù)
   * @param key 鍵
   * @param index 索引
   * @param value 值
   * @return
   */
  public <T> boolean lUpdateIndex(String key, long index, T value) {
    try {
      redisTemplate.opsForList().set(key, index, value);
      return true;
    } catch (Exception e) {
      e.printStackTrace();
      return false;
    }
  }
  /**
   * 移除N個(gè)值為value
   * @param key 鍵
   * @param count 移除多少個(gè)
   * @param value 值
   * @return 移除的個(gè)數(shù)
   */
  public <T> long lRemove(String key, long count, T value) {
    try {
      Long remove = redisTemplate.opsForList().remove(key, count, value);
      return remove;
    } catch (Exception e) {
      e.printStackTrace();
      return 0;
    }
  }

  /**------------------zSet相關(guān)操作--------------------------------*/

  /**
   * 添加元素,有序集合是按照元素的score值由小到大排列
   *
   * @param key
   * @param value
   * @param score
   * @return
   */
  public Boolean zAdd(String key, String value, double score) {
    return redisTemplate.opsForZSet().add(key, value, score);
  }

  /**
   *
   * @param key
   * @param values
   * @return
   */
  public Long zAdd(String key, Set<ZSetOperations.TypedTuple<String>> values) {
    return redisTemplate.opsForZSet().add(key, values);
  }

  /**
   *
   * @param key
   * @param values
   * @return
   */
  public Long zRemove(String key, Object... values) {
    return redisTemplate.opsForZSet().remove(key, values);
  }

  /**
   * 增加元素的score值奏纪,并返回增加后的值
   *
   * @param key
   * @param value
   * @param delta
   * @return
   */
  public Double zIncrementScore(String key, String value, double delta) {
    return redisTemplate.opsForZSet().incrementScore(key, value, delta);
  }

  /**
   * 返回元素在集合的排名,有序集合是按照元素的score值由小到大排列
   *
   * @param key
   * @param value
   * @return 0表示第一位
   */
  public Long zRank(String key, Object value) {
    return redisTemplate.opsForZSet().rank(key, value);
  }

  /**
   * 返回元素在集合的排名,按元素的score值由大到小排列
   *
   * @param key
   * @param value
   * @return
   */
  public Long zReverseRank(String key, Object value) {
    return redisTemplate.opsForZSet().reverseRank(key, value);
  }

  /**
   * 獲取集合的元素, 從小到大排序
   *
   * @param key
   * @param start
   *            開始位置
   * @param end
   *            結(jié)束位置, -1查詢所有
   * @return
   */
  public Set<String> zRange(String key, long start, long end) {
    return redisTemplate.opsForZSet().range(key, start, end);
  }

  /**
   * 獲取集合元素, 并且把score值也獲取
   *
   * @param key
   * @param start
   * @param end
   * @return
   */
  public Set<ZSetOperations.TypedTuple<String>> zRangeWithScores(String key, long start,
      long end) {
    return redisTemplate.opsForZSet().rangeWithScores(key, start, end);
  }

  /**
   * 根據(jù)Score值查詢集合元素
   *
   * @param key
   * @param min
   *            最小值
   * @param max
   *            最大值
   * @return
   */
  public Set<String> zRangeByScore(String key, double min, double max) {
    return redisTemplate.opsForZSet().rangeByScore(key, min, max);
  }

  /**
   * 根據(jù)Score值查詢集合元素, 從小到大排序
   *
   * @param key
   * @param min
   *            最小值
   * @param max
   *            最大值
   * @return
   */
  public Set<ZSetOperations.TypedTuple<String>> zRangeByScoreWithScores(String key,
      double min, double max) {
    return redisTemplate.opsForZSet().rangeByScoreWithScores(key, min, max);
  }

  /**
   *
   * @param key
   * @param min
   * @param max
   * @param start
   * @param end
   * @return
   */
  public Set<ZSetOperations.TypedTuple<String>> zRangeByScoreWithScores(String key,
      double min, double max, long start, long end) {
    return redisTemplate.opsForZSet().rangeByScoreWithScores(key, min, max,
        start, end);
  }

  /**
   * 獲取集合的元素, 從大到小排序
   *
   * @param key
   * @param start
   * @param end
   * @return
   */
  public Set<String> zReverseRange(String key, long start, long end) {
    return redisTemplate.opsForZSet().reverseRange(key, start, end);
  }

  /**
   * 獲取集合的元素, 從大到小排序, 并返回score值
   *
   * @param key
   * @param start
   * @param end
   * @return
   */
  public Set<ZSetOperations.TypedTuple<String>> zReverseRangeWithScores(String key,
      long start, long end) {
    return redisTemplate.opsForZSet().reverseRangeWithScores(key, start,
        end);
  }

  /**
   * 根據(jù)Score值查詢集合元素, 從大到小排序
   *
   * @param key
   * @param min
   * @param max
   * @return
   */
  public Set<String> zReverseRangeByScore(String key, double min,
      double max) {
    return redisTemplate.opsForZSet().reverseRangeByScore(key, min, max);
  }

  /**
   * 根據(jù)Score值查詢集合元素, 從大到小排序
   *
   * @param key
   * @param min
   * @param max
   * @return
   */
  public Set<ZSetOperations.TypedTuple<String>> zReverseRangeByScoreWithScores(
      String key, double min, double max) {
    return redisTemplate.opsForZSet().reverseRangeByScoreWithScores(key,
        min, max);
  }

  /**
   *
   * @param key
   * @param min
   * @param max
   * @param start
   * @param end
   * @return
   */
  public Set<String> zReverseRangeByScore(String key, double min,
      double max, long start, long end) {
    return redisTemplate.opsForZSet().reverseRangeByScore(key, min, max,
        start, end);
  }

  /**
   * 根據(jù)score值獲取集合元素?cái)?shù)量
   *
   * @param key
   * @param min
   * @param max
   * @return
   */
  public Long zCount(String key, double min, double max) {
    return redisTemplate.opsForZSet().count(key, min, max);
  }

  /**
   * 獲取集合大小
   *
   * @param key
   * @return
   */
  public Long zSize(String key) {
    return redisTemplate.opsForZSet().size(key);
  }

  /**
   * 獲取集合大小
   *
   * @param key
   * @return
   */
  public Long zZCard(String key) {
    return redisTemplate.opsForZSet().zCard(key);
  }

  /**
   * 獲取集合中value元素的score值
   *
   * @param key
   * @param value
   * @return
   */
  public Double zScore(String key, Object value) {
    return redisTemplate.opsForZSet().score(key, value);
  }

  /**
   * 移除指定索引位置的成員
   *
   * @param key
   * @param start
   * @param end
   * @return
   */
  public Long zRemoveRange(String key, long start, long end) {
    return redisTemplate.opsForZSet().removeRange(key, start, end);
  }

  /**
   * 根據(jù)指定的score值的范圍來移除成員
   *
   * @param key
   * @param min
   * @param max
   * @return
   */
  public Long zRemoveRangeByScore(String key, double min, double max) {
    return redisTemplate.opsForZSet().removeRangeByScore(key, min, max);
  }

  /**
   * 獲取key和otherKey的并集并存儲(chǔ)在destKey中
   *
   * @param key
   * @param otherKey
   * @param destKey
   * @return
   */
  public Long zUnionAndStore(String key, String otherKey, String destKey) {
    return redisTemplate.opsForZSet().unionAndStore(key, otherKey, destKey);
  }

  /**
   *
   * @param key
   * @param otherKeys
   * @param destKey
   * @return
   */
  public Long zUnionAndStore(String key, Collection<String> otherKeys,
      String destKey) {
    return redisTemplate.opsForZSet()
        .unionAndStore(key, otherKeys, destKey);
  }

  /**
   * 交集
   *
   * @param key
   * @param otherKey
   * @param destKey
   * @return
   */
  public Long zIntersectAndStore(String key, String otherKey,
      String destKey) {
    return redisTemplate.opsForZSet().intersectAndStore(key, otherKey,
        destKey);
  }

  /**
   * 交集
   *
   * @param key
   * @param otherKeys
   * @param destKey
   * @return
   */
  public Long zIntersectAndStore(String key, Collection<String> otherKeys,
      String destKey) {
    return redisTemplate.opsForZSet().intersectAndStore(key, otherKeys, destKey);
  }

  /**
   *
   * @param key
   * @param options
   * @return
   */
  public Cursor<TypedTuple<String>> zScan(String key, ScanOptions options) {
    return redisTemplate.opsForZSet().scan(key, options);
  }

}

對(duì)于鎖、分布式集合操作斩启、隊(duì)列等分布式對(duì)象或服務(wù)序调,使用Redisson
RedissonClient對(duì)鎖、集合兔簇、隊(duì)列等分布式操作進(jìn)行了封裝发绢,開發(fā)者只需要簡單的調(diào)用其接口進(jìn)行操作:

  • 使用Redisson實(shí)現(xiàn)Redis分布式鎖
    Redisson RLock實(shí)現(xiàn)了java.util.concurrent.locks.Lock接口,開發(fā)者可以顯示調(diào)用Lock接口進(jìn)行相關(guān)鎖操作
//根據(jù)key獲取分布式鎖對(duì)象-普通可重入鎖
    RLock lock = redissonClient.getLock("redisKey");
    //根據(jù)key獲取分布式鎖對(duì)象-公平可重入鎖
    RLock fairLock = redissonClient.getFairLock("redisKey");

    //可重入鎖
    //不指定時(shí)間垄琐,當(dāng)加鎖成功后會(huì)創(chuàng)建一個(gè)鎖監(jiān)聽看門狗的監(jiān)聽器边酒,
    // Redisson實(shí)例被關(guān)閉前,不斷的延長鎖的有效期此虑。
    // 默認(rèn)情況下甚纲,看門狗的檢查鎖的超時(shí)時(shí)間是30秒鐘
    //fairLock.lock();
    lock.lock();
    try {
     //...
    } finally {
      //fairLock.unlock();
      lock.unlock();  //需要顯示釋放鎖
    }

    //2. 顯示指定加鎖時(shí)間
    // 加鎖以后10秒鐘自動(dòng)解鎖
    // 無需調(diào)用unlock方法手動(dòng)解鎖
    //fairLock.lock(10, TimeUnit.SECONDS);
    lock.lock(10, TimeUnit.SECONDS);
    try {
      //...
    } finally {
      //fairLock.unlock();
      lock.unlock();  //需要顯示釋放鎖
    }

    //3. 嘗試獲取鎖
    // 嘗試加鎖,最多等待100秒朦前,上鎖以后10秒自動(dòng)解鎖
    boolean res = false;
    try {
      //res = fairLock.tryLock(100, 10, TimeUnit.SECONDS);
      res = lock.tryLock(100, 10, TimeUnit.SECONDS);
      if (res) {
        
      }
    } catch (InterruptedException e) {
      e.printStackTrace();
    } finally {
      if (res) {
        //fairLock.unlock();
        lock.unlock();  //需要顯示釋放鎖
      }
      
    }
  • 使用Redisson實(shí)現(xiàn)集合操作

@Autowired
private RedissonClient redissonClient;

//List操作
//RList Java對(duì)象在實(shí)現(xiàn)了java.util.List介杆,同時(shí)確保了插入順序
RList nameList = client.getList("redisKey");    
nameList.clear();   
nameList.add("bingo");  
nameList.add("yanglbme");   
nameList.add("https://github.com/redisson");    
nameList.remove(-1);    
boolean contains = nameList.contains("redisson");       
nameList.forEach(System.out::println);  

//Hash表
//RMap對(duì)象實(shí)現(xiàn)了`java.util.concurrent.ConcurrentMap`接口和`java.util.Map`接/口。與HashMap不同的是韭寸,RMap保持了元素的插入順序春哨。
RMap<String, SomeObject> map = redisson.getMap("redisKey");
SomeObject prevObject = map.put("redisson1", new SomeObject());
SomeObject currentObject = map.putIfAbsent("redisson2", new SomeObject());
SomeObject obj = map.remove("redisson1");
map.fastPut("redisson1", new SomeObject());
map.fastRemove("redisson2");
//鎖定map內(nèi)元素
RLock keyLock = map.getLock("mapKey");
keyLock.lock();
try {
   SomeObjectv = map.get("mapKey");
   // 其他業(yè)務(wù)邏輯
} finally {
   keyLock.unlock();
}
//讀寫鎖
RReadWriteLock rwLock = map.getReadWriteLock("mapKey");
rwLock.readLock().lock();
try {
   MyValue v = map.get("mapKey");
   // 其他業(yè)務(wù)邏輯
} finally {
   keyLock.readLock().unlock();
}       
  • 使用Redisson實(shí)現(xiàn)延遲隊(duì)列
//延遲隊(duì)列工具類
@Component
@Slf4j
public class RedisDelayedQueue<T> {

  @Autowired
  RedissonClient redissonClient;

  /**
   * 任務(wù)回調(diào)監(jiān)聽
   *
   * @param
   */
  public abstract static class TaskEventListener<T> {

      /**
       * 執(zhí)行方法
       *
       * @param t
       */
      public abstract void invoke(T t);
  }



  /**
   * 添加隊(duì)列
   *
   * @param t        DTO傳輸類
   * @param delay    時(shí)間數(shù)量
   * @param timeUnit 時(shí)間單位
   */
  public void addQueue(T t, long delay, TimeUnit timeUnit) {
    RBlockingQueue blockingFairQueue = redissonClient.getBlockingQueue(t.getClass().getName());
    RDelayedQueue delayedQueue = redissonClient.getDelayedQueue(blockingFairQueue);
    delayedQueue.offer(t, delay, timeUnit);
    delayedQueue.destroy();
  }

  /**
   * 獲取隊(duì)列
   *
   * @param zClass            DTO泛型
   * @param taskEventListener 任務(wù)回調(diào)監(jiān)聽
   * @return
   */
  public void getQueue(Class zClass, TaskEventListener taskEventListener) {
    RBlockingQueue blockingFairQueue = redissonClient.getBlockingQueue(zClass.getName());
    ((Runnable) () -> {
      while (true) {
        try {
          T t = (T) blockingFairQueue.take();
          taskEventListener.invoke(t);
        } catch (InterruptedException e) {
          log.error("獲取隊(duì)列異常,原因:{}", e);
        }
      }
    }).run();
  }
}
//添加延遲隊(duì)列監(jiān)聽
@Component
@Slf4j
public class NotifySubscribeListener implements CommandLineRunner {

  @Autowired
  private RedisDelayedQueue redisDelayedQueue;

  @Autowired
  private EmsEventQueueHelper emsEventQueueHelper;

  @Override
  public void run(String... args) throws Exception {

    //監(jiān)聽延遲隊(duì)列
    RedisDelayedQueue.TaskEventListener taskEventListener = new RedisDelayedQueue.TaskEventListener() {

      @Override
      public void invoke(Object o) {
        EmsEventInfo info = (EmsEventInfo) o;
        log.info("延遲隊(duì)列執(zhí)行任務(wù):{}", info.toString());
        emsEventQueueHelper.publishEvent(info);
      }

    };
    redisDelayedQueue.getQueue(EmsEventInfo.class, taskEventListener);
  }
}

//隊(duì)列入列
//省略......
    @Autowired
    private RedisDelayedQueue redisDelayedQueue;
//省略......
//入列
redisDelayedQueue.addQueue(info, emsTmpProperties.getNotifyTime(), TimeUnit.MINUTES);

更多用法參考Redisson官網(wǎng)https://github.com/redisson/redisson

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末恩伺,一起剝皮案震驚了整個(gè)濱河市赴背,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌晶渠,老刑警劉巖凰荚,帶你破解...
    沈念sama閱讀 218,640評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異褒脯,居然都是意外死亡便瑟,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,254評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門番川,熙熙樓的掌柜王于貴愁眉苦臉地迎上來到涂,“玉大人,你說我怎么就攤上這事颁督〖模” “怎么了?”我有些...
    開封第一講書人閱讀 165,011評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵沉御,是天一觀的道長屿讽。 經(jīng)常有香客問我,道長吠裆,這世上最難降的妖魔是什么聂儒? 我笑而不...
    開封第一講書人閱讀 58,755評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮硫痰,結(jié)果婚禮上衩婚,老公的妹妹穿的比我還像新娘。我一直安慰自己效斑,他們只是感情好非春,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,774評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著缓屠,像睡著了一般奇昙。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上敌完,一...
    開封第一講書人閱讀 51,610評(píng)論 1 305
  • 那天储耐,我揣著相機(jī)與錄音,去河邊找鬼滨溉。 笑死什湘,一個(gè)胖子當(dāng)著我的面吹牛长赞,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播闽撤,決...
    沈念sama閱讀 40,352評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼得哆,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了哟旗?” 一聲冷哼從身側(cè)響起贩据,我...
    開封第一講書人閱讀 39,257評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎闸餐,沒想到半個(gè)月后饱亮,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,717評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡舍沙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,894評(píng)論 3 336
  • 正文 我和宋清朗相戀三年近上,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片场勤。...
    茶點(diǎn)故事閱讀 40,021評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡戈锻,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出和媳,到底是詐尸還是另有隱情格遭,我是刑警寧澤,帶...
    沈念sama閱讀 35,735評(píng)論 5 346
  • 正文 年R本政府宣布留瞳,位于F島的核電站拒迅,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏她倘。R本人自食惡果不足惜璧微,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,354評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望硬梁。 院中可真熱鬧前硫,春花似錦、人聲如沸荧止。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,936評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽跃巡。三九已至危号,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間素邪,已是汗流浹背外莲。 一陣腳步聲響...
    開封第一講書人閱讀 33,054評(píng)論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留兔朦,地道東北人偷线。 一個(gè)月前我還...
    沈念sama閱讀 48,224評(píng)論 3 371
  • 正文 我出身青樓磨确,卻偏偏與公主長得像,于是被迫代替她去往敵國和親淋昭。 傳聞我的和親對(duì)象是個(gè)殘疾皇子俐填,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,974評(píng)論 2 355

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