JAVA学习笔记_Redis进阶

文章目录

  • 初识redis
    • redis简介
    • windows启动redis服务器
    • linux启动redis服务器
    • 图形用户界面客户端RDM
  • redis命令
    • 常用数据类型
    • 特殊类型
    • 字符串操作命令
    • Key的层级格式
    • 哈希操作命令
    • 列表操作命令
    • 集合操作命令
    • 有序集合操作命令
    • 通用命令
  • java客户端
    • Jedis
    • jedis连接池
    • SpringDataRedis
    • 序列化
    • 手动序列化
    • redisTemplate的方法习惯
  • 实战篇
    • 短信登录
      • 发送验证码
      • 短信验证码登录和注册
      • 登录校验拦截器
      • 隐藏用户敏感信息
      • session共享问题
      • 基于redis代替session登录流程
      • 基于redis实现短信登陆
      • 解决状态登录刷新的问题
    • 商户查询缓存
      • 添加商户缓存
      • 缓存更新策略
      • 缓存穿透
      • 缓存雪崩
      • 缓存击穿
      • 缓存工具封装
    • 分布式锁
      • 基于redis的分布式锁
      • 分布式锁误删问题
      • 多条redis命令原子性操作
      • java调用lua脚本
      • 基于redis的分布式锁实现思路
    • redisson
      • 入门
      • 可重入锁
      • 重试和超时续约
      • 主从一致性

初识redis

redis简介

Redis(远程词典服务器),是一个基于内存的键值型NoSQL数据库
Redis是一个基于内存的key-value结构数据库
官网,www.redis.net.cn

  • 基于内存存储,读写性能高
  • 适合存储热点数据(热点商品、资讯、新闻)
  • 企业应用广泛

windows启动redis服务器

redis在windows上启动服务端
redis属于绿色软件(文件夹),解压即可使用
启动redis服务
cmd中启动redis-server.exe
客户端(这里的客户指的是开发人员)使用服务
cmd中启动redis-cli.exe
命令示例,redis-cli.exe -h localhost -p 6379 -a 123456
-h 地址 -p 端口号 -a 密码
密码需要在redis.windows-service.conf手动设置
虽然可以在命令行使用redis,但一般还是图形界面的redis更好用更主流(还是要手动启动redis服务的)

linux启动redis服务器

windows版本的redis都是微软自己重写的,redis官方并没有windows版本,只有linux版本

linux版本
下载安装略过
三种启动方式
- 默认启动,redis-server
- 指定配置启动,需要修改redis.conf文件中的一些配置,主要是设置哪些ip可访问redis,设置为守护进程(运行状态不显示在前端),用户密码,日志文件等,启动命令,redis-server redis.conf
- 开机自启动,需要配置vi /etc/systemd/system/redis.service
systemctl enable redis//redis开机自启
systemctl daemon-reload//重载系统服务
# 启动
systemctl start redis
# 停止
systemctl stop redis
# 重启
systemctl restart redis
# 查看状态
systemctl status redis

图形用户界面客户端RDM

GitHub上的大神编写了Redis的图形化桌面客户端,地址:https://github.com/uglide/RedisDesktopManager(收费)
在下面这个仓库可以找到安装包:https://github.com/lework/RedisDesktopManager-Windows/releases(免费)

redis命令

常用数据类型

各种命令可通过官网查看’https://www.redis.net.cn/’
key为字符串类型,value有5种常用的基本数据类型

  • 字符串string
  • 哈希hash ,类似HashMap
  • 列表list ,类似LinkedList
  • 集合set ,类似HashSet
  • 有序集合sorted set/zset 集合中每个元素关联一个分数score,根据分数升序

特殊类型

  • GEO
  • BitMap
  • HyperLog

字符串操作命令

  • SET key value ,设置指定key的值
  • GET key ,获取指定key的值
  • MSET key value,批量添加或修改
  • MGET key value,批量获取
  • SETEX key seconds value ,设置指定key值,并将key的过期时间设置为sencond秒
    复合命令,等同于SET key value ex seconds
  • SETNX key value ,只有在key不存在时,设置key的值
    复合命令,等同于SET key value nx
  • INCR key,自增1
  • INCRBY key increment,自增指定步长
  • INCRBYFLOAT key increment,浮点数自增并指定步长
    string类型的三种格式,字符串,int,float,虽然都是string类型但是存储规则不同,都是怎么节省空间怎么存储

Key的层级格式

