Redis场景应用

环境搭建

安装和配置

# 下载Redis
wget http://download.redis.io/releases/redis-6.2.6.tar.gz
tar xzf redis-6.2.6.tar.gz
cd redis-6.2.6
make

# 基础配置 (redis.conf)
daemonize yes                          # 后台运行
bind 0.0.0.0                           # 允许远程连接
protected-mode no                      # 关闭保护模式
requirepass yourpassword               # 设置密码
maxmemory 8gb                          # 最大内存
maxmemory-policy allkeys-lru           # 内存淘汰策略

集群配置

# 创建6个节点(3主3从)
redis-server --port 7000 --cluster-enabled yes --cluster-config-file nodes-7000.conf --cluster-node-timeout 5000
# ...为每个节点重复以上命令,修改端口号

# 创建集群
redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 \
127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \
--cluster-replicas 1

在这里插入图片描述
在这里插入图片描述
properties配置文件如下:

spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=12
spring.redis.lettuce.pool.max-active=10
spring.redis.lettuce.pool.max-wait= 10s
spring.redis.lettuce.pool.max-idle=5
spring.redis.lettuce.pool.min-idle=1
spring.redis.timeout=10s

# application.yml
spring:
  redis:
    host: redis-cluster.example.com
    port: 6379
    password: yourpassword
    jedis:
      pool:
        max-active: 100
        max-idle: 50
        min-idle: 10
    cluster:
      nodes:
        - 10.0.0.1:7000
        - 10.0.0.2:7001
        - 10.0.0.3:7002
        - 10.0.0.4:7003
        - 10.0.0.5:7004
        - 10.0.0.6:7005
      max-redirects: 3

使用自定义的RedisTemplate
因为默认的自动化key的序列化器
在这里插入图片描述

@Configuration   //指示这个类是一个配置类,它定义了一个或多个@Bean方法,用于创建和配置Spring应用程序上下文中的Bean
@EnableCaching  //开启注解式的缓存支持
public class RedisConfig extends CachingConfigurerSupport{
	
	private static final Logger logger = LoggerFactory.getLogger(RedisConfig.class);
    @Value("${spring.redis.database}")
    private Integer database;
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private Integer port;
    @Value("${spring.redis.password}")
    private String password;
    @Value("${spring.redis.lettuce.pool.max-active}")
    private Integer maxActive;
    @Value("${spring.redis.lettuce.pool.max-wait}")
    private Integer maxWait;
    @Value("${spring.redis.lettuce.pool.max-idle}")
    private Integer maxIdle;
    @Value("${spring.redis.lettuce.pool.min-idle}")
    private Integer minIdle;
    @Value("${spring.redis.lettuce.shutdown-timeout}")
    private Integer timeout;

	/**
		在使用@Cacheable,如果不指定key,则使用这个默认的key生成器生成key
	*/
	@Override
	@Bean
	public KeyGenerator keyGenerator(){
		
		return (target,method,params)->{
			StringBuilder sb = new StringBuilder();
            sb.append(target.getClass().getName());
            sb.append(method.getName());
            for (Object obj : params) {
                sb.append(obj.toString());
            }
            return sb.toString();	
		};
	}

	@Bean(name = "redisTemplate")
	public RedisTemplate<Object,Object> redisTemplate(){
		return getTemplate(redisConnectionFactory());
	}

	privae RedisConnectionFactory redisConnectionFactory(){
		return connectionFactory(maxActive, maxIdle, minIdle, maxWait, host, password, timeout, port, database);
	}

	private RedisConnectionFactory connectionFactory(Integer maxActive,
                                                     Integer maxIdle,
                                                     Integer minIdle,
                                                     Integer maxWait,
                                                     String host,
                                                     String password,
                                                     Integer timeout,
                                                     Integer port,
                                                     Integer database) {
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setHostName(host);
        redisStandaloneConfiguration.setPort(port);
        redisStandaloneConfiguration.setDatabase(database);
        redisStandaloneConfiguration.setPassword(RedisPassword.of(password));

        GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
        poolConfig.setMaxTotal(maxActive);
        poolConfig.setMinIdle(minIdle);
        poolConfig.setMaxIdle(maxIdle);
        poolConfig.setMaxWaitMillis(maxWait);
        LettuceClientConfiguration lettucePoolingConfig = LettucePoolingClientConfiguration.builder()
                .poolConfig(poolConfig).shutdownTimeout(Duration.ofMillis(timeout)).build();
        LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(redisStandaloneConfiguration,
                lettucePoolingConfig);
        connectionFactory.afterPropertiesSet();

        return connectionFactory;
    }
	
