将Token保存到Redis中



将手机号码(String)为key, 验证码(String)为value存入Redis
key, value均是字符串的方法:
stringRedisTemplate.opsForValue().set(String key, String value, long timeout, TimeUnit unit)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @Autowired private StringRedisTemplate stringRedisTemplate;
@Override
public Result sendCode(String phone, HttpSession session) { if(RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号不合法"); }
String code = RandomUtil.randomString(6);
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
log.debug("验证码是:" + code);
return Result.ok(); }
|
以Token(String)为key, 用户(Map)为value存入Redis
key为String, value为Map的方法:
stringRedisTemplate.opsForHash().putAll(String key, Map, ?> m)
设置存在时长:
stringRedisTemplate.expire(String key, long timeout, TimeUnit unit)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| @Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
String phone = loginForm.getPhone();
String code = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone); if(code == null) { return Result.fail("手机号不一致"); } if(!(code.equals(loginForm.getCode()))) { return Result.fail("验证码错误~"); } User user = query().eq("phone", phone).one();
if(user == null) { user = createUserByPhone(phone); }
String token = UUID.randomUUID().toString(); String tokenKey = LOGIN_USER_KEY +token; UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions.create().setIgnoreNullValue(true) .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString())); stringRedisTemplate.opsForHash().putAll(tokenKey, userMap); stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
return Result.ok(token); }
|
前端拦截器
前端将会拦截每一次请求, 在每次请求头中加入Token


拦截器

全部拦截
访问完毕后需要将ThreadLocal中的信息删除
RefreshTokenInterceptor类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.util.StrUtil;
import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;
import java.util.Map; import java.util.concurrent.TimeUnit;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY; import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL;
public class RefreshTokenInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; }
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("authorization"); String tokenKey = LOGIN_USER_KEY + token;
if(StrUtil.isBlank(token)) { return true; }
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);
if(userMap == null) { response.setStatus(401); return false; }
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false); UserHolder.saveUser(userDTO);
stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
return true; }
@Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserHolder.removeUser(); } }
|
登录拦截
LoginInterceptor类
用于登录拦截
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import com.hmdp.dto.UserDTO;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { UserDTO user = UserHolder.getUser();
if(user == null) { response.setStatus(401); return false; }
return true; } }
|
对ThreadLocal的管理
UserHolder类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class UserHolder {
private static final ThreadLocal<User> tl = new ThreadLocal<>();
public static void saveUser(User user){ tl.set(user); }
public static User getUser(){ return tl.get(); }
public static void removeUser(){ tl.remove(); } }
|
启用登录拦截器
拦截的优先级可用order(int num)来指定, num越小优先级越高, 若不设置默认为0, 按添加的顺序拦截.
MvcConfig类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import com.hmdp.utils.LoginInterceptor; import com.hmdp.utils.RefreshTokenInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
@Configuration public class MvcConfig implements WebMvcConfigurer {
@Resource private StringRedisTemplate stringRedisTemplate;
@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0); registry.addInterceptor(new LoginInterceptor()).excludePathPatterns( "/shop/**", "shop-type/**", "/upload/**", "/blog/hot", "/user/code", "/user/login" ).order(1); } }
|
添加Redis缓存

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| @Resource private StringRedisTemplate stringRedisTemplate;
@Override public Result queryShopById(Long id) { String key = CACHE_SHOP_KEY + id;
String shopStr = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(shopStr)) { Shop shop = JSONUtil.toBean(shopStr, Shop.class); return Result.ok(shop); }
Shop shop = getById(id);
if(shop == null) { return Result.fail("商铺不存在"); }
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));
return Result.ok(shop); }
|
缓存更新策略



根据分析可得对于较为频繁访问的数据采用主动更新的策略, 更新时选择先操作数据库, 再删除缓存.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Override public Result update(Shop shop) { Long id = shop.getId();
if(id == null) { return Result.fail("商品不存在"); }
updateById(shop);
stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
return Result.ok(); }
|
缓存穿透

因为布隆过滤器实现较为复杂, 在此选择实现缓存空对象

在商铺缓存中采用缓存空对象来解决缓存击穿的问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| @Resource private StringRedisTemplate stringRedisTemplate;
@Override public Result queryShopById(Long id) {
String key = CACHE_SHOP_KEY + id;
String shopStr = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(shopStr)) { Shop shop = JSONUtil.toBean(shopStr, Shop.class); return Result.ok(shop); }
if(shopStr != null) { return Result.fail("商铺信息出错~"); }
Shop shop = getById(id);
if(shop == null) { stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES); return Result.fail("商铺不存在"); }
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));
return Result.ok(shop); }
|
缓存雪崩

缓存击穿



互斥锁

