Redis 实战篇——Redis 客户端(Jedis,Luttece,Redisson)

一、Jedis,Redisson,Lettuce三者的区别

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

不同点:

1.1、Jedis

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

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

1.2、Redisson

优点点:分布式锁,分布式集合,可通过Redis支持延迟队列。

1.3、 Lettuce

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

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

二、RedisTemplate

2.1、使用配置

maven配置引入,(要加上版本号,我这里是因为Parent已声明)

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

application-dev.yml

spring:
  redis:
    host: 192.168.1.140
    port: 6379
    password:
    database: 15 # 指定redis的分库(共16个0到15)

2.2、使用示例

 @Resource
 private StringRedisTemplate stringRedisTemplate;
 
    @Override
    public CustomersEntity findById(Integer id) {
        // 需要缓存
        // 所有涉及的缓存都需要删除,或者更新
        try {
            String toString = stringRedisTemplate.opsForHash().get(REDIS_CUSTOMERS_ONE, id + "").toString();
            if (toString != null) {
                return JSONUtil.toBean(toString, CustomersEntity.class);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 缓存为空的时候,先查,然后缓存redis
        Optional<CustomersEntity> byId = customerRepo.findById(id);
        if (byId.isPresent()) {
            CustomersEntity customersEntity = byId.get();
            try {
                stringRedisTemplate.opsForHash().put(REDIS_CUSTOMERS_ONE, id + "", JSONUtil.toJsonStr(customersEntity));
            } catch (Exception e) {
                e.printStackTrace();
            }
            return customersEntity;
        }
        return null;
    }

2.3、扩展

2.3.1、spring-boot-starter-data-redis的依赖包
3.3.2、stringRedisTemplate API(部分展示)
  • opsForHash --> hash操作
  • opsForList --> list操作
  • opsForSet --> set操作
  • opsForValue --> string操作
  • opsForZSet --> Zset操作
3.3.3 StringRedisTemplate默认序列化机制
public class StringRedisTemplate extends RedisTemplate<String, String> {

 /**
  * Constructs a new <code>StringRedisTemplate</code> instance. {@link #setConnectionFactory(RedisConnectionFactory)}
  * and {@link #afterPropertiesSet()} still need to be called.
  */
 public StringRedisTemplate() {
  RedisSerializer<String> stringSerializer = new StringRedisSerializer();
  setKeySerializer(stringSerializer);
  setValueSerializer(stringSerializer);
  setHashKeySerializer(stringSerializer);
  setHashValueSerializer(stringSerializer);
 }
 }

三、RedissonClient 操作示例

3.1 基本配置

3.1.1、Maven pom 引入
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.8.2</version>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>LATEST</version>
</dependency>
3.1.2、添加配置文件Yaml或者json格式

redisson-config.yml

# Redisson 配置
singleServerConfig:
  address: "redis://192.168.1.140:6379"
  password: null
  clientName: null
  database: 15 #选择使用哪个数据库0~15
  idleConnectionTimeout: 10000
  pingTimeout: 1000
  connectTimeout: 10000
  timeout: 3000
  retryAttempts: 3
  retryInterval: 1500
  reconnectionTimeout: 3000
  failedAttempts: 3
  subscriptionsPerConnection: 5
  subscriptionConnectionMinimumIdleSize: 1
  subscriptionConnectionPoolSize: 50
  connectionMinimumIdleSize: 32
  connectionPoolSize: 64
  dnsMonitoringInterval: 5000
  #dnsMonitoring: false

threads: 0
nettyThreads: 0
codec:
  class: "org.redisson.codec.JsonJacksonCodec"
transportMode: "NIO"

或者,配置 redisson-config.json

{
  "singleServerConfig": {
    "idleConnectionTimeout": 10000,
    "pingTimeout": 1000,
    "connectTimeout": 10000,
    "timeout": 3000,
    "retryAttempts": 3,
    "retryInterval": 1500,
    "reconnectionTimeout": 3000,
    "failedAttempts": 3,
    "password": null,
    "subscriptionsPerConnection": 5,
    "clientName": null,
    "address": "redis://192.168.1.140:6379",
    "subscriptionConnectionMinimumIdleSize": 1,
    "subscriptionConnectionPoolSize": 50,
    "connectionMinimumIdleSize": 10,
    "connectionPoolSize": 64,
    "database": 0,
    "dnsMonitoring": false,
    "dnsMonitoringInterval": 5000
  },
  "threads": 0,
  "nettyThreads": 0,
  "codec": null,
  "useLinuxNativeEpoll": false
}
3.1.3、读取配置

新建读取配置类

@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redisson() throws IOException {

        // 两种读取方式,Config.fromYAML 和 Config.fromJSON
//        Config config = Config.fromJSON(RedissonConfig.class.getClassLoader().getResource("redisson-config.json"));
        Config config = Config.fromYAML(RedissonConfig.class.getClassLoader().getResource("redisson-config.yml"));
        return Redisson.create(config);
    }
}

或者,在 application.yml中配置如下

spring:
  redis:
    redisson:
      config: classpath:redisson-config.yaml

3.2 使用示例


@RestController
@RequestMapping("/")
public class TeController {

    @Autowired
    private RedissonClient redissonClient;

    static long i = 20;
    static long sum = 300;

//    ========================== String =======================
    @GetMapping("/set/{key}")
    public String s1(@PathVariable String key) {
        // 设置字符串
        RBucket<String> keyObj = redissonClient.getBucket(key);
        keyObj.set(key + "1-v1");
        return key;
    }

    @GetMapping("/get/{key}")
    public String g1(@PathVariable String key) {
        // 设置字符串
        RBucket<String> keyObj = redissonClient.getBucket(key);
        String s = keyObj.get();
        return s;
    }

    //    ========================== hash =======================-=

    @GetMapping("/hset/{key}")
    public String h1(@PathVariable String key) {

        Ur ur = new Ur();
        ur.setId(MathUtil.randomLong(1,20));
        ur.setName(key);
      // 存放 Hash
        RMap<String, Ur> ss = redissonClient.getMap("UR");
        ss.put(ur.getId().toString(), ur);
        return ur.toString();
    }

    @GetMapping("/hget/{id}")
    public String h2(@PathVariable String id) {
        // hash 查询
        RMap<String, Ur> ss = redissonClient.getMap("UR");
        Ur ur = ss.get(id);
        return ur.toString();
    }

    // 查询所有的 keys
    @GetMapping("/all")
    public String all(){
        RKeys keys = redissonClient.getKeys();
        Iterable<String> keys1 = keys.getKeys();
        keys1.forEach(System.out::println);
        return keys.toString();
    }

    // ================== ==============读写锁测试 =============================

    @GetMapping("/rw/set/{key}")
    public void rw_set(){
//        RedissonLock.
        RBucket<String> ls_count = redissonClient.getBucket("LS_COUNT");
        ls_count.set("300",360000000l, TimeUnit.SECONDS);
    }

    // 减法运算
    @GetMapping("/jf")
    public void jf(){

        String key = "S_COUNT";

//        RAtomicLong atomicLong = redissonClient.getAtomicLong(key);
//        atomicLong.set(sum);
//        long l = atomicLong.decrementAndGet();
//        System.out.println(l);

        RAtomicLong atomicLong = redissonClient.getAtomicLong(key);
        if (!atomicLong.isExists()) {
            atomicLong.set(300l);
        }

        while (i == 0) {
            if (atomicLong.get() > 0) {
                long l = atomicLong.getAndDecrement();
                        try {
                            Thread.sleep(1000l);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                i --;
                System.out.println(Thread.currentThread().getName() + "->" + i + "->" + l);
            }
        }


    }

    @GetMapping("/rw/get")
    public String rw_get(){

        String key = "S_COUNT";
        Runnable r = new Runnable() {
            @Override
            public void run() {
                RAtomicLong atomicLong = redissonClient.getAtomicLong(key);
                if (!atomicLong.isExists()) {
                    atomicLong.set(300l);
                }
                if (atomicLong.get() > 0) {
                    long l = atomicLong.getAndDecrement();
                    i --;
                    System.out.println(Thread.currentThread().getName() + "->" + i + "->" + l);
                }
            }
        };

        while (i != 0) {
            new Thread(r).start();
//            new Thread(r).run();
//            new Thread(r).run();
//            new Thread(r).run();
//            new Thread(r).run();
        }


        RBucket<String> bucket = redissonClient.getBucket(key);
        String s = bucket.get();
        System.out.println("================线程已结束================================" + s);

        return s;
    }

}

4.3 扩展

4.3.1 丰富的jar支持,尤其是对 Netty NIO框架

4.3.2 丰富的配置机制选择,这里是详细的配置说明

https://github.com/redisson/redisson/wiki/2.-Configuration

关于序列化机制中,就有很多

4.3.3 API支持(部分展示),具体的 Redis --> RedissonClient ,可查看这里

https://github.com/redisson/redisson/wiki/11.-Redis-commands-mapping

4.3.4 轻便的丰富的锁机制的实现

  • Lock
  • Fair Lock
  • MultiLock
  • RedLock
  • ReadWriteLock
  • Semaphore
  • PermitExpirableSemaphore
  • CountDownLatch

四、基于注解实现的Redis缓存

4.1 Maven 和 YML配置

参考 RedisTemplate 配置。另外推荐:Java进阶视频资源

另外,还需要额外的配置类

// todo 定义序列化,解决乱码问题
@EnableCaching
@Configuration
@ConfigurationProperties(prefix = "spring.cache.redis")
public class RedisCacheConfig {

    private Duration timeToLive = Duration.ZERO;

    public void setTimeToLive(Duration timeToLive) {
        this.timeToLive = timeToLive;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        // 解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        // 配置序列化(解决乱码的问题)
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(timeToLive)
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();

        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }

}

4.2 使用示例

@Transactional
@Service
public class ReImpl implements RedisService {

    @Resource
    private CustomerRepo customerRepo;
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    public static final String REDIS_CUSTOMERS_ONE = "Customers";

    public static final String REDIS_CUSTOMERS_ALL = "allList";

    // =====================================================================使用Spring cahce 注解方式实现缓存
    // ==================================单个操作

    @Override
    @Cacheable(value = "cache:customer", unless = "null == #result",key = "#id")
    public CustomersEntity cacheOne(Integer id) {
        final Optional<CustomersEntity> byId = customerRepo.findById(id);
        return byId.isPresent() ? byId.get() : null;
    }

    @Override
    @Cacheable(value = "cache:customer", unless = "null == #result", key = "#id")
    public CustomersEntity cacheOne2(Integer id) {
        final Optional<CustomersEntity> byId = customerRepo.findById(id);
        return byId.isPresent() ? byId.get() : null;
    }

     // todo 自定义redis缓存的key,
    @Override
    @Cacheable(value = "cache:customer", unless = "null == #result", key = "#root.methodName + '.' + #id")
    public CustomersEntity cacheOne3(Integer id) {
        final Optional<CustomersEntity> byId = customerRepo.findById(id);
        return byId.isPresent() ? byId.get() : null;
    }

    // todo 这里缓存到redis,还有响应页面是String(加了很多转义符\,),不是Json格式
    @Override
    @Cacheable(value = "cache:customer", unless = "null == #result", key = "#root.methodName + '.' + #id")
    public String cacheOne4(Integer id) {
        final Optional<CustomersEntity> byId = customerRepo.findById(id);
        return byId.map(JSONUtil::toJsonStr).orElse(null);
    }

     // todo 缓存json,不乱码已处理好,调整序列化和反序列化
    @Override
    @Cacheable(value = "cache:customer", unless = "null == #result", key = "#root.methodName + '.' + #id")
    public CustomersEntity cacheOne5(Integer id) {
        Optional<CustomersEntity> byId = customerRepo.findById(id);
        return byId.filter(obj -> !StrUtil.isBlankIfStr(obj)).orElse(null);
    }



    // ==================================删除缓存
    @Override
    @CacheEvict(value = "cache:customer", key = "'cacheOne5' + '.' + #id")
    public Object del(Integer id) {
        // 删除缓存后的逻辑
        return null;
    }

    @Override
    @CacheEvict(value = "cache:customer",allEntries = true)
    public void del() {

    }

    @CacheEvict(value = "cache:all",allEntries = true)
    public void delall() {

    }
    // ==================List操作

    @Override
    @Cacheable(value = "cache:all")
    public List<CustomersEntity> cacheList() {
        List<CustomersEntity> all = customerRepo.findAll();
        return all;
    }

    // todo 先查询缓存,再校验是否一致,然后更新操作,比较实用,要清楚缓存的数据格式(明确业务和缓存模型数据)
    @Override
    @CachePut(value = "cache:all",unless = "null == #result",key = "#root.methodName")
    public List<CustomersEntity> cacheList2() {
        List<CustomersEntity> all = customerRepo.findAll();
        return all;
    }

}

4.3 扩展

基于spring缓存实现

------------------------------------------------------------------------------------------------------------------------------------------------------------------------

官网推荐的 Java 客户端有 3 个 Jedis,Redisson 和 Luttuce。

Spring 连接 Redis 用的是什么?

RedisConnectionFactory 接口支持多种实现,例如: JedisConnectionFactory 、 JredisConnectionFactory、LettuceConnectionFactory、SrpConnectionFactory。

Jedis

https://github.com/xetorthio/jedis

Jedis 是我们最熟悉和最常用的客户端。轻量,简洁,便于集成和改造。

BasicTest.java

public static void main(String[] args) {
	Jedis jedis = new Jedis("127.0.0.1", 6379);
	jedis.set("ck", "2673");
	System.out.println(jedis.get("ck"));
	jedis.close();
}

Jedis 多个线程使用一个连接的时候线程不安全。可以使用连接池,为每个请求创建不同的连接,基于 Apache common pool 实现。跟数据库一样,可以设置最大连接数等参数。
Jedis 中有多种连接池的子类:

 

 

 例如:ShardingTest.java

public static void main(String[] args) {
	JedisPool pool = new JedisPool(ip, port);
	Jedis jedis = jedisPool.getResource();
	//
}

Jedis 有 4 种工作模式:单节点、分片、哨兵、集群。

3 种请求模式:Client、Pipeline、事务。Client 模式就是客户端发送一个命令,阻塞等待服务端执行,然后读取 返回结果。Pipeline 模式是一次性发送多个命令,最后一次取回所有的返回结果,这种模式通过减少网络的往返时间和 io 读写次数,大幅度提高通信性能。第三种是事务模式。Transaction 模式即开启 Redis 的事务管理,事务模式开启后,所有的命令(除了 exec,discard,multi 和 watch)到达服务端以后不会立即执行,会进入一个等待队列。

Sentinel 获取连接原理
问题:Jedis 连接 Sentinel 的时候,我们配置的是全部哨兵的地址。Sentinel 是如何返回可用的 master 地址的呢?

在构造方法中:

pool = new JedisSentinelPool(masterName, sentinels);
1
调用了:

HostAndPort master = initSentinels(sentinels, masterName);
查看:

private HostAndPort initSentinels(Set<String> sentinels, final String masterName) {
	HostAndPort master = null;
	boolean sentinelAvailable = false;
	log.info("Trying to find master from available Sentinels...");
	
	//有多个 sentinels,遍历这些个 sentinels 
	for (String sentinel : sentinels) {
		//host:port 表示的 sentinel 地址转化为一个 HostAndPort 对象。 
		final HostAndPort hap = HostAndPort.parseString(sentinel);
		log.fine("Connecting to Sentinel " + hap);
		Jedis jedis = null;
		try {
			// 连接到 sentinel
			jedis = new Jedis(hap.getHost(), hap.getPort());
			//根据 masterName 得到 master 的地址,返回一个 list,
			host= list[0], port =// list[1] 
			List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);
			//connected to sentinel...
			sentinelAvailable = true;
			if (masterAddr == null || masterAddr.size() != 2) {
				log.warning("Can not get master addr, master name: " + masterName + ". Sentinel: " + hap +"."); continue;
			}
			//如果在任何一个 sentinel 中找到了 master,不再遍历 
			sentinels master = toHostAndPort(masterAddr);
			log.fine("Found Redis master at " + master); break;
		} catch (JedisException e) {
			//resolves #1036, it should handle JedisException there's another chance
			//of raising JedisDataException
			log.warning("Cannot get master address from sentinel running @ " + hap + ". Reason: " + e +". Trying next one.");
		 } finally {
			if (jedis != null) {
				jedis.close();
			}
		}
	}

	//到这里,如果 master 为 null,则说明有两种情况,一种是所有的 sentinels 节点都 down 掉了,一种是 master 节点没有被存活的 sentinels 监控到
	if (master == null) {
		if (sentinelAvailable) {
			//can connect to sentinel, but master name seems to not
			//monitored
			throw new JedisException("Can connect to sentinel, but " + masterName+" seems to be not monitored...");
		} else {
			throw new JedisConnectionException("All sentinels down, cannot determine where is "+masterName + " master is running...");
		}
	}

	//如果走到这里,说明找到了 master 的地址
	log.info("Redis master running at " + master + ", starting Sentinel listeners...");

	//启动对每个 sentinels 的监听为每个 sentinel 都启动了一个监听者 MasterListener。MasterListener 本身是一个线程,它会去订阅 sentinel 上关于 master 节点地址改变的消息。
	for (String sentinel : sentinels) {
		final HostAndPort hap = HostAndPort.parseString(sentinel);
		MasterListener masterListener = new MasterListener(masterName, hap.getHost(), hap.getPort());
		//whether MasterListener threads are alive or not, process can be stopped 
		masterListener.setDaemon(true); 
		masterListeners.add(masterListener);
		masterListener.start();
	}
	
	return master;

}

Cluster 获取连接原理
问题:使用 Jedis 连接 Cluster 的时候,我们只需要连接到任意一个或者多个 redis group 中的实例地址,那我们是怎么获取到需要操作的 Redis Master 实例的?

关键问题:在于如何存储 slot 和 Redis 连接池的关系。

1、程序启动初始化集群环境,读取配置文件中的节点配置,无论是主从,无论多少个,只拿第一个,获取 redis 连接实例(后面有个 break)。

// redis.clients.jedis.JedisClusterConnectionHandler#initializeSlotsCache
private void initializeSlotsCache(Set<HostAndPort> startNodes, GenericObjectPoolConfig poolConfig, String password){
	for (HostAndPort hostAndPort : startNodes) {
		//获取一个 Jedis 实例
		Jedis jedis = new Jedis(hostAndPort.getHost(), hostAndPort.getPort()); 
		if (password != null) {
			jedis.auth(password);
		}
		try {
			// 获取 Redis 节点和 Slot 虚拟槽
			cache.discoverClusterNodesAndSlots(jedis);
			//直接跳出循环 break;
		} catch (JedisConnectionException e) {
			//try next nodes 
		} finally {
			if (jedis != null) {
				jedis.close();
			}
		}
}

2、用获取的 redis 连接实例执行 clusterSlots ()方法,实际执行 redis 服务端 cluster slots 命令,获取虚拟槽信息。

该集合的基本信息为[long, long, List, List], 第一,二个元素是该节点负责槽点的起始位置,第三个元素是主节点信息,第四个元素为主节点对应的从节点信息。

该 list 的基本信息为[string,int,string],第一个为 host 信息,第二个为 port 信息,第三个为唯一id。

3、获取有关节点的槽点信息后,调用 getAssignedSlotArray(slotinfo)来获取所有的槽点值。

4、再获取主节点的地址信息,调用 generateHostAndPort(hostInfo)方法,生成一个 ostAndPort 对象。

5 、再根据节点地址信息来设置节点对应的 JedisPool ,即设置 Map<String, JedisPool> nodes 的值。

接下来判断若此时节点信息为主节点信息时,则调用 assignSlotsToNodes 方法,设置每个槽点值对应的连接池,即设置 Map<Integer, JedisPool> slots 的值。

//redis.clients.jedis.JedisClusterInfoCache#discoverClusterNodesAndSlots 
public void discoverClusterNodesAndSlots(Jedis jedis) {
	w.lock();
	try { 
		reset();
		//获取节点集合
		List<Object> slots = jedis.clusterSlots();
		// 遍历 3 个 master 节点
		for (Object slotInfoObj : slots) {
			//slotInfo 槽开始,槽结束,主,从
			//{[0,5460,7291,7294],[5461,10922,7292,7295],[10923,16383,7293,7296]} 
			List<Object> slotInfo = (List<Object>) slotInfoObj;
			//如果<=2,代表没有分配 slot
			if (slotInfo.size() <= MASTER_NODE_INDEX) {
				continue;
			}
			//获取分配到当前 master 节点的数据槽,例如 7291 节点的{0,1,2,3……5460} 
			List<Integer> slotNums = getAssignedSlotArray(slotInfo);
			// hostInfos
			int size = slotInfo.size(); // size 是 4,槽最小最大,主,从
			//第 3 位和第 4 位是主从端口的信息
			for (int i = MASTER_NODE_INDEX; i < size; i++) {
				List<Object> hostInfos = (List<Object>) slotInfo.get(i);
				if (hostInfos.size() <= 0) {
					continue;
				}
				// 根据 IP 端口生成 HostAndPort 实例
				HostAndPort targetNode = generateHostAndPort(hostInfos);
				//据 HostAndPort 解析出 ip:port 的 key 值,再根据 key 从缓存中查询对应的 jedisPool 实例。如果没有 jedisPool 实例,就创建 JedisPool 实例,最后放入缓存中。nodeKey 和 nodePool 的关系
				setupNodeIfNotExist(targetNode);
				//把 slot 和 jedisPool 缓存起来(16384 个),key 是 slot 下标,value 是连接池
				if (i == MASTER_NODE_INDEX) {
					assignSlotsToNode(slotNums, targetNode);
				}
			}
		}
	} finally {
		w.unlock();
	}
}

从集群环境存取值:

把 key 作为参数,执行 CRC16 算法,获取 key 对应的 slot 值。
通过该 slot 值,去 slots 的 map 集合中获取 jedisPool 实例。
通过 jedisPool 实例获取 jedis 实例,最终完成 redis 数据存取工作。
pipeline
前面我们实操的时候, 看到 set 2 万个 key 用了好几分钟,这个速度太慢了,完全没有把 Redis 10 万的 QPS 利用起来。但是单个命令的执行到底慢在哪里?

Redis 使用的是客户端/服务器(C/S)模型和请求/响应协议的 TCP 服务器。这意味着通常情况下一个请求会遵循以下步骤:

客户端向服务端发送一个查询请求,并监听 Socket 返回,通常是以阻塞模式,等待服务端响应。
服务端处理命令,并将结果返回给客户端。
Redis 客户端与 Redis 服务器之间使用 TCP 协议进行连接,一个客户端可以通过一个 socket 连接发起多个请求命令。每个请求命令发出后 client 通常会阻塞并等待 redis 服务器处理,redis 处理完请求命令后会将结果通过响应报文返回给 client,因此当执行多条命令的时候都需要等待上一条命令执行完毕才能执行。

执行过程如图:

 

 

 

Redis 本身提供了一些批量操作命令,比如 mget,mset,可以减少通信的时间,但是大部分命令是不支持 multi 操作的,例如 hash 就没有。

由于通信会有网络延迟,假如 client 和 server 之间的包传输时间需要 10 毫秒,一次交互就是 20 毫秒(RTT:Round Trip Time)。这样的话,client 1 秒钟也只能也只能发送 50 个命令。这显然没有充分利用 Redis 的处理能力。另外一个,Redis 服务端执行 I/O 的次数过多也会有影响。

Pipeline 管道

https://redis.io/topics/pipelining

那我们能不能像数据库的 batch 操作一样,把一组命令组装在一起发送给 Redis 服务端执行,然后一次性获得返回结果呢?这个就是 Pipeline 的作用。Pipeline 通过一个队列把所有的命令缓存起来,然后把多个命令在一次连接中发送给服务器。

先来看一下效果(先 flushall):

PipelineSet.java,PipelineGet.java

要实现 Pipeline,既要服务端的支持,也要客户端的支持。对于服务端来说,需要能够处理客户端通过一个 TCP 连接发来的多个命令,并且逐个地执行命令一起返回 。
对于客户端来说,要把多个命令缓存起来,达到一定的条件就发送出去,最后才处理 Redis 的应答(这里也要注意对客户端内存的消耗)。

jedis-pipeline 的 client-buffer 限制:8192bytes,客户端堆积的命令超过 8192 bytes 时,会发送给服务端。

源码:redis.clients.util.RedisOutputStream.java

public RedisOutputStream(final OutputStream out) {
	this(out, 8192);
}

pipeline 对于命令条数没有限制,但是命令可能会受限于 TCP 包大小。

如果 Jedis 发送了一组命令,而发送请求还没有结束,Redis 响应的结果会放在接收缓冲区。如果接收缓冲区满了,jedis 会通知 redis win=0,此时 redis 不会再发送结果给 jedis 端,转而把响应结果保存在 Redis 服务端的输出缓冲区中。

输出缓冲区的配置:redis.conf

client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60

每个客户端使用的输出缓冲区的大小可以用 client list 命令查看

redis> client list
id=5 addr=192.168.8.1:10859 fd=8 name= age=5 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=5 qbuf-free=32763 obl=16380 oll=227 omem=4654408 events=rw cmd=set
1
2
obl : 输出缓冲区的长度(字节为单位, 0 表示没有分配输出缓冲区)

oll : 输出列表包含的对象数量(当输出缓冲区没有剩余空间时,命令回复会以字符串对象的形式被入队到这个队列里)

omem : 输出缓冲区和输出列表占用的内存总量

使用场景

Pipeline 适用于什么场景呢?
如果某些操作需要马上得到 Redis 操作是否成功的结果,这种场景就不适合。
有些场景,例如批量写入数据,对于结果的实时性和成功性要求不高,就可以用Pipeline。
————————————————

Jedis 实现分布式锁
原文地址:https://redis.io/topics/distlock

中文地址:http://redis.cn/topics/distlock.html

分布式锁的基本特性或者要求:

互斥性:只有一个客户端能够持有锁。

不会产生死锁:即使持有锁的客户端崩溃,也能保证后续其他客户端可以获取锁。

只有持有这把锁的客户端才能解锁。

distlock.DistLock.java

public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) { 
	// set 支持多个参数 NX(not exist) XX(exist) EX(seconds) PX(million seconds)
	String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); 
	if (LOCK_SUCCESS.equals(result)) {
		return true;
	}
	return false;
}