	 /**
     * 创建 RedisTemplate 连接类型,此处为hash
     *
     * @param factory
     * @return
     */
    private RedisTemplate<Object, Object> getTemplate(RedisConnectionFactory factory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        template.setValueSerializer(jackson2JsonRedisSerializer(new ObjectMapper()));
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(jackson2JsonRedisSerializer(new ObjectMapper()));

        template.afterPropertiesSet();
        return template;
    }

    /**
     * 对value 进行序列化
     *
     * @param objectMapper
     * @return
     */
    private Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer(ObjectMapper objectMapper) {
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer =
                new Jackson2JsonRedisSerializer<>(Object.class);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        return jackson2JsonRedisSerializer;
    }
}

@RestController
@RequestMapping("/user")
public class UserController{
	@Autowired
	private RedisTemplate redisTemplate;
	//将生成的验证码缓存到redis中,并设置有效期为5分钟
	redisTemplate.opsForValue().set(phone,code,5,TimeUnit.MINUTES);

	//从redis中获取缓存验证码
	redisTemplate.opsForValue().get(phone);

	//如果登录成功,删除redis中的验证码
	redisTemplate.delete(phone);
}
@RestController
@RequestMapping("/dish")
public class DishController{
	@Autowired
	private RedisTemplate redisTemplate;
	
	@GetMapping("/list")
	public R<List<DishDto>> list(Dish dish){
		List<DishDto> dishDtoList = null;
		String key = "dish"+dish.getCategoryId()+"_"+dish.getStatus();
		//先从redis中获取数据,如果有则直接返回,无需查询数据库
		dishDtoList = redisTemplate.opsForValue(key);

		if(dishDtoList!=null){
			如果存在直接返回
		}
		//如果没有则查询数据库,并将查询到的数据放入redis中
		redisTemplate.opsForValue().set(key,dishDtoList,60,TimeUnit.MINUTES);
	}
	
	//使用缓存的过程中保证数据库和缓存中数据的一致,如果数据库中的数据发生变化,需要即使清理缓存数据
	@PutMapping
	public R<String> update(@RequestBody DishDto dishDto){
		Set keys = redisTemplate.keys("dish_*");
		redisTemplate.delete(keys);
	}
}

Controller

@RestController
@RequestMapping("/publisher")
public class PublisherController{
	
	@Autowired
	private ProducerService producerService;
	@Autowired
	private ReceiverService receiverService;

	@RequestMapping(value = "{name}",method = RequestMethod.GET)
	public String sendMessage(@PathVariable("name") String name){
		return producerService.sendMessage(name);
	}

	@RequestMapping(value = "/get",method = RequestMethod.GET)
	public String getMessage(){
		return receiverService.getMessage();
	}
}

ProducerService

消息生产者

@Service
public class ProducerService{
	
	@Autowired
	private StringRedisTemplate redisTemplate;

	public String sendMessage(String name){
		
		try{
			redisTemplate.opsForList().leftPush("quere",name);
			return "消息发送成功了";	
		}catch(Exception e){
			e.printStackTrace();
			return "消息发送失败了";
		}
	}
}

ReceiverService

消息消费者

@Service
public class ReceiverService{
	
	@Autowired
	private StringRedisTemplate redisTemplate;

	public String getMessage(){
		String value = redisTemplate.opsForList().rightPop("quere");
		return value;
	}
}

==================================================================

Redis事务

在Redis中,事务是一组命令的集合,可以在一个单独的流程中执行,以保证这些命令的原子性、一致性、隔离性和持久性。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、开始和提交事务

  • 使用MULTI命令开启事务
  • 执行需要在事务中执行的命令(使用常规的Redis命令,例如SET、GET、HSET、ZADD等等。这些命令会被添加到事务队列中,直到执行EXEC命令)
  • 使用EXEC命令提交事务,执行事务中的所有命令
//创建与Redis服务器的连接
Jedis jedis = new Jedis("localhost",6379);

//开启事务
Transaction transaction = jedis.multi();//(如果在MULTI和EXEC之间有错误发生,事务会被取消,命令不会执行)

