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