Quick.Data.Redis.pas 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  1. { ***************************************************************************
  2. Copyright (c) 2015-2021 Kike Pérez
  3. Unit : Quick.Data.Redis
  4. Description : Redis client
  5. Author : Kike Pérez
  6. Version : 1.0
  7. Created : 22/02/2020
  8. Modified : 15/10/2021
  9. This file is part of QuickLib: https://github.com/exilon/QuickLib
  10. ***************************************************************************
  11. Licensed under the Apache License, Version 2.0 (the "License");
  12. you may not use this file except in compliance with the License.
  13. You may obtain a copy of the License at
  14. http://www.apache.org/licenses/LICENSE-2.0
  15. Unless required by applicable law or agreed to in writing, software
  16. distributed under the License is distributed on an "AS IS" BASIS,
  17. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  18. See the License for the specific language governing permissions and
  19. limitations under the License.
  20. *************************************************************************** }
  21. unit Quick.Data.Redis;
  22. {$i QuickLib.inc}
  23. interface
  24. uses
  25. {$IFDEF DEBUG_REDIS}
  26. Quick.Debug.Utils,
  27. {$ENDIF}
  28. System.SysUtils,
  29. System.DateUtils,
  30. IdTCPClient,
  31. Quick.Commons;
  32. type
  33. IRedisResponse = interface
  34. ['{21EF7ABF-E678-4F18-AE56-8A7C6B817AE3}']
  35. function GetIsDone: Boolean;
  36. function GetResponse: string;
  37. procedure SetIsDone(const Value: Boolean);
  38. procedure SetResponse(const Value: string);
  39. property IsDone : Boolean read GetIsDone write SetIsDone;
  40. property Response : string read GetResponse write SetResponse;
  41. end;
  42. TRedisResponse = class(TInterfacedObject,IRedisResponse)
  43. private
  44. fIsDone : Boolean;
  45. fResponse : string;
  46. function GetIsDone: Boolean;
  47. function GetResponse: string;
  48. procedure SetIsDone(const Value: Boolean);
  49. procedure SetResponse(const Value: string);
  50. public
  51. constructor Create;
  52. property IsDone : Boolean read GetIsDone write SetIsDone;
  53. property Response : string read GetResponse write SetResponse;
  54. end;
  55. TRedisSortedItem = record
  56. Value : string;
  57. Score : Int64;
  58. end;
  59. TRedisClient = class
  60. private
  61. fTCPClient : TIdTCPClient;
  62. fHost : string;
  63. fPort : Integer;
  64. fDataBaseNumber : Integer;
  65. fMaxSize : Int64;
  66. fPassword : string;
  67. fConnectionTimeout : Integer;
  68. fReadTimeout : Integer;
  69. fConnected : Boolean;
  70. fRaiseErrorIfCommandFails : Boolean;
  71. procedure SetConnectionTimeout(const Value: Integer);
  72. procedure SetReadTimeout(const Value: Integer);
  73. function Command(const aCommand : string; const aArguments : string = '') : IRedisResponse; overload;
  74. function Command(const aCommand, aArgumentsFormat : string; aValues : array of const) : IRedisResponse; overload;
  75. function EscapeString(const json: string) : string;
  76. public
  77. constructor Create;
  78. destructor Destroy; override;
  79. property Host : string read fHost write fHost;
  80. property Port : Integer read fPort write fPort;
  81. property DataBaseNumber : Integer read fDataBaseNumber write fDataBaseNumber;
  82. property MaxSize : Int64 read fMaxSize write fMaxSize;
  83. property Password : string read fPassword write fPassword;
  84. property ConnectionTimeout : Integer read fConnectionTimeout write SetConnectionTimeout;
  85. property ReadTimeout : Integer read fReadTimeout write SetReadTimeout;
  86. property RaiseErrorIfCommandFails : Boolean read fRaiseErrorIfCommandFails write fRaiseErrorIfCommandFails;
  87. property Connected : Boolean read fConnected;
  88. function RedisSELECT(dbIndex : Integer) : Boolean;
  89. function RedisSET(const aKey, aValue : string; aTTLMs : Integer = -1) : Boolean;
  90. function RedisGET(const aKey : string; out oValue : string) : Boolean;
  91. function RedisDEL(const aKey : string) : Boolean;
  92. function RedisRPUSH(const aKey, aValue : string) : Boolean;
  93. function RedisLPUSH(const aKey, aValue : string) : Boolean;
  94. function RedisRPOP(const aKey : string; out oValue : string) : Boolean;
  95. function RedisBRPOP(const aKey: string; out oValue: string; aWaitTimeoutSecs : Integer): Boolean;
  96. function RedisLPOP(const aKey : string; out oValue : string) : Boolean;
  97. function RedisBLPOP(const aKey: string; out oValue: string; aWaitTimeoutSecs : Integer): Boolean;
  98. function RedisBRPOPLPUSH(const aKey, aKeyToMove: string; out oValue: string; aWaitTimeoutSecs : Integer): Boolean;
  99. function RedisLTRIM(const aKey : string; aFirstElement, aMaxSize : Int64) : Boolean;
  100. function RedisEXPIRE(const aKey : string; aTTLMs : Integer) : Boolean; overload;
  101. function RedisEXPIRE(const aKey : string; aExpireDate : TDateTime) : Boolean; overload;
  102. function RedisLINDEX(const aKey: string; aIndex: Integer; out oValue : string): Boolean;
  103. function RedisLREM(const aKey, aValue: string; aNumOccurrences: Integer): Boolean;
  104. function RedisZADD(const aKey, aValue : string; aScore : Int64) : Boolean;
  105. function RedisZREM(const aKey, aValue : string) : Boolean;
  106. function RedisZRANGE(const aKey : string; aStartPosition, aEndPosition : Int64) : TArray<string>;
  107. function RedisZRANGEBYSCORE(const aKey : string; aMinScore, aMaxScore : Int64) : TArray<TRedisSortedItem>;
  108. function RedisLLEN(const aKey : string): Integer;
  109. function RedisTTL(const aKey, aValue : string): Integer;
  110. function RedisAUTH(const aPassword : string) : Boolean;
  111. function RedisPING : Boolean;
  112. function RedisQUIT : Boolean;
  113. procedure Connect;
  114. procedure Disconnect;
  115. end;
  116. ERedisConnectionError = class(Exception);
  117. ERedisAuthError = class(Exception);
  118. ERedisCommandError = class(Exception);
  119. implementation
  120. const
  121. DEF_REDIS_PORT = 6379;
  122. CRLF = #10#13;
  123. DEF_CONNECTIONTIMEOUT = 30000;
  124. DEF_READTIMETOUT = 10000;
  125. { TRedisResponse }
  126. constructor TRedisResponse.Create;
  127. begin
  128. fIsDone := False;
  129. fResponse := '';
  130. end;
  131. function TRedisResponse.GetIsDone: Boolean;
  132. begin
  133. Result := fIsDone;
  134. end;
  135. function TRedisResponse.GetResponse: string;
  136. begin
  137. Result := fResponse;
  138. end;
  139. procedure TRedisResponse.SetIsDone(const Value: Boolean);
  140. begin
  141. fIsDone := Value;
  142. end;
  143. procedure TRedisResponse.SetResponse(const Value: string);
  144. begin
  145. fResponse := Value;
  146. end;
  147. { TRedisClient }
  148. constructor TRedisClient.Create;
  149. begin
  150. inherited;
  151. fConnected := False;
  152. fHost := 'localhost';
  153. fPort := DEF_REDIS_PORT;
  154. fDataBaseNumber := 0;
  155. fMaxSize := 0;
  156. fPassword := '';
  157. fConnectionTimeout := DEF_CONNECTIONTIMEOUT;
  158. fReadTimeout := DEF_READTIMETOUT;
  159. fRaiseErrorIfCommandFails := False;
  160. fTCPClient := TIdTCPClient.Create;
  161. end;
  162. destructor TRedisClient.Destroy;
  163. begin
  164. try
  165. try
  166. Disconnect;
  167. finally
  168. fTCPClient.Free;
  169. end;
  170. except
  171. //avoid closing errors
  172. end;
  173. inherited;
  174. end;
  175. procedure TRedisClient.Disconnect;
  176. begin
  177. if fTCPClient.Connected then
  178. begin
  179. RedisQUIT;
  180. fTCPClient.IOHandler.InputBuffer.Clear;
  181. fTCPClient.IOHandler.WriteBufferFlush;
  182. if fTCPClient.Connected then fTCPClient.Disconnect(False);
  183. end;
  184. fConnected := False;
  185. end;
  186. procedure TRedisClient.Connect;
  187. begin
  188. try
  189. //connect password and database
  190. if not fTCPClient.Connected then
  191. begin
  192. fTCPClient.Host := fHost;
  193. fTCPClient.Port := fPort;
  194. fTCPClient.ConnectTimeout := fConnectionTimeout;
  195. fTCPClient.ReadTimeout := fConnectionTimeout;
  196. fTCPClient.Connect;
  197. if not fTCPClient.Connected then raise ERedisConnectionError.Create('Can''t connect to Redis Server!');
  198. end;
  199. fTCPClient.Socket.Binding.SetKeepAliveValues(True,5000,1000);
  200. if fPassword <> '' then
  201. begin
  202. if not RedisAUTH(fPassword) then raise ERedisAuthError.Create('Redis authentication error!');
  203. end;
  204. if fDataBaseNumber > 0 then
  205. begin
  206. if not RedisSELECT(fDataBaseNumber) then raise ERedisConnectionError.CreateFmt('Can''t select Redis Database "%d"',[fDataBaseNumber]);
  207. end;
  208. fTCPClient.IOHandler.MaxLineLength := MaxInt;
  209. fConnected := True;
  210. except
  211. on E : Exception do raise ERedisConnectionError.CreateFmt('Can''t connect to Redis service %s:%d (%s)',[Self.Host,Self.Port,e.Message]);
  212. end;
  213. end;
  214. function TRedisClient.EscapeString(const json: string): string;
  215. begin
  216. Result := StringReplace(json,'\','\\',[rfReplaceAll]);
  217. Result := StringReplace(Result,'"','\"',[rfReplaceAll]);
  218. Result := StringReplace(Result,#13,'\r',[rfReplaceAll]);
  219. Result := StringReplace(Result,#10,'\n',[rfReplaceAll]);
  220. //Result := StringReplace(Result,'/','\/"',[rfReplaceAll]);
  221. end;
  222. procedure TRedisClient.SetConnectionTimeout(const Value: Integer);
  223. begin
  224. if fConnectionTimeout <> Value then
  225. begin
  226. fConnectionTimeout := Value;
  227. if Assigned(fTCPClient) then fTCPClient.ConnectTimeout := fConnectionTimeout;
  228. end;
  229. end;
  230. procedure TRedisClient.SetReadTimeout(const Value: Integer);
  231. begin
  232. if fReadTimeout <> Value then
  233. begin
  234. fReadTimeout := Value;
  235. if Assigned(fTCPClient) then fTCPClient.ConnectTimeout := fReadTimeout;
  236. end;
  237. end;
  238. function TRedisClient.Command(const aCommand, aArgumentsFormat : string; aValues : array of const) : IRedisResponse;
  239. begin
  240. Result := Command(aCommand,Format(aArgumentsFormat,aValues));
  241. end;
  242. function TRedisclient.Command(const aCommand : string; const aArguments : string = '') : IRedisResponse;
  243. function TrimResponse(const aResponse : string) : string;
  244. begin
  245. Result := Copy(aResponse,Low(aResponse) + 1, aResponse.Length);
  246. end;
  247. var
  248. res : string;
  249. begin
  250. Result := TRedisResponse.Create;
  251. try
  252. if not fTCPClient.Connected then Connect;
  253. fTCPClient.IOHandler.Write(aCommand + ' ' + aArguments + CRLF);
  254. if fTCPClient.IOHandler.CheckForDataOnSource(fReadTimeout) then
  255. begin
  256. res := fTCPClient.IOHandler.ReadLn;
  257. {$IFDEF DEBUG_REDIS}
  258. TDebugger.Trace(Self,Format('Command "%s"',[res]));
  259. {$ENDIF}
  260. if not res.IsEmpty then
  261. case res[Low(res)] of
  262. '+' :
  263. begin
  264. if res.Contains('+OK') then
  265. begin
  266. Result.IsDone := True;
  267. end
  268. else Result.Response := TrimResponse(res);
  269. end;
  270. '-' : Result.Response := TrimResponse(res);
  271. ':' :
  272. begin
  273. Result.Response := TrimResponse(res);
  274. Result.IsDone := Result.Response.ToInteger > -1;
  275. end;
  276. '$' :
  277. begin
  278. Result.Response := TrimResponse(res);
  279. if IsInteger(Result.Response) then
  280. begin
  281. if Result.Response.ToInteger > -1 then Result.IsDone := True;
  282. end
  283. else Result.IsDone := True;
  284. end;
  285. '*' :
  286. begin
  287. Result.Response := TrimResponse(res);
  288. Result.IsDone := True;
  289. end;
  290. else Result.Response := TrimResponse(res);
  291. end;
  292. end;
  293. if (fRaiseErrorIfCommandFails) and (not Result.IsDone) then raise ERedisCommandError.CreateFmt('Command fail (%s)',[Result.Response]);
  294. except
  295. on E : Exception do raise ERedisCommandError.CreateFmt('%s error: %s',[aCommand,e.message]);
  296. end;
  297. end;
  298. function TRedisClient.RedisRPUSH(const aKey, aValue : string) : Boolean;
  299. begin
  300. Result := Command('RPUSH','%s "%s"',[aKey,EscapeString(aValue)]).IsDone;
  301. end;
  302. function TRedisClient.RedisSELECT(dbIndex: Integer): Boolean;
  303. begin
  304. Result := Command('SELECT',dbIndex.ToString).IsDone;
  305. end;
  306. function TRedisClient.RedisSET(const aKey, aValue: string; aTTLMs: Integer = -1): Boolean;
  307. begin
  308. Result := Command('SET','%s "%s" PX %d',[aKey,EscapeString(aValue),aTTLMs]).IsDone;
  309. end;
  310. function TRedisClient.RedisRPOP(const aKey: string; out oValue: string): Boolean;
  311. begin
  312. Result := False;
  313. if Command('RPOP','%s',[aKey]).IsDone then
  314. begin
  315. oValue := fTCPClient.IOHandler.ReadLn;
  316. Result := True;
  317. end;
  318. end;
  319. function TRedisClient.RedisBRPOP(const aKey: string; out oValue: string; aWaitTimeoutSecs : Integer): Boolean;
  320. var
  321. response : IRedisResponse;
  322. begin
  323. Result := False;
  324. response := Command('BRPOP','%s %d',[aKey,aWaitTimeoutSecs]);
  325. if response.IsDone then
  326. begin
  327. //if response.Response = '-1' then Exit;
  328. fTCPClient.IOHandler.ReadLn; //$int
  329. fTCPClient.IOHandler.ReadLn; //key
  330. fTCPClient.IOHandler.ReadLn; //$int
  331. oValue := fTCPClient.IOHandler.ReadLn; //value
  332. if not oValue.IsEmpty then Result := True;
  333. end
  334. else
  335. begin
  336. if not response.Response.IsEmpty then ERedisCommandError.CreateFmt('BRPOP Error: %s',[response.Response]);
  337. end;
  338. end;
  339. function TRedisClient.RedisBRPOPLPUSH(const aKey, aKeyToMove: string; out oValue: string; aWaitTimeoutSecs: Integer): Boolean;
  340. var
  341. response : IRedisResponse;
  342. begin
  343. Result := False;
  344. response := Command('BRPOPLPUSH','%s %s %d',[aKey,aKeyToMove,aWaitTimeoutSecs]);
  345. if response.IsDone then
  346. begin
  347. oValue := fTCPClient.IOHandler.ReadLn; //value
  348. if not oValue.IsEmpty then Result := True;
  349. end
  350. else raise ERedisCommandError.CreateFmt('BRPOPLPUSH Error: %s',[response.Response]);
  351. end;
  352. function TRedisClient.RedisDEL(const aKey: string): Boolean;
  353. begin
  354. Result := Command('DEL',aKey).IsDone;
  355. end;
  356. function TRedisClient.RedisLLEN(const aKey : string): Integer;
  357. var
  358. response : IRedisResponse;
  359. begin
  360. Result := 0;
  361. response := Command('LLEN',aKey);
  362. if response.IsDone then
  363. begin
  364. Result := response.Response.ToInteger;
  365. end;
  366. end;
  367. function TRedisClient.RedisTTL(const aKey, aValue : string): Integer;
  368. var
  369. response : IRedisResponse;
  370. begin
  371. Result := 0;
  372. response := Command('TTL','%s "%s"',[aKey,EscapeString(aValue)]);
  373. if response.IsDone then
  374. begin
  375. Result := response.Response.ToInteger;
  376. end;
  377. end;
  378. function TRedisClient.RedisZADD(const aKey, aValue: string; aScore: Int64): Boolean;
  379. var
  380. response : IRedisResponse;
  381. begin
  382. response := Command('ZADD','%s %d "%s"',[aKey,aScore,EscapeString(aValue)]);
  383. if response.IsDone then
  384. begin
  385. Result := response.Response.ToInteger = 1;
  386. end
  387. else raise ERedisCommandError.CreateFmt('ZADD %s',[response.Response]);
  388. end;
  389. function TRedisClient.RedisZRANGE(const aKey: string; aStartPosition, aEndPosition: Int64): TArray<string>;
  390. var
  391. response : IRedisResponse;
  392. value : string;
  393. i : Integer;
  394. begin
  395. Result := [];
  396. response := Command('ZRANGE','%s %d %d',[aKey,aStartPosition,aEndPosition]);
  397. if response.IsDone then
  398. begin
  399. for i := 1 to (response.Response.ToInteger) do
  400. begin
  401. fTCPClient.IOHandler.ReadLn; //$int
  402. value := fTCPClient.IOHandler.ReadLn; //value
  403. Result := Result + [value];
  404. end;
  405. end
  406. else raise ERedisCommandError.CreateFmt('ZRANGE Error: %s',[response.Response]);
  407. end;
  408. function TRedisClient.RedisZRANGEBYSCORE(const aKey: string; aMinScore, aMaxScore: Int64): TArray<TRedisSortedItem>;
  409. var
  410. response : IRedisResponse;
  411. item : TRedisSortedItem;
  412. i : Integer;
  413. value : string;
  414. score : string;
  415. begin
  416. Result := [];
  417. response := Command('ZRANGEBYSCORE','%s %d %d WITHSCORES',[aKey,aMinScore,aMaxScore]);
  418. if response.IsDone then
  419. begin
  420. for i := 1 to (response.Response.ToInteger Div 2) do
  421. begin
  422. fTCPClient.IOHandler.ReadLn; //$int
  423. value := fTCPClient.IOHandler.ReadLn; //value
  424. fTCPClient.IOHandler.ReadLn; //$int
  425. score := fTCPClient.IOHandler.ReadLn; //score
  426. item.Value := value;
  427. item.Score := score.ToInt64;
  428. Result := Result + [item];
  429. end;
  430. end
  431. else raise ERedisCommandError.CreateFmt('ZRANGE Error: %s',[response.Response]);
  432. end;
  433. function TRedisClient.RedisZREM(const aKey, aValue: string): Boolean;
  434. var
  435. response : IRedisResponse;
  436. begin
  437. Result := False;
  438. response := Command('ZREM','%s "%s"',[aKey,EscapeString(aValue)]);
  439. if response.IsDone then
  440. begin
  441. Result := response.Response.ToInteger = 1;
  442. end;
  443. end;
  444. function TRedisClient.RedisLPOP(const aKey: string; out oValue: string): Boolean;
  445. begin
  446. Result := False;
  447. if Command('LPOP','%s',[aKey]).IsDone then
  448. begin
  449. oValue := fTCPClient.IOHandler.ReadLn;
  450. Result := True;
  451. end;
  452. end;
  453. function TRedisClient.RedisBLPOP(const aKey: string; out oValue: string; aWaitTimeoutSecs : Integer): Boolean;
  454. var
  455. response : IRedisResponse;
  456. begin
  457. response := Command('BLPOP','%s %d',[aKey,aWaitTimeoutSecs]);
  458. if response.IsDone then
  459. begin
  460. fTCPClient.IOHandler.ReadLn; //$int
  461. fTCPClient.IOHandler.ReadLn; //key
  462. fTCPClient.IOHandler.ReadLn; //$int
  463. oValue := fTCPClient.IOHandler.ReadLn; //value
  464. Result := True;
  465. end
  466. else raise ERedisCommandError.CreateFmt('BLPOP Error: %s',[response.Response]);
  467. end;
  468. function TRedisClient.RedisLPUSH(const aKey, aValue : string) : Boolean;
  469. begin
  470. Result := Command('LPUSH','%s "%s"',[aKey,EscapeString(aValue)]).IsDone;
  471. end;
  472. function TRedisClient.RedisLREM(const aKey, aValue: string; aNumOccurrences: Integer): Boolean;
  473. begin
  474. Result := Command('LREM','%s %d "%s"',[aKey,aNumOccurrences * -1,EscapeString(aValue)]).IsDone;
  475. end;
  476. function TRedisClient.RedisLTRIM(const aKey : string; aFirstElement, aMaxSize : Int64) : Boolean;
  477. begin
  478. Result := Command('LTRIM','%s %d %d',[aKey,aFirstElement,fMaxSize]).IsDone;
  479. end;
  480. function TRedisClient.RedisAUTH(const aPassword : string) : Boolean;
  481. begin
  482. Result := Command('AUTH',fPassword).IsDone;
  483. end;
  484. function TRedisClient.RedisEXPIRE(const aKey: string; aExpireDate: TDateTime): Boolean;
  485. begin
  486. Result := RedisEXPIRE(aKey,MilliSecondsBetween(Now(),aExpireDate));
  487. end;
  488. function TRedisClient.RedisEXPIRE(const aKey: string; aTTLMs: Integer): Boolean;
  489. begin
  490. Result := Command('PEXPIRE','%s %d',[aKey,aTTLMs]).IsDone;
  491. end;
  492. function TRedisClient.RedisLINDEX(const aKey: string; aIndex: Integer; out oValue : string): Boolean;
  493. var
  494. response : IRedisResponse;
  495. begin
  496. Result := False;
  497. response := Command('LINDEX','%s %d',[aKey,aIndex]);
  498. if response.IsDone then
  499. begin
  500. oValue := response.response;
  501. Result := True;
  502. end;
  503. end;
  504. function TRedisClient.RedisGET(const aKey: string; out oValue: string): Boolean;
  505. begin
  506. Result := False;
  507. if Command('GET','%s',[aKey]).IsDone then
  508. begin
  509. oValue := fTCPClient.IOHandler.ReadLn;
  510. Result := True;
  511. end;
  512. end;
  513. function TRedisClient.RedisPING : Boolean;
  514. begin
  515. Result := False;
  516. if Command('PING').IsDone then
  517. begin
  518. Result := fTCPClient.IOHandler.ReadLn = 'PONG';
  519. end;
  520. end;
  521. function TRedisClient.RedisQUIT : Boolean;
  522. begin
  523. try
  524. Result := Command('QUIT').IsDone;
  525. except
  526. Result := False;
  527. end;
  528. end;
  529. end.