//执行事务中的命令(这两个命令会被添加到事务队列中)
transaction.set("key1","value1");
transaction.set("key2","value2");

 transaction.set("name", "Alice");
 transaction.hset("user:1", "name", "Bob");
 transaction.zadd("scores", 100, "Alice");
 transaction.zadd("scores", 200, "Bob");

//提交事务并获取执行结果
List<Object> results = transaction.exec();

//打印执行结果
for(Object result:results){

	System.out.println("Result: " + result);
}

// 关闭连接
jedis.close();

WATCH命令

在事务中使用WATCH命令可以监视一个或多个键,如果被监视的键在事务执行过程中被其他客户端修改,事务会被中断。这是为了保证事务的一致性和避免竞态条件。

Jedis jedis = new Jedis("localhost", 6379);
Transaction transaction = jedis.multi();

// 监视键"balance"
transaction.watch("balance");

// ... 在此期间可能有其他客户端修改了"balance"键的值 ...

// 执行事务
List<Object> results = transaction.exec();
Jedis jedis = new Jedis("localhost", 6379);
Transaction transaction = jedis.multi();

// 监视键"balance"
transaction.watch("balance");

// ... 在此期间其他客户端修改了"balance"键的值 ...

// 尝试执行事务,但由于"balance"键被修改,事务会被中断
List<Object> results = transaction.exec();

更加复杂的业务
在这里插入图片描述

Redis管道

在单次通信中发送多个命令到Redis服务器,从而显著减少了通信开销,提高了性能
管道可以将多个命令一次性发送给服务器,而不需要等待每个命令的响应,这使得Redis能够更高效地处理批量操作和大规模数据的读写
在这里插入图片描述
在这里插入图片描述

public class RedisPipelineExample{
	
	public static void main(String[] args){
		Jedis jedis = new Jedis("localhost",6379);

		//创建管道
		Pipeline pipeline = jedis.pipelined();

		//向管道中添加命令
		for(int i = 0;i < 10000;i++){
			pipeline.set("key" + i,"value" +i);
		}

		//执行管道中的命令
		List<Object> results = pipeline.syncAndReturnAll();

		//关闭连接
		jdis.close();
	}
}

注意:

  • 管道不支持事务,不能保证多个命令的原子性执行。
  • 使用管道时,命令的执行顺序可能与添加顺序不一致,这需要根据业务需求进行考虑。
  • 管道并非在所有场景下都能带来性能提升,需要根据实际情况进行评估。

管道VS事务:

①、事务在某些场景下可以保证原子性和一致性的操作,特别适用于强一致性要求的业务操作,例如支付操作
②、务是一种适用于需要强一致性操作的机制。当多个命令需要在一个操作序列中原子性地执行时,事务可以确保这些命令要么全部执行,要么全部不执行,以保持数据的一致性。

/**
	银行转账要求的强一致性
*/
Jedis jedis = new Jedis("localhost",6379);

Transaction transacition = jedis.multi();
transaction.decrBy("account1",100);
transaction.incrBy("account2",100);

List<Object> results = transaction.exec();

jedis.close();

=============================================

Redis应用场景
在这里插入图片描述

缓存

热点数据缓存,对象缓存,全页缓存等,可以提升热点数据的访问效率

在这里插入图片描述

@Service
pubic class ProductService{
	
	private final RedisTemplate<String,Object> redisTemplate;
	private final ProductRepository productRepository;

	//缓存前缀
	private static final String PRODUCT_CACHE_PREFIX = "prod:";
	private static final long CACHE_EXPIRE_SECONDS = 3600;

	@Autowired
	public ProductService(RedisTemplate<String, Object> redisTemplate, 
                         ProductRepository productRepository){
		this.redisTemplate = redisTemplate;
        this.productRepository = productRepository;
	}
	
	public Product getProductById(Long id){
		
		String cacheKey = PRODUCT_CACHE_PREFIX + id;

		//1.先查缓存
		Product product = (Product) redisTemplate.opsForValue().get(cacheKey);
		if(product != null){
			return product;
		}

		//2.缓存未命中,查数据库
		product = productRepository.findById(id)
			.orElseThrow(() -> new ResourceNotFoundException("Product not found"));
		
		//3.写入缓存
		redisTemplate.opsForValue().set(cacheKey, product, CACHE_EXPIRE_SECONDS, TimeUnit.SECONDS);
	}

	@CacheEvict(key = "'prod:' + #id")//使用Spring Cache注解删除缓存
	public void updateProduct(Long id, Product product){
		productRepository.save(product);
	}
}