用Redis实现锁
redis的SETNX命令(SET if Not eXists)
语法:SETNX key value
功能:当且仅当 key 不存在,将 key 的值设为 value ,并返回1;若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0。
1 2 3 4 5 6 7 8
| public boolean tryLock(String key) { Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS); return BooleanUtil.isTrue(flag); }
public void unLock(String key) { stringRedisTemplate.delete(key); }
|
用递归实现互斥锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| @Resource private StringRedisTemplate stringRedisTemplate;
@Override public Result queryShopById(Long id) { String key = CACHE_SHOP_KEY + id;
String shopStr = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(shopStr)) {
Shop shop = JSONUtil.toBean(shopStr, Shop.class); return Result.ok(shop); }
if(shopStr != null) { return Result.fail("商铺信息出错~"); }
String lockKey = LOCK_SHOP_KEY + id;
if(tryLock(lockKey)) {
Shop shop = getById(id);
if(shop == null) { stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES); return Result.fail("商铺不存在"); }
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));
unLock(lockKey);
return Result.ok(shop); }
try { Thread.sleep(50); String shopJSON = stringRedisTemplate.opsForValue().get(key); if(StrUtil.isNotBlank(shopJSON)) { Shop shop = JSONUtil.toBean(shopJSON, Shop.class); return Result.ok(shop); }
return queryShopById(id); } catch (InterruptedException e) { unLock(String key) stringRedisTemplate.delete(key); } }
|
用while循环实现互斥锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| @Resource private StringRedisTemplate stringRedisTemplate;
@Override public Result queryShopById(Long id) {
String key = CACHE_SHOP_KEY + id; while(true) { String shopStr = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(shopStr)) { Shop shop = JSONUtil.toBean(shopStr, Shop.class); return Result.ok(shop); }
if(shopStr != null) { return Result.fail("商铺信息出错~"); }
String lockKey = LOCK_SHOP_KEY + id;
if(tryLock(lockKey)) { try { Shop shop = getById(id); Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }
if(shop == null) { stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES); return Result.fail("商铺不存在"); }
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));
unLock(lockKey);
return Result.ok(shop); }
try { Thread.sleep(50); } catch (InterruptedException e) { return Result.fail("睡出事了~~"); } } }
|
逻辑过期