哈希操作命令

  • HSET key field value,设置值
  • HGET key field ,获取值
  • HMSET key field value field value,批量设置
  • HGET key field1 field2,批量获取
  • HGETALL key,获取全部field字段和value
  • HKEYS key,获取表中所有字段
  • HVALS key,获取表中所有值
  • HINCRBY key field increment,设置一个key中的字段自增
  • HSETNX,添加一个Hash类型的key的field值,前提是这个field不存在,否则不执行
  • HDEL key field,删除值

列表操作命令

  • LPUSH key element,将一个或多个值插入头部(后插入的那端为头部)
  • LPUSH key element,将一个或多个值插入尾部
  • LPOP key,移除并返回列表左侧第一个元素,没有则返回nil
  • RPOP key,移除并获取列表最后一个元素,也就是第一个被插入的
  • LRANGE key start stop,获取列表指定范围的元素
  • BLPOP和BRPOP,与lpop和rpop类似,只不过在没有元素时,等待指定时间,而不是直接返回nil
  • LLEN KEY,获取列表长度

集合操作命令

  • SADD key mamber1 [member2],向集合添加一个或多个成员
  • SMEMBERS key,返回集合中的所有成员
  • SISMEMBER s1 a,判断a是否在集合中
  • SCARD key,获取集合的成员数
  • SINTER key1 [key2],返回给定集合的交集
  • SUNION key1 [key2],返回给定集合的并集
  • SREM key member1 [member2],删除集合中的一个或多个成员

有序集合操作命令

每个元素关联一个double类型的分数

  • ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员
  • ZSCORE KEY member,获取sorted set中的指定元素的score值
  • ZRANK KEY member,获取sorted set中的指定元素的排名
  • ZCARD key,获取元素个数
  • ZCOUNT KEY min max,统计score值在指定范围内的所有元素的个数
  • ZRANGE key start stop [WITHSCORS],通过索引区间返回指定区间的成员
  • ZINCRBY key increment member,添加上增量increment
  • ZRANGEBYSCORE key min max,按照score排序后,获取指定score范围内的元素
  • ZREM key member [member…],移除集合中的一个或多个成员
  • ZDIFF\ZINTER\ZUNION,求差集\交集\并集
    所有的排名默认都是升序的,如果要降序则在命令的Z后面添加REV即可

通用命令

help [command]查看一个命令的信息

  • KEYS pattern ,查找所有符合给定模式的key
  • EXISTS key, 检查给定key是否存在
  • TYPE key , 返回key所存储的值的类型
  • DEL key ,该命令用于在key存在时,删除key
  • EXPIRE key age,设置key的有效期
  • TTL,查看一个KEY的剩余有效期

java客户端

Jedis和Lettuce和Redisson

Jedis

引入依赖

<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>5.2.0</version>
</dependency>
public class JedisTest {private Jedis jedis;@BeforeEachvoid setUp(){jedis = new Jedis("192.168.88.130", 6379);jedis.auth("123321");jedis.select(0);}@Testvoid testString(){String set = jedis.set("name", "虎哥");System.out.println(set);String name = jedis.get("name");System.out.println(name);}@AfterEachvoid tearDown(){if(jedis != null){jedis.close();}}
}

jedis连接池

配置连接池