数据共享分布式

Redis是分布式独立服务,可以在多个应用之间共享。
分布式Session

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

分布式锁

使用redis带nx和px参数的set指令去完成。
Strng类型的setnx方法,只有不存在时才能添加成功,返回true

/**
	实现分布式锁工具类
*/
@Component
public class RedisDistributedLock{
	
	private final RedisTemplate<String,String> redisTemplate;

	@Autowired
    public RedisDistributedLock(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

	/**
     * 获取分布式锁
     * @param lockKey 锁的key
     * @param requestId 请求标识(可使用UUID)
     * @param expireTime 过期时间(秒)
     * @return 是否获取成功
     */
    public boolean tryGetLock(String lockKey, String requestId, long expireTime) {
        return Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(
            lockKey, 
            requestId, 
            expireTime, 
            TimeUnit.SECONDS
        ));
    }

	/**
     * 释放分布式锁
     * @param lockKey 锁的key
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public boolean releaseLock(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";
        Long result = redisTemplate.execute(
            new DefaultRedisScript<>(script, Long.class),
            Collections.singletonList(lockKey),
            requestId
        );
        return result != null && result == 1;
    }
}

redisson的RedLock,是使用最普遍的分布式锁解决方案,有读写锁的差别,并处理了多redis实例情况下的异常问题。

秒杀业务中使用分布式锁

@Service
public class SeckillService {
    
    private final RedisDistributedLock redisLock;
    private static final String SECKILL_LOCK_PREFIX = "seckill:lock:";
    private static final int LOCK_EXPIRE_TIME = 10; // 秒
    
    @Autowired
    public SeckillService(RedisDistributedLock redisLock) {
        this.redisLock = redisLock;
    }
    
    public boolean seckillProduct(Long productId, Long userId) {
        String lockKey = SECKILL_LOCK_PREFIX + productId;
        String requestId = UUID.randomUUID().toString();
        
        try {
            // 获取分布式锁
            if (!redisLock.tryGetLock(lockKey, requestId, LOCK_EXPIRE_TIME)) {
                throw new BusinessException("系统繁忙,请稍后再试");
            }
            
            // 执行业务逻辑
            return doSeckill(productId, userId);
        } finally {
            // 释放锁
            redisLock.releaseLock(lockKey, requestId);
        }
    }
    
    private boolean doSeckill(Long productId, Long userId) {
        // 实际的秒杀逻辑
        // 1. 检查库存
        // 2. 扣减库存
        // 3. 创建订单
        // ...
    }
}

全局ID

int类型,incrby利用原子性

incrby userrid 1000

计数器

int类型,incr方法
文章的阅读量、微博点赞数、允许一定的延迟,先写入Redis再定时同步到数据库
在这里插入图片描述

限流

int类型,incr方法 只需要使用incr配合expire指令即可
以访问者的ip和其他信息作为key,访问一次增加一次计数,超过次数则返回false
在这里插入图片描述
这种简单的实现,通常来说不会有问题,但在流量比较大的情况下,在时间跨度上会有流量突然飙升的风险。根本原因,就是这种时间切分方式太固定了,没有类似滑动窗口这种平滑的过度方案。

同样是redisson的RRateLimiter,实现了与guava中类似的分布式限流工具类,使用非常便捷。

RRateLimiter limiter = redisson.getRateLimiter("xjjdogLimiter");
//只需要初始化一次
//每2秒5给许可
limiter.trySetRate(RateType.OVERALL,5,2,RateIntervalUnit.SECONDS);

//没有可用许可,将一直阻塞
limiter.acquire(3);

位统计

String类型的bitcount字符是8位二进制存储的

set k1 a
setbit k1 6 1
setbit k1 7 0
get k1 


在线用户统计,留存用户统计
setbit onlineusers 01 
setbit onlineusers 11 
setbit onlineusers 20

支持按位与、按位或等等操作
BITOPANDdestkeykey[key...] ,对一个或多个 key 求逻辑并,并将结果保存到 destkey 。       
BITOPORdestkeykey[key...] ,对一个或多个 key 求逻辑或,并将结果保存到 destkey 。 
BITOPXORdestkeykey[key...] ,对一个或多个 key 求逻辑异或,并将结果保存到 destkey 。 
BITOPNOTdestkeykey ,对给定 key 求逻辑非,并将结果保存到 destkey 。

计算出7天都在线的用户
BITOP "AND" "7_days_both_online_users" "day_1_online_users" "day_2_online_users" ...  "day_7_online_users"

购物车

key:用户id;field:商品id;value:商品数量。
+1:hincr。-1:hdecr。删除:hdel。全选:hgetall。商品数:hlen。
在这里插入图片描述

用户消息时间线timeline

list,双向链表,直接作为timeline就好了。插入有序

消息队列

在生产者端,使用LPUSH加入到某个列表中;在消费端,不断的使用RPOP指令取出这些数据,或者使用阻塞的BRPOP指令获取数据,适合小规模的抢购需求。

List提供了两个阻塞的弹出操作:blpop/brpop,可以设置超时时间
blpop:blpop key1 timeout 移除并获取列表的第一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
brpop:brpop key1 timeout 移除并获取列表的最后一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
队列:先进先除:rpush blpop,左头右尾,右边进入队列,左边出队列
栈:先进后出:rpush brpop

Redis还有PUB/SUB模式,不过pubsub更适合做消息广播之类的业务。

在Redis5.0中,增加了stream类型的数据结构。它比较类似于Kafka,有主题和消费组的概念,可以实现多播以及持久化,已经能满足大多数业务需求了。

/**
	配置消息监听器
*/
@Configuration
public class RedisMessageConfig{
	