RedisData类
需要在原先类上加上一个逻辑过期日期, 为了避免改源代码, 所以有两种方法, 让RedisData类继承原始类, 或者设置一个Object类型的变量, 这里选择后者.
1 2 3 4 5 6 7 8
| import lombok.Data; import java.time.LocalDateTime;
@Data public class RedisData { private LocalDateTime expireTime; private Object data; }
|
ServiceImpl类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| public Result queryShopByIdByExpire(Long id) {
String key = CACHE_SHOP_KEY + id;
while(true) { String shopJSON = stringRedisTemplate.opsForValue().get(key);
if(StrUtil.isBlank(shopJSON)) {
return Result.fail("店铺不存在"); }
RedisData shopData = JSONUtil.toBean(shopJSON, RedisData.class); if(!shopData.getExpireTime().isBefore(LocalDateTime.now())) { return Result.ok(shopData.getData()); }
String lockKey = LOCK_SHOP_KEY + id;
if(!tryLock(lockKey)) { return Result.ok(shopData.getData());
}
String shopJSON2 = stringRedisTemplate.opsForValue().get(key); RedisData shopData2 = JSONUtil.toBean(shopJSON2, RedisData.class); if(shopData2.getExpireTime().isAfter(LocalDateTime.now())) {
return Result.ok(shopData2.getData()); }
CACHE_REBUILD_EXECUTOR.submit(() -> { try { this.saveByExpire(id, 20L); } catch (Exception e) { throw new RuntimeException(e); } finally { unLock(lockKey); } }); } }
|
封装Redis工具类
初始化与方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Slf4j @Component public class CacheClient { private StringRedisTemplate stringRedisTemplate;
public CacheClient(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; }
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public boolean tryLock(String key) { Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS); return BooleanUtil.isTrue(flag); }
public void unLock(String key) { stringRedisTemplate.delete(key); } }
|
1 2 3 4 5 6 7 8
| import lombok.Data; import java.time.LocalDateTime;
@Data public class RedisData { private LocalDateTime expireTime; private Object data; }
|
缓存与逻辑缓存(String)
1 2 3 4 5 6 7 8
| public void set(String key, Object value, Long timeout, TimeUnit unit) { stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), timeout, unit); }
public void setWithLogicalExpire(String key, Object value, Long timeout, TimeUnit unit) { RedisData objectRedisData = new RedisData<>(LocalDateTime.now().plusSeconds(unit.toSeconds(timeout)), value); stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(objectRedisData)); }
|
缓存穿透
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long timeout, TimeUnit unit) { String key = keyPrefix + id;
while(true) { String json = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(json)) { R result = JSONUtil.toBean(json, type); return result; }
if(json != null) { return null; }
R result = dbFallback.apply(id);
if(result == null) { set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES); return null; }
set(key, result, timeout, unit);
return result; } }
|
缓存穿透+缓存击穿(while互斥锁)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| public <R, ID> R queryWithPassThroughAndLock(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long timeout, TimeUnit unit) { String key = keyPrefix + id; while(true) { String json = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(json)) { R result = JSONUtil.toBean(json, type); return result; }
if(json != null) { return null; }
String lockKey = LOCK_SHOP_KEY + id; if(tryLock(lockKey)) { R result = dbFallback.apply(id);
if(result == null) { set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES); return null; }
set(key, result, timeout, unit);
unLock(lockKey);
return result; }
try { Thread.sleep(50); } catch (InterruptedException e) { throw new RuntimeException("睡出事了~~"); } } }
|
缓存击穿(逻辑过期)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long timeout, TimeUnit unit) { String key = keyPrefix + id;
while(true) { String json = stringRedisTemplate.opsForValue().get(key);
if(StrUtil.isBlank(json)) { return null; }
RedisData redisData = JSONUtil.toBean(json, RedisData.class); if(redisData.getExpireTime().isAfter(LocalDateTime.now())) { R r = JSONUtil.toBean((JSONObject) redisData.getData(), type); return r; }
String lockKey = LOCK_SHOP_KEY + id; if(!tryLock(lockKey)) { return (R)redisData.getData();
}
String json2 = stringRedisTemplate.opsForValue().get(key); RedisData redisData2 = JSONUtil.toBean(json2, RedisData.class); if(redisData2.getExpireTime().isAfter(LocalDateTime.now())) { R r = JSONUtil.toBean((JSONObject) redisData.getData(), type); return r; }
CACHE_REBUILD_EXECUTOR.submit(() -> { R r = dbFallback.apply(id); setWithLogicalExpire(key, r, timeout, unit);
unLock(lockKey); return r }); } }
|
工具类的调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| @Service public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Resource private StringRedisTemplate stringRedisTemplate;
@Resource private CacheClient cacheClient;
public Result queryShopByIdByLock(Long id) { Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES); if(shop == null) { return Result.fail("店铺不存在"); } return Result.ok(shop); }
@Override public Result queryShopByIdByExpire(Long id) { Shop shop = cacheClient.queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, 30L, TimeUnit.SECONDS); if(shop == null) { return Result.fail("店铺为空~~"); } return Result.ok(shop); } }
|
基于Redis的全局ID生成器



1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| @Component public class RedisIdWorker { private static final long BEGIN_TIMESTAMP = 1664582400L;
@Resource private StringRedisTemplate stringRedisTemplate;
public long nextId(String keyPrefix) { LocalDateTime now = LocalDateTime.now(); long nowSecond = now.toEpochSecond(ZoneOffset.UTC); long timestamp = nowSecond - BEGIN_TIMESTAMP;
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd")); long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
return timestamp << 32 | count; }
public static void main(String[] args) { LocalDateTime time = LocalDateTime.of(2022, 10, 1, 0, 0, 0); long second = time.toEpochSecond(ZoneOffset.UTC); System.out.println(second); } }
|
乐观锁 & 悲观锁

乐观锁
版本号法

CAS法