参数解读:

lockKey 是 Redis key 的名称,也就是谁添加成功这个 key 代表谁获取锁成功。

requestId 是客户端的 ID(设置成 value),如果我们要保证只有加锁的客户端才能释放锁,就必须获得客户端的 ID(保证第 3 点)。

SET_IF_NOT_EXIST 是我们的命令里面加上 NX(保证第 1 点)。

SET_WITH_EXPIRE_TIME,PX 代表以毫秒为单位设置 key 的过期时间(保证第 2 点)。expireTime 是自动释放锁的时间,比如 5000 代表 5 秒。

释放锁,直接删除 key 来释放锁可以吗?就像这样:

public static void wrongReleaseLock1(Jedis jedis, String lockKey) {
jedis.del(lockKey);
}

没有对客户端 requestId 进行判断,可能会释放其他客户端持有的锁。先判断后删除呢?

public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {
	//判断加锁与解锁是不是同一个客户端 
	if (requestId.equals(jedis.get(lockKey))) {
		//若在此时,这把锁突然不是这个客户端的,则会误解锁 
		jedis.del(lockKey);
	}
}

如果在释放锁的时候,这把锁已经不属于这个客户端(例如已经过期,并且被别的客户端获取锁成功了),那就会出现释放了其他客户端的锁的情况。

