五大数据类型使用场景
String
对象缓存
- set user:1 value(序列化对象数据、json 格式数据)
- 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。
- 添加商品: hset cart:10001 10088 1
- 增加数量:hincrby cart:10001 10088
- 商品总数:hlen cart:10001
- 删除商品:hdel cart:10001 10088
- 获取购物车所有商品:hgetall cart:10001
利于数据的管理,但是集群架构下不适合大规模使用。
List
常用数据结构
- Stack(栈) = lpush + lpop
- Queue(队列)= lpush + rpop
- Blocking MQ(阻塞队列)= lpush + brpop(BRPOP key timeout 监听 key 操作)
消息推流
- 公众号 1 发消息:lpush msg:{用户id} {消息id}
- 公众号 2 发消息:lpush msg:{用户id} {消息id}
- 查看最新消息:lrange msg:{用户id} 0 5
Set
抽奖
- 点击抽奖加人:sadd users {userid}
- 查看参与抽奖用户:smembers users
- 抽取 count 名中奖用户:srandmember users {count}(单次抽奖)、spop users {count} (多次抽奖)
点赞、收藏
- 点赞:sadd like:{消息id} {用户id}
- 取消点赞:srem like:{消息id} {用户id}
- 检查用户是否点赞:sismember like:{消息id} {用户id}
- 获取点赞用户列表:smembers like:{消息id}
- 获取点赞用户数:scard like:{消息id}
关注模型
我关注的人:zhangsanSet -> {lisi,wangwu,zhaoliu}
李四关注的人:lisiSet -> {zhangsan,wangwu}
王五关注的人:wangwuSet -> {zhangsan,xiaohei}
- 我和李四的共同关注:sinter zhangsanSet lisiSet -> {wangwu}
- 我关注的人也关注了他:sismember lisiSet xiaohei -> 当前浏览的用户,我关注的用户也关注了他
- 我可能认识的人:sidff wangwuSet zhangsanSet -> {xiaohei}
Zset
排行榜
-
点击新闻:zincrby hotNews:20200918 {新闻id}
-
展示当日排行前十:zrerange hotNews:20200918 0 9 WITHSCORES
-
七日搜索榜单: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";
}
上面的代码可以实现简单的分布式锁,但是依然存在问题。
- 拿到锁的线程在执行业务代码的过程中出现异常,那么锁就无法被释放。这种情况可以使用 try...finally 代码块进行优化。
- 拿到锁的线程所在的服务器在执行业务代码的过程中宕机(或者运维重启服务器),那么锁也无法释放。这种情况可以给锁设置一个过期时间(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 逻辑图: