Quick.Data.Redis.pas 17 KB

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