所以我们把判断客户端是否相等和删除 key 的操作放在 Lua 脚本里面执行。

public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
	String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; 
	Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
	if (RELEASE_SUCCESS.equals(result)) {
		return true;
	}
	return false;
}

这个是 Jedis 里面分布式锁的实现。

我们了解了一下 Jedis 的使用和实现,在 ck-jedis 里面还有很多其他的案例,大家可以再去学习一下。

Luttece
https://lettuce.io/

特点
与 Jedis 相比,Lettuce 则完全克服了其线程不安全的缺点:Lettuce 是一个可伸缩的线程安全的 Redis 客户端,支持同步、异步和响应式模式(Reactive)。多个线程可以共享一个连接实例,而不必担心多线程并发问题。

同步调用

异步的结果使用 RedisFuture 包装,提供了大量回调的方法。

异步调用

它基于 Netty 框架构建,支持 Redis 的高级功能,如 Pipeline、发布订阅,事务、Sentinel,集群,支持连接池。
Lettuce 是 Spring Boot 2.x 默认的客户端,替换了 Jedis。集成之后我们不需要单独使用它,直接调用 Spring 的 RedisTemplate 操作,连接和创建和关闭也不需要我们操心。

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

Redisson
https://redisson.org/

https://github.com/redisson/redisson/wiki/目录