public class JedisConnectionFactory {private static JedisPool jedisPool;static {//配置连接池,JedisPoolConfig poolConfig = new JedisPoolConfig();//最大连接poolConfig.setMaxIdle(8);//临时连接poolConfig.setMaxIdle(8);//超过等待时间清零连接poolConfig.setMinIdle(0);//最大等待时间poolConfig.setMaxWaitMillis(1000);//创建连接池jedisPool = new JedisPool(poolConfig,"192.168.88.130",6379,1000,"123321");}public static Jedis getJedis(){return jedisPool.getResource();}
}

修改为从连接池中获取jedis资源

jedis = JedisConnectionFactory.getJedis();

SpringDataRedis

提供了redisTemplate工具类,其中封装了各种对redis的操作,并且将不同数据类型的操作API封装到了不同的类型中
SpringDataRedis默认使用Lettuce
引入依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId>
</dependency>

spring配置

spring:redis:host: 192.168.88.130port: 6379lettuce:pool:max-active: 8max-idle: 8min-idle: 0max-wait: 100mspassword: 123321

注入RedisTemplate

@Autowired
private RedisTemplate redisTemplate;
@Test
void testString(){redisTemplate.opsForValue().set("name","虎哥");Object name = redisTemplate.opsForValue().get("name");System.out.println(name);
}

序列化

redisTemplate可以接收到任意object作为值写入redis,只不过写入之前会把object序列化为字节形式,默认是采用JDK序列化(可读性差,内存占用较大)
所以更改key和value的默认序列化

@Configuration
public class RedisConfig {@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory){//创建redisteplate对象RedisTemplate<String, Object> template = new RedisTemplate<>();//设置连接工厂template.setConnectionFactory(connectionFactory);//创建json序列化工具GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();//设置key的序列化为string序列化template.setValueSerializer(RedisSerializer.string());template.setHashValueSerializer(RedisSerializer.string());//设置value的序列化为json序列化template.setValueSerializer(genericJackson2JsonRedisSerializer);template.setHashValueSerializer(genericJackson2JsonRedisSerializer);return template;};
}
@Test
void testSaveUser(){//写入数据redisTemplate.opsForValue().set("user:100",new User("虎哥",100));//获取数据Object o = redisTemplate.opsForValue().get("user:100");System.out.println(o);}

但是通常会定义一个类去与redis传输,redis中要存储这个类的信息,也比较耗内存

手动序列化

为了节省内存空间,我们并不会使用json序列化器来处理value,而是统一使用string序列化器,要求只能存储string类型的key和value,当需要存储java对象时,手动完成对象的序列化和反序列化
spring默认提供了一个StringRedisTemplate类,它的key和value的序列化方式默认就似乎string方式,省去我们自定义RedisTemplate的过程

@Test
void testSaveUser() throws JsonProcessingException {//创建对象User user = new User("虎哥", 21);//手动序列化String s = objectMapper.writeValueAsString(user);//写入数据stringRedisTemplate.opsForValue().set("user:200",s);//获得数据String jsonUser = stringRedisTemplate.opsForValue().get("user:200");//手动反序列化User user1 = objectMapper.readValue(jsonUser, User.class);System.out.println(user1);}

redisTemplate的方法习惯

redisTemplate的方法命名习惯更贴近java的习惯比如redis的Hash的使用更贴近集合中的Hashmap的方法命名而不是HSET或HGET这样的方法名

@Test
void testHash(){stringRedisTemplate.opsForHash().put("user:400","name","虎哥");stringRedisTemplate.opsForHash().put("user:400","age","20");Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries("user:400");System.out.println(entries);
}

实战篇

  • 短信登录
  • 用户查询缓存
  • 达人探店
  • 优惠券秒杀
  • 好友关注
  • 附近商户
  • 用户签到
  • UV统计

短信登录

发送验证码

@Override
public Result sendCode(String phone, HttpSession session) {//校验手机号if(RegexUtils.isPhoneInvalid(phone)){//不符合返回失败return Result.fail("手机号格式错误");}//符合生成验证码String code = RandomUtil.randomNumbers(6);//将验证码放入sessionsession.setAttribute("code",code);//模拟发送验证码log.debug("发送验证码: {}"+ code);//返回okreturn Result.ok();
}

短信验证码登录和注册

登录校验拦截器

隐藏用户敏感信息

session共享问题

session共享问题,多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题.
多台Tomcat可以互相传输session信息,但是问题是数据重复,内存浪费,而且传输也需要一定的延迟
所以需要替代方案满足:数据共享,内存存储,key\value结构(redis)

基于redis代替session登录流程

基于redis实现短信登陆

解决状态登录刷新的问题

  • service层
  • 拦截器
  • 常量类
    service层
    @Overridepublic Result sendCode(String phone, HttpSession session) {//校验手机号if(RegexUtils.isPhoneInvalid(phone)){//不符合返回失败return Result.fail("手机号格式错误");}//符合生成验证码String code = RandomUtil.randomNumbers(6);//将验证码放入redisstringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);//模拟发送验证码log.debug("发送验证码: {}", code);//返回okreturn Result.ok();}@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {// 1.校验手机号String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {// 2.如果不符合,返回错误信息return Result.fail("手机号格式错误!");}// 3.从redis获取验证码并校验String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);String code = loginForm.getCode();if (cacheCode == null || !cacheCode.equals(code)) {// 不一致,报错return Result.fail("验证码错误");}// 4.一致,根据手机号查询用户 select * from tb_user where phone = ?User user = query().eq("phone", phone).one();//判断用户是否存在if (user == null) {user = createUserWithPhone(phone);}//随机生成tokenString token = UUID.randomUUID().toString(true);//token和用户信息存入redisUserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));//设置token的有效期String tokenKey = LOGIN_USER_KEY +token;stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);//返回token给前端return Result.ok(token);}private User createUserWithPhone(String phone){//创建用户User user = new User();user.setPhone(phone);user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));//保存用户save(user);return user;}

第一级拦截器拦截所有

public class RefreshTokenInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//获取请求头中的tokenString token = request.getHeader("authorzation");//还没token放行if(StrUtil.isBlank(token)) {return true;}//基于token获取redis中的用户String key = LOGIN_USER_KEY + token;Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);//判断用户是否存在if(userMap.isEmpty()){return true;}//将查询到的hash数据转为UserDTOUserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);//存在,保存用户信息到threalocalUserHolder.saveUser(userDTO);//刷新token有效期stringRedisTemplate.expire(key,LOGIN_USER_TTL, TimeUnit.MINUTES);//放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 移除用户UserHolder.removeUser();}
}

第二级校验是否为登录用户,不是登录用户不用处理请求了(热点访问,登录,发送验证码要排除)

public class LoginInterceptor implements HandlerInterceptor {public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//判断是否需要拦截(threalocal中是否有用户,看看是不是有人瞎jb点)//之前那个拦截器已经用token获取threadlocal中的用户信息了if(UserHolder.getUser()==null){//没有,需要拦截,设置状态码response.setStatus(401);//拦截return false;}return true;}
}

为了代码的简洁性优雅性和开闭原则,需要封装常量

public class RedisConstants {public static final String LOGIN_CODE_KEY = "login:code:";public static final Long LOGIN_CODE_TTL = 2L;public static final String LOGIN_USER_KEY = "login:token:";public static final Long LOGIN_USER_TTL = 36000L;public static final Long CACHE_NULL_TTL = 2L;public static final Long CACHE_SHOP_TTL = 30L;public static final String CACHE_SHOP_KEY = "cache:shop:";public static final String LOCK_SHOP_KEY = "lock:shop:";public static final Long LOCK_SHOP_TTL = 10L;public static final String SECKILL_STOCK_KEY = "seckill:stock:";public static final String BLOG_LIKED_KEY = "blog:liked:";public static final String FEED_KEY = "feed:";public static final String SHOP_GEO_KEY = "shop:geo:";public static final String USER_SIGN_KEY = "sign:";
}

商户查询缓存

缓存是数据交换的缓冲区(cache),是存贮数据的临时地方,一般读写性能较高
CPU的缓存就在cpu内部,比磁盘和内存更快,一般1MB-64MB
redis缓存还是在CPU中

添加商户缓存

先到redis中查商户信息,查不到再到mysql中查,查出来放入redis中

缓存更新策略

  • 内存淘汰,reids自动实现,但一致性差
  • 超时剔除,可以给数据添加TTL,一致性一般
  • 主动更新,编写逻辑,主动实现更新,一致性好
    主动更新是最好的方案,当然也可以结合其他方案使用
    主动更新策略
  • cache aside pattern,调用者主动更新,
  • read/write through pattern,缓存与数据库整合为一个服务,但是找一个现成的这样的业务很难
  • write behind caching pattern,只改缓存
    cache aside pattern是最好的方案
    先写数据库,再删缓存要比先删缓存再写数据库的出错率低
高一致性需求,主动更新,并以超时剔除作为兜底方案
读操作:
缓存命中则直接返回
缓存未命中查询数据库,并写入缓存,设定超时时间
写操作:
先写数据库,然后再删除缓存
要确保数据库与缓存操作的原子性

缓存穿透

缓存这一系列问题就是为了减少对sql数据库的查询,因为对数据库的操作相当一次网络请求,耗时长,对计算资源的消耗也高
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库,给数据库带来巨大压力

  • 缓存空对象
    优点:实现简单,维护方便
    缺点:额外的内存消耗,可能造成短期的不一致
  • 布隆过滤
    优点:内存占用较少,没有多余key
    缺点:实现复杂,存在误判可能
    缓存穿透的解决方案:
  • 缓存null值
  • 布隆过滤
  • 增强id的复杂度,避免被猜测id规律
  • 做好数据的基础格式校验
  • 加强用户权限校验
  • 做好热点参数的限流

缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效或者redis服务宕机,导致大量请求到达数据库,带来巨大压力
解决方案:

  • 给不同的key的TTL添加随机值
  • 利用redis集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

缓存击穿

缓存击穿问题也叫热点key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效,无数的请求访问会在瞬间给数据库带来巨大的冲击
常见的解决方案:

  • 互斥锁
    优点:没有额外的内存消耗,保证取数据一致性,实现简单
    缺点:线程需要等待,性能受影响,可能有死锁风险
  • 逻辑过期
    优点:线程无需等待,性能较好
    缺点:不保证一致性,有额外内存消耗,实现复杂
    Apache JMeter,高并发压力测试工具
    可以显示线程处理的最大\最小\平均值,异常值,吞吐量等
    基于互斥锁解决缓存击穿
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) {// 将空值写入redisstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 6.存在,写入redisthis.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);
}

基于逻辑过期解决缓存击穿

    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;}

缓存工具封装

为了缓存工具开发维护成本,需要将缓存常用代码封装成工具类

分布式锁

sychronized只能对同一个jvm内的线程进行锁操作
分布式系统:多个 Java 程序可能在不同的物理或虚拟机器上运行,每个程序启动一个 JVM。
分布式锁,满足分布式系统或集群模式下多进程可见并且互斥的锁(对多个jvm的所有线程锁操作)
分布式锁的实现
Mysql:利用mysql本身的互斥锁机制
redis:利用setnx这样的互斥命令
zookeeper:利用节点的唯一性和有序性实现互斥

基于redis的分布式锁

需要实现获取锁(设置一个redis键值对)和释放锁,确保只有一个线程获取锁,也要保证获取锁和释放锁操作设置的原子性,否则某个线程获取了锁,但进程突然宕机,就无法释放锁
set lock thread1 nx ex 10//是最好的选择,nx保证互斥,ex 10在一定时间后释放锁

分布式锁误删问题

需要判断是不是自己的锁再删除

