Redis 实战应用

五大数据类型使用场景

String

对象缓存

  1. set user:1 value(序列化对象数据、json 格式数据)
  2. mset user:1:name zhangsan user:1:sex male 适用于对象的单值高频操作

分布式锁

setnx product:10001 xxx  //  返回 1 表示获取锁成功

执行业务操作

del product:10001   // 执行完业务释放锁

但是存在死锁问题:服务器在执行业务过程中宕机,锁无法释放。如果给锁加过期时间,时间应该设置为多久?(这里建议使用 zookeeper 实现分布式锁)

计数器

incr article:readcount:{文章id}

web 集群 session 共享

spring session + redis 实现 session 共享

分布式系统全局序列号

incrby orderid 1000  // 批量生成序列号提高性能

分布式系统下,多个服务每次生成订单 id 为自增操作,对 redis 访问频繁。可以每个服务取一段 id,放在内存中自用。

缺点是可能一段 id 没有使用完就宕机了,丢失一段 id。

Hash

对象缓存

hmset user {userid}:name zhangsan {userid}:age 18

和使用 String 缓存对象一样,适用于单值高频操作。

电商购物车

以用户 id 为 key,商品 id 为 field,商品数量为 value。

  1. 添加商品: hset cart:10001 10088 1
  2. 增加数量:hincrby cart:10001 10088
  3. 商品总数:hlen cart:10001
  4. 删除商品:hdel cart:10001 10088
  5. 获取购物车所有商品:hgetall cart:10001

利于数据的管理,但是集群架构下不适合大规模使用。

List

常用数据结构

  1. Stack(栈) = lpush + lpop
  2. Queue(队列)= lpush + rpop
  3. Blocking MQ(阻塞队列)= lpush + brpop(BRPOP key timeout 监听 key 操作)

消息推流

  1. 公众号 1 发消息:lpush msg:{用户id} {消息id}
  2. 公众号 2 发消息:lpush msg:{用户id} {消息id}
  3. 查看最新消息:lrange msg:{用户id} 0 5

Set

抽奖

  1. 点击抽奖加人:sadd users {userid}
  2. 查看参与抽奖用户:smembers users
  3. 抽取 count 名中奖用户:srandmember users {count}(单次抽奖)、spop users {count} (多次抽奖)

点赞、收藏

  1. 点赞:sadd like:{消息id} {用户id}
  2. 取消点赞:srem like:{消息id} {用户id}
  3. 检查用户是否点赞:sismember like:{消息id} {用户id}
  4. 获取点赞用户列表:smembers like:{消息id}
  5. 获取点赞用户数:scard like:{消息id}

关注模型

我关注的人:zhangsanSet -> {lisi,wangwu,zhaoliu}

李四关注的人:lisiSet -> {zhangsan,wangwu}

王五关注的人:wangwuSet -> {zhangsan,xiaohei}

  1. 我和李四的共同关注:sinter zhangsanSet lisiSet -> {wangwu}
  2. 我关注的人也关注了他:sismember lisiSet xiaohei -> 当前浏览的用户,我关注的用户也关注了他
  3. 我可能认识的人:sidff wangwuSet zhangsanSet -> {xiaohei}

Zset

排行榜

  1. 点击新闻:zincrby hotNews:20200918 {新闻id}

  2. 展示当日排行前十:zrerange  hotNews:20200918 0 9 WITHSCORES

  3. 七日搜索榜单:zunionstore hotNews:20200911-20200918 7 hotNews:20200911 ... hotNews20200918(求并集,新闻id 相同则分数相加)

 

Redis 分布式锁

在单机架构中,使用下面的传统锁并没有什么问题:

@RestController
public class Test {

	@Autowired
	private StringRedisTemplate stringRedisTemplate;
	
	@GetMapping("/reduceStock")
	public String reduceStock() {
		synchronized (this) {
			String key = "stock";
			int stock = Integer.valueOf(stringRedisTemplate.opsForValue().get(key));
			if (stock > 0) {
				stock--;
				stringRedisTemplate.opsForValue().set(key, String.valueOf(stock));
				System.out.println("扣除库存成功,剩余库存: " + stock);
			} else {
				System.out.println("扣除库存失败,库存不足");
			}	
		}
		return "success";
	}
	
}

但是在集群、分布式架构中,由于代码部署在不同服务器上,传统锁无法发挥作用。这种情况下,我们可以使用 Redis 作为分布式锁。 

Redis 锁演进

使用简单的分布式锁重构 reduceStock 方法:

public String reduceStock() {
		String lockKey = "lockKey";
		Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "true");
		// 由于 Redis 是单线程的,所有只会有一个线程可以设置成功(对应 Redis命令setnx)
		if (!result) {
			return "error";
		}
		String key = "stock";
		int stock = Integer.valueOf(stringRedisTemplate.opsForValue().get(key));
		if (stock > 0) {
			stock--;
			stringRedisTemplate.opsForValue().set(key, String.valueOf(stock));
			System.out.println("扣除库存成功,剩余库存: " + stock);
		} else {
			System.out.println("扣除库存失败,库存不足");
		}	
		stringRedisTemplate.delete(lockKey);
		return "success";
	}

上面的代码可以实现简单的分布式锁,但是依然存在问题。 

  1. 拿到锁的线程在执行业务代码的过程中出现异常,那么锁就无法被释放。这种情况可以使用 try...finally 代码块进行优化。
  2. 拿到锁的线程所在的服务器在执行业务代码的过程中宕机(或者运维重启服务器),那么锁也无法释放。这种情况可以给锁设置一个过期时间(setex <key> <seconds> <value>)。

解决以上两个问题,基本上可以算一个比较完善的分布式锁。

但是过期时间依然存在一些问题。例如,线程一在执行业务代码的时候,由于某些原因执行时间超过了过期时间。那么此时线程二就可以加锁,然后执行业务代码,而线程一此时执行业务代码结束,删除了线程二设置的锁,此时线程三又可以加锁并执行业务代码。之后以此类推,那么锁机制就形同虚设。

  • 第一,我们可以通过为线程定制锁来解决问题,即线程自己设置的锁,只有自己才能释放。
  • 第二,线程在执行业务代码的时候,可以使用子线程查看锁过期时间还剩余多少,如果过期时间所剩不多,将延长过期时间。即锁续命。

大体上完善的分布式锁如下所示(锁续命代码未给出):

public String reduceStock() {
		String lockKey = "lockKey";
		String threadId = UUID.randomUUID().toString();
		try {
			Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, threadId, 
					10, TimeUnit.SECONDS);
			if (!result) {
				return "error";
			}
			String key = "stock";
			int stock = Integer.valueOf(stringRedisTemplate.opsForValue().get(key));
			if (stock > 0) {
				stock--;
				stringRedisTemplate.opsForValue().set(key, String.valueOf(stock));
				System.out.println("扣除库存成功,剩余库存: " + stock);
			} else {
				System.out.println("扣除库存失败,库存不足");
			}				
		} finally {
			if (threadId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
				stringRedisTemplate.delete(lockKey);				
			}
		}
		return "success";
	}

Redisson

Redisson 是一个在 Java 代码中操作 Redis 的第三方类库,相较于 Jedis,Redisson 提供了很多分布式工具类。其中就包含上面提及的分布式锁。

使用 Redisson 重构上面的代码:

@RestController
public class Test {

	@Autowired
	private StringRedisTemplate stringRedisTemplate;
	@Autowired
	private Redisson redisson;
	
	@GetMapping("/reduceStock")
	public String reduceStock() {
		String lockKey = "lockKey";
		RLock redissionLlock = redisson.getLock(lockKey);
		try {
			redissionLlock.lock();
			String key = "stock";
			int stock = Integer.valueOf(stringRedisTemplate.opsForValue().get(key));
			if (stock > 0) {
				stock--;
				stringRedisTemplate.opsForValue().set(key, String.valueOf(stock));
				System.out.println("扣除库存成功,剩余库存: " + stock);
			} else {
				System.out.println("扣除库存失败,库存不足");
			}				
		} finally {
			redissionLlock.unlock();
		}
		return "success";
	}
	
}

Redisson 背后执行逻辑如下: 

Redisson 底层使用 Lua 脚本(同一个代码块中的命令,Redis 将之作为原子操作)操作 Redis。

面试必备之深入理解自旋锁

主从复制带来的问题

在上图 Redisson 逻辑中,若主机加锁成功,还没来得及同步给从机,主机就宕机了。从机成为主机后,内部并没有锁的信息,此时新的线程又可以加锁。

针对这一问题,Zookeeper 的解决思路是,加锁时只有半数以上的从机同步成功后,才通知客户端加锁成功。这样一来,在从机中选出主机时,就能够保证新的主机依然有锁信息。这样带来的问题是 Zookeeper 性能不如 Redis。

而 Redis 使用 RedLock 解决主从复制问题的思路和 Zookeeper 一样,因此使用 RedLock 会存在性能问题。

RedLock 逻辑图:

解决主从架构的redis分布式锁主节点宕机锁丢失的问题

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值