本质
Redisson 是一个在 Redis 的基础上实现的 Java 驻内存数据网格(In-Memory Data Grid),提供了分布式和可扩展的 Java 数据结构。

特点
基于 Netty 实现,采用非阻塞 IO,性能高

支持异步请求

支持连接池、pipeline、LUA Scripting、Redis Sentinel、Redis Cluster 不支持事务,官方建议以 LUA Scripting 代替事务

主从、哨兵、集群都支持。Spring 也可以配置和注入 RedissonClient。

实现分布式锁
在 Redisson 里面提供了更加简单的分布式锁的实现。

加锁:

public static void main(String[] args) throws InterruptedException { 
	RLock rLock=redissonClient.getLock("updateAccount");
	//最多等待 100 秒、上锁 10s 以后自动解锁
	if(rLock.tryLock(100,10, TimeUnit.SECONDS)){
	System.out.println("获取锁成功");
	}
	//do something 
	rLock.unlock();
}

在获得 RLock 之后,只需要一个 tryLock 方法,里面有 3 个参数:
1、watiTime:获取锁的最大等待时间,超过这个时间不再尝试获取锁
2、leaseTime:如果没有调用 unlock,超过了这个时间会自动释放锁
3、TimeUnit:释放时间的单位

Redisson 的分布式锁是怎么实现的呢?