  • 在获取锁时存入线程标识(可以用UUID表示,因为不同jvm里可能有不同的线程有同一线程id)
  • 在释放锁时先获取锁中的线程标识,判断是否与当前线程标识一致,如果一致再释放锁

多条redis命令原子性操作

需要保证判断锁和释放锁的原子性操作,需要用到lua脚本,否则在极端情况会出现线程乱套的问题(比如线程A释放线程B的锁)
lua脚本,redis提供了lua脚本功能,在一个脚本中编写多条redis命令,确保原子性
官网:https://www.runoob.com/lua/lua-tutorial.html

java调用lua脚本

redisTemplate提供了调用lua脚本的API

基于redis的分布式锁实现思路

  • 利用set nx ex获取锁,并设置过期时间,保存线程标识(重点1)
  • 释放锁时,先判断线程标识是否与自己一致,一致则删除锁(重点2)
    特性:
  • 利用set nx满足互斥性
  • 利用set ex保证故障时锁依然能释放,避免死锁.提高安全性
  • 利用lua脚本保证redis命令原子性操作
  • 利用redis集群保证高可用性和高并发性

redisson

redisson是一个在redis的基础上实现的java驻内存数据网格(in-memory data grid),不仅提供了一系列的分布式的java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现
官网:https://redisson.org/
reidsson中的API方案不仅集成了上面的优化策略,还有解决以下问题的策略:

  • 不可重入:同一个线程无法多次获取同一把锁
  • 不可重试:获取锁只尝试一次就返回false,没有重试机制
  • 超时释放:锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患
  • 主从一致性:如果redis提供了主从集群,主从同步存在延迟,当主宕机时,如果从并没有同步主中的锁数据,则会出现锁实现

入门

引入依赖

<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.13.6</version>
</dependency>

配置redisson

@Configuration
public class RedissonConfig {@Beanpublic RedissonClient redissonClient(){// 配置Config config = new Config();config.useSingleServer().setAddress("redis://192.168.88.130:6379").setPassword("123321");// 创建RedissonClient对象return Redisson.create(config);}
}

使用redisson

@Transactional
public Result createVoucherOrder(Long voucherId) {// 5.一人一单Long userId = UserHolder.getUser().getId();// 创建锁对象RLock redisLock = redissonClient.getLock("lock:order:" + userId);// 尝试获取锁boolean isLock = redisLock.tryLock();// 判断if(!isLock){// 获取锁失败,直接返回失败或者重试return Result.fail("不允许重复下单!");}try {.....} finally {// 释放锁redisLock.unlock();}}

可重入锁

利用hash结构记录线程id和重入次数

既要记录线程id和重入次数还是hash比string要方便

重试和超时续约

可重试:利用信号量和PubSub功能实现等待,唤醒,获取锁失败的重试机制
超时续约:利用watchDog,每隔一段时间(releaseTime/3),重置超时时间

主从一致性

redisson的multiLock:

  • 原理: 多个独立的redis节点(redis集群,相当于多个mysql数据库备份),必须在所有节点都获取重入锁,才算获取锁成功
  • 缺陷:运维成本高,实现复杂

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/64622.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

1月第一讲:WxPython跨平台开发框架之前后端结合实现附件信息的上传及管理

1、功能描述和界面 前端&#xff08;wxPython GUI&#xff09;&#xff1a; 提供文件选择、显示文件列表的界面。支持上传、删除和下载附件。展示上传状态和附件信息&#xff08;如文件名、大小、上传时间&#xff09;。后端&#xff08;REST API 服务&#xff09;&#xff1a…

12.29~12.31[net][review]need to recite[part 2]

网络层 IP 首部的前一部分是固定长度&#xff0c;共 20 字节&#xff0c;是所有 IP 数据报必须具有的 路由器 路由选择协议属于网络层控制层面的内容 l 路由器 的 主要工作&#xff1a; 转发分组。 l 路由 信息协议 RIP (Routing Information Protocol ) 是 一种 分布式的…

免费下载 | 2024网络安全产业发展核心洞察与趋势预测

《2024网络安全产业发展核心洞察与趋势预测》报告的核心内容概要&#xff1a; 网络安全产业概况&#xff1a; 2023年中国网络安全产业市场规模约992亿元&#xff0c;同比增长7%。 预计2024年市场规模将增长至1091亿元&#xff0c;2025年达到1244亿元。 网络安全企业数量超过4…

记忆旅游系统|Java|SSM|VUE| 前后端分离

【技术栈】 1⃣️&#xff1a;架构: B/S、MVC 2⃣️&#xff1a;系统环境&#xff1a;Windowsh/Mac 3⃣️&#xff1a;开发环境&#xff1a;IDEA、JDK1.8、Maven、Mysql5.7 4⃣️&#xff1a;技术栈&#xff1a;Java、Mysql、SSM、Mybatis-Plus、VUE、jquery,html 5⃣️数据库可…

微信小程序:定义页面标题,动态设置页面标题,json

1、常规设置页面标题 正常微信小程序中&#xff0c;设置页面标题再json页面中进行设置&#xff0c;例如 {"usingComponents": {},"navigationBarTitleText": "标题","navigationBarBackgroundColor": "#78b7f7","navi…

理解生成协同促进?华为诺亚提出ILLUME,15M数据实现多模态理解生成一体化

多模态理解与生成一体化模型&#xff0c;致力于将视觉理解与生成能力融入同一框架&#xff0c;不仅推动了任务协同与泛化能力的突破&#xff0c;更重要的是&#xff0c;它代表着对类人智能&#xff08;AGI&#xff09;的一种深层探索。通过在单一模型中统一理解与生成&#xff…

学习vue3的笔记

一、vue和react的对比 1、基础介绍 vue&#xff1a;https://cn.vuejs.org/ vue3是2020年创建的 react&#xff1a;https://react.dev/ react是一个2013年开源的JavaScript库&#xff0c;严格意义上来说不是一个框架 2、diff算法 两个框架采用的都是同级对比策略 两节点对…

SQLiteDataBase数据库

XML界面设计 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"xmlns:tools"http://schemas.android.com/tools"android:layout_width"match_paren…

k8s部署nginx+sshd实现文件上传下载

要通过 nginx 和 sshd 实现文件的上传和下载&#xff0c;通常的做法是结合 SSH 协议和 HTTP 协议&#xff0c;使用 nginx 提供 Web 服务器功能&#xff0c;同时使用 sshd&#xff08;即 SSH 服务&#xff09;来处理通过 SSH 协议进行的文件传输。 SSH 实现文件的上传和下载&…

clickhouse-backup配置及使用(Linux)

一、下载地址 Releases Altinity/clickhouse-backup GitHub 二、上传到服务器解压安装 自行上传至服务器&#xff0c;解压命令&#xff1a; tar xvf clickhouse-backup-linux-amd64.tar.gz 三、创建软连接 sudo ln -sv build/linux/amd64/clickhouse-backup /usr/local/bin/…

如何在群晖NAS上安装并配置MySQL与phpMyAdmin远程管理数据库

文章目录 前言1. 安装MySQL2. 安装phpMyAdmin3. 修改User表4. 本地测试连接MySQL5. 安装cpolar内网穿透6. 配置MySQL公网访问地址7. 配置MySQL固定公网地址8. 配置phpMyAdmin公网地址9. 配置phpmyadmin固定公网地址 前言 大家是不是经常遇到需要随时随地访问自己数据的情况&am…

《向量数据库指南》——Milvus Cloud 2.5:Sparse-BM25引领全文检索新时代

Milvus Cloud BM25:重塑全文检索的未来 在最新的Milvus Cloud 2.5版本中,我们自豪地引入了“全新”的全文检索能力,这一创新不仅巩固了Milvus Cloud在向量数据库领域的领先地位,更为用户提供了前所未有的灵活性和效率。作为大禹智库的向量数据库高级研究员,以及《向量数据…

ESP32-CAM开发板入门 (下载示例程序)

ESP32-CAM开发板例程使用 1、准备工作1.1、硬件准备1.2、软件准备 2、选择示例程序并录入第一步 1、准备工作 1.1、硬件准备 1.2、软件准备 Arduino IDE &#xff1a; 编程与写入&#xff08;下载地址 https://www.arduino.cc/en/software&#xff09; 安装好后将软件设置到…

企业赋能是什么意思-国际数字影像产业园解读

在当今竞争激烈的商业环境中&#xff0c;企业赋能已成为推动企业发展、提升竞争力的关键策略。国际数字影像产业园作为数字影像产业的重要集聚地&#xff0c;通过一系列创新举措为入驻园区的我众多企业赋能。那么&#xff0c;企业赋能究竟是什么意思呢&#xff1f; 企业赋能是…

混合并行训练框架性能对比

混合并行训练框架性能对比 1. 框架类型 DeepSpeed、Megatron - LM、Colossal - AI、SageMaker、Merak、FasterMoE、Tutel、Whale、Alpa、DAPPLE、Mesh - TensorFlow 2. 可用并行性(Available parallelisms) DNN framework(深度神经网络框架)DP(数据并行,Data Parallelis…

客户案例:基于慧集通集成平台,打通屠宰管理系统与用友U8C 系统的全攻略

一、引言 本原型客户成立于2014年&#xff0c;是一家集饲草种植、肉牛养殖、精深加工、冷链物流、餐饮服务于一体的大型农牧综合体。公司下设三个子公司分别涵盖农业、畜牧业、肉制品加工业与餐饮物流服务业。公司严格按照一二三产业融合发展要求&#xff0c;以肉牛产业化为支…

HTML5滑块(Slider)

HTML5 的滑块&#xff08;Slider&#xff09;控件允许用户通过拖动滑块来选择数值。以下是如何实现一个简单的滑块组件的详细说明。 HTML5 滑块组件 1. 基本结构 使用 <input type"range"> 元素可以创建一个滑块。下面是基本实现的代码示例&#xff1a; <…

25. C++继承 1 (继承的概念与基础使用, 继承的复制兼容规则,继承的作用域)

⭐上篇模板文章&#xff1a;24. C模板 2 (非类型模板参数&#xff0c;模板的特化与模板的分离编译)-CSDN博客 ⭐本篇代码&#xff1a;c学习 橘子真甜/c-learning-of-yzc - 码云 - 开源中国 (gitee.com) ⭐标⭐是比较重要的部分 目录 一. 继承的基础使用 1.1 继承的格式 1.2 …

露营小程序搭建有哪些步骤?小程序里面可以找个露营搭子

露营不仅仅是走进大自然的旅程&#xff0c;它也成为了一种社交和体验式的活动。随着小程序的普及&#xff0c;露营活动也越来越多地开始在线上开展。通过搭建一个露营小程序&#xff0c;商家不仅可以为用户提供更多的露营选择&#xff0c;还可以帮助他们找到合适的露营搭子。那…

XIAO ESP32 S3网络摄像头——2视频获取

本文主要是使用XIAO Esp32 S3制作网络摄像头的第2步,获取摄像头图像。 1、效果如下: 2、所需硬件 3、代码实现 3.1硬件代码: #include "WiFi.h" #include "WiFiClient.h" #include "esp_camera.h" #include "camera_pins.h"// 设…