Redis的安装
Redis官网 redis.io/
最新版本的Redis已经到7.0了
这个是在centos7.6上的安装教程
1、首先确保需虚拟机上安装了 gcc , 可以使用以下的命令进行检查
gcc -v
我这没有安装,使用以下命令安装
yum install -y gcc
2、手动上传安装包 我这里上传到的是/usr/local目录下,并减压
cd /usr/local
tar -zxvf redis的压缩包名字
3、编译文件
进入刚才压缩出来的redis文件
执行make指令进行编译
4、安装到指定的目录
make install PREFIX=/usr/local/redis
5、修改配置文件
先拷贝一个redis的配置文件到/usr/local/redis/bin的目录下
修改为远程链接
修改成守护线程
设置密码
启动redis服务
6、设置开机自动启动
cd /lib/systemd/system/
# 新建文件
vim redis.service
[Unit]
Description=redis-server
After=network.target
[Service]
Type=forking
# ExecStart需要按照实际情况修改成自己的地址
ExecStart=/usr/local/redis/bin/redis-server /usr/local/redis/bin/redis.conf
PrivateTmp=true
[Install]
WantedBy=multi-user.target
设置开机自动启动
# 开机自动启动
systemctl enable redis.service
# 启动redis服务
systemctl start redis.service
# 查看服务状态
systemctl status redis.service
# 停止服务
systemctl stop redis.service
# 取消开机自动启动(卸载服务)
systemctl disabled redis.service
开放redis的应用端口
# 开放redis端口
firewall-cmd --zone=public --add-port=6379/tcp --permanent
# 应用
firewall-cmd --reload
Redis基础
Redis中的基本数据类型和常用命令
命令参考地址:redisdoc.com/string/set.…
使用Jedis连接Redis
准备以下的pom依赖
<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.7.0</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
</dependencies>
连接代码
public class ClientDemo {
public static void main(String[] args) {
//建立链接
Jedis jedis = new Jedis("192.168.216.140",6379);
//设置密码
jedis.auth("123456");
//选择数据库
jedis.select(0);
String isSetSuccess = jedis.set("key", "老牛爱redis01");
String value = jedis.get("key");
System.out.println(isSetSuccess + ":" + value);
}
}
由于Jedis本省是线程不安全的,并且频繁的创建连接会有性能上的损耗,因此我们可以使用jedis的连接池代替原来的连接方式。
//jedis 连接池的代码
public class JedisConnectionFactory {
private static final JedisPool jedisPool;
static {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
//最大的连接数
jedisPoolConfig.setMaxTotal(8);
//最大的空闲连接
jedisPoolConfig.setMaxIdle(8);
//设置等待的时长
jedisPoolConfig.setMinIdle(0);
//设置连接池的等待时长
jedisPoolConfig.setMaxWaitMillis(200);
jedisPool = new JedisPool(jedisPoolConfig, "192.168.216.140", 6379,
1000, "123456");
}
//返回jedis 对象
public static Jedis getJedis(){
return jedisPool.getResource();
}
}
SpringDataRedis
SpringDataRedis 是Spring中的数据操作模块,包含了对各种数据库的集成,其中对Redis集成的模块就是SpringDataRedis
SpringDataRedis 提供了对不同Redis客户端的整合(JedisLettuce)
提供了RedisTemplate统一API来操作Redis
支持Redis 的发布订阅模式
支持Redis的哨兵和集群
支持Lettuce的响应式编程
支持基于JDK、JSON、字符串、Spring对象的数据序列化及反序列化
支持基于Redis的JDKCollection实现
API
演示案例:
1、创建一个新的模块SpringDataRedis,并且引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.2</version>
</dependency>
2、写配置文件
spring:
redis:
host: 192.168.216.140
port: 6379
password: 123456
lettuce:
pool:
max-active: 8 # 最大连接
max-idle: 8 # 最大空闲连接
min-idle: 0 # 最小空闲连接
max-wait: 100 # 连接等待时间
3、写测试代码
@SpringBootTest
class SpringDataRedisApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
//向redis数据库中写入一条数据
redisTemplate.opsForValue().set("name","niuxiaon");
//从数据库中读取一条数据
Object name = redisTemplate.opsForValue().get("name");
System.out.println(name);
}
}
执行结果:
可以看到数据库里已经添加进入了,但是会发现出现了一堆不认识的字符,这就要说一说RedisTemplate序列化的方式了,他是将Object对象序列化成字节的方式,采用的是JDK默认的序列化方式。这种序列化方式缺点就是可读性差,然后占用内存也会比较多一点。
我们可以自定义RedisTemplate的序列化方式。
编写自己指定的配置类
@Configuration
public class RedisTemplateConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
//创建Template
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
//设置工厂的连接
redisTemplate.setConnectionFactory(redisConnectionFactory);
//设置序列化工具
GenericJackson2JsonRedisSerializer jsonRedisSerializer =
new GenericJackson2JsonRedisSerializer();
//Key和HashKey采用String的方式进行序列化
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
redisTemplate.setValueSerializer(jsonRedisSerializer);
redisTemplate.setHashValueSerializer(jsonRedisSerializer);
return redisTemplate;
}
}
还需要以下依赖,依赖的版本让他自己去和RedisTemplate相互适配。所以不要指定版本,否则可能会产生一些问题。
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
重新编写一个测试方法
@Test
public void saveUser(){
User user = new User();
user.setName("牛小牛");
user.setAge(22);
redisTemplate.opsForValue().set("user",user);
//从数据库中读取一条数据
Object name = redisTemplate.opsForValue().get("user");
System.out.println(name);
}
}
他会将这个对象的类型名称也写入进入造成了额外的内存开销。
为了解决这个问题,SpringData还提供了一个 StringRedisTemplate
Redis应用实战
实战项目使用的是黑马程序员的黑马点评项目
短信登录功能
通过Session 登录
session登录流程
项目启动以后张这个样子,部署过程略。
1、首先要完成的是验证码的获取
完善这个方法
@Override
public Result sendCode(String phone, HttpSession session) {
//1、手机号格式校验
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式不正确");
}
//2、如果手机号符合要求则生成验证码
String code = RandomUtil.randomNumbers(6);
//3、将验证码保存到Session中
session.setAttribute("code",code);
//4、模拟发送验证码
log.debug("发送短信验证码成功,验证码为{}",code);
return Result.ok();
}
测验:输入手机号,点击发送
在这里可以看到已经成功发送了验证码
2、完成模拟登陆注册功能
完善这个login方法
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
String phone = loginForm.getPhone();
//1、手机号校验
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式不正确");
}
//2、验证码校验
Object code = session.getAttribute("code");
if (!code.toString().equals(loginForm.getCode()) || code == null){
return Result.fail("验证码不正确");
}
//3、从数据库查询用户
User user = query().eq("phone", phone).one();
//3.1、如果这个对象为空,则创建
if (user == null){
user = createUserWithPhone(phone);
}
//4、将用户信息保存在session中
session.setAttribute("user",user);
return Result.ok();
}
private User createUserWithPhone(String phone) {
User user = new User();
user.setPhone(phone);
user.setNickName("user_"+RandomUtil.randomString(10));
return user;
}
3、做我们的登录状态校验
配置拦截器
public class LoginIntercept implements HandlerInterceptor {
//前置
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1、获取session
HttpSession session = request.getSession();
//2、获取session中的内容
Object user = session.getAttribute("user");
//3、判断用户是否存在
if (user == null) {
//3.1、进行拦截
response.setStatus(401);
return false;
}
//4、将用户保存在ThreadLocal中
UserHolder.saveUser((User) user);
return true;
}
//后置
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}
让拦截器生效
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginIntercept()).excludePathPatterns(
"/user/code",
"/user/login",
"/blog/hot",
"/shop/**",
"/shop-type/**",
"/upload/**",
"/voucher/**"
);
}
}
去通过 me方法获取到线程池里面的内容
运行测试
可以看到已经运行成功了。但是一些敏感信息也在前端返回了,这个不合理的,所以修改。这个返回内容
创建一个新的ThreadLocal
修改Login方法
修改前置拦截器
修改me方法
这个时候再次进行测试,这个时候就没有敏感信息了
通过Redis 登录
为什么要通过Redis实现登录,Session的登录方式有什么弊端。
Session 共享问题:多个tomcat 的服务器,没有办法共享Session的存储空间,当请求切换到不同的tomcat服务器时候导致数据丢失问题。
基于Redis的登录流程
使用redis以后他们的key和value分别采用什么结构比较合适?value 存储的是验证码,显然我们使用String结构即可,但是Key呢?,要保证key是唯一,如果Key不是唯一的,会导致数据覆盖问题。
用户对象使用什么结构来保存,用户对象可以使用两种结构来保存,String结构和Hash结构
1、修改发送验证码的业务逻辑
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public Result sendCode(String phone, HttpSession session) {
//Redis的方式进行验证码的发送
//1、验证手机号
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式不正确");
}
//2、如果手机号符合要求则生成验证码
String code = RandomUtil.randomNumbers(6);
//3、将验证码保存到Redis中
stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY +phone,code,RedisConstants.LOGIN_CODE_TTL,TimeUnit.MINUTES);
//4、模拟发送验证码
log.debug("发送短信验证码成功,验证码为{}",code);
//5、返回
return Result.ok();
}
运行测试一下,可以看到验证码已经存到Redis数据库里面了
2、修改登录的业务逻辑
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
//1、手机号校验
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式不正确");
}
//2、从Redis中获取验证码并校验
String cacheCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);
if (!cacheCode.equals(loginForm.getCode()) || cacheCode == null){
return Result.fail("验证码不正确");
}
//3、从数据库查询用户
User user = query().eq("phone", phone).one();
//3.1、如果数据库中不存在这个用户则创建
if (user == null){
user = createUserWithPhone(phone);
}
//4、将用户信息保存在Redis中
//4.1、生成随机的Token,作为登录令牌
String token = UUID.randomUUID().toString(true);
//4.2、将User对象转为Hash进行存储
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO);
//4.3、存储在redis中
String tokenKey = RedisConstants.LOGIN_USER_KEY+token;
stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
//设置Token有效期
stringRedisTemplate.expire(tokenKey,30,TimeUnit.MINUTES);
//5、返回
return Result.ok(token);
}
private User createUserWithPhone(String phone) {
User user = new User();
user.setPhone(phone);
user.setNickName("user_"+RandomUtil.randomString(10));
return user;
}
3、修改拦截器,在拦截器中做token更新操作
public class LoginIntercept implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public LoginIntercept(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
//前置
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1、获取请求头中的token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)){
response.setStatus(401);
return false;
}
//2、从Redis中获取用户
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY+token);
//3、判断用户是否存在
if (userMap.isEmpty()){
response.setStatus(401);
return false;
}
//4、将查询到的Hash数据转化成UserDTO对象
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//5、将用户的信息保存在ThreadLocal中
UserDTOHolder.saveUser(userDTO);
//6、刷新token的有效期
stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY+token,
30, TimeUnit.MINUTES);
//7、返回
return true;
}
//后置
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}
4、me方法不用修改,直接操作即可
5、运行测试
可以看到用户的信息都存在了Redis中
使用redis代替session需要考虑到的问题
选择合适的数据结构,选择合适的key,选择合适的存储粒度
拦截器优化
使用第一个拦截器用来做token的动态刷新操作,第二个拦截器则是做用户检查操作
创建一个新的拦截器RefreshTokenIntercept
这个拦截器做的工作就是刷新token然后将用户保存在ThreadLocal中
public class RefreshTokenIntercept implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenIntercept(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
//前置
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1、获取请求头中的token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)){
return true;
}
//2、从Redis中获取用户
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY+token);
//3、判断用户是否存在
if (userMap.isEmpty()){
return true;
}
//4、将查询到的Hash数据转化成UserDTO对象
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//5、将用户的信息保存在ThreadLocal中
UserDTOHolder.saveUser(userDTO);
//6、刷新token的有效期
stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY+token,
30, TimeUnit.MINUTES);
//7、返回
return true;
}
//后置
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}
修改登录拦截器LoginIntercept
public class LoginIntercept implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public LoginIntercept(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public LoginIntercept() {
}
//前置
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1、判断ThreadLocal中有没有我们的用户
UserDTO user = UserDTOHolder.getUser();
//没有用户则进行拦截
if (user == null){
response.setStatus(401);
return false;
}
//有用户放行
return true;
}
//后置
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}
注册拦截器
Order用来指定拦截器的执行顺序,值越大执行级别越低
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginIntercept()).excludePathPatterns(
"/user/code",
"/user/login",
"/blog/hot",
"/shop/**",
"/shop-type/**",
"/upload/**",
"/voucher/**"
).order(1);
//拦截所有的请求
registry.addInterceptor(new RefreshTokenIntercept(stringRedisTemplate))
.addPathPatterns("/**").order(0);
}
}
商户查询缓存
什么是缓存?缓存就是数据交换的缓冲区,是存储数据的临时地方,一般情况下读写性能比较高。
缓存的作用:在查询的时候可以先查询缓存,降低后端的负载,提高读写效率,降低响应时间
缓存的成本:数据一致性问题,代码维护复杂度提高,运维成本。
添加Redis缓存
1、根据这个流程来添加Redis的缓存,让他先查询缓存如果缓存中没有在查询数据库。
这个方法的业务实现
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryById(Long id) {
//1、从redis中查询店铺缓存
String key = RedisConstants.CACHE_SHOP_KEY + id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2、判断店铺是否存在
if (StrUtil.isNotBlank(shopJson)){
//如果存在,则直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
//3、如果不存在,则根据id查询数据库
Shop shop = getById(id);
//3.1、如果查询数据库了还不存在,则返回错误信息
if (shop == null){
return Result.fail("店铺不存在");
}
//4、如果店铺存在则放入到redis中
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));
//5、返回
return Result.ok(shop);
}
启动测试
可以看到这个缓存已经成功添加到Redis中了
缓存的更新策略
缓存的更新策略正是为了解决缓存和数据库数据内容不一致问题。
案例:
1、在刚才的店铺查询方法中设置他的有效期
2、完成修改店铺的操作
先修改数据库,然后在删除缓存。
完善这个更新方法
@Override
@Transactional
public Result update(Shop shop) {
Long id = shop.getId();
if (id == null) {
return Result.fail("店铺的Id不能为空");
}
//1、更新数据库
updateById(shop);
//2、删除缓存
stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY + id);
return Result.ok();
}
接下来就可以进行测试了。在这里使用postMan进行模拟测试
可以看到这个数据已经被添加到redis中去了,同时设置了他的过期时间
缓存穿透
什么是缓存的穿透?缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远都不会生效,这些请求都会统统打到数据库
解决缓存穿透的两种方案:
方案一:缓存空对象
优点:实现简单,方便维护
缺点:使用了额外的内存消耗,可能会造成短期的不一致问题
方案二:布隆过滤器
优点:内存的占用比较少,没有多余的key
缺点:可能会造成误判
案例演示:
使用方案一进行解决
1、修改店铺的查询方法
启动测试:
可以看到这个redis里面存储了一个空的字符串
总结:以及解决方案
缓存雪崩
缓存雪崩: 指的是在同一时间端大量的缓存key同时失效或者redis宕机,导致大量的请求到达数据库,带来的巨大压力
解决方案:
-
方案1、给不同的key添加随机的时间值
-
方案2、利用redis集群提高服务的可用性
-
方案3、给缓存业务添加降级限流策略
-
方案4、给业务添加多级缓存
缓存击穿
缓存击穿问题: 缓存击穿问题也称之为热点key的问题,是一个被高并发并且缓存重建业务比较复杂的key突然失效了,无数的请求访问会在一瞬去访问数据库。
常见的两种解决方案:互斥锁、逻辑过期
这两种方案的优劣对比
案例:通过互斥锁的方式用来解决缓存击穿问题
业务流程做出以下的调整
实现
//缓存击穿
public Shop queryWithMutex(Long id) {
//1、从redis中查询店铺缓存
String key = RedisConstants.CACHE_SHOP_KEY + id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2、判断店铺是否存在
if (StrUtil.isNotBlank(shopJson)){
//如果存在,则直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return shop;
}
if (shopJson!=null){
return null;
}
String lockKey = "lock:shop:" + id;
Shop shop = null;
try {
//3、实现缓存的重建
//3.1、获取互斥锁
boolean isLock = tryLock(lockKey);
//3.2、判断是否获取锁成功
if (!isLock){
//3.3、获取锁失败,失败以后休眠并重试
Thread.sleep(50);
return queryWithMutex(id);
}
//3.4、获取锁成功
shop = getById(id);
Thread.sleep(200);
if (shop == null){
//将空信息写入到Redis
stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
return null;
}
//4、如果店铺存在则放入到redis中
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),30, TimeUnit.MINUTES);
}catch (InterruptedException e){
throw new RuntimeException();
}finally {
//5、释放互斥锁
unlock(lockKey);
}
//6、返回
return shop;
}
进行测试:
使用压力测试工具
可以看到整个过程只请求了一次数据库
案例:使用逻辑过期方式解决缓存击穿问题
逻辑删除过期方式的业务流程
1、首先要设置数据的过期时间
public void saveShop2Redis(Long id,Long expireSeconds){
//1、查询店铺的数据
Shop shop = getById(id);
//2、封装逻辑过期时间
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id,
JSONUtil.toJsonStr(redisData));
}
测试方法:
@SpringBootTest
class HmDianPingApplicationTests {
@Autowired
private ShopServiceImpl shopService;
@Test
public void testSaveShop(){
shopService.saveShop2Redis(1L,10L);
}
}
测试结果
在这里可以看到我们指定的过期时间
2、逻辑过期解决方法代码
@Override
public Result queryById(Long id) {
//利用逻辑过期的方式解决缓存击穿问题
Shop shop = queryWithLogicalExpire(id);
return Result.ok(shop);
}
//缓存击穿 逻辑过期方式解决
public Shop queryWithLogicalExpire(Long id){
//1、从redis中查询店铺缓存
String key = RedisConstants.CACHE_SHOP_KEY + id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2、判断店铺是否存在
if (StrUtil.isBlank(shopJson)){
//如果不存在,则直接返回
return null;
}
//3、如果命中了需要判断过期的时间
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
//4、判断是否过期
if (expireTime.isAfter(LocalDateTime.now())){
//4.1、没过期,直接返回店铺信息
return shop;
}
//4.2、过期,需要重新创建缓存
//5、缓存重新建立
//5.1、拿到互斥锁
String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
//5.2、没有拿到互斥锁
if (isLock){
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
//重新建立缓存
this.saveShop2Redis(id,20L);
}catch (Exception e){
throw new RuntimeException(e);
}finally {
//释放锁
unlock(lockKey);
}
});
}
return shop;
}
运行测试
缓存工具封装
一个工具类
@Slf4j
@Component
public class CacheClient {
private final StringRedisTemplate stringRedisTemplate;
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public void set(String key, Object value, Long time, TimeUnit unit) {
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
}
public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
// 设置逻辑过期
RedisData redisData = new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
// 写入Redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
public <R,ID> R queryWithPassThrough(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){
String key = keyPrefix + id;
// 1.从redis查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(json)) {
// 3.存在,直接返回
return JSONUtil.toBean(json, type);
}
// 判断命中的是否是空值
if (json != null) {
// 返回一个错误信息
return null;
}
// 4.不存在,根据id查询数据库
R r = dbFallback.apply(id);
// 5.不存在,返回错误
if (r == null) {
// 将空值写入redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// 6.存在,写入redis
this.set(key, r, time, unit);
return r;
}
public <R, ID> R queryWithLogicalExpire(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyPrefix + id;
// 1.从redis查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isBlank(json)) {
// 3.存在,直接返回
return null;
}
// 4.命中,需要先把json反序列化为对象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
LocalDateTime expireTime = redisData.getExpireTime();
// 5.判断是否过期
if(expireTime.isAfter(LocalDateTime.now())) {
// 5.1.未过期,直接返回店铺信息
return r;
}
// 5.2.已过期,需要缓存重建
// 6.缓存重建
// 6.1.获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
// 6.2.判断是否获取锁成功
if (isLock){
// 6.3.成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// 查询数据库
R newR = dbFallback.apply(id);
// 重建缓存
this.setWithLogicalExpire(key, newR, time, unit);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
// 释放锁
unlock(lockKey);
}
});
}
// 6.4.返回过期的商铺信息
return r;
}
public <R, ID> R queryWithMutex(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyPrefix + id;
// 1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3.存在,直接返回
return JSONUtil.toBean(shopJson, type);
}
// 判断命中的是否是空值
if (shopJson != null) {
// 返回一个错误信息
return null;
}
// 4.实现缓存重建
// 4.1.获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
R r = null;
try {
boolean isLock = tryLock(lockKey);
// 4.2.判断是否获取成功
if (!isLock) {
// 4.3.获取锁失败,休眠并重试
Thread.sleep(50);
return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
}
// 4.4.获取锁成功,根据id查询数据库
r = dbFallback.apply(id);
// 5.不存在,返回错误
if (r == null) {
// 将空值写入redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// 6.存在,写入redis
this.set(key, r, time, unit);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
// 7.释放锁
unlock(lockKey);
}
// 8.返回
return r;
}
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
private void unlock(String key) {
stringRedisTemplate.delete(key);
}
}
优惠卷秒杀
全局唯一性ID
@Component
public class RedisIdWorker {
/**
* 开始时间戳
*/
private static final long BEGIN_TIMESTAMP = 1640995200L;
/**
* 序列号的位数
*/
private static final int COUNT_BITS = 32;
private StringRedisTemplate stringRedisTemplate;
public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public long nextId(String keyPrefix) {
// 1.生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond - BEGIN_TIMESTAMP;
// 2.生成序列号
// 2.1.获取当前日期,精确到天
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
// 2.2.自增长
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
// 3.拼接并返回
return timestamp << COUNT_BITS | count;
}
}
全唯一性ID的生成策略
- UUID
- Redis自增
- 雪花算法
- 数据库自增
Redis自增ID策略
- 每天一个Key,方便统计订单量
- ID构造器是时间戳 + 计数器
秒杀功能实现
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Autowired
private ISeckillVoucherService seckillVoucherService;
@Autowired
private RedisIdWorker redisIdWorker;
@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
//1、查询优惠卷
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
//2、判断秒杀是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())){
return Result.fail("秒杀活动尚未开始");
}
//3、判断秒杀是否结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())){
return Result.fail("秒杀活动已经结束");
}
//4、判断库存是否充足
if (voucher.getStock()<1){
return Result.fail("抱歉,优惠券已经被抢光了");
}
//5、扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.update();
if (!success){
//扣减失败
return Result.fail("库存不足");
}
//6、创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//生成唯一ID
Long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//存储用户ID
Long userId = UserDTOHolder.getUser().getId();
voucherOrder.setUserId(userId);
//代金卷Id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
return Result.ok(orderId);
}
}
启动测试
此时已经实现了基本的秒杀功能,但是在高并发的场景下可能会存在买超的问题,所以还要对一下的代码进行改进
解决优惠券买超的问题
出现这种问题的原因就是,多个线程操作了同一个共享变量,这个时候我们可以使用悲观锁(synchronized、Lock)的或者乐观锁(CAS)的方式进行解决。
只需要将这段代码修改成如下的即可,使用stock这个字段用来充当version这个字段的信息,也就是使用了乐观锁的方式进行解决
小总结
一人一单
一个人只可以抢购一个单优惠券,这个时候要考虑到多线程的安全问题
主调方法
public Result seckillVoucher(Long voucherId) {
//1、查询优惠卷
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
//2、判断秒杀是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())){
return Result.fail("秒杀活动尚未开始");
}
//3、判断秒杀是否结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())){
return Result.fail("秒杀活动已经结束");
}
//4、判断库存是否充足
if (voucher.getStock()<1){
return Result.fail("抱歉,优惠券已经被抢光了");
}
//使用intern()方法会直接到常量池中找相对应的字符串
Long userId = UserDTOHolder.getUser().getId();
synchronized (userId.toString().intern()){
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createOrder(voucherId);
}
}
方法抽取
因为要保证数据的一致性和原子性,所以使用@Transactional注解来控制事物。
@Transactional
public Result createOrder(Long voucherId){
//一人一单的功能
Long userId = UserDTOHolder.getUser().getId();
//查询订单
Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count>0){
return Result.fail("您已经抢购过了,请不要重复请购");
}
//5、扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId).gt("stock",0)
.update();
if (!success){
//扣减失败
return Result.fail("库存不足");
}
//6、创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//生成唯一ID
Long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//存储用户ID
voucherOrder.setUserId(userId);
//代金卷Id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
return Result.ok(orderId);
}
暴露代理对象
在单机模式下这样写完全没有问题,也解决了线程的安全问题,但是但是在集群模式下,还是会出现线程的安全问题。如何可以解决这样的问题,请使用分布式锁
分布式锁
什么是分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。
分布式锁的实现方案
基于Redis实现分布式锁
可以利用setnx的互斥性
如果我们获取到锁了,但是同时redis服务又宕机了,这个锁没有来得及释放,其他的线程就进不来,如何解决。所以需要在添加锁的时候设置超时释放。
分布式锁实现1
1、创建接口
/**
* 分布式锁
*/
public interface ILock {
/**
* 尝试获取锁
* @param timeOutSec
* @return
*/
boolean tryLock(long timeOutSec);
/**
* 释放锁
*/
void unlock();
}
2、接口实现
public class SimpleRedisLock implements ILock {
private String name;
private StringRedisTemplate stringRedisTemplate;
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
private static final String KEY_PREFIX = "lock:";
@Override
public boolean tryLock(long timeOutSec) {
long threadId = Thread.currentThread().getId();
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeOutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
//所释放
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
3、一人一单代码修改
将原来使用synchronized方式修改成分布式事务方式
4、模拟测试
在Ide工具里面将services调出来
按Ctrl + D 在这里添加一个启动类
调整nginx配置
创建两个请求用来进行模拟测试
可能会出现的问题:
分布式锁实现1:改进
修改之前的分布式锁满足: 1、在获取锁时存入线程标示(可以用UUID) 2、在释放时候先获取锁中的线程标示,判断是否与当前线程标示一致
- 如果一致则释放锁
- 如果不一致则不释放锁
修改分布式锁实现类
public class SimpleRedisLock implements ILock {
private String name;
private StringRedisTemplate stringRedisTemplate;
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override
public boolean tryLock(long timeOutSec) {
String threadId = ID_PREFIX + Thread.currentThread().getId();
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeOutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
//获取线程中的标示
String threadId = ID_PREFIX + Thread.currentThread().getId();
//获取锁中的标示
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
//判断ID是否一致
if (threadId.equals(id)){
//所释放
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
}
分布式锁实现1:改进改进
使用Lua语言脚本来解决一致性的问题
1、创建Lua脚本
-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
-- 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
-- 一致,则删除锁
return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0
2、读取Lua脚本
3、执行调用Lua脚本
Redisson 解决线程安全问题
1、引入pom文件
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
2、将RedissonClient 加入到spring中管理
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissionClient(){
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.216.140:6379")
.setPassword("123456");
return Redisson.create(config);
}
}
3、修改抢票的代码
4、并发测试
Redisson可重入锁的原理
Redisson分布式锁原理
Redisson分布式主从一致问题
一个线程两次获取锁,就是锁的重入。
秒杀优化
改进秒杀业务,提高并发
① 将优惠券信息保存在Redis中
达人探店
发布探店笔记功能
UploadController 里面文件上传的地址修改成自己的
保存信息的时候,如果出现乱码的现象,请修改yml配置
查看探店详情信息
@Override
public Result queryBlogById(Long id) {
//1、查询blog
Blog blog = getById(id);
if (blog == null){
return Result.fail("博客不存在");
}
//2、查询blog相关用户
User user = userService.getById(blog.getUserId());
blog.setName(user.getNickName()).setIcon(user.getIcon());
return Result.ok(blog);
}
实现点赞
实现思路:
首先要知道用户点赞的是那一条评论。在这里就需要两个信息,一个是用户信息,另外一个就是评论的信息。 使用redis存储用户信息和评论信息。为了避免同一个用户重复点赞,所以可以使用set结构。
获取到用户的信息,判断当前的用户是否已经点赞。没有点赞,则可以点赞,数据库点赞字段 + 1,并将点赞的信息村到redis中。如果已经点赞,则从数据库点赞字段 - 1,并从redis中剔除点赞信息。
@Override
public Result likeBlog(Long id) {
//1、获取登录用户
Long userId = UserHolder.getUser().getId();
//2、判断当前登录用户是否已经点赞
String key = RedisConstants.BLOG_LIKED_KEY + id;
Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
if (BooleanUtil.isFalse(isMember)) {
//3、如果未点赞,可以点赞
//3.1、数据库点赞数 + 1
//3.2、保存用户到redis的set集合
boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();
if (isSuccess) {
stringRedisTemplate.opsForSet().add(key, userId.toString());
}
} else {
//4、如果是已经点赞了,取消点赞
//4.1、数据库点赞数量 - 1
//4.2、吧用户从Redis中移除
boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();
if (isSuccess) {
stringRedisTemplate.opsForSet().remove(key, userId.toString());
}
}
return Result.ok();
}
实现点赞排序
//实现点赞排序功能
@Override
public Result likeBlogs(Long id) {
String key = RedisConstants.BLOG_LIKED_KEY + id;
Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);
if (top5 == null || top5.isEmpty()){
return Result.ok(Collections.emptyList());
}
List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());
String idStr = StrUtil.join(",", ids);
List<UserDTO> userDTOS = userService.query().in("id",ids)
.last("ORDER BY FIELD(id,"+idStr+")").list().stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
return Result.ok(userDTOS);
}
好友关注
关注和取关
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private IUserService userService;
@Override
public Result follow(Long followUserId, Boolean isFollow) {
//1、获取登录用户
Long userId = UserHolder.getUser().getId();
//关注
if (isFollow){
Follow follow = new Follow();
follow.setUserId(userId).setFollowUserId(followUserId);
boolean isSuccess = save(follow);
if (isSuccess){
//关注的用户集合放入到redis中
stringRedisTemplate.opsForSet().add(RedisConstants.FOLLOWS + userId , followUserId.toString());
}
}else{
//取消关注
boolean isSuccess = remove(new QueryWrapper<Follow>().eq("user_id", userId)
.eq("follow_user_id", followUserId)
);
//关注的用户集合从redis中移除
if (isSuccess) {
stringRedisTemplate.opsForSet().remove(RedisConstants.FOLLOWS + userId , followUserId.toString());
}
}
return Result.ok();
}
@Override
public Result followOrNot(Long followUserId) {
//1、获取登录用户
Long userId = UserHolder.getUser().getId();
Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count();
return Result.ok(count > 0);
}
共同关注
查询用户信息
查看用户发布的博文
共同关注的方法
@Override
public Result followCommons(Long id) {
//1、获取当前用户
Long userId = UserHolder.getUser().getId();
String key1 = RedisConstants.FOLLOWS + userId;
String key2 = RedisConstants.FOLLOWS + id;
Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key1, key2);
if (intersect == null || intersect.isEmpty()){
return Result.ok(Collections.emptyList());
}
//2、求交集
List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
//3、根据交集进行用户查询
List<User> users = userService.listByIds(ids);
return Result.ok(users);
}
关注推送
Feed流
推送到粉丝收件箱
Feed流中的数据会不断的进行更新,所以角标也会不断的进行变化,所以不能使用角标
@Override
public Result saveBlog(Blog blog) {
// 获取登录用户
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
// 保存探店博文
boolean isSuccess = save(blog);
if (!isSuccess){
return Result.fail("新增笔记失败");
}
//查询笔记作者的所有粉丝
List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list();
//将笔记id推送给所有粉丝
for (Follow follow : follows) {
Long userId = follow.getUserId();
String key = RedisConstants.FEED_KEY + userId;
stringRedisTemplate.opsForZSet().add(key,blog.getId().toString(),System.currentTimeMillis());
}
// 返回id
return Result.ok(blog.getId());
}
滚动刷新
@Override
public Result queryBlogOfFellow(Long max, Integer offset) {
//1、获取当前用户
Long userId = UserHolder.getUser().getId();
//2、查询收件箱
String key = RedisConstants.FEED_KEY + userId;
Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, 0, max, offset, 2);
//3、非空判断
if (typedTuples == null || typedTuples.isEmpty()) {
return Result.ok();
}
//4、解析数据:blogId、minTime、offset
long minTime = 0;
int os = 1;
List<Long> ids = new ArrayList<>(typedTuples.size());
for (ZSetOperations.TypedTuple<String> tuple : typedTuples) {
String idStr = tuple.getValue();
ids.add(Long.valueOf(idStr));
Long time = tuple.getScore().longValue();
if (time == minTime) {
os++;
}else{
minTime = time;
os = 1;
}
}
//5、根据id查询blog
String idStr = StrUtil.join(",", ids);
List<Blog> blogs = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();
for (Blog blog : blogs) {
//2、查询blog相关用户
User user = userService.getById(blog.getUserId());
blog.setName(user.getNickName()).setIcon(user.getIcon());
//3、查询blog是否被点赞
isBlogLiked(blog);
}
//6、封装并及逆行拼接
return Result.ok(blogs);
}
附近商店
附近店铺信息导入
@GetMapping("/importData")
public Result importData(){
//1、查询店铺信息
List<Shop> list = shopService.list();
//2、把店铺分组,按照typeId,typeId一致的放到一个集合
Map<Long, List<Shop>> map = list.stream().collect(Collectors.groupingBy(Shop::getTypeId));
//3、分配完成写入redis
for (Map.Entry<Long, List<Shop>> entry : map.entrySet()) {
//3.1、获取同类型的店铺id
Long typeId = entry.getKey();
String key = RedisConstants.SHOP_GEO_KEY + typeId;
//3.2、获取同类型的店铺的集合
List<Shop> value = entry.getValue();
List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>();
//3.3、写入redis geoadd key 经度 维度
for (Shop shop : value) {
locations.add(new RedisGeoCommands.GeoLocation<>(
shop.getId().toString(),
new Point(shop.getX(),shop.getY())
));
}
stringRedisTemplate.opsForGeo().add(key,locations);
}
代码实现
@Override
public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
//1、判断是否需要使用坐标进行查询
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());
}
//2、计算分页参数
int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
int end = current * SystemConstants.DEFAULT_PAGE_SIZE;
//3、查询redis,按照距离排序,分页,结果shopId,distance
String key = RedisConstants.SHOP_GEO_KEY + typeId;
GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo().search(
key,
GeoReference.fromCoordinate(x, y),
new Distance(5000),
RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeCoordinates().limit(end)
);
//4、解析出id
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());
}
//4.1、截取from ~ end部分
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);
});
//5、根据id查询shop
String idStr = StrUtil.join(",", ids);
List<Shop> shops = query().in("id", ids).last("ORDER BY FIELD (id," + idStr + ")").list();
//6、返回
for (Shop shop : shops) {
shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
}
return Result.ok(shops);
}
用户签到
使用bitMap 使用位运算来实现
用户签到
@Override
public Result sign() {
//1、获取当前用户
Long userId = UserHolder.getUser().getId();
//2、获取当前日期
LocalDateTime now = LocalDateTime.now();
//3、拼接key
String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
String key = RedisConstants.USER_SIGN_KEY+userId+keySuffix;
//4、获取今天是本月的第几天
int dayOfMonth = now.getDayOfMonth();
stringRedisTemplate.opsForValue().setBit(key,dayOfMonth-1,true);
return Result.ok();
}
统计用户连续签到
@Override
public Result signCount() {
//1、获取当前用户
Long userId = UserHolder.getUser().getId();
//2、获取当前日期
LocalDateTime now = LocalDateTime.now();
//3、拼接key
String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
String key = RedisConstants.USER_SIGN_KEY+userId+keySuffix;
//4、获取今天是本月的第几天
int dayOfMonth = now.getDayOfMonth();
//5、获取本月截至今天为止的所有签到记录,返回的是一个十进制的数字
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);
}
//6、循环遍历
Long num = result.get(0);
if (num == null || num == 0){
return Result.ok(0);
}
int count = 0;
while(true){
if ((num & 1) == 0){
//判断这个比特位是否为0
break;
}else{
//不为0,证明已经签到,计数器+1
count++;
}
num>>>=1;
}
return Result.ok(count);
}
UV统计
---- 持续更新种 ------