在加锁的时候,在 Redis 写入了一个 HASH,key 是锁名称,field 是线程名称,value是 1(表示锁的重入次数)。

源码:

tryLock()——tryAcquire()——tryAcquireAsync()——tryLockInnerAsync()

最终也是调用了一段 Lua 脚本。里面有一个参数,两个参数的值。

//KEYS[1] 锁名称 updateAccount
//ARGV[1] key 过期时间 10000ms
//ARGV[2] 线程名称
//锁名称不存在
if (redis.call('exists', KEYS[1]) == 0) then
	//创建一个 hash,key=锁名称,field=线程名,value=1 
	redis.call('hset', KEYS[1], ARGV[2], 1);
	//设置 hash 的过期时间
	redis.call('pexpire', KEYS[1], ARGV[1]);
	return nil;
end;
//锁名称存在,判断是否当前线程持有的锁
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
	//如果是,value+1,代表重入次数+1 
	redis.call('hincrby', KEYS[1], ARGV[2], 1);
	//重新获得锁,需要重新设置 Key 的过期时间 
	redis.call('pexpire', KEYS[1], ARGV[1]);
	return nil;
end;
//锁存在,但是不是当前线程持有,返回过期时间(毫秒) 
return redis.call('pttl', KEYS[1]);

释放锁,源码:

unlock——unlockInnerAsync

//KEYS[1] 锁的名称 updateAccount
//KEYS[2] 频道名称 redisson_lock__channel:{updateAccount}
//ARGV[1] 释放锁的消息 0
//ARGV[2] 锁释放时间 10000
//ARGV[3] 线程名称
//锁不存在(过期或者已经释放了)
if (redis.call('exists', KEYS[1]) == 0) then
	//发布锁已经释放的消息
	redis.call('publish', KEYS[2], ARGV[1]);
	return 1;
end;
//锁存在,但是不是当前线程加的锁
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
	return nil;
end;
//锁存在,是当前线程加的锁
//重入次数-1
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
//-1 后大于 0,说明这个线程持有这把锁还有其他的任务需要执行
if (counter > 0) then
	//重新设置锁的过期时间
	redis.call('pexpire', KEYS[1], ARGV[2]);
	return 0;
else
	//-1 之后等于 0,现在可以删除锁了 
	redis.call('del', KEYS[1]);
	//删除之后发布释放锁的消息
	redis.call('publish', KEYS[2], ARGV[1]);
	return 1;
end;
//其他情况返回 nil 
return nil;

这个是 Redisson 里面分布式锁的实现,我们在调用的时候非常简单。

Redisson 跟 Jedis 定位不同,它不是一个单纯的 Redis 客户端,而是基于 Redis 实现的分布式的服务,如果有需要用到一些分布式的数据结构,比如我们还可以基于Redisson 的分布式队列实现分布式事务,就可以引入 Redisson 的依赖实现。


————————————————

自定义Redis客户端

下面我们通过抓包相关的命令,了解Redis客户端的工作机制。

定义常量池。

public class CommandConstant {
    /**
     * 开始符
     */
    public static final String START = "*";
    /**
     * 指令长度符
     */
    public static final String LENGTH = "$";
    /**
     * 换行符
     */
    public static final String LINE = "\r\n";
    public enum CommandEnum {
        SET,
        GET,
        INCR
    }
}

CustomClientSocket

CustomClientSocket用来建立网络通信连接,并且发送数据指定到RedisServer。

public class CustomClientSocket {
    private Socket socket;
    private InputStream inputStream;
    private OutputStream outputStream;
 
