一、前言
今天本來應該學習netty基礎傳輸的相關內容,但是由於對基礎知識掌握的不足,出現學習的瓶頸,先學習一下冪等性壓壓驚,晚上再梳理一下netty的相關內容,認認真真學習,爭取明晚可以完成netty基礎傳輸相關內容,今晚就看一下冪等性吧!
二、什麼是冪等性?百度百科的解釋
冪等(idempotent、idempotence)是一個數學與計算機學概念,常見於抽象代數中。
在程式設計中一個冪等操作的特點是其任意多次執行所產生的影響均與一次執行的影響相同。冪等函式,或冪等方法,是指可以使用相同引數重複執行,並能獲得相同結果的函式。這些函式不會影響系統狀態,也不用擔心重複執行會對系統造成改變。例如,“setTrue()”函式就是一個冪等函式,無論多次執行,其結果都是一樣的.更復雜的操作冪等保證是利用唯一交易號(流水號)實現。
理解
所謂冪等性,就是無論執行多少次,都會只執行一次,多次的影響和一次是一樣的,比如我們新增使用者,無論執行多少次,都只會儲存一次,避免了重複提交
冪等性運用的範圍也是很多,比如以下幾個:
儲存使用者資訊,前端重複提交相同的資料,後端介面對於這個資料只會儲存一次,無論重新提交多少次,也只會完成一次使用者支付,無論提交多少次,他只能有一次成功,只能扣一次錢**驗證碼,**相同的的驗證碼只能傳送一次,不能重複傳送 等等三、實現冪等性1、實現的方式
mysql的唯一索引,如果索引存在,就會丟擲異常,也就保證了重複提交問題悲觀鎖樂觀鎖redis實現,將生成的token儲存在redis中,提交之後,將token刪除,重複發起請求,獲取不到redis儲存的token就無法重複提交,直接提示使用者:重複儲存2、redis和lua實現冪等性
我們這裡使用redis和Lua的方式實現,為什麼使用lua?
因為lua是原子操作,原子操作從開始就要一直到執行結束,中間不會有執行緒切換,所以可以解決高併發的問題
2.1 不使用冪等性存在的問題
我們先看一下使用者儲存操作:
controller
@Log("使用者建立")@ApiOperation(value = "使用者建立",notes = "使用者建立")@PostMapping("/createUser")public GoflyResponse createUser(@Valid UserDto userDto) throws GoflyException { User user = new User(); BeanUtils.copyProperties(userDto,user); iUserService.createUser(user); return new GoflyResponse().code(HttpStatus.OK).message("成功");}
postman多次提交使用者儲存操作
提交三次之後,新增了同樣的三個使用者,所以這個是錯誤的,需要修改
| USER_ID | USERNAME | PASSWORD | NAME | 9 | yangyangyang | fcea920f7412b5da7be0cf42b8c93759 | 楊羽茉 | 10 | yangyangyang | fcea920f7412b5da7be0cf42b8c93759 | 楊羽茉 | 11 | yangyangyang | fcea920f7412b5da7be0cf42b8c93759 | 楊羽茉
2.2 redis和lua解決問題
lua指令碼----> checkidem.lua
放在resources目錄下
//獲取token----> redis: get tokenlocal current = redis.call('GET',KEYS[1]) //如果不存在返回-1if current == false then return '-1'end//訪問一次之後,刪除tokenlocal isdel = redis.call('DEL',KEYS[1])//如果刪除成功就返回'1'if isdel == 1 then return '1' else //刪除失敗,說明token不存在 return '0'end
redis工具類------> RedisLuaUtil.java
用於執行lua指令碼
/** * redis解析lua工具類 */@Componentpublic class RedisLuaUtil { //注入stringRedisTemplate @Resource private StringRedisTemplate stringRedisTemplate; //執行lua指令碼 public String runLuaScript(String luaFileName, List<String> keys){ DefaultRedisScript<String> redisScript = new DefaultRedisScript<>(); //讀取lua指令碼檔案 redisScript.setScriptSource( new ResourceScriptSource( new ClassPathResource(luaFileName) ) ); //lua返回值型別 redisScript.setResultType(String.class); //值引數 String argson = "none"; //執行lua指令碼,獲取返回值,lua返回的內容主要看 String result = stringRedisTemplate.execute(redisScript, keys, argson); return result; } /** * 校驗token * @param request */ public String checkToken(HttpServletRequest request) { //獲取token,從request中獲取 String token = request.getParameter("token"); //將token儲存到list中 List<String> keys = new ArrayList<>(); keys.add(token); //呼叫執行lua指令碼的方法,並且傳入lua檔案的名稱及token集合 String result = this.runLuaScript("checkidem.lua", keys); //根據自定義的lua指令碼,會返回三種返回值: //1. GET token 不存在時返回-1 //2. DEL token 成功時,返回1 //3. DEL token 不成功,返回0 if(result.equals("1")){ return "儲存成功"; }else{ return "請勿重複提交"; } }}
生成token、模擬儲存使用者操作、測試
//自動注入redisTemplate,用於儲存token到redis中@Autowiredprivate StringRedisTemplate stringRedisTemplate;//自動注入自定義的Redis Lua工具類@Autowiredprivate RedisLuaUtil redisLuaUtil;
(1) 生成token
@GetMapping("/getToken")public String getToken(){ //生成UUID,作為token String result = UUID.randomUUID().toString(); //將生成的token儲存到redis中 stringRedisTemplate.opsForValue().set(result,result); //返回給使用者 return result;}
(2) 模擬儲存使用者操作
@GetMapping("/saveUser")public String saveUser(HttpServletRequest request){ //執行自定義的RedisLuaUtil類,用於校驗是否為重複儲存 String result = redisLuaUtil.checkToken(request); //返回給使用者 return result;}
(3) 測試第一:首先呼叫**/getToken**操作,生成token
request: http://localhost:8080/getTokenrespones: 91a23e2b-52ab-4867-90bc-737f31bb7ac3
儲存使用者
request: http://localhost:8080/saveUser?token=91a23e2b-52ab-4867-90bc-737f31bb7ac3 將生成的token作為引數傳到後端 第一次提交: respones: 儲存成功 第二次提交: respones: 請勿重複提交
四、執行流程第一步:呼叫**/getToken** 生成UUID,儲存到redis中,並且返回給使用者 @GetMapping("/getToken") public String getToken(){ //生成token String result = UUID.randomUUID().toString(); //儲存到redis stringRedisTemplate.opsForValue().set(result,result); return result; } 複製程式碼第二步:將token作為引數,呼叫**/saveUser** 首先,呼叫redisLuaUtil中的checkToken方法,判斷token是否存在 @GetMapping("/saveUser") public String saveUser(HttpServletRequest request){ //驗證token是否存在redis中 String result = redisLuaUtil.checkToken(request); return result; } 複製程式碼 如果存在,lua會對redis中的token刪除 local current = redis.call('GET',KEYS[1]) //如果不存在,返回-1,如果存在執行下面DEL操作 if current == false then return '-1' end local isdel = redis.call('DEL',KEYS[1]) if isdel == 1 then return '1' else return '0' end 複製程式碼 如果不存在,直接返回-1 如果刪除成功,返回1 如果刪除失敗,返回0 然後,將結果返回給使用者(後端根據返回-1 0 1,對使用者做出響應)總結起來,就是使用者在訪問其他介面的時候,需要先生成token並儲存到redis中,返回將token作為引數傳遞到目標方法,如果redis中有這個token,返回可以訪問的標誌(比如1),並且刪除redis中的這個token,當第二次訪問的時候,redis就查詢不到該token,就返回重複提交錯誤(比如 -1)