	@Bean
	RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
                                           MessageListenerAdapter listenerAdapter){
                                           
    	RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.addMessageListener(listenerAdapter, new PatternTopic("order:*"));
        return container;
    }
    
    @Bean
    MessageListenerAdapter listenerAdapter(MessageReceiver receiver) {
        return new MessageListenerAdapter(receiver, "receiveMessage");
    }
}
/**
	实现消息监听
*/
@Component
public class MessageReceiver {
    
    private static final Logger logger = LoggerFactory.getLogger(MessageReceiver.class);
    
    public void receiveMessage(String message, String channel) {
        logger.info("Received message: {} from channel: {}", message, channel);
        
        // 根据不同的频道处理不同的业务逻辑
        if (channel.startsWith("order:create")) {
            handleOrderCreate(message);
        } else if (channel.startsWith("order:pay")) {
            handleOrderPay(message);
        }
    }
    
    private void handleOrderCreate(String message) {
        // 处理订单创建消息
    }
    
    private void handleOrderPay(String message) {
        // 处理订单支付消息
    }
}
/**
	发送消息
*/
@Service
public class OrderService {
    
    private final RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    public OrderService(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
    
    public void createOrder(Order order) {
        // 保存订单到数据库
        orderRepository.save(order);
        
        // 发送订单创建消息
        redisTemplate.convertAndSend("order:create", order.getId());
    }
    
    public void payOrder(Long orderId) {
        // 更新订单状态为已支付
        orderRepository.updateStatus(orderId, OrderStatus.PAID);
        
        // 发送订单支付消息
        redisTemplate.convertAndSend("order:pay", orderId);
    }
}

抽奖

自带一个随机获得值:spop myset

点赞、签到、打卡

微博ID是t1001,用户ID是u3001 like:t1001 来维护 t1001 这条微博的所有点赞用户

点赞了这条微博:sadd like:t1001 u3001
取消点赞:srem like:t1001 u3001
是否点赞:sismember like:t1001 u3001
点赞的所有用户:smembers like:t1001
点赞数:scard like:t1001
在这里插入图片描述

商品标签

用 tags:i5001 来维护商品所有的标签。

sadd tags:i5001 画面清晰细腻
sadd tags:i5001 真彩清晰显示屏
sadd tags:i5001 流程至极
在这里插入图片描述

商品筛选

// 获取差集
sdiff set1 set2
// 获取交集(intersection )
sinter set1 set2
// 获取并集
sunion set1 set2

iPhone11 上市了,筛选商品,苹果的、ios的、屏幕在6.0-6.24之间的,屏幕材质是LCD屏幕

sadd brand:apple iPhone11

sadd brand:ios iPhone11

sad screensize:6.0-6.24 iPhone11

sad screentype:lcd iPhone 11

在这里插入图片描述

用户关注、推荐模型

follow 关注 fans 粉丝
相互关注:
sadd 1:follow 2
sadd 2:fans 1
sadd 1:fans 2
sadd 2:follow 1
我关注的人也关注了他(取交集):
sinter 1:follow 2:fans
可能认识的人:
用户1可能认识的人(差集):sdiff 2:follow 1:follow
用户2可能认识的人:sdiff 1:follow 2:follow

排行榜

Redis中有一个叫做zset的数据结构,使用跳表实现的有序列表,可以很容易实现排行榜一类的问题。当存入zset中的数据,达到千万甚至是亿的级别,依然能够保持非常高的并发读写,且拥有非常棒的平均响应时间(5ms以内)

id 为6001 的新闻点击数加1:
zincrby hotNews:20190926 1 n6001

获取今天点击最多的15条:
zrevrange hotNews:20190926 0 15 withscores

在这里插入图片描述
使用zadd 可以添加新的记录,我们会使用排行相关的分数,作为记录的score值,然后使用zrevrange指令即可获取实时的排行榜数据,而zrevrank则可以非常容易的获取用户的实时排名
在这里插入图片描述

/**
	实现排行榜服务
*/
@Service
public class RankingService {
    