    public CustomClientSocket(String ip,int port) {
        try {
            socket=new Socket(ip,port);
            inputStream=socket.getInputStream();
            outputStream=socket.getOutputStream();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public void send(String cmd){
        try {
            outputStream.write(cmd.getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public String read(){
        byte[] bytes = new byte[1024];
        int count = 0;
        try {
            count = inputStream.read(bytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new String(bytes, 0, count);
    }
}

封装客户端

public class CustomRedisClient {
 
    private CustomClientSocket customClientSocket;
 
    public CustomRedisClient(String host,int port) {
        customClientSocket=new CustomClientSocket(host,port);
    }
    public String set(String key, String value) {
        customClientSocket.send(convertToCommand(CommandConstant.CommandEnum.SET, key.getBytes(), value.getBytes()));
        return customClientSocket.read();
    }
 
    public String get(String key) {
        customClientSocket.send(convertToCommand(CommandConstant.CommandEnum.GET, key.getBytes()));
        return customClientSocket.read();
    }
 
    public static String convertToCommand(CommandConstant.CommandEnum command, byte[]... bytes) {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(CommandConstant.START).append(bytes.length + 1).append(CommandConstant.LINE);
        stringBuilder.append(CommandConstant.LENGTH).append(command.toString().length()).append(CommandConstant.LINE);
        stringBuilder.append(command.toString()).append(CommandConstant.LINE);
 
        for (byte[] aByte : bytes) {
            stringBuilder.append(CommandConstant.LENGTH).append(aByte.length).append(CommandConstant.LINE);
            stringBuilder.append(new String(aByte)).append(CommandConstant.LINE);
        }
        return stringBuilder.toString();
    }
}

测试方法

public static void main(String[] args) {
    CustomRedisClient redisClient=new CustomRedisClient("192.168.221.128",6379);
    System.out.println(redisClient.set("name","mic"));
    System.out.println(redisClient.get("name"));
}

总结

你看,理解了原理之后,自己去实现起来发现并不难。

但是实际开发过程中,我们难倒也需要开发自己开发客户端吗?当然不用,官方推荐了以下三种客户端

配置 作用
Jedis A blazingly small and sane redis java client
lettuce Advanced Redis client for thread-safe sync, async, and reactive usage. Supports Cluster, Sentinel, Pipelining, and codecs.
Redisson distributed and scalable Java data structures on top of Redis server
Jedis
Jedis是我们最熟悉和最常用的客户端。轻量,简洁,便于集成和改造。

简单使用方法

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.6.1</version>
</dependency>

public static void main(String[] args) {
    Jedis jedis = new Jedis("127.0.0.1", 6379);
    jedis.set("qingshan", "2673");
    System.out.println(jedis.get("qingshan"));
    jedis.close();
}

public static void main(String[] args) {
    JedisPool pool = new JedisPool(ip, port);
    Jedis jedis = jedisPool.getResource();
}

Luttece
Lettuce是一个Redis的Java驱动包,大家常用的spring-boot-starter-data-redis中默认就采用的Lettuce。Lettuce是一个高性能基于Java编写的Redis驱动框架,底层集成了Project Reactor提供天然的反应式编程,通信框架集成了Netty使用了非阻塞IO,5.x版本之后融合了JDK1.8的异步编程特性,在保证高性能的同时提供了十分丰富易用的API。

简单使用方法

Lettuce使用的时候依赖于四个主要组件:

RedisURI:连接信息。
RedisClient:Redis客户端,特殊地,集群连接有一个定制的RedisClusterClient。
Connection:Redis连接,主要是StatefulConnection或者StatefulRedisConnection的子类,连接的类型主要由连接的具体方式(单机、哨兵、集群、订阅发布等等)选定,比较重要。
RedisCommands:Redis命令API接口,基本上覆盖了Redis发行版本的所有命令,提供了同步(sync)、异步(async)、反应式(reative)的调用方式,对于使用者而言,会经常跟RedisCommands系列接口打交道。

<dependency>
    <groupId>io.lettuce</groupId>
    <artifactId>lettuce-core</artifactId>
    <version>5.1.8.RELEASE</version>
</dependency>

public static void main(String[] args) {
        RedisURI redisUri = RedisURI.builder()                    // <1> 创建单机连接的连接信息
                .withHost("192.168.221.128")
                .withPort(6379)
                .withTimeout(Duration.of(10, ChronoUnit.SECONDS))
                .build();
        RedisClient redisClient = RedisClient.create(redisUri);   // <2> 创建客户端
        StatefulRedisConnection<String, String> connection = redisClient.connect();     // <3> 创建线程安全的连接
        RedisCommands<String, String> redisCommands = connection.sync();                // <4> 创建同步命令
        SetArgs setArgs = SetArgs.Builder.nx().ex(5);
        String result = redisCommands.set("name", "throwable", setArgs);
        System.out.println(result);
        result = redisCommands.get("name");
        System.out.println(result);
        // ... 其他操作
        connection.close();   // <5> 关闭连接
        redisClient.shutdown();  // <6> 关闭客户端
    }

和Spring Boot集成使用

Lettuce是Spring Boot 2.x 默认的客户端,替换了Jedis。集成之后我们不需要单独使用它,直接调用Spring的RedisTemplate操作,连接和创建和关闭也不需要我们操心。

引入依赖jar包

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

application.yml配置文件如下

redis:
    port: 6379
    host: 192.168.221.128
    lettuce:
      pool:
        max-active: -1
        max-idle: 2000
        max-wait: -1
        min-idle: 1
        time-between-eviction-runs: 5000
使用方法
@RestController
@RequestMapping("/")
public class LutteceController {
    @Autowired
    RedisTemplate redisTemplate;
 
    @GetMapping
    public ResponseEntity get(){
        String name=(String)redisTemplate.opsForValue().get("name");
        return ResponseEntity.ok(name);
    }
}

Redisson
Redisson: Redis Java client with features of In-Memory Data Grid

https://github.com/redisson/redisson/wiki/目录

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(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的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

简单使用方法

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.16.0</version>
</dependency>

时间单节点连接和操作

public static void main(String[] args) {
    Config config=new Config();
    config.useSingleServer().setAddress("redis://192.168.221.128:6379");
    RedissonClient redissonClient= Redisson.create(config);
    redissonClient.getBucket("test").set("mic");
    System.out.println(redissonClient.getBucket("test").get());
}

和Spring Boot集成

Spring Boot的集成方式。

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.16.0</version>
</dependency>

application.yml中的配置。

spring:
  redis:
    timeout: 2000
    host: 192.168.221.128
    port: 6379
使用方法。

@RestController
public class RedissonController {
    @Autowired
    RedissonClient redissonClient;
 
    @GetMapping("/")
    public String get(){
        return redissonClient.getBucket("test").get().toString();
    }
}

另外一种配置方式如下

修改application.yml

spring:
  redis:
    redisson:
      file: classpath:redisson.yml

创建一个redisson.yml文件,内容如下

singleServerConfig:
  address: redis://192.168.221.128:6379
  #---------------------------------------------
  # 连接空闲超时,单位:毫秒
  idleConnectionTimeout: 10000
  # 连接超时,单位:毫秒
  connectTimeout: 10000
  # 命令等待超时,单位:毫秒
  timeout: 3000
  # 命令失败重试次数,如果尝试达到 retryAttempts(命令失败重试次数) 仍然不能将命令发送至某个指定的节点时,将抛出错误。
  # 如果尝试在此限制之内发送成功,则开始启用 timeout(命令等待超时) 计时。
  retryAttempts: 3
  # 命令重试发送时间间隔,单位:毫秒
  retryInterval: 1500

  

 点击查看代码

@Component
public class RedisUtils {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 根据key 获取过期时间
     *
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    /**
     * 判断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 可以传一个值 或多个
     */
    @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));
            }
        }
    }
    // ============================String=============================

    /**
     * 普通缓存获取
     *
     * @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;
        }
    }

    /**
     * 普通缓存放入并设置时间
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @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;
        }
    }

    /**
     * 递增
     *
     * @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 项 不能为null
     * @return 值
     */
    public Object hget(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }

    /**
     * 获取hashKey对应的所有键值
     *
     * @param key 键
     * @return 对应的多个键值
     */
    public Map<Object, Object> hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * HashSet
     *
     * @param key 键
     * @param map 对应多个键值
     * @return true 成功 false 失败
     */
    public boolean hmset(String key, Map<String, Object> map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 指定缓存失效时间
     *
     * @param key  键
     * @param time 时间(秒)
     * @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;
        }
    }

    /**
     * HashSet 并设置时间
     *
     * @param key  键
     * @param map  对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map<String, Object> 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表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @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表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @param time  时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @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 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... item) {
        redisTemplate.opsForHash().delete(key, item);
    }

    /**
     * 判断hash表中是否有该项的值
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String item) {
        return redisTemplate.opsForHash().hasKey(key, item);
    }

    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     *
     * @param key  键
     * @param item 项
     * @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 项
     * @param by   要减少记(小于0)
     * @return
     */
    public double hdecr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, -by);
    }

