环境搭建
安装和配置
# 下载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的一些限制,做到安全登出。