    private final RedisTemplate<String, String> redisTemplate;
    private static final String RANKING_KEY = "product:ranking";
    
    @Autowired
    public RankingService(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
  
	/**
		增加商品分数
	*/
	public void incrementProductScore(Long productId,double score){
		redisTemplate.opsForZSet().incrementScore(RANKING_KEY, productId.toString(), score);
	}

	/**
		获取商品排名
	*/
	public Long getProductRank(Long productId){
		return redisTemplate.opsForZSet().reverseRank(RANKING_KEY, productId.toString());
	}

	/**
		获取排行榜前N名
	*/
	public List<Long> getTopProducts(int topN){
		Set<String> productIds = redisTemplate.opsForZSet().reverseRange(RANKING_KEY, 0, topN - 1);
        return productIds.stream()
                .map(Long::valueOf)
                .collect(Collectors.toList());
	}

	/**
		获取商品分数
	*/
	public Double getProductScore(Long productId) {
        return redisTemplate.opsForZSet().score(RANKING_KEY, productId.toString());
    }
}  
/**
	在商品服务中使用排行榜
*/
@Service
public class ProductService {
    
    private final RankingService rankingService;
    
    @Autowired
    public ProductService(RankingService rankingService) {
        this.rankingService = rankingService;
    }
    
    @Transactional
    public void productViewed(Long productId) {
        // 记录商品浏览
        productViewRepository.save(new ProductView(productId, LocalDateTime.now()));
        
        // 增加商品热度分数
        rankingService.incrementProductScore(productId, 1);
    }
    
    @Transactional
    public void productPurchased(Long productId, int quantity) {
        // 记录商品购买
        productPurchaseRepository.save(new ProductPurchase(productId, quantity, LocalDateTime.now()));
        
        // 增加商品销量分数(假设每卖出一件加10分)
        rankingService.incrementProductScore(productId, quantity * 10);
    }
    
    public List<Product> getHotProducts(int topN) {
        List<Long> hotProductIds = rankingService.getTopProducts(topN);
        return productRepository.findAllById(hotProductIds);
    }
}

好友关系

set结构,是一个没有重复数据的集合,你可以将某个用户的关注列表、粉丝列表、双向关注列表、黑名单、点赞列表等,使用独立的zset进行存储。

使用ZADD、ZRANK等,将用户的黑名单使用ZADD添加,ZRANK使用返回的sorce值判断是否存在黑名单中。使用sinter指令,可以获取A和B的共同好友。

除了好友关系,有着明确黑名单、白名单业务场景的数据,都可以使用set结构进行存储。这种业务场景还有很多,比如某个用户上传的通讯录,计算通讯录的好友关系等等。

在实际使用中,使用zset存储这类关系的更多一些。zset同set一样,都不允许有重复值,但zset多了一个score字段,我们可以存储一个时间戳,用来标明关系建立所发生的时间,有更明确的业务含义。

统计活跃用户数

类似统计每天的活跃用户、用户签到、用户在线状态,这种零散的需求,实在是太多了。如果为每一个用户存储一个bool变量,那占用的空间就太多了。这种情况下,我们可以使用bitmap结构,来节省大量的存储空间。
在这里插入图片描述

功能性需求

redis还能玩很多花样。举个例子,全文搜索。很多人都会首选es,但redis生态就提供了一个模块:RediSearch,可以做查询,可以做filter。

但我们通常还会有更多的需求,比如统计类、搜索类、运营效果分析等。这类需求与大数据相关,即使是传统的DB也不能胜任。这时候,我们当然要把redis中的数据,导入到其他平台进行计算啦。

如果你选择的是redis数据库,那么dba打交道的,就是rdb,而不是binlog。有很多的rdb解析工具(比如redis-rdb-tools),能够定期把rdb解析成记录,导入到hadoop等其他平台。

此时,rdb成为所有团队的中枢,成为基本的数据交换格式。导入到其他db后的业务,该怎么玩怎么玩,完全不会因为业务系统选用了redis就无法运转。

冷热数据分离

redis的特点是,不管什么数据,都一股脑地搞到内存里做计算,这对于有时间序列概念,有冷热数据之分的业务,造成了非常大的成本考验。为什么大多数开发者喜欢把数据存放在MySQL中,而不是Redis中?除了事务性要求以外,很大原因是历史数据的问题。

通常,这种冷热数据的切换,是由中间件完成的。我们上面也谈到了,Redis是一个文本协议,非常简单。做一个中间件,或者做一个协议兼容的Redis模拟存储,是比较容易的。

比如我们Redis中,只保留最近一年的活跃用户。一个好几年不活跃的用户,突然间访问了系统,这时候我们获取数据的时候,就需要中间件进行转换,从容量更大,速度更慢的存储中查找。

这个时候,Redis的作用,更像是一个热库,更像是一个传统cache层做的事情,发生在业务已经上规模的时候。但是注意,直到此时,我们的业务层代码,一直都是操作的redis的api。它们使用这众多的函数指令,并不关心数据到底是真正存储在redis中,还是在ssdb中。

高可用

redis提供了主从、哨兵、cluster等三种集群模式,其中cluster模式为目前大多数公司所采用的方式。

但是,redis的cluster模式,有不少的硬伤。redis cluster采用虚拟槽的概念,把所有的key映射到 0~16383个整数槽内,属于无中心化的架构。但它的维护成本较高,slave也不能够参与读取操作。

它的主要问题,在于一些批量操作的限制。由于key被hash到多台机器上,所以mget、hmset、sunion等操作就非常的不友好,经常发生性能问题。

redis的主从模式是最简单的模式,但无法做到自动failover,通常在主从切换后,还需要修改业务代码,这是不能忍受的。即使加上haproxy这样的负载均衡组件,复杂性也是非常高的。

哨兵模式在主从数量比较多的时候,能够显著的体现它的价值。一个哨兵集群,能够监控成百上千个集群,但是哨兵集群本身的维护是比较困难的。幸运的是,redis的文本协议非常简单,在netty中,甚至直接提供了redis的codec。自研一套哨兵系统,加强它的功能,是可行的。

LBS应用

早早在Redis3.2版本,就推出了GEO功能。通过GEOADD指令追加lat、lng经纬数据,可以实现坐标之间的距离计算、包含关系计算、附近的人等功能。

关于GEO功能,最强大的开源方案是基于PostgreSQL的PostGIS,但对于一般规模的GEO服务,redis已经足够用了

更多扩展应用场景

要看redis能干什么,就不得不提以下java的客户端类库redisson。redisson包含丰富的分布式数据结构,全部是基于redis进行设计的。

redisson提供了比如Set、 SetMultimap、 ScoredSortedSet、 SortedSet, Map、 ConcurrentMap、 List、 ListMultimap、 Queue、BlockingQueue等非常多的数据结构,使得基于redis的编程更加的方便。在github上,可以看到有上百个这样的数据结构:https://github.com/redisson/redisson/tree/master/redisson/src/main/java/org/redisson/api。

对于某个语言来说,基本的数组、链表、集合等api,配合起来能够完成大部分业务的开发。Redis也不例外,它拥有这些基本的api操作能力,同样能够组合成分布式的、线程安全的高并发应用。

由于Redis是基于内存的,所以它的速度非常快,我们也会把它当作一个中间数据的存储地去使用。比如一些公用的配置,放到redis中进行分享,它就充当了一个配置中心的作用;比如把JWT的令牌存放到Redis中,就可以突破JWT的一些限制,做到安全登出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值