    /**
     * 根据key获取Set中的所有值
     *
     * @param key 键
     * @return
     */
    public Set<Object> sGet(String key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 根据value从一个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;
        }
    }

    /**
     * 将数据放入set缓存
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSet(String key, Object... values) {
        try {
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 将set数据放入缓存
     *
     * @param key    键
     * @param time   时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    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 值 可以是多个
     * @return 移除的个数
     */
    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缓存的内容
     *
     * @param key   键
     * @param start 开始
     * @param end   结束 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时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
     * @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 boolean lSet(String key, Object value) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return
     */
    public boolean lSet(String key, Object 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 boolean lSet(String key, List<Object> value) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return
     */
    public boolean lSet(String key, List<Object> value, long time) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据索引修改list中的某条数据
     *
     * @param key   键
     * @param index 索引
     * @param value 值
     * @return
     */
    public boolean lUpdateIndex(String key, long index, Object value) {
        try {
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 移除N个值为value
     *
     * @param key   键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */
    public long lRemove(String key, long count, Object value) {
        try {
            Long remove = redisTemplate.opsForList().remove(key, count, value);

            return remove;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 加锁
     *
     * @param key
     * @param value
     * @param expire
     * @return 是否加锁成功
     */
    public boolean lock(String key, Object value, long expire) {
        if (null == value) {
            value = new Byte[]{1};
        }
        if (redisTemplate.opsForValue().setIfAbsent(key, value)) {
            expire(key, expire);
            return true;
        }
        return false;
    }

    /**
     * 解锁
     *
     * @param key
     */
    public void delLock(String key) {
        redisTemplate.delete(key);
    }


    public Set<String> keys(String nameSpace) {
        return redisTemplate.keys("*" + nameSpace + "*");
    }

    /**
     * 当前方法的主要作用是什么?
     * boundValueOps主要用于操作Redis的字符串的,它可以先在boundValueOps(“key”)中写上key名,然后接方法名,
     * 这样以后的操作就不需要写key的名称,比如redisTemplate.boundValueOps(“key”).set(“value”);
     * 当我们需要对一个key同时做多个操作时,我们做如下操作:
     *先设置为2然后自增1
     *      BoundValueOperations operations = redisTemplate.boundValueOps("key");
     *      operations.set("2");
     *      operations.increment();
     */
    public boolean checkFreq(String key, long count, long ttl) {
        boolean exists = redisTemplate.hasKey(key);
        BoundValueOperations<String, Object> valueOps = redisTemplate.boundValueOps(key);
        Long value = valueOps.increment(1);
        if (value == null) value = count;

        if (!exists) {
            redisTemplate.expire(key, ttl, TimeUnit.SECONDS);
        }
        return value <= count;
    }
}
posted @ 2022-07-07 21:41  hanease  阅读(10814)  评论(0)    收藏  举报