喜欢就关注博主吧,没有最卷,只有更卷!!!
在介绍redis使用场景之前,首先要明白为什么使用?哪些情况下使用?使用的时候可能会遇到哪些常见问题等。不要为了想用而用,更不能为了那虚无缥缈的成就感而过度设计redis,增加系统的复杂度。
哪些数据不适合放到redis中?
哪些数据适合放到redis中
商品的分类属于读多写少类型的数据,一般会放在网站或app的首页,访问频次比较高。对于中型或大型电商来说分了数据可能会有上千条虽然数据不算太多,但是用到的地方很多,为了提高系统并发能了,该类数据可以考虑放入缓存,如果小型电商项目只有几十条数据就没必要放缓存了。
使用分类列表的地方主要有网站或app首页,管理后台的分类列表、在品牌列表建立品牌与分类的管理关系需要选择分类、添加商品规格的时候需要绑定规格与三级分类的关联关系等、发布商品的时候需要选择分类等。
从redis中取数据常见的三大问题:缓存穿透、缓存雪崩、缓存击穿。这三个问题我在Redis 手把手教程(3/3) - Redis集群及常见企业级解决方案中已经介绍过了,在这里再结合场景简述以下。
注意事项:
缓存穿透和缓存雪崩都很好解决,缓存击穿如果用加锁的方式解决的话,有些地方必须要注意以下。
如果我们的系统是微服务或分布式部署的话,那么我们就要考虑使用分布式锁。因为本地锁只能锁住当前实例,如果部署的实例不多的话倒也没事,顶多多查询几次数据库;如果部署的实例比较多的话,就要使用分布式锁了。
使用分布式锁可以用手动使用redis的分布式锁,也可以使用redisson框架提供的分布式锁。具体使用方法可以大概参考下面的代码片段。
温馨提示
redisson的看门狗机制,可以解决锁自动延期的问题。比如一个复杂的业务执行时间比较长,在高并发下执行时间可能为30s,分布式锁设置的到期时间为20s。这样就会导致业务还未执行完锁就自动释放了,会让其他的线程获取到锁。而redisson框架可以不用考虑这个问题,因为其内部实现了锁的自动续期(通过定时任务的方式,定时任务执行的间隔为:锁的有效期【默认30s】 / 3)。当然了维护锁的续期也会额外占用系统资源。至于要不要启用看门狗机制,可以根据实际情况考虑。
演示redis分布式锁的使用
public List<CategoryEntity> listWithTree() {
List<CategoryEntity> entities;
//先从redis中获取,如果获取不到,再从数据库中获取并存到redis中。
String categoryStringJson = redisTemplate.opsForValue().get(RedisConstant.KEY_PRODUCT_CATEGORY_LIST);
if (StringUtils.isEmpty(categoryStringJson)) {
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent(RedisConstant.LOCK_PRODUCT_CATEGORY, uuid,30, TimeUnit.SECONDS);
if (lock) {
System.out.println("获取分布式锁成功");
try {
System.out.println("查询数据库");
//加锁成功,再次查询redis中是否存在数据,存在直接返回,不存在去查数据库
//查询数据库,查询数据库前先加锁,第一个抢到锁定线程从数据库获取数据然后缓存。防止热点数据缓存过期后有大量的请求同时请求数据库。
entities = baseMapper.selectList(null);
//存到redis
redisTemplate.opsForValue().set(RedisConstant.KEY_PRODUCT_CATEGORY_LIST, JSON.toJSONString(entities));
} finally {
//业务执行完,要释放锁
//查锁和删锁也要是原子操作,否则查询的时候锁还在,删除的时候已经过期了,可能就会把别人的锁给删除了,使用rua脚本解锁
String script = "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
//1代表删除成功 0代表失败
Long lockRes = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(RedisConstant.LOCK_PRODUCT_CATEGORY), uuid);
}
} else {
//加锁失败的服务实例,重试->自旋锁
System.out.println("获取分布式锁失败");
return listWithTree();
}
} else {
entities = JSON.parseObject(categoryStringJson, new TypeReference<List<CategoryEntity>>() {
});
}
List<CategoryEntity> finalEntities = entities;
List<CategoryEntity> Menus = entities.stream().filter(categoryEntity ->
categoryEntity.getParentCid() == 0
).map((menu) -> {
menu.setChildren(getChildrens(menu, finalEntities));
return menu;
}).sorted(Comparator.comparingInt(menu -> (menu.getSort() == null ? 0 : menu.getSort()))).collect(Collectors.toList());
return Menus;
}
redisson分布式框架部分代码演示
自定义配置文件
/**
* 自定义redisson配置文件
*/
@Configuration
public class RedissonConfig {
@Bean(destroyMethod="shutdown")
RedissonClient redisson() throws IOException {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.0:6379").setPassword("123456");
return Redisson.create(config);
}
}
使用redisson的分布式锁功能
public List<CategoryEntity> listWithTree() {
List<CategoryEntity> entities;
//先从redis中获取,如果获取不到,再从数据库中获取并存到redis中。
String categoryStringJson = redisTemplate.opsForValue().get(RedisConstant.KEY_PRODUCT_CATEGORY_LIST);
if (StringUtils.isEmpty(categoryStringJson)) {
String uuid = UUID.randomUUID().toString();
RLock lock = redissonClient.getLock(RedisConstant.LOCK_PRODUCT_CATEGORY);
try {
//不手动指定超时时间,会默认使用看门狗机制
lock.lock();
//手动指定超时时间,就不会使用看门狗机制
//lock.lock(30, TimeUnit.SECONDS);
System.out.println("查询数据库");
//加锁成功,再次查询redis中是否存在数据,存在直接返回,不存在去查数据库
//查询数据库,查询数据库前先加锁,第一个抢到锁定线程从数据库获取数据然后缓存。防止热点数据缓存过期后有大量的请求同时请求数据库。
entities = baseMapper.selectList(null);
//存到redis
redisTemplate.opsForValue().set(RedisConstant.KEY_PRODUCT_CATEGORY_LIST, JSON.toJSONString(entities));
} finally {
lock.unlock();
}
} else {
entities = JSON.parseObject(categoryStringJson, new TypeReference<List<CategoryEntity>>() {
});
}
List<CategoryEntity> finalEntities = entities;
List<CategoryEntity> Menus = entities.stream().filter(categoryEntity ->
categoryEntity.getParentCid() == 0
).map((menu) -> {
menu.setChildren(getChildrens(menu, finalEntities));
return menu;
}).sorted(Comparator.comparingInt(menu -> (menu.getSort() == null ? 0 : menu.getSort()))).collect(Collectors.toList());
return Menus;
}
在电商项目中,购物车是比较重要的一部分,访问量也比较大。所以适合放入redis中,来提高系统性能,减轻数据库压力。购物车功能一般包括:购物车列表、加入购物车、修改购物车内商品数量、选择/取消购物项、全选/取消全选、删除购物车等,某些电商项目中还会涉及合并购物车功能。
由于每个人的购物车都是不一样的,所以如何设计购物车在redis中的数据模型就及其重要。购物车中存放的是商品的列表,所以首先想到的是list类型。list类型比较方便获取数据,但是用户修改购物车内某一个购物项的时候就比较麻烦了(比如删除某个商品或者修改数量等),需要一个个去遍历找到要修改的商品。这里直接给出结果,购物车比较常用的数据模型是Hash,redis的key存用户Id,hash中的key存商品Id,value存具体的商品信息。要找某一个商品的话,直接通过商品Id就可以了。如下图:
合并购物车是指用户登录后访问购物车列表时,把登录前加入购物车的数据合并到登录后的购物车。
实现思路是,在登录拦截器里判断用户是否登录,如果没有登录就生成一个临时的userKey返回给客户端,客户端加入购物车或请求其他购物车接口时header中携带这个userKey,加入购物车成功后将userKey作为redis的key临时购物车存到redis中。用户登录后请求购物车列表时也要携带userKey,通过userKey找到临时购物车,然后将临时购物车合并到正式购物车,然后删除临时购物车。
购物车登录拦截器
**
* 购物车拦截器
* 如果用户没有登录就生成一个临时的用户Id返给app,用于合并购物车
*/
public class CartInterceptor implements HandlerInterceptor {
public static ThreadLocal<UserTempVo> threadLocal = new ThreadLocal<>();
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 拦截所有请求
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
UserTempVo userTempVo = new UserTempVo();
//用户是否已登录
boolean hasLogin = false;
//获取客户端传来的userKey,没登录的话就会传userKey
String userKey = request.getHeader(CartConstant.TEMP_USER_KEY);
//获取客户端传来的token,登录的话就会传token
String token = request.getHeader(AuthServerConstant.TOKEN_NAME);
LoginUser loginUser = JwtUtils.getUser(token, AuthServerConstant.LOGIN_USER);
if (loginUser != null) {
hasLogin = true;
//判断用户传来的token和redis中的token是否相同,如果不相同代表登录环境发生了变化,需要重新登录
String redisJwtToken = redisTemplate.opsForValue().get(RedisConstant.PRE_KEY_AUTH_LOGIN_TOKEN + loginUser.getId().toString());
if (!redisJwtToken.equals(token)) {
hasLogin = false;
}
}
//判断用户是否登录,如果没有登录就设置一个临时的key
if (hasLogin) {
userTempVo.setUserId(loginUser.getId());
userTempVo.setUserKey(userKey);
}
else{
//首先判断客户端有没有传userKey,没有传的话就自动生成一个返回给客户端
if(StringUtils.isEmpty(userKey)){
String uuid = UUID.randomUUID().toString().replace("-","");
userTempVo.setUserKey(uuid);
}
else{
userTempVo.setUserKey(userKey);
}
}
threadLocal.set(userTempVo);
return true;
}
}
加入购物车、绑定redis操作等
/**
* 将购物出数据保存到redis中,
* 数据类型为hash,k->对应用户Id v->对应购物车数据
*
* @param vo
* @return
*/
@Override
public Cart addToCart(AddCartVo vo) {
Integer num = vo.getNum();
Long skuId = vo.getSkuId();
BoundHashOperations<String, Object, Object> cartOps = cartOps();
//从redis中获取商品详情
String cartJsonString = (String) cartOps.get(skuId.toString());
//购物车中有该商品,就修改数据
if (!StringUtils.isEmpty(cartJsonString)) {
Cart.CartItem item = JSON.parseObject(cartJsonString, Cart.CartItem.class);
item.setNum(item.getNum() + num);
String s = JSON.toJSONString(item);
cartOps.put(skuId.toString(), s);
} else {
//购物出中没有该商品就添加到购物车中
Cart.CartItem item = getCartItemByDB(skuId, num);
String s = JSON.toJSONString(item);
cartOps.put(skuId.toString(), s);
}
return getCart();
}
/**
* 根据cartkey获取用户所有购物项
*
* @param cartKey 用户Id或临时用户Id
* @return
*/
private List<Cart.CartItem> getCartItemByCartKey(String cartKey) {
List<Cart.CartItem> items = new ArrayList<>();
BoundHashOperations<String, Object, Object> cartOps = redisTemplate.boundHashOps(cartKey);
List<Object> values = cartOps.values();
if (values != null && values.size() > 0) {
items = values.stream().map(item -> {
String str = item.toString();
Cart.CartItem cartItem = JSON.parseObject(str, Cart.CartItem.class);
return cartItem;
}).collect(Collectors.toList());
}
return items;
}
/**
* 获取购物车详情
*
* @return
*/
private Cart getCart() {
Cart cart = new Cart();
BoundHashOperations<String, Object, Object> cartOps = cartOps();
List<Object> values = cartOps.values();
if (values != null && values.size() > 0) {
List<Cart.CartItem> items = values.stream().map(item -> {
String str = item.toString();
Cart.CartItem cartItem = JSON.parseObject(str, Cart.CartItem.class);
return cartItem;
}).collect(Collectors.toList());
cart.setItems(items);
}
return cart;
}
/**
* 获取购物出绑定操作项
*
* @return
*/
private BoundHashOperations<String, Object, Object> cartOps() {
UserTempVo userTempVo = CartInterceptor.threadLocal.get();
String cartKey = "";
if (userTempVo.getUserId() != null) {
cartKey = RedisConstant.PRE_KEY_CART + userTempVo.getUserId();
} else {
cartKey = RedisConstant.PRE_KEY_CART + userTempVo.getUserKey();
}
BoundHashOperations<String, Object, Object> cartOps = redisTemplate.boundHashOps(cartKey);
return cartOps;
}
在网络不好或者用户量较大的情况下,用户下单的时候如果连续多次点击提交订单按钮的话,就可能会生成重复的订单或者报错。
解决方法多种多样,目的就是为了保证接口的幂等性。最好的办法就是前端加上放重复点击,后端保证接口的幂等性。
在订单确认页即结算页,生成一个token放到redis中。提交订单接口接收这个token然后验证和redis中的token是否一致,如果一致就可以下单,并且删除token,如果不一致就返回异常信息。
注意:验证token和删除token要保证原子性。因为用户连续点击多次提交订单的话,可能第一次验证刚通过就下单了,还未来得及删除token,第二次验证就开始了,这时候redis中的token没有删除,所以还会验证通过。保证原子性的话可以使用redis的lua脚本。
详情见代码片段。
订单确认接口
/**
* 订单确认接口
*
* @return
*/
@Override
public OrderConfirm orderConfirm() {
OrderConfirm orderConfirm = new OrderConfirm();
LoginUser loginUser = LoginUserInterceptor.threadLocal.get();
//获取主线程的请求上下文
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//1.异步远程查询所有收获地址列表
CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> {
//异步开启新的线程,会丢失请求上下文信息,这里需要将主线程的请求放到新线程中
RequestContextHolder.setRequestAttributes(requestAttributes);
//这个userId可以不用传,再member的拦截器可以获取到
Result addressRes = memberFeignClient.memberAddresses(loginUser.getId());
if (addressRes.getCode() == ResultCodeEnum.SUCCESS.getCode()) {
List<MemberAddress> addresses = addressRes.getData(new TypeReference<List<MemberAddress>>() {
});
orderConfirm.setAddresses(addresses);
}
}, executor);
//2.远程查询购物车所有选中的购物项,并重新计算价格,购物车内的价格可能已发生变化
CompletableFuture<OrderConfirm> itemsFuture = CompletableFuture.supplyAsync(() -> {
//异步开启新的线程,会丢失请求上下文信息,这里需要将主线程的请求放到新线程中
RequestContextHolder.setRequestAttributes(requestAttributes);
List<SkuItem> orderItems = orderItemService.getSkuItems();
orderConfirm.setItems(orderItems);
return orderConfirm;
}, executor);
//3.远程查询优惠券信息,自动选中的优惠券
CompletableFuture<Void> couponFuture = itemsFuture.thenAcceptAsync((res) -> {
//异步开启新的线程,会丢失请求上下文信息,这里需要将主线程的请求放到新线程中
RequestContextHolder.setRequestAttributes(requestAttributes);
//当前用户当前订单所有可用的优惠券列表
List<CouponInfo> userCoupons = new ArrayList<>();
//获取订单应付金额
BigDecimal payPrice = res.getPayPrice();
//查找当前订单的所有商品的分类Id,购物车里没存该字段,所以暂时用假的
List<Long> itemCategoryIds = res.getItems().stream().map(v -> v.getCatalogId()).collect(Collectors.toList());
//查找当前订单的所有商品的spuId,购物车里没存该字段,所以暂时用假的
List<Long> itemSpuIds = res.getItems().stream().map(v -> v.getSpuId()).collect(Collectors.toList());
Result couponRes = couponFeignClient.confirmCoupon();
if (couponRes.getCode() == ResultCodeEnum.SUCCESS.getCode()) {
OrderConfirmCoupon confirmCoupon = couponRes.getData(new TypeReference<OrderConfirmCoupon>() {
});
//用户所有未使用的优惠券
List<CouponInfo> usables = confirmCoupon.getUsables();
//查询全场通用券,并且满足使用门槛
List<CouponInfo> allCoupons = usables.stream().filter(v -> v.getUseType().equals(0) && v.getMinPoint().compareTo(payPrice) < 1).collect(Collectors.toList());
userCoupons.addAll(allCoupons);
//查询当前用户所有指定品类的优惠券
List<CouponInfo> categoryCoupons = usables.stream().filter(v -> v.getUseType().equals(1)).collect(Collectors.toList());
//当前用户所有优惠券与品类的关联
List<CouponCategory> couponCategory = confirmCoupon.getCategoryCoupons();
for (Long itemCategoryId : itemCategoryIds) {
//获取当前品类的订单总额,也就是优惠券的使用门槛
BigDecimal reduce = res.getItems().stream().filter(v -> v.getCatalogId().equals(itemCategoryId)).map(v -> v.getPrice().multiply(new BigDecimal(v.getNum()))).reduce(BigDecimal.ZERO, BigDecimal::add);
List<CouponCategory> collect = couponCategory.stream().filter(v -> v.getCategoryId().equals(itemCategoryId)).collect(Collectors.toList());
for (CouponCategory category : collect) {
//获取指定当前分类且满足使用门槛的优惠券
List<CouponInfo> couponCategorys = categoryCoupons.stream().filter(v -> v.getId().equals(category.getCouponId()) && v.getMinPoint().compareTo(reduce) < 1).collect(Collectors.toList());
userCoupons.addAll(couponCategorys);
}
}
//查询当前用户所有指定商品的优惠券,并且满足使用门槛,实际使用门槛应该是指定商品的总金额而不是订单总金额
List<CouponInfo> spuCoupons = usables.stream().filter(v -> v.getUseType().equals(2)).collect(Collectors.toList());
List<CouponSpu> spuCoupon = confirmCoupon.getSpuCoupons();
for (Long itemSpuId : itemSpuIds) {
//获取当前商品的订单总额,也就是优惠券的使用门槛
BigDecimal reduce = res.getItems().stream().filter(v -> v.getSpuId().equals(itemSpuId)).map(v -> v.getPrice().multiply(new BigDecimal(v.getNum()))).reduce(BigDecimal.ZERO, BigDecimal::add);
List<CouponSpu> collect = spuCoupon.stream().filter(v -> v.getSpuId().equals(itemSpuId)).collect(Collectors.toList());
for (CouponSpu couponSpu : collect) {
List<CouponInfo> couponSpus = spuCoupons.stream().filter(v -> v.getId().equals(couponSpu.getCouponId()) && v.getMinPoint().compareTo(reduce) < 1).collect(Collectors.toList());
userCoupons.addAll(couponSpus);
}
}
//从优惠券集合中,找到自动使用的优惠券,自动使用规则:优先金额最大的,其次是快过期的。
userCoupons.sort(Comparator.comparing(CouponInfo::getAmount).reversed());
userCoupons.sort(Comparator.comparing(CouponInfo::getEndTime).reversed());
int index = 0;
for (CouponInfo userCoupon : userCoupons) {
if (index == 0) {
userCoupon.setChecked(true);
} else {
userCoupon.setChecked(false);
}
index++;
}
orderConfirm.setCoupons(userCoupons);
}
}, executor);
//4.计算运费,这里暂时不计算了默认10元
orderConfirm.setFare(BigDecimal.TEN);
//5.防重令牌
String token = UUID.randomUUID().toString().replace("-", "");
orderConfirm.setOrderToken(token);
redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX + loginUser.getId(), token, 30, TimeUnit.MINUTES);
try {
CompletableFuture.allOf(addressFuture, couponFuture).get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
return orderConfirm;
}
下单接口
/**
* 提交订单
*
* @param vo
* @return
*/
@Override
@Transactional
public OrderEntity submit(OrderSumbit vo) {
LoginUser loginUser = LoginUserInterceptor.threadLocal.get();
//获取前端传过来的token
String orderToken = vo.getOrderToken();
//原子性,验证令牌并且删除令牌的redis脚本
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
//执行redis脚本,返回1代表成功,0代表失败
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + loginUser.getId()), orderToken);
//生成订单基本信息
OrderEntity orderEntity = buildOrder(vo);
//生成订单详情基本信息
List<OrderItemEntity> itemEntities = orderItemService.BuildOrderItems(orderEntity);
//计算订单价格
computePrice(orderEntity,itemEntities);
//验价
if (Math.abs(orderEntity.getPayAmount().subtract(vo.getPayPrice()).doubleValue()) >= 0.01) {
throw new GloableException(ResultCodeEnum.ORDER_SUBMIT_PRICE_EXCEPTION);
}
//令牌验证失败
if (result != 1) {
throw new GloableException(ResultCodeEnum.ORDER_SUBMIT_REPEAT_EXCEPTION);
}
//保存订单
this.save(orderEntity);
//保存订单详情
orderItemService.saveBatch(itemEntities);
//锁定库存
return orderEntity;
}
待更新。。。