用CAS法(乐观锁)解决超卖问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| @Resource private ISeckillVoucherService seckillVoucherService;
@Resource private RedisIdWorker redisIdWorker;
@Transactional @Override public Result seckillVoucher(Long voucherId) { SeckillVoucher voucher = seckillVoucherService.getById(voucherId); if(voucher == null) { return Result.fail("优惠券不存在");
}
if(voucher.getBeginTime().isAfter(LocalDateTime.now())) { return Result.fail("秒杀尚未开始"); }
if(voucher.getEndTime().isBefore(LocalDateTime.now())) { return Result.fail("秒杀已经结束"); }
if(voucher.getStock() < 1) { return Result.fail("库存不足"); }
boolean success = seckillVoucherService.update() .setSql("stock = stock - 1") .eq("voucher_id", voucherId) .gt("stock", 0) .update(); if(!success) { return Result.fail("库存不足"); }
VoucherOrder voucherOrder = new VoucherOrder(); voucherOrder.setUserId(UserHolder.getUser().getId()); long orderId = redisIdWorker.nextId("order"); voucherOrder.setId(orderId); voucherOrder.setVoucherId(voucherId); save(voucherOrder);
return Result.ok(orderId); }
|
用悲观锁解决一人一卖问题
注意: 事务的有效性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| @Override public Result seckillVoucher(Long voucherId) {
SeckillVoucher voucher = seckillVoucherService.getById(voucherId); if(voucher == null) { return Result.fail("优惠券不存在"); }
if(voucher.getBeginTime().isAfter(LocalDateTime.now())) { return Result.fail("秒杀尚未开始"); }
if(voucher.getEndTime().isBefore(LocalDateTime.now())) { return Result.fail("秒杀已经结束"); }
if(voucher.getStock() < 1) { return Result.fail("库存不足"); }
Long userId = UserHolder.getUser().getId(); synchronized (userId.toString().intern()) { IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); return proxy.createVoucherOrder(voucherId); } }
@Transactional public Result createVoucherOrder(Long voucherId) { Long userId = UserHolder.getUser().getId(); int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count(); if(count > 0) { return Result.fail("每位用户限购一张"); }
boolean success = seckillVoucherService.update() .setSql("stock = stock - 1") .eq("voucher_id", voucherId) .gt("stock", 0) .update(); if(!success) { return Result.fail("库存不足"); }
VoucherOrder voucherOrder = new VoucherOrder(); voucherOrder.setUserId(UserHolder.getUser().getId()); long orderId = redisIdWorker.nextId("order"); voucherOrder.setId(orderId); voucherOrder.setVoucherId(voucherId); save(voucherOrder);
return Result.ok(orderId); }
|
pom.xml
pom文件需要加入以下依赖
1 2 3 4
| <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency>
|
分布式锁
前边用悲观锁实现的一人一卖存在的问题: 不同的JVM获取的不是同一把锁, 因为NGINX是负载均衡的, 当一个用户同时访问页面会被分配到不同的服务器, 不同的服务器之间的锁不共用, 从而出现错误. 而分布式锁可以解决这个问题.


基于Redis的分布式锁

如果释放锁时不判断锁标识是否是自己

用Redis分布式锁解决一人一卖
SimpleRedisLock类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import cn.hutool.core.lang.UUID; import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
public class SimpleRedisLock implements ILock{ private String name; private StringRedisTemplate stringRedisTemplate; private static final String KEY_PREFIX = "lock:"; private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) { this.name = name; this.stringRedisTemplate = stringRedisTemplate; }
public boolean tryLock(long timeout) { String threadId = ID_PREFIX + Thread.currentThread().getId(); Boolean tryLock = stringRedisTemplate.opsForValue() .setIfAbsent(KEY_PREFIX + name, threadId, timeout, TimeUnit.SECONDS); return Boolean.TRUE.equals(tryLock); }
public void unlock() { String threadId = ID_PREFIX + Thread.currentThread().getId(); String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name); if(threadId.equals(id)) { stringRedisTemplate.delete(KEY_PREFIX + name); } } }
|
VoucherOrderServiceImpl类
1 2 3 4 5 6 7 8 9 10 11 12 13
| Long userId = UserHolder.getUser().getId(); SimpleRedisLock redisLock = new SimpleRedisLock("order" + userId, stringRedisTemplate); boolean isLock = redisLock.tryLock(1200); if(!isLock) { return Result.fail("黄牛滚啊[○・`Д´・ ○]"); } try { IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); return proxy.createVoucherOrder(voucherId); } finally { redisLock.unlock(); }
|
不足

Redisson

官网地址: https://redisson.org
GitHub地址: https://github.com/redisson/redisson

Redisson配置
注意: 不推荐使用在创建项目时SpringBoot选中Redisson
引入依赖
1 2 3 4 5
| <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.13.6</version> </dependency>
|
配置Redisson客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Configuration public class RedisConfig { @Bean public RedissonClient redissonClient() { Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.150.101:6379").setPassowrd("123321");
return Redisson.create(config); } }
|
使用Redisson分布式锁解决一人一卖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Resource private RedissonClient redissonClient; Long userId = UserHolder.getUser().getId(); RLock lock = redissonClient.getLock("lock:order:" + userId); boolean isLock = lock.tryLock(); if(!isLock) { return Result.fail("黄牛滚啊[○・`Д´・ ○]"); } try { IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); return proxy.createVoucherOrder(voucherId); } finally { lock.unlock(); }
|
Redisson分布式锁原理

Redisson可重入锁原理

获取锁的Lua脚本

释放锁的Lua脚本

