当前位置: 首页 > news >正文

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数据库备份),必须在所有节点都获取重入锁,才算获取锁成功
  • 缺陷:运维成本高,实现复杂

相关文章:

JAVA学习笔记_Redis进阶

文章目录 初识redisredis简介windows启动redis服务器linux启动redis服务器图形用户界面客户端RDM redis命令常用数据类型特殊类型字符串操作命令Key的层级格式哈希操作命令列表操作命令集合操作命令有序集合操作命令通用命令 java客户端Jedisjedis连接池SpringDataRedis序列化手…...

LabVIEW手部运动机能实验系统

在运动科学、人机交互和康复训练等领域&#xff0c;手部运动功能的研究具有重要的应用价值。开发了一个基于LabVIEW的手部运动机能实验系统设计&#xff0c;该系统利用力量作为关键参数&#xff0c;通过实时数据采集和反馈帮助受试者完成精确的手部动作&#xff0c;同时为研究人…...

SpringBoot的注解@SpringBootApplication及自动装配

目录 一、pom文件 二、SpringBootApplication注解 1.SpringBootApplication 2.Configuration 3.这个启动类也可以被看成是一个配置类 三、SpringBootApplication注解2 1.SpringBootConfiguration 2.Configuration 3.EnableAutoConfiguration&#xff01;&#xff01;&…...

STM32学习之EXTI外部中断(以对外式红外传感器 / 旋转编码器为例)

中断:在主程序运行过程中&#xff0c;出现了特定的中断触发条件(中断源)&#xff0c;使得CPU暂停当前正在运行的程序&#xff0c;转而去处理中断程序处理完成后又返回原来被暂停的位置继续运行 中断优先级:当有多个中断源同时申请中断时&#xff0c;CPU会根据中断源的轻重缓急…...

数字赋能:制造企业如何靠“数字能力”实现可持续“超车”?

如今&#xff0c;制造业数字化转型可是个热门话题&#xff0c;全球都在积极推进。我国更是出台了一系列给力的政策来助力制造业数字化转型&#xff0c;像《中国制造 2025》就明确提出要加快制造业数字化、网络化、智能化发展&#xff0c;各省市也纷纷响应&#xff0c;从资金、税…...

.NET在中国的就业前景:开源与跨平台带来的新机遇

随着技术的不断发展和市场需求的变化&#xff0c;.NET在中国的就业前景正变得愈加广阔。尤其是在开源和跨平台的推动下&#xff0c;越来越多的中国中小型企业选择了.NET技术作为其开发平台&#xff0c;进一步提升了.NET技术人才的市场需求。尽管在中国市场&#xff0c;.NET的市…...

【基础篇】一、MySQL数据库基础知识

文章目录 Ⅰ. 什么是数据库1、普通文件的缺点2、数据库的概念3、主流数据库4、MySQL Ⅱ. MySQL中客户端、服务端、数据库的关系Ⅲ. 见一见数据库1、数据库文件存放的位置2、创建数据库3、使用数据库4、创建数据库表结构5、表中插入数据6、查询表中数据7、数据的存储逻辑 &#…...

预训练深度双向 Transformers 做语言理解

大家读完觉得有意义记得关注和点赞&#xff01;&#xff01;&#xff01; 与 GPT 一样&#xff0c;BERT 也基于 transformer 架构&#xff0c; 从诞生时间来说&#xff0c;它位于 GPT-1 和 GPT-2 之间&#xff0c;是有代表性的现代 transformer 之一&#xff0c; 现在仍然在很多…...

理解js闭包,原型,原型链

闭包 一个函数嵌套了另一个函数&#xff0c;内部函数引用了外部函数的变量&#xff0c;这样&#xff0c;当外部函数在执行环境中执行完毕后&#xff0c;因为某个变量被引用就无法被GC回收&#xff0c;导致这个变量会一直保持在内存中不能被释放。因此可以用来封装一个私有变量…...

linux tar 文件解压压缩

文件压缩和解压 tar -c: 建立压缩档案 -x&#xff1a;解压 -t&#xff1a;查看内容 -r&#xff1a;向压缩归档文件末尾追加文件 -u&#xff1a;更新原压缩包中的文件 -z&#xff1a;有gzip属性的 -j&#xff1a;有bz2属性的 -v&#xff1a;显示所有过程 -O&#xff1a;…...

【SQL server】教材数据库(5)

使用教材数据库&#xff08;1&#xff09;中的数据表完成以下题目&#xff1a; 1 根据上面基本表的信息定义视图显示每个学生姓名、应缴书费 2 观察基本表数据变化时&#xff0c;视图中数据的变化。 3利用视图&#xff0c;查询交费最高的学生。 1、create view 学生应缴费视…...

Oracle 11G还有新BUG?ORACLE 表空间迷案!

前段时间遇到一个奇葩的问题&#xff0c;在开了SR和oracle support追踪两周以后才算是有了不算完美的结果&#xff0c;在这里整理出来给大家分享。 1.问题描述 12/13我司某基地MES全厂停线&#xff0c;系统卡死不可用&#xff0c;通知到我排查&#xff0c;查看alert log看到是…...

java实现预览服务器文件,不进行下载,并增加水印效果

通过文件路径获取文件&#xff0c;对不同类型的文件进行不同处理&#xff0c;将Word文件转成pdf文件预览&#xff0c;并早呢更加水印&#xff0c;暂不支持Excel文件&#xff0c;如果浏览器不支持PDF文件预览需要下载插件。文中currentUser.getUserid()&#xff0c;即为增加的水…...

SAP月结、年结前重点检查事项(后勤与财务模块)

文章目录 一、PP生产模块相关的事务检查二、SD销售模块相关的事务检查:三、MM物料管理模块相关的事务检查四、FICO财务模块相关的事务检查五、年结前若干注意事项【SAP系统PP模块研究】 #SAP #生产订单 #月结 #年结 一、PP生产模块相关的事务检查 1、月末盘点后,生产用料的…...

MYSQL 高阶语句

目录 1、排列查询 2、区间判断 3、对结果进行分组查询 4、limit和distinct 5、设置别名 通配符 6、子查询 7、exists语句&#xff0c;判断子查询的结果是否为空 8、视图表 9、连接查询 1. 内连接 2. 左连接 3. 右连接 create table info ( id int primary key, name…...

VS Code中怎样查看某分支的提交历史记录

VsCode中无法直接查看某分支的提交记录&#xff0c;需借助插件才行&#xff0c;常见的插件如果git history只能查看某页面的改动记录&#xff0c;无法查看某分支的整体提交记录&#xff0c;我们可以安装GIT Graph插件来解决这个问题 1.在 VSCode的插件库中搜索 GIT Graph安装&a…...

知识库搭建实战一、(基于 Qianwen 大模型的知识库搭建)

基于 Qianwen 大模型的知识库开发规划 基础环境搭建可以参考文章:基础环境搭建 在构建智能应用时,知识库是一个重要的基础模块。以下将基于 Qianwen 大模型,详细介绍构建一个标准知识库的设计思路及其实现步骤。 知识库的核心功能模块 知识库开发的核心功能模块主要包括…...

ctr方法下载的镜像能用docker save进行保存吗?

ctr 和 docker 是两个不同的容器运行时工具,它们使用的镜像存储格式是兼容的(都是 OCI 标准镜像),但它们的镜像管理方式和存储路径不同。因此,直接使用 docker save 保存 ctr 拉取的镜像可能会遇到问题。 关键点 ctr 和 docker 的镜像存储位置不同: ctr(containerd)的镜…...

win32汇编环境下,窗口程序中生成listview列表控件及显示

;运行效果 ;抄下面源码在radasm里面&#xff0c;可以直接编译运行。重要部分加了备注。 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>&…...

运维之网络安全抓包—— WireShark 和 tcpdump

为什么要抓包&#xff1f;何为抓包&#xff1f; 抓包&#xff08;packet capture&#xff09;就是将网络传输发送与接收的数据包进行截获、重发、编辑、转存等操作&#xff0c;也用来检查网络安全。抓包也经常被用来进行数据截取等。为什么要抓包&#xff1f;因为在处理 IP网络…...

HTML 列表、表格、表单

1 列表标签 作用&#xff1a;布局内容排列整齐的区域 列表分类&#xff1a;无序列表、有序列表、定义列表。 例如&#xff1a; 1.1 无序列表 标签&#xff1a;ul 嵌套 li&#xff0c;ul是无序列表&#xff0c;li是列表条目。 注意事项&#xff1a; ul 标签里面只能包裹 li…...

postgresql|数据库|只读用户的创建和删除(备忘)

CREATE USER read_only WITH PASSWORD 密码 -- 连接到xxx数据库 \c xxx -- 授予对xxx数据库的只读权限 GRANT CONNECT ON DATABASE xxx TO read_only; GRANT USAGE ON SCHEMA public TO read_only; GRANT SELECT ON ALL TABLES IN SCHEMA public TO read_only; GRANT EXECUTE O…...

Mac软件卸载指南,简单易懂!

刚和Adobe分手&#xff0c;它却总在Library里给你写"回忆录"&#xff1f;卸载的Final Cut Pro像电子幽灵般阴魂不散&#xff1f;总是会有残留文件&#xff0c;别慌&#xff01;这份Mac软件卸载指南&#xff0c;将用最硬核的方式教你"数字分手术"&#xff0…...

css的定位(position)详解:相对定位 绝对定位 固定定位

在 CSS 中&#xff0c;元素的定位通过 position 属性控制&#xff0c;共有 5 种定位模式&#xff1a;static&#xff08;静态定位&#xff09;、relative&#xff08;相对定位&#xff09;、absolute&#xff08;绝对定位&#xff09;、fixed&#xff08;固定定位&#xff09;和…...

今日科技热点速览

&#x1f525; 今日科技热点速览 &#x1f3ae; 任天堂Switch 2 正式发售 任天堂新一代游戏主机 Switch 2 今日正式上线发售&#xff0c;主打更强图形性能与沉浸式体验&#xff0c;支持多模态交互&#xff0c;受到全球玩家热捧 。 &#x1f916; 人工智能持续突破 DeepSeek-R1&…...

Swagger和OpenApi的前世今生

Swagger与OpenAPI的关系演进是API标准化进程中的重要篇章&#xff0c;二者共同塑造了现代RESTful API的开发范式。 本期就扒一扒其技术演进的关键节点与核心逻辑&#xff1a; &#x1f504; 一、起源与初创期&#xff1a;Swagger的诞生&#xff08;2010-2014&#xff09; 核心…...

Linux离线(zip方式)安装docker

目录 基础信息操作系统信息docker信息 安装实例安装步骤示例 遇到的问题问题1&#xff1a;修改默认工作路径启动失败问题2 找不到对应组 基础信息 操作系统信息 OS版本&#xff1a;CentOS 7 64位 内核版本&#xff1a;3.10.0 相关命令&#xff1a; uname -rcat /etc/os-rele…...

Go 语言并发编程基础:无缓冲与有缓冲通道

在上一章节中&#xff0c;我们了解了 Channel 的基本用法。本章将重点分析 Go 中通道的两种类型 —— 无缓冲通道与有缓冲通道&#xff0c;它们在并发编程中各具特点和应用场景。 一、通道的基本分类 类型定义形式特点无缓冲通道make(chan T)发送和接收都必须准备好&#xff0…...

Java数值运算常见陷阱与规避方法

整数除法中的舍入问题 问题现象 当开发者预期进行浮点除法却误用整数除法时,会出现小数部分被截断的情况。典型错误模式如下: void process(int value) {double half = value / 2; // 整数除法导致截断// 使用half变量 }此时...

uniapp 字符包含的相关方法

在uniapp中&#xff0c;如果你想检查一个字符串是否包含另一个子字符串&#xff0c;你可以使用JavaScript中的includes()方法或者indexOf()方法。这两种方法都可以达到目的&#xff0c;但它们在处理方式和返回值上有所不同。 使用includes()方法 includes()方法用于判断一个字…...