文章目錄
- 初識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數據庫備份),必須在所有節點都獲取重入鎖,才算獲取鎖成功
- 缺陷:運維成本高,實現復雜