Redis作为一款性能优异的内存数据库,在互联网公司有着多种应用场景,下面介绍下Redis在京东到家的订单列表中的使用场景。主要从以下几个方面来介绍:
订单列表在Redis中的存储结构
Redis和DB数据一致性保证
只要有多份数据,就会涉及到数据一致性的问题。Redis和数据库的数据一致性,也是必然要面对的问题。我们这边的订单数据是先更新数据库,数据库更新成功后,再更新缓存,若数据库操作成功,缓存操作失败了,就出现了数据不一致的情况。保证数据一致性我们前后使用过两种方式:
代码示例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IAWyqRGR-1658982747693)(https://upload-images.jianshu.io/upload_images/27964194-567b2e49be9514ac.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
Redis中的分布式锁
分布式锁常用的实现方式有Redis和zookeeper,本文主要介绍下Redis的分布式锁,然后再介绍下我们使用分布式锁的场景。
Redis分布式锁在2.6.12版本之后的实现方式比较简单,只需要使用一个命令即可:
SET key value [EX seconds] [NX]
其中,可选参数EX seconds :设置键的过期时间为 seconds 秒;NX :只在键不存在时,才对键进行设置操作。
这个命令相当于2.6.12之前的setNx和expire两个命令的原子操作命令。Redis的JAVA客户端分布式锁实现示例代码:
2.6.12版本之后:
2.6.12版本之前,由于没有一个上述的原子命令,需要一些命令组合实现,但不能简单的使用setNx、expire这两个命令,因为如果setNx成功,expire命令失败时,恰好执行删除lockKey的也执行失败,key就永远不会过期,就会出现死锁问题,如:
第(1)步设置lockKey失效时间失败,lockKey在缓存永久保存。
第(2)步没来得及释放锁时,系统崩溃,finally块没来得及执行,最终导致锁永远在缓存中,所有其他线程再也获取不到锁。所以不能单纯的依靠设置锁的失效时间来防止释放锁失败,需要通过下列方法防止这种情况,但比较繁琐,不过2.6.12版本之前也必须通过如下方法才更为妥当:
public booelan getLock(String lockKey) { boolean lock = false; while (!lock) { String expireTime = String.valueOf(System.currentTimeMillis() + 5000); // (1)第一个获得锁的线程,将lockKey的值设置为当前时间+5000毫秒,后面会判断,如果5秒之后,获得锁的线程还没有执行完,会忽略之前获得锁的线程,而直接获取锁,所以这个时间需要根据自己业务的执行时间来设置长短。 lock = shardedXCommands.setNX(lockKey, expireTime); if (lock) { // 已经获取了这个锁 直接返回已经获得锁的标识 return lock; } // 没获得锁的线程可以执行到这里:从Redis获取老的时间戳 String oldTimeStr = shardedXCommands.get(lockKey); if (oldTimeStr != null && !"".equals(oldTimeStr.trim())) { Long oldTimeLong = Long.valueOf(oldTimeStr); // 当前的时间戳 Long currentTimeLong = System.currentTimeMillis(); // (2)如果oldTimeLong小于当前时间了,说明之前持有锁的线程执行时间大于5秒了,就强制忽略该线程所持有的锁,重新设置自己的锁 if (oldTimeLong < currentTimeLong) { // (3)调用getset方法获取之前的时间戳,注意这里会出现多个线程竞争,但肯定只会有一个线程会拿到第一次获取到锁时设置的expireTime String oldTimeStr2 = shardedXCommands.getSet(lockKey, String.valueOf(System.currentTimeMillis() + 5000)); // (4)如果刚获取的时间戳和之前获取的时间戳一样的话,说明没有其他线程在占用这个锁,则此线程可以获取这个锁. if (oldTimeStr2 != null && oldTimeStr.equals(oldTimeStr2)) { lock = true; // 获取锁标记 break; } } } // 暂停50ms,重新循环 try { Thread.sleep(50); } catch (InterruptedException e) { log.error(e); } } return lock;}
上述方法主要使用了Redis的setNX、getSet两个方法,不依赖Redis的expire方法,即便是删除锁失败时,上面逻辑第(2)步也会规避这个问题。
缓存防穿透和雪崩
代码示例:
防止穿透和雪崩的关键地方在于使用分布式锁和锁的粒度控制。首先初始化了128(0-127)个锁,然后让所有缓存没命中的用户去竞争这128个锁,得到锁后并且再一次判断缓存中依然没有数据的,才有权利去查询数据库。没有将锁粒度限制到用户级别,是因为如果粒度太小的话,某一个时间点有太多的用户去请求,同样会有很多的请求打到数据库。比如:
在时间点T1有10000个用户的缓存数据失效了,恰恰他们又在时间点T1都请求数据,如果锁粒度是用户级别,那么这10000个用户都会有各自的锁,也就意味着他们都可以去访问数据库,同样会对数据库造成巨大压力。而如果是通过用户id去hashcode和127取模,意味着最多会产生128个锁,最多会有128个并发请求访问到数据库,其他的请求会由于没有竞争到锁而阻塞,待第一批获取到锁的线程释放锁之后,剩下的请求再进行竞争锁,但此次竞争到锁的线程,在执行代码段2中第4步时:orderRedisCache.isOrderListExist(userId),缓存中有可能已经有数据了,就不用再查数据库了,依次类推,从而可以挡住很多数据库请求,起到很好的保护数据库的作用。