Redisson分布式锁主从一致性
需要所有Redis服务器同时获得锁才可以执行业务代码

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Resource private RedissonClient redissonClient;
@Resource private RedissonClient redissonClient2;
@Resource private RedissonClient redissonClient3;
private RLock lock;
@BeforeEach void setUp() { RLock lock1 = redissonClient.getLock("order"); RLock lock2 = redissonClient2.getLock("order"); RLock lock3 = redissonClient3.getLock("order");
lock = redissonClient.getMultiLock(lock1, lock2, lock3); }
|
Redis在点赞业务中的应用
上传图片
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| @PostMapping("blog") public Result uploadImage(@RequestParam("file") MultipartFile image) { try { String originalFilename = image.getOriginalFilename();
String fileName = createNewFileName(originalFilename);
image.transferTo(new File(SystemConstants.IMAGE_UPLOAD_DIR, fileName));
return Result.ok(fileName); } catch (IOException e) { throw new RuntimeException("文件上传失败", e); } }
private String createNewFileName(String originalFilename) { String suffix = StrUtil.subAfter(originalFilename, ".", true);
String name = UUID.randomUUID().toString(); int hash = name.hashCode(); int d1 = hash & 0xF; int d2 = (hash >> 4) & 0xF;
File dir = new File(SystemConstants.IMAGE_UPLOAD_DIR, StrUtil.format("/blogs/{}/{}", d1, d2)); if (!dir.exists()) { dir.mkdirs(); }
return StrUtil.format("/blogs/{}/{}/{}.{}", d1, d2, name, suffix); }
|
@TableField(exist = false)注解
@TableField(exist = false) 注解可以解决表中表的问题,加载bean属性上,表示当前属性不是数据库的字段,但在项目中必须使用,这样可以用来把一个数据表当作一个字段来输出,用来实现表中表数据输出。这样设置在新增等使用bean的时候,mybatis-plus就会忽略这个,不会报错
点赞高亮
如果当前用户已经点赞,则点赞按钮高亮显示(前端已实现,判断字段Blog类的isLike属性)
Blog类
包含用@TableField(exist = false)注解的属性isLike
1 2
| @TableField(exist = false) private Boolean isLike;
|
BlogServiceImpl类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Override public Result getBlogById(Long blogId) { Blog blog = getById(blogId); if(blog == null) { return Result.fail("Blog不存在"); } isLike(blog); return Result.ok(blog); }
private void isLike(Blog blog) { String key = BLOG_LIKED_KEY + blog.getId(); Long userId = UserHolder.getUser().getId(); Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString()); blog.setIsLike(score != null); }
|
Redis在点赞业务中的应用

Redis数据结构的选择

点赞的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| @Override public Result likeBlog(Long blogId) { Blog blog = getById(blogId);
if(blog == null) { return Result.fail("Blog不存在"); }
Long userId = UserHolder.getUser().getId();
String key = BLOG_LIKED_KEY + blogId; Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
if(score != null) { boolean cancel = update().setSql("liked = liked - 1").eq("id", blogId).update(); if(cancel) { stringRedisTemplate.opsForZSet().remove(key, userId.toString()); blog.setIsLike(false); return Result.ok(); } return Result.fail("点赞出错"); }
boolean success = update().setSql("liked = liked + 1").eq("id", blogId).update(); if(!success) { return Result.fail("点赞出错"); } stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis()); blog.setIsLike(true);
return Result.ok(); }
|
点赞排行榜的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| @Override public Result queryLikes(Long blogId) { Blog blog = getById(blogId); if(blog == null) { return Result.fail("blog不存在"); }
String key = BLOG_LIKED_KEY + blogId;
Set<String> strings = stringRedisTemplate.opsForZSet().range(key, 0, 4);
if(strings == null || strings.isEmpty()) { return Result.ok(Collections.emptyList()); }
List<Long> ids = strings.stream().map(Long::valueOf).collect(Collectors.toList());
List<UserDTO> userDTOS = userService.listByIds(ids) .stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)) .collect(Collectors.toList()); return Result.ok(userDTOS);
}
|
Redis消息队列实现异步秒杀
!!!注意: 因为Redis Stream 是 Redis 5.0 版本新增加的数据结构, 因此需要Redis版本至少为5.0, 不然就会疯狂报错o(╥﹏╥)o
写在前面: 虽然主流的消息队列技术是MQ等, 这些技术与Redis的消息队列思想大同小异, 并且有些小企业因为业务较小, 不搭建MQ集群, 选择使用Redis来完成消息队列功能, 因此该技术的学习还是很有必要的.
异步秒杀
当前我们采用串行的方法执行一人一单, 下单流程如下:
当用户发起请求,此时会请求 nginx,nginx 会访问到 tomcat,而 tomcat 中的程序,会进行串行操作,分成如下几个步骤
1、查询优惠卷
2、判断秒杀库存是否足够
3、查询订单
4、校验是否是一人一单
5、扣减库存
6、创建订单

优化方案
将订单资格判断放入到 Redis 中,只要资格通过就返回一个订单消息, 并且将下单任务添加到消息队列中.
后台有一个线程时刻读取并执行消息队列中的任务, 当队列中没有任务时就阻塞等待, 直到有新任务进入队列.
该方案将判断订单资格和将订单写入数据库分开执行, 用户在很短时间内就可以得到下单成功与否的反馈, 而写入数据库的操作可以慢慢完成.

消息队列

消息队列数据结构的选择

基于Stream的消息队列



消费者组


基于Stream的异步秒杀实现
创建一个Stream类型的消息队列,名为stream.orders.
新增秒杀优惠券的同时,将优惠券信息保存到 Redis 中.
基于Lua 脚本,判断秒杀库存、一人一单,决定用户是否抢购成功. 成功直接向stream.orders中添加消息,内容包含voucherId、userId、orderId(因为整个过程需要保证原子性, 所以需要使用Lua脚本).
项目启动时,开启一个线程任务,尝试获取stream.orders中的消息,完成下单. 实现抢购与下单异步.

Lua脚本
用Lua脚本实现资格判断和向消息队列中添加信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
local voucherId = ARGV[1]
local userId = ARGV[2]
local orderId = ARGV[3]
local stockKey = 'seckill:stoc:' .. voucherId
local orderKey = 'seckill:order:' .. voucherId
if(tonumber(redis.call('get', stockKey)) <= 0) then return 1 end
if(redis.call('sismember', orderKey, userId) == 1) then return 2 end
redis.call('incrby', stockKey, -1)
redis.call('sadd', orderKey, userId)
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId) return 0
|
VoucherOrderServiceImpl类
异步处理线程池
@PostConstruct注解: 被注解的方法,在对象加载完依赖注入后执行。
此注解是在Java EE5规范中加入的,在Servlet生命周期中有一定作用,它通常都是一些初始化的操作,但初始化可能依赖于注入的其他组件,所以要等依赖全部加载完再执行。
1 2 3 4 5 6 7 8
| private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
@PostConstruct private void init() { SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler()); }
|
线程处理业务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| private class VoucherOrderHandler implements Runnable {
@Override public void run() { while (true) { try { List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read( Consumer.from("g1", "c1"), StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)), SreamOffset.create("stream.orders", ReadOffset.lastConsumed()) ); if (list == null || list.isEmpty()) { continue; } MapRecord<String, Object, Object> record = list.get(0); Map<Object, Object> value = record.getValue(); VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
createVoucherOrder(voucherOrder);
stringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId()); } catch (Exception e) { log.error("处理订单异常", e); handlePendingList(); } } }
private void handlePendingList() { while (true) { try { List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read( Consumer.from("g1", "c1"), StreamReadOptions.empty().count(1), StreamOffset.create("stream.orders", ReadOffset.from("0")) );
if (list == null || list.isEmpty()) { break; }
MapRecord<String, Object, Object> record = list.get(0); Map<Object, Object> value = record.getValue(); VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
createVoucherOrder(voucherOrder);
stringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId()); } catch (Exception e) { log.error("处理pendding订单异常", e); try{ Thread.sleep(20); }catch(Exception e){ e.printStackTrace(); } } } } }
|
调用Lua脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| private static final DefaultRedisScript<Long> SECKILL_SCRIPT; static { SECKILL_SCRIPT = new DefaultRedisScript<>(); SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua")); SECKILL_SCRIPT.setResultType(Long.class); }
IVoucherOrderService proxy; @Override public Result seckillVoucher(Long voucherId) { Long userId = UserHolder.getUser().getId(); long orderId = redisIdWorker.nextId("order"); Long result = stringRedisTemplate.execute( SECKILL_SCRIPT, Collections.emptyList(), voucherId.toString(), userId.toString(), String.valueOf(orderId) ); int r = result.intValue(); if (r != 0) { return Result.fail(r == 1 ? "库存不足" : "不能重复下单"); }
proxy = (IVoucherOrderService) AopContext.currentProxy();
return Result.ok(orderId); }
|
添加秒杀券业务 & 创建订单业务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| @Override @Transactional public void addSeckillVoucher(Voucher voucher) { save(voucher);
SeckillVoucher seckillVoucher = new SeckillVoucher(); seckillVoucher.setVoucherId(voucher.getId()); seckillVoucher.setStock(voucher.getStock()); seckillVoucher.setBeginTime(voucher.getBeginTime()); seckillVoucher.setEndTime(voucher.getEndTime()); seckillVoucherService.save(seckillVoucher); stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString()); }
@Transactional public void createVoucherOrder(VoucherOrder voucherOrder) { boolean success = seckillVoucherService.update() .setSql("stock = stock - 1") .eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0) .update(); if (!success) { log.error("库存不足!"); return; }
save(voucherOrder);
|
Redis实现共同关注
数据结构的选择
业务步骤
在关注时以关注人id为key, 被关注人id为value放到一个set集合中.
取关用户时将被取关人id从set集合中删除.
将当前用户与被关注人的关注列表求交集, 即可得到共同关注列表.
代码实现
关注业务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| @Override public Result follow(Long followUserId, Boolean isFollow) { Long userId = UserHolder.getUser().getId();
if(BooleanUtil.isTrue(isFollow)) { Follow follow = new Follow(); follow.setUserId(userId); follow.setFollowUserId(followUserId); boolean save = save(follow); if(save) { stringRedisTemplate.opsForSet().add(LIKE_COMMON_KEY + userId, followUserId.toString()); } } else { boolean remove = remove(new QueryWrapper<Follow>() .eq("user_id", userId).eq("follow_user_id", followUserId)); if(remove) { stringRedisTemplate.opsForSet().remove(LIKE_COMMON_KEY + userId, followUserId.toString()); } } return Result.ok(); }
@Override public Result isFollow(Long followUserId) { Long userId = UserHolder.getUser().getId(); Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count(); return Result.ok(count > 0); }
|
共同关注业务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @Override public Result followCommons(Long followId) { Long userId = UserHolder.getUser().getId();
Set<String> intersect = stringRedisTemplate.opsForSet( .intersect(LIKE_COMMON_KEY + userId, LIKE_COMMON_KEY + followId);
if(intersect == null || intersect.isEmpty()) { return Result.ok(Collections.emptyList()); }
List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList()); List<UserDTO> commonUsers = userService.listByIds(ids) .stream() .map(user -> BeanUtil.copyProperties(user, UserDTO.class)) .collect(Collectors.toList());
return Result.ok(commonUsers); }
|
Redis实现关注推送

Feed流模式

Feed流的三种实现方案
拉模式

推模式

推拉结合模式

三种模式的比较
在此我们选择实现最容易的推模式
基于推模式实现关注推送功能
业务需求

滚动分页VS角标分页
滚动分页更好更人性化. 可以参考刷朋友圈, 角标分页可能会出现向下滚动分页时出现了两条作者和内容都相同的朋友圈., 而滚动分页就不会出现这样的情况. 当需要阅读新发布的一条朋友圈时可以滑到最前面查看.
角标分页

滚动分页
当出现时间戳一样的情况时, 需要借助偏移量offset来实现

业务步骤
需要满足时间戳排序和滚动分页, 可以使用ZSet集合存放关注列表发布的Blog
作者发布Blog时需要获取其粉丝列表
发布成功后需要将BlogId推送至粉丝的ZSet集合中
当用户查看点击关注按钮时获取其ZSet集合中的BlogId集合
将BlogId集合转化为Blog集合, 再加上minTime和offset形成一个新的类对象返回给前端实现分页
代码实现
发布博客业务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| @Override public Result saveBlog(Blog blog) { Long userId = UserHolder.getUser().getId(); if(userId == null) { return Result.fail("用户不存在"); } blog.setUserId(userId); boolean save = save(blog); if(!save) { return Result.fail("发布博客失败"); }
List<Follow> follows = followService.query().eq("follow_user_id", userId).list();
for (Follow follow : follows) { Long followId = follow.getId(); stringRedisTemplate.opsForZSet() .add(FEED_KEY + followId, blog.getId().toString(), System.currentTimeMillis()); }
return Result.ok(blog.getId()); }
|
查看关注列表博客
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| @Override
public Result getLikeBlog(Long max, Integer offset) {
Long userId = UserHolder.getUser().getId();
Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet() .reverseRangeByScoreWithScores(FEED_KEY + userId, 0, max, offset, PRE_BLOG);
if (typedTuples == null || typedTuples.isEmpty()) { return Result.ok(Collections.emptyList()); }
ArrayList<Object> ids = new ArrayList<>(typedTuples.size());
long minTime = 0; int os = 1; for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {
ids.add(Long.valueOf(typedTuple.getValue()));
long time = typedTuple.getScore().longValue(); if(time == minTime) { os++; } else { minTime = time; os = 1; } }
if(max == minTime) { os += offset; }
String idStr = StrUtil.join(",", ids); List<Blog> blogList = query().in("id", ids).last("order by field(id, " + idStr + ")").list(); for (Blog blog : blogList) { isLike(blog); } LikeBlogs blogs = new LikeBlogs(); blogs.setList(blogList); blogs.setLastId(minTime); blogs.setOffset(os); return Result.ok(blogs); }
|
Redis实现附近商铺功能
GEO数据结构
附近商铺搜索

将店铺存入GEO集合

这里选择在for循环里将商铺位置放到集合中, 最后在将集合存到Redis里.
而不选择在for循环中将商铺一个一个存入Redis, 因为这样每次都要与Redis建立连接, 效率不如直接将集合存入Redis.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| List<Shop> list = list();
Map<Long, List<Shop>> map = list.stream().collect(Collectors.groupingBy(Shop::getTypeId));
for (Map.Entry<Long, List<Shop>> entry : map.entrySet()) { Long typeId = entry.getKey(); String key = "shop:geo:" + typeId; List<Shop> value = entry.getValue(); List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>(value.size()); for (Shop shop : value) { locations.add(new RedisGeoCommands.GeoLocation<>( shop.getId().toString(), new Point(shop.getX(), shop.getY()) )); } stringRedisTemplate.opsForGeo().add(key, locations); }
|
代码实现
环境配置
spring-data-redis 2.3.9 版本并不支持 Redis 6.2 提供的 GEOSEARCH 命令,因此我们需要提升版本,修改自己的 POM文件.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <exclusions> <exclusion> <artifactId>spring-data-redis</artifactId> <groupId>org.springframework.data</groupId> </exclusion> <exclusion> <artifactId>lettuce-core</artifactId> <groupId>io.lettuce</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-redis</artifactId> <version>2.6.2</version> </dependency> <dependency> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> <version>6.1.6.RELEASE</version> </dependency>
|
实现类方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| @Override
public Result queryShopp(Integer typeId, Integer current, Double x, Double y) {
if (x == null || y == null) { Page<Shop> page = query() .eq("type_id", typeId) .page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE)); return Result.ok(page.getRecords()); }
int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE; int end = current * SystemConstants.DEFAULT_PAGE_SIZE;
String key = SHOP_GEO_KEY + typeId; GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo() .search( key, GeoReference.fromCoordinate(x, y), new Distance(5000), RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end) );
if (results == null) { return Result.ok(Collections.emptyList()); } List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent(); if (list.size() <= from) { return Result.ok(Collections.emptyList()); }
List<Long> ids = new ArrayList<>(list.size()); Map<String, Distance> distanceMap = new HashMap<>(list.size()); list.stream().skip(from).forEach(result -> { String shopIdStr = result.getContent().getName(); ids.add(Long.valueOf(shopIdStr)); Distance distance = result.getDistance(); distanceMap.put(shopIdStr, distance); });
String idStr = StrUtil.join(",", ids); List<Shop> shops = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list(); for (Shop shop : shops) { shop.setDistance(distanceMap.get(shop.getId().toString()).getValue()); }
return Result.ok(shops); }
|
Redis实现签到功能
BitMap


BitMap的用法

签到功能

代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Override
public Result sign() {
Long userId = UserHolder.getUser().getId(); if(userId == null) { return Result.fail("用户未登录"); }
LocalDateTime now = LocalDateTime.now(); String key = USER_SIGN_KEY + userId + now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
int dayOfMonth = now.getDayOfMonth();
stringRedisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true); return Result.ok("签到成功"); }
|
签到统计

实现签到统计功能

代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| @Override public Result signCount() { Long userId = UserHolder.getUser().getId(); if(userId == null) { return Result.fail("用户未登录"); }
LocalDateTime now = LocalDateTime.now(); String key = USER_SIGN_KEY + userId + now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
int dayOfMonth = now.getDayOfMonth();
List<Long> result = stringRedisTemplate.opsForValue().bitField( key, BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0) );
if(result == null || result.isEmpty()) { return Result.ok(0); }
Long num = result.get(0); if(num == null || num == 0) { return Result.ok(0); }
int count = 0; while(true) { if((num.intValue() & 1) == 1 ) { count++; } else { break; } num >>>= 1; } return Result.ok("已连续签到" + count + "天"); }
|
Redis实现UV统计

HyperLogLog用法
Hyperloglog(HLL)是从Loglog算法派生的概率算法,用于确定非常大的集合的基数,而不需要存储其所有值。
相关算法原理大家可以参考:https://juejin.cn/post/6844903785744056333#heading-0
Redis中的HLL是基于string结构实现的,单个HLL的内存永远小于16kb,内存占用低的令人发指!作为代价,其测量结果是概率性的,有小于0.81%的误差。不过对于UV统计来说,这完全可以忽略。

实现UV统计
在这里我们直接在UserController类里加入一个uv统计接口,向HyperLogLog中添加100万条数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @GetMapping("/uv") public Result uv() { String[] values = new String[1000]; int j = 0; for (int i = 0; i < 1000000; i++) { j = i % 1000; values[j] = "user_" + i; if(j == 999) { stringRedisTemplate.opsForHyperLogLog().add("h12", values); } } Long count = stringRedisTemplate.opsForHyperLogLog().size("h12"); return Result.ok(count); }
|

可见结果与实际十分接近, 用计算器求得误差为: 0.2407%

并且